Home Development for Android Embedding RecyclerView into CardView

Embedding RecyclerView into CardView

by admin

Embedding RecyclerView into CardView
After reading a post on hubra about the new widgets "RecyclerView and CardView.New widgets in Android L" , decided to try to use. There are a lot of examples on the web where CardView is embedded in RecyclerView. I was interested in the opposite, to build RecyclerView into CardView. So that this design is still a snippet.
I downloaded the example from the article. Immediately I had a problem with deleting several elements. After looking through the code, I put a check for :

private void delete(Record record) {int position = records.indexOf(record);Log.i("> " , "position=" + position);if( position != -1 ) {records.remove(position);notifyItemRemoved(position);}}

This was just the beginning… After adding the fragment, another problem came up. CardView can’t properly "wrap" the list from RecyclerView in height. wrap_content doesn’t help. Turns out a lot of people have already encountered it and there are solutions : "Nested Recycler view height doesn’t wrap its content"
Having first looked at A First Glance at Android’s RecyclerView Thought about using layoutManager.getDecoratedMeasuredHeight()…and similar methods, but that didn’t work. The dimensions were returning 0.
I had to rewrite onMeasure in LinearLayoutManager. Taken from stackoverflow:
MyLinearLayoutManager

public class MyLinearLayoutManager extends LinearLayoutManager {public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {super(context, orientation, reverseLayout);}public MyLinearLayoutManager(Context context) {super(context);}private int[] mMeasuredDimension = new int[2];@Overridepublic 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;for (int i = 0; i < getItemCount(); i++) {if (getOrientation() == HORIZONTAL) {measureScrapChild(recycler, i, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), heightSpec, mMeasuredDimension);width = width + mMeasuredDimension[0];if (i == 0) {height = mMeasuredDimension[1];}} else {measureScrapChild(recycler, i, widthSpec, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), mMeasuredDimension);height = height + 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:}setMeasuredDimension(width, height);}private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec, int[] measuredDimension) {View view = recycler.getViewForPosition(position);recycler.bindViewToPosition(view, 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);}}}

It worked. Deletion is now possible only from the end. Removing from the middle or the beginning of the list caused exclusion :

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 3(offset:-1).state:5

Weird. Googling some more, I came across similar problems with people IndexOutOfBoundsException Invalid item position XX(XX). Item count:XX #134 After looking through the whole topic I read :
It is indeed a RecyclerView bug and is yet to be fixed.
For more information check:
code.google.com/p/android/issues/detail?id=77846
code.google.com/p/android/issues/detail?id=77232
And the line above was just the solution :
Now I am doing some dirty workaround like
if (index == 0) {notifydatasetchange();} else {notifyItemRemoved(index)}

More specifically, after looking at how elements are removed, I realized that I need to replace notifyItemRemoved(index) with notifydatasetchange(). It’s the same with addition. The solution is not optimal, but it works and in current widget implementation probably is the only one.
This solution killed the animation completely. This is where the research would have ended…
As a result, we found out the location of the crash in the overridden onMeasure()

IndexOutOfBoundsException = java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 0(offset:-1).state:7

Further investigation of the RecyclerView code to somehow hijack the situation or request an advance offset was unsuccessful. Made a hard hack! Don’t judge)

View view = null;try {view = recycler.getViewForPosition(position);}catch (IndexOutOfBoundsException ex){Log.i("> ", "IndexOutOfBoundsException = " + ex + "position : " + position);}

Now the animation appeared, but after the first addition (initialization) the list did not appear. Although items were added and everything appeared, after the next operation. I made one more hack to the method of adding elements. I hope it’s clear what it does

if ( adapter.getItemCount() == 1 ) {adapter.notifyDataSetChanged();}

In Art. Building a RecyclerView LayoutManager – Part 1 There seems to be a solution to the whole problem, but it didn’t work for me. Maybe I should have updated the support library or the SDK. I don’t know.
This is actually the only required override to get your LayoutManager to compile. The implementation is pretty straightforward, just return a new instance of the RecyclerView.LayoutParams that you want applied by default to all the child views returned from the Recycler. These parameters will be applied to each child before they return from getViewForPosition()

@Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams() {return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);}

The result is a "hackneyed" approach, which is worth applying…
Although such a PopUp widget can be useful for displaying program messages. Instead of progress windows. On a timer, you can delete the top message after a certain time or when you click on it at once
We got a rounded list-fragment with shadow and animation. Correctly handles screen rotation. Easy to integrate into the application. The only small thing. After rearranging the snippet stack, when the user worked with the application, the window didn’t always appear. Probably some callback not in the UI Thread… Solution, refer to the fragment via Handler.

