📜 ⬆️ ⬇️

We built RecyclerView in CardView



After reading the post on Habré about new widgets «RecyclerView and CardView. New Widgets in Android L , I decided to try using. There are many examples on the web where CardView is embedded in RecyclerView. It was interesting to integrate the RecyclerView into CardView. To even this design was a fragment.

I downloaded an example from the article. Immediately there was a problem when deleting several items. Having looked at the code, I put the check:

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

This was only the beginning ... After adding the fragment, another problem emerged. CardView cannot correctly “wrap” a list of RecyclerViews in height. wrap_content does not help. It turned out that many have already encountered and there are solutions: "Nested Recycler view height doesn't wrap its content . "
')
First, looking at A First Glance at Android's RecyclerView thought of using the methods layoutManager.getDecoratedMeasuredHeight () ... and the like, but that did not help. Sizes returned 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]; @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; 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. Removal became possible only from the end. Deletion from the middle or from the beginning of the list resulted in an exception:

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

Strange. Googling a bit more, I came across similar problems for the people IndexOutOfBoundsException Invalid item position XX (XX). Item count: XX # 134 . After watching the whole topic read:

It is indeed a bug.

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 precisely, looking at how items are deleted, I realized that I need to replace notifyItemRemoved (index) with notifydatasetchange (). With the addition of the same. The solution is not optimal, but working and in the current implementation of the widget is probably the only one.

Such a decision completely killed the animation. There would be to finish the research ...

As a result, the crash location was found 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 intercept the situation or request the offset in advance was unsuccessful. Made a hard hack! Do not judge strictly)
 View view = null; try { view = recycler.getViewForPosition(position); }catch (IndexOutOfBoundsException ex){ Log.i(">", "IndexOutOfBoundsException = " + ex + "position : " + position); } 


Now the animation has appeared, but after the first addition (initialization) the list did not appear. Although the elements were added and everything appeared after the next operation. Made another hack in the method of adding items. I hope it is clear what he is doing.
 if ( adapter.getItemCount() == 1 ) { adapter.notifyDataSetChanged(); } 

The article Building a RecyclerView LayoutManager - Part 1 has a solution to the seemingly whole problem, but it did not work for me. Maybe the support library version needed to be updated or the SDK. I do not know.
Overlay to get your LayoutManager to compile. It is a straightforward implementation of the Recycler. They get from getViewForPosition ()

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


As a result, we have a “hacking” approach, which is worth using ...

Although this PopUp widget can be useful for displaying messages from the program. Instead of progress windows. By timer, you can delete the top message after a certain time or by clicking on it immediately

The result was a rounded list with a shadow and animation. Correctly processed screen rotation. Easily embedded in the application. The only little thing. After reordering the stack of fragments, when the user worked with the application, the window did not always appear. Perhaps some kind of callback is not in the UI Thread ... The solution is to access the fragment through the Handler.

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


Communicating with the UI Thread
UI objects such as View objects; this thread is called the UI thread. Only objects are running. Because you aren’t running, you don’t have access to objects. To use the UI thread, it is running on the UI thread.
developer.android.com/training/multiple-threads/communicate-ui.html


Communicating with Other Fragments
For example, you will want to use it. 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 feature for the fragment:

 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // keep the fragment and all its data across screen rotation setRetainInstance(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 } @Override public 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()); } } @Override public 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() { @Override public 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); } }; @Override public void onPause() { super.onPause(); timerHandler.removeCallbacks(timerRunnable); } @Override public void onResume() { super.onResume(); timerHandler.postDelayed(timerRunnable, timer_interval); if ( adapter.getItemCount() == 0 ) { cardView.setVisibility(View.GONE); mListener.onHidePopUpFrugment(); } } @Override public void onStart() { super.onStart(); mListener.onPopUpFragmentStart(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View 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 RecyclerView if ( 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(); } } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mListener = (OnFragmentInteractionListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnFragmentInteractionListener"); } } @Override public 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 @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams( RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT); } // Not worked @Override public boolean canScrollVertically() { //We do allow scrolling return true; } private int[] mMeasuredDimension = new int[2]; @Override public 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; } @Override public 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.CardView android: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.RecyclerView android: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; } @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recyclerview_item, viewGroup, false); return new ViewHolder(v); } @Override public 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); } @Override public 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; @Override public void onClick(View v) { copy(record); } public void setRecord(Record record) { this.record = record; } } private class DeleteButtonListener implements View.OnClickListener { private Record record; @Override public void onClick(View v) { delete(record); } public void setRecord(Record record) { this.record = record; } } } 


The original code was taken here: github.com/renal128/RecyclerViewDemo

Implementing with Handler's and Timer's: github.com/app-z/PopUpWidget

See how it works from Google Play: play.google.com/store/apps/details?id=net.appz.iconfounder

Source: https://habr.com/ru/post/255397/


All Articles