Dynamically Setting a Fixed Height for a Staggered Grid View
Asked Answered
C

1

19

I'm trying to take a RecyclerView with a StaggeredGridLayout and make it a fixed height by having it measure the views and set the height dynamically. I'm overriding the onMeasure(), but it does not always seem to measure correctly. I'd say it works about 50% of the time. The other 50% of the time it under measures it. I think it has to do with when the text wraps in the view_tile_small.xml, but I'm not sure.

Fragment

public class AtTheMuseumFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener{

        //Odds n Ends Variables
        private MapFragment mapFragment;
        private FragmentTransaction fragmentTransaction;
        private User user = new User();
        private MuseumCollection museumCollection;
        private Context context;

        //Story Grid Adapter Variables
        private AtTheMuseumAdapter nearMeAdapter;
        private TileFactory tileFactory;

        //Interfaces
        private OnFragmentChangeListener changeListener;

        @Bind(R.id.stories_list_view) RecyclerView storiesListView;
        @Bind(R.id.swipe_container) SwipeRefreshLayout swipeRefreshLayout;

        public static AtTheMuseumFragment newInstance(MuseumCollection museumCollection) {
            AtTheMuseumFragment fragment = new AtTheMuseumFragment();
            Bundle args = new Bundle();
            fragment.setArguments(args);
            return fragment;
        }

        public AtTheMuseumFragment() {
            // Required empty public constructor
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            museumCollection = ((MainActivity) getActivity()).getMuseumCollection();
            context = getActivity().getApplicationContext();
            tileFactory = new TileFactory();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {

            View view = inflater.inflate(R.layout.fragment_museum, container, false);
            ButterKnife.bind(this, view);

            //Sets up the layoutManager to the Mason View
            storiesListView.setLayoutManager(new MeasuredStaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));

            //Sets up The map
            try{
                fragmentTransaction = getChildFragmentManager().beginTransaction();
                mapFragment = MapFragment.newInstance(MapFragment.MUSEUM);
                fragmentTransaction.add(R.id.museum_map, mapFragment).commit();
            }catch (Exception e){
                Log.d("Map - Initial Inflate:", e.getMessage());
            }


            //Sets up the swipe to refresh jawn
            swipeRefreshLayout.setOnRefreshListener(this);
            setNearMeAdapter();

            return view;
        }

        @Override
        public void onResume(){
            super.onResume();
        }

        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            try {
                changeListener = (OnFragmentChangeListener) activity;
            } catch (ClassCastException e) {
                throw new ClassCastException(activity.toString()
                        + " must implement OnFragmentInteractionListener");
            }
        }

        @Override
        public void onDetach() {
            super.onDetach();
            changeListener = null;
        }

        /**
         *  Loads the adapter once the data is ready
         *
         */

        public void setNearMeAdapter(){
            List<MuseumObject> newList = museumCollection.getObjectsNearUser(User.position, 4);
            List<Tile> tiles = tileFactory.createAtMuseumFeed(newList, true);
            nearMeAdapter = new AtTheMuseumAdapter(context, tiles, getActivity());
            storiesListView.setAdapter(nearMeAdapter);
        }

        /**
         *  Refreshes Adapter with new data
         *
         */

        public void refreshNearMeAdapter(){
            //TODO CHANGE THIS TO LOCATION BASED WHEN TIME IS RIGHT - Peter
            //nearMeAdapter.setNewData(MuseumCollection.getObjectsNearUser(User.position, 4));
            List<MuseumObject> newList = museumCollection.getRandomOrder();
            nearMeAdapter.setNewData(tileFactory.createAtMuseumFeed(newList,false));
        }

        /**
         *  Adds past data to the Adapter
         *
         */

        public void loadPastObjects(){
            //TODO MAKE THIS NOT ONLY LOAD RANDOM DATA - Peter
            List<MuseumObject> newList = museumCollection.getRandomOrder();
            nearMeAdapter.addNewData(tileFactory.createAtMuseumFeed(newList, false));
            nearMeAdapter.notifyDataSetChanged();
        }

        @Override
        public void onRefresh() {
            user.updateUserLocation();
            refreshNearMeAdapter();
            mapFragment.refreshMap(museumCollection.getObjectFeed());
            swipeRefreshLayout.setRefreshing(false);
        }

        public interface OnFragmentChangeListener {
            void onFragmentChange(String fragment);
        }

        @OnClick(R.id.explore_map)
        public void exploreMap(){
            changeListener.onFragmentChange("map");
        }

        @OnClick(R.id.load_more)
        public void loadMore(){
            loadPastObjects();
        }

    }