new Handler(Looper.getMainLooper()).post(new Runnable() {@Overridepublic void run() {mOverlayMessageFragment.addMessage(text);}});

Communicating with the UI Thread Every app has its own special thread that runs UI objects such as View objects; this thread is called the UI thread. Only objects running on the UI thread have access to other objects on that thread. Because tasks that you run on a thread from a thread pool aren’t running on your UI thread, they don’t have access to UI objects. To move data from a background thread to the UI thread, use a Handler that’s running on the UI thread.
developer.android.com/training/multiple-threads/communicate-ui.html
Communicating with Other Fragments Often you will want one Fragment to communicate with another, for example to change the content based on a user event. All Fragment-to-Fragment communication is done through the associated Activity. Two Fragments should never communicate directly.
developer.android.com/training/basics/fragments/communicating.html
And another useful chip for the fragment :

@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// keep the fragment and all its data across screen rotationsetRetainInstance(true);}

Code changes are listed below :
PopUpFragment.java

package net.appz.iconfounder.popupwidget.fragment;import android.app.Activity;import android.content.Context;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.support.v4.app.Fragment;import android.support.v7.widget.CardView;import android.support.v7.widget.DefaultItemAnimator;import android.support.v7.widget.LinearLayoutManager;import android.support.v7.widget.RecyclerView;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import net.appz.iconfounder.R;import net.appz.iconfounder.popupwidget.adapter.RecyclerViewAdapter;import net.appz.iconfounder.popupwidget.model.Record;import java.util.ArrayList;import java.util.List;public class PopUpFragment extends Fragment{private static final String ARG_TIMER_INTERVAL = "timer_interval";private OnFragmentInteractionListener mListener;private HandlerPopUpMessages messageHandler;private int TIMER_INTERVAL_DEFAULT = 2000;private int timer_interval;private RecyclerViewAdapter adapter;private CardView cardView;private RecyclerView recyclerView;private List<Record> records = new ArrayList<Record> ();/*** Use this factory method to create a new instance of* this fragment using the provided parameters.** @param timer_interval .* @return A new instance of fragment PopUpFragment.*/public static PopUpFragment newInstance(int timer_interval) {PopUpFragment fragment = new PopUpFragment();Bundle args = new Bundle();args.putInt(ARG_TIMER_INTERVAL, timer_interval);fragment.setArguments(args);return fragment;}public static PopUpFragment newInstance() {PopUpFragment fragment = new PopUpFragment();return fragment;}public PopUpFragment() {// Required empty public constructor}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);if (getArguments() != null) {timer_interval = getArguments().getInt(ARG_TIMER_INTERVAL);} else {timer_interval = TIMER_INTERVAL_DEFAULT;}// keep the fragment and all its data across screen rotation//setRetainInstance(true);messageHandler = new HandlerPopUpMessages(this);if (savedInstanceState != null) {records = savedInstanceState.getParcelableArrayList(PopUpFragment.class.getSimpleName());}}@Overridepublic void onSaveInstanceState(Bundle outState) {outState.putParcelableArrayList(PopUpFragment.class.getSimpleName(), (java.util.ArrayList<? extends android.os.Parcelable> ) records);super.onSaveInstanceState(outState);}Handler timerHandler = new Handler();Runnable timerRunnable = new Runnable() {@Overridepublic void run() {if (adapter.getItemCount() > 0) {Record record = adapter.getRecords().get(0);long ts = record.getTimestamp();if (ts < System.currentTimeMillis() - timer_interval){if (adapter.getItemCount() > 1){record = adapter.getRecords().get(1);record.setTimestamp(System.currentTimeMillis());}removeMessagePopUp();}}timerHandler.postDelayed(this, timer_interval);}};@Overridepublic void onPause() {super.onPause();timerHandler.removeCallbacks(timerRunnable);}@Overridepublic void onResume() {super.onResume();timerHandler.postDelayed(timerRunnable, timer_interval);if ( adapter.getItemCount() == 0 ) {cardView.setVisibility(View.GONE);mListener.onHidePopUpFrugment();}}@Overridepublic void onStart() {super.onStart();mListener.onPopUpFragmentStart();}@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {// Inflate the layout for this fragmentView view = inflater.inflate(R.layout.fragment_popup, container, false);recyclerView = (RecyclerView)view.findViewById(R.id.recyclerView);// recyclerView.setHasFixedSize(true);adapter = new RecyclerViewAdapter(records);LinearLayoutManager layoutManager = new MyLinearLayoutManager(getActivity());RecyclerView.ItemAnimator itemAnimator = new DefaultItemAnimator();recyclerView.setAdapter(adapter);recyclerView.setLayoutManager(layoutManager);recyclerView.setItemAnimator(itemAnimator);cardView = (CardView)view.findViewById(R.id.cardView);return view;}public void addMessage0ToPopUp(int type, String text){Bundle msgBundle = new Bundle();msgBundle.putInt(HandlerPopUpMessages.ICON_ARG, type);msgBundle.putString(HandlerPopUpMessages.TEXT_ARG, text);Message msg = new Message();msg.what = HandlerPopUpMessages.ADD_MESSAGE;msg.setData(msgBundle);messageHandler.sendMessage(msg);}public void removeMessagePopUp() {Bundle msgBundle = new Bundle();Message msg = new Message();msg.what = HandlerPopUpMessages.REMOVE_MESSAGE_0;msg.setData(msgBundle);messageHandler.sendMessage(msg);}private void addMessageInternal(int type, String text) {Record record = new Record();record.setName(text);record.setType(Record.Type.values()[type]);record.setTimestamp(System.currentTimeMillis());adapter.getRecords().add(record);adapter.notifyItemInserted(adapter.getItemCount()-1);//adapter.notifyDataSetChanged();// Bellow there is hack. First show RecyclerViewif ( adapter.getItemCount() == 1 ) {adapter.notifyDataSetChanged();}if ( adapter.getItemCount() > 0 ) {cardView.setVisibility(View.VISIBLE);mListener.onShowPopUpFrugment();}}private void removeMessage0Internal(){if ( adapter.getItemCount() > 0 ) {adapter.getRecords().remove(0);adapter.notifyItemRemoved(0);//adapter.notifyDataSetChanged();}if ( adapter.getItemCount() == 0 ) {cardView.setVisibility(View.GONE);mListener.onHidePopUpFrugment();}}@Overridepublic void onAttach(Activity activity) {super.onAttach(activity);try {mListener = (OnFragmentInteractionListener) activity;} catch (ClassCastException e) {throw new ClassCastException(activity.toString()+ " must implement OnFragmentInteractionListener");}}@Overridepublic void onDetach() {super.onDetach();mListener = null;messageHandler.removeCallbacksAndMessages(null);}/*** This interface must be implemented by activities that contain this* fragment to allow an interaction in this fragment to be communicated* to the activity and potentially other fragments contained in that* activity.* <p/>* See the Android Training lesson <a href=* "http://developer.android.com/training/basics/fragments/communicating.html"* > Communicating with Other Fragments</a> for more information.*/public interface OnFragmentInteractionListener {void onPopUpFragmentStart();void onHidePopUpFrugment();void onShowPopUpFrugment();}public class MyLinearLayoutManager extends LinearLayoutManager {public MyLinearLayoutManager(Context context) {super(context);}// Not worked@Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams() {return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);}// Not worked@Overridepublic boolean canScrollVertically() {//We do allow scrollingreturn true;}private int[] mMeasuredDimension = new int[2];@Overridepublic void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {Log.i("> ", "state " + state.toString());//if ( state.isPreLayout() ) {// super.onMeasure(recycler, state, widthSpec, heightSpec);//} else{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;for (int i = 0; i < getItemCount(); i++) {if (getOrientation() == HORIZONTAL) {measureScrapChild(recycler, i, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), heightSpec, mMeasuredDimension);width = width + mMeasuredDimension[0];if (i == 0) {height = mMeasuredDimension[1];}} else {measureScrapChild(recycler, i, widthSpec, View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), mMeasuredDimension);height = height + 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:}setMeasuredDimension(width, height);}}private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec, int heightSpec, int[] measuredDimension) {View view = null;// Bellow there is strong hack!try {view = recycler.getViewForPosition(position);}catch (IndexOutOfBoundsException ex){Log.i("> ", "IndexOutOfBoundsException = " + ex + "position : " + position);}if (view != null) {// For adding Item Decor Insets to view//super.measureChildWithMargins(view, 0, 0);//recycler.bindViewToPosition(view, position);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);}}}private class HandlerPopUpMessages <T> extends Handler {public static final int ADD_MESSAGE = 100;public static final int REMOVE_MESSAGE_0 = 101;public static final String TEXT_ARG = "text";public static final String ICON_ARG = "icon";private final T fragment;public HandlerPopUpMessages(T fragment ){this.fragment = fragment;}@Overridepublic void handleMessage(Message message){if (this.fragment != null){Bundle b = message.getData();switch (message.what){case ADD_MESSAGE:if(b == null)new IllegalArgumentException("Message should be have params !");String text = b.getString(TEXT_ARG);int type = b.getInt(ICON_ARG);((PopUpFragment)fragment).addMessageInternal(type, text);break;case REMOVE_MESSAGE_0:((PopUpFragment)fragment).removeMessage0Internal();break;}}}}}

Layout

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:card_view="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="wrap_content"tools:context=".PopUpFragment"><android.support.v7.widget.CardViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:id="@+id/cardView"card_view:cardCornerRadius="6dp"card_view:cardBackgroundColor="#84ffff"><android.support.v7.widget.RecyclerViewandroid:layout_width="400dp"android:layout_height="wrap_content"android:id="@+id/recyclerView" /></android.support.v7.widget.CardView></FrameLayout>

MainActivity.java

..mPopupWidget = (PopUpFragment)getSupportFragmentManager().findFragmentById(R.id.popup);if (DEBUG) Log.d(TAG, "onCreate() : mPopupWidget = " + mPopupWidget);if( mPopupWidget == null ){getSupportFragmentManager().beginTransaction().replace(R.id.popup, PopUpFragment.newInstance(), PopUpFragment.class.getSimpleName()).commit();}...

RecyclerViewAdapter

package com.renal128.demo.recyclerviewdemo.adapter;import android.support.v7.widget.RecyclerView;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.Button;import android.widget.ImageView;import android.widget.TextView;import com.renal128.demo.recyclerviewdemo.R;import com.renal128.demo.recyclerviewdemo.model.Record;import java.util.List;public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {private List<Record> records;public RecyclerViewAdapter(List<Record> records) {this.records = records;}public List<Record> getRecords() {return records;}@Overridepublic ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recyclerview_item, viewGroup, false);return new ViewHolder(v);}@Overridepublic void onBindViewHolder(ViewHolder viewHolder, int i) {Record record = records.get(i);int iconResourceId = 0;switch (record.getType()) {case GREEN:iconResourceId = R.drawable.green_circle;break;case RED:iconResourceId = R.drawable.red_circle;break;case YELLOW:iconResourceId = R.drawable.yellow_circle;break;}viewHolder.icon.setImageResource(iconResourceId);viewHolder.name.setText(record.getName());viewHolder.deleteButtonListener.setRecord(record);viewHolder.copyButtonListener.setRecord(record);}@Overridepublic int getItemCount() {return records.size();}private void copy(Record record) {int position = records.indexOf(record);Record copy = record.copy();records.add(position + 1, copy);//notifyItemInserted(position + 1);notifyDataSetChanged();}private void delete(Record record) {int position = records.indexOf(record);Log.i("> " , "position=" + position);if( position != -1 ) {records.remove(position);//notifyItemRemoved(position);notifyDataSetChanged();}}class ViewHolder extends RecyclerView.ViewHolder {private TextView name;private ImageView icon;private Button deleteButton;private Button copyButton;private DeleteButtonListener deleteButtonListener;private CopyButtonListener copyButtonListener;public ViewHolder(View itemView) {super(itemView);name = (TextView) itemView.findViewById(R.id.recyclerViewItemName);icon = (ImageView) itemView.findViewById(R.id.recyclerViewItemIcon);deleteButton = (Button) itemView.findViewById(R.id.recyclerViewItemDeleteButton);copyButton = (Button) itemView.findViewById(R.id.recyclerViewItemCopyButton);deleteButtonListener = new DeleteButtonListener();copyButtonListener = new CopyButtonListener();deleteButton.setOnClickListener(deleteButtonListener);copyButton.setOnClickListener(copyButtonListener);}}private class CopyButtonListener implements View.OnClickListener {private Record record;@Overridepublic void onClick(View v) {copy(record);}public void setRecord(Record record) {this.record = record;}}private class DeleteButtonListener implements View.OnClickListener {private Record record;@Overridepublic void onClick(View v) {delete(record);}public void setRecord(Record record) {this.record = record;}}}

The original code was taken here : github.com/renal128/RecyclerViewDemo
Implementation with Handler’s and Timer’s: github.com/app-z/PopUpWidget
You can see how it works from Google Play: play.google.com/store/apps/details?id=net.appz.iconfounder

You may also like