MeasuredStaggeredGridLayoutManager

public class MeasuredStaggeredGridLayoutManager extends StaggeredGridLayoutManager {

    public MeasuredStaggeredGridLayoutManager(int spanCount, int orientation) {
        super(spanCount, orientation);
    }

    private int[] mMeasuredDimension = new int[2];

    @Override
    public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
                          int widthSpec, int heightSpec) {
        final int widthMode = View.MeasureSpec.getMode(widthSpec);
        final int heightMode = View.MeasureSpec.getMode(heightSpec);
        final int widthSize = View.MeasureSpec.getSize(widthSpec);
        final int heightSize = View.MeasureSpec.getSize(heightSpec);
        int width = 0;
        int height = 0;
        int heightR = 0;
        int heightL = 0;
        for (int i = 0; i < getItemCount(); i++) {
            measureScrapChild(recycler, i,
                    View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
                    mMeasuredDimension);

            if (getOrientation() == HORIZONTAL) {
                width = width + mMeasuredDimension[0];
                if (i == 0) {
                    height = mMeasuredDimension[1];
                }
            } else {

                if(i % 2 == 0){
                    heightL += mMeasuredDimension[1];
                }else{
                    heightR += mMeasuredDimension[1];
                }

                if (i == 0) {
                    width = mMeasuredDimension[0];
                }
            }
        }
        switch (widthMode) {
            case View.MeasureSpec.EXACTLY:
                width = widthSize;
            case View.MeasureSpec.AT_MOST:
            case View.MeasureSpec.UNSPECIFIED:
        }

        switch (heightMode) {
            case View.MeasureSpec.EXACTLY:
                height = heightSize;
            case View.MeasureSpec.AT_MOST:
            case View.MeasureSpec.UNSPECIFIED:
        }

        if(heightL != 0 || heightR != 0){
            height = (heightL > heightR) ? heightL : heightR;
        }

        //TODO come up with a better way to fix the slightly wrong height
        // must be not accounting for padding or margin or something - Peter
        height += (20 * (getItemCount() / 2)) + 5;

        setMeasuredDimension(width, height);
    }

    private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
                                   int heightSpec, int[] measuredDimension) {
        View view = recycler.getViewForPosition(position);
        if (view != null) {
            RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
            int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
                    getPaddingLeft() + getPaddingRight(), p.width);
            int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
                    getPaddingTop() + getPaddingBottom(), p.height);
            view.measure(childWidthSpec, childHeightSpec);
            measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
            measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
            recycler.recycleView(view);
        }
    }
}

AtTheMuseumAdapter

public class AtTheMuseumAdapter extends RecyclerView.Adapter<AtTheMuseumAdapter.MuseumStoriesViewHolder> {

    private List<Tile> tiles;
    private LayoutInflater inflater;
    private AdapterCallback mListener;



    private Context context;

    public AtTheMuseumAdapter(Context context, List<Tile> tiles, Activity activity) {
        this.tiles = tiles;
        this.context = context;
        inflater = LayoutInflater.from(this.context);

        //Sets up interface between Stock Adapter and Fragment
        try {
            this.mListener = ((AdapterCallback) activity);
        } catch (ClassCastException e) {
            throw new ClassCastException("Fragment must implement AdapterCallback.");
        }
    }

    @Override
    public MuseumStoriesViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View view = inflater.inflate(R.layout.view_tile_small, viewGroup, false);
        MuseumStoriesViewHolder holder = new MuseumStoriesViewHolder(view);

        return holder;
    }

    @Override
    public void onBindViewHolder(MuseumStoriesViewHolder holder, int position) {
        Tile currentTile = tiles.get(position);

        holder.title.setText(currentTile.getTitle());
        holder.desc.setText(currentTile.getDescription());
        holder.type.setText(currentTile.getObjectTypeName());


        //Using Picasso since it handles caching and all that jazz
        Picasso.with(context)
                .load(currentTile.getImg())
                .into(holder.img);
    }

    @Override
    public int getItemCount() {
        return tiles.size();
    }

    public void setNewData(List<Tile> newItems){
        tiles = newItems;
        notifyDataSetChanged();
    }

    public void addNewData(final List<Tile> newItems){
        tiles.addAll(newItems);
        notifyDataSetChanged();
    }

    class MuseumStoriesViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        public TextView type,title,desc;
        public ImageView img;

        public MuseumStoriesViewHolder(View itemView) {
            super(itemView);
            //Tried Butterknife, but it doesn't seem like it was working in the view holder. - Peter
            type = (TextView) itemView.findViewById(R.id.small_box_type);
            title = (TextView) itemView.findViewById(R.id.small_box_title);
            desc = (TextView) itemView.findViewById(R.id.small_box_desc);
            img = (ImageView) itemView.findViewById(R.id.small_box_image);

            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            Tile t = tiles.get(getPosition());
            switch (t.getObjectTypeName()){
                case Tile.OBJECT_NAME:
                    mListener.onObjectClick(t.getObjectID());
                    break;
                case Tile.TOUR_NAME:
                    mListener.onTourCLick(t.getTourID());
                    break;
                case Tile.STORY_NAME:
                    mListener.onStoryClick(t.getObjectID(), t.getStoryID());
                    break;

            }
        }

    }

    public interface AdapterCallback {
        public void onObjectClick(String objectID);
        public void onStoryClick(String objectID, String storyID);
        public void onTourCLick(String tourID);
    }

}

view_tile_small.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="@dimen/margin_small"
    android:background="@color/small_box_background_color">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <!--TODO Make layout_height wrap contenet -->
        <ImageView
            android:id="@+id/small_box_image"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:adjustViewBounds="true"
            android:scaleType="centerCrop"
            android:maxHeight="150dp"
            android:background="@color/transparent"/>

        <ImageView
            android:layout_width="35dp"
            android:layout_height="35dp"
            android:layout_gravity="right"
            android:src="@drawable/abc_btn_rating_star_off_mtrl_alpha"
            />

        <TextView
            android:id="@+id/small_box_type"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/margin_normal"
            android:paddingBottom="@dimen/margin_normal"
            android:textSize="@dimen/font_small"
            android:textColor="@color/font_white"
            android:background="@drawable/small_box_text_bottom_border"
            android:layout_gravity="bottom"
            android:text="Object Story"
            />

    </FrameLayout>

    <TextView
        android:id="@+id/small_box_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/margin_larger"
        android:layout_marginBottom="@dimen/margin_normal"
        android:layout_marginRight="@dimen/margin_larger"
        android:layout_marginTop="@dimen/margin_larger"
        android:textSize="@dimen/font_large"
        android:textColor="@color/font_black"
        android:text="Sample Text Here"
    />

    <TextView
        android:id="@+id/small_box_desc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/margin_larger"
        android:layout_marginBottom="@dimen/margin_larger"
        android:textSize="@dimen/font_normal"
        android:textColor="@color/font_black"
        android:textStyle="italic"
        android:text="Sample Text Here"
    />



</LinearLayout>

fragment_museum

<android.support.v4.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.bluecadet.android.nasm.ui.AtTheMuseumFragment"
    android:id="@+id/swipe_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="100dp">

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#FFFFFF"
            android:descendantFocusability="blocksDescendants"
            >

            <TextView
                android:id="@+id/museum_header"
                style="@style/header"
                android:text="@string/museum_header"
                android:layout_margin="@dimen/margin_larger"
                android:elevation="8dp"
                />

            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="275dp"
                android:elevation="2dp"
                >

                <FrameLayout
                    android:id="@+id/museum_map"
                    android:layout_height="fill_parent"
                    android:layout_width="match_parent"
                    />

                <include
                    layout="@layout/view_explore_map" />

            </FrameLayout>

            <android.support.v7.widget.RecyclerView
                android:id="@+id/stories_list_view"
                xmlns:android="http://schemas.android.com/apk/res/android"
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="@dimen/margin_normal"
                android:layout_marginLeft="@dimen/margin_small"
                android:layout_marginRight="@dimen/margin_small"
                android:layout_marginTop="@dimen/margin_normal"
                android:stretchMode="columnWidth"
                />

            <Button
                android:id="@+id/load_more"
                style="@style/home_button"
                android:gravity="center"
                android:text="@string/button_load_more"
            />

        </LinearLayout>
    </ScrollView>
</android.support.v4.widget.SwipeRefreshLayout>

ViewTreeObserver code I'm playing with now. It's in Fragment.

    mTopStoriesListView.setLayoutManager(new NewMeasuredStaggeredLayoutManager(2, StaggeredGridLayoutManager.VERTICAL, mTopStoriesListView));
    mTopStoriesListView.setNestedScrollingEnabled(false);

    //Testing Issue 54
    final ViewTreeObserver viewTreeObserver = mTopStoriesListView.getViewTreeObserver();
    if (viewTreeObserver.isAlive()) {
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mTopStoriesListView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int l = 0,r = 0;
                for(int i = 0 ; i < mNearMeAdapter.getItemCount(); i++){
                    int h =  mTopStoriesListView.getLayoutManager().findViewByPosition(i).getHeight();
                    ViewGroup.MarginLayoutParams layout = (ViewGroup.MarginLayoutParams) mTopStoriesListView.getLayoutManager()
                            .findViewByPosition(i).getLayoutParams();

                    int t = layout.topMargin;
                    int b = layout.bottomMargin;
                    if(i % 2 == 0){
                        l += h + t + b;
                    }else{
                        r += h + t + b;
                    }
                }
                int viewHeight = (l > r) ? l : r;
                mTopStoriesListView.getLayoutParams().height = viewHeight;
                Log.d("TAG", String.valueOf(viewHeight));
            }
        });
    }
    //END TEST
Cremator answered 21/10, 2015 at 17:48 Comment(4)
Did you got any solution for this? I have the same problemThole
No I did not. I've been meaning to try the solution below, but the bug has been put on the back burner for the time being due to more important stories. I should be trying again to solve it tomorrow.Cremator
Ok cool ill try to make it work and post a solution if i fix it.. would appreciate if you do the same if you get it ;)Thole
Nah couldn't make it work. I finally ended up using LinearLayoutManager, which i found a MeasuredLinearLayoutManager. Let me know if you need the referenceThole
B
5

Have you look at ViewTreeObserver ?

I got a similar problem on a passed project and I have found it more reliable than onMesure to dynamically get Layout properties

You can go through it from here : http://developer.android.com/reference/android/view/ViewTreeObserver.html

Bespatter answered 2/11, 2015 at 13:12 Comment(2)
No I have not looked into that. I will take a look at it later today. Thanks for the help!Cremator
I have been finally was able to get back to this issue... So how would I use ViewTreeObserver to measure the each view inside the RecyclerView? I see examples of measuring a single view, but not views from inside an adapter.Cremator

© 2022 - 2024 — McMap. All rights reserved.