RecyclerView使用技巧(item动画及嵌套高度适配解决方案)
原文地址 · Frank-Zhu http://frank-zhu.github.io/android/2015/02/26/android-recyclerview-part-3/?utm_source=tuicool&utm_medium=referral
在上一篇(RecyclerView使用详解(二))文章中介绍了RecyclerView的多Item布局实现,接下来要来讲讲RecyclerView的Cursor实现,相较于之前的实现,Cursor有更多的使用场景,也更加的常用,特别是配合LoaderManager和CursorLoader进行数据的缓存及加载显示,基于此我们来重点看看RecyclerView的CursorAdapter具体要怎么实现。
一、CursorAdapter实现(配合LoaderManager和CursorLoader)
如果之前你用过ListView实现过此功能(CursorAdapter),那么你一定对下面这两个方法并不陌生
1 @Override 2 public View newView(Context context, Cursor cursor, ViewGroup parent) { 3 return null; 4 } 5 6 @Override 7 public void bindView(View view, Context context, Cursor cursor) { 8 9 }
其中newView方法是用来创建Item布局的,bindView 方法是用来绑定当前View数据的,就相当于之前的getView方法拆成了两个方法实现。
如果你用RecyclerView,你会发现CursorAdapter这个类没有了,既然没有了,那我们就自己仿照着ListView的CursorAdapter类来实现,具体的代码没什么大的出入,无非就是注册两个观察者去监听数据库数据的变化,但是有两个地方需要注意一下,一个就是hasStableIds() 这个方法RecyclerView.Adapter中不能复写父类的方法,需要在初始化的时候调用setHasStableIds(true); 来完成相同功能,第二个就是notifyDataSetInvalidated() 这个方法没有,统一修改成notifyDataSetChanged() 方法即可。
1 void init(Context context, Cursor c, int flags) { 2 boolean cursorPresent = c != null; 3 mCursor = c; 4 mDataValid = cursorPresent; 5 mContext = context; 6 mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; 7 if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { 8 mChangeObserver = new ChangeObserver(); 9 mDataSetObserver = new MyDataSetObserver(); 10 } else { 11 mChangeObserver = null; 12 mDataSetObserver = null; 13 } 14 15 if (cursorPresent) { 16 if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); 17 if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver); 18 } 19 20 setHasStableIds(true);//这个地方要注意一下,需要将表关联ID设置为true 21 } 22 23 //************// 24 public Cursor swapCursor(Cursor newCursor) { 25 if (newCursor == mCursor) { 26 return null; 27 } 28 Cursor oldCursor = mCursor; 29 if (oldCursor != null) { 30 if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver); 31 if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver); 32 } 33 mCursor = newCursor; 34 if (newCursor != null) { 35 if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver); 36 if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver); 37 mRowIDColumn = newCursor.getColumnIndexOrThrow("_id"); 38 mDataValid = true; 39 // notify the observers about the new cursor 40 notifyDataSetChanged(); 41 } else { 42 mRowIDColumn = -1; 43 mDataValid = false; 44 // notify the observers about the lack of a data set 45 //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter 46 notifyDataSetChanged();//注意此处 47 } 48 return oldCursor; 49 } 50 //************// 51 private class MyDataSetObserver extends DataSetObserver { 52 @Override 53 public void onChanged() { 54 mDataValid = true; 55 notifyDataSetChanged(); 56 } 57 58 @Override 59 public void onInvalidated() { 60 mDataValid = false; 61 //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter 62 notifyDataSetChanged();//注意此处 63 } 64 }
怎么样,是不是很简单,没错,就是这么简单,这里是完整的BaseAbstractRecycleCursorAdapter代码,用法和ListView的CursorAdapter用法一致,具体的可以看看我的Recyclerview LoaderManager Provider
二、Item的动画实现 RecyclerView本身就已经实现了ITEM的动画,只需要调用以下几个函数来增删Item即可出现默认动画。
1 notifyItemChanged(int) 2 notifyItemInserted(int) 3 notifyItemRemoved(int) 4 notifyItemRangeChanged(int, int) 5 notifyItemRangeInserted(int, int) 6 notifyItemRangeRemoved(int, int)
怎么样,是不是很轻松,如果你不满足系统默认动画,那么你可以自定义实现RecyclerView.ItemAnimator的接口方法,实现代码可以参考DefaultItemAnimator.当然,如果你不想自己实现,那么也没关系,这里有人已经写了开源库,你可以去看看recyclerview-animators,这里给出默认动画实现方式代码AnimFragment
三、嵌套RecycleView
一般是不推荐使用嵌套RecycleView的,和ListView是类似的,遇到这种需要嵌套的View一般都是想别的办法来规避,比如动态AddView,或者通过RecycleView的MultipleItemAdapter来实现,通过设置不同的ItemType布局不同的View,但是有时候会闲麻烦,想直接就用嵌套的方式来做,那么和ListView实现方式不同的是,ListView的实现一般都是继承ListView然后复写onMeasure方法,如下所示:
1 @Override 2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 3 int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); 4 super.onMeasure(widthMeasureSpec, expandSpec); 5 }
但是需要特别注意的一点,RecycleView的实现方式不再是继承RecycleView来做,而是通过修改LayoutManager的方式,即通过继承LinearLayoutManager GridLayoutManagerStaggeredGridLayoutManager来修改子控件的测量,下面给出主要代码:
1 private int[] mMeasuredDimension = new int[2]; 2 3 @Override 4 public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, 5 int widthSpec, int heightSpec) { 6 7 final int widthMode = View.MeasureSpec.getMode(widthSpec); 8 final int heightMode = View.MeasureSpec.getMode(heightSpec); 9 final int widthSize = View.MeasureSpec.getSize(widthSpec); 10 final int heightSize = View.MeasureSpec.getSize(heightSpec); 11 12 Log.i(TAG, "onMeasure called. \nwidthMode " + widthMode 13 + " \nheightMode " + heightSpec 14 + " \nwidthSize " + widthSize 15 + " \nheightSize " + heightSize 16 + " \ngetItemCount() " + getItemCount()); 17 18 int width = 0; 19 int height = 0; 20 for (int i = 0; i < getItemCount(); i++) { 21 measureScrapChild(recycler, i, 22 View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), 23 View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), 24 mMeasuredDimension); 25 26 if (getOrientation() == HORIZONTAL) { 27 width = width + mMeasuredDimension[0]; 28 if (i == 0) { 29 height = mMeasuredDimension[1]; 30 } 31 } else { 32 height = height + mMeasuredDimension[1]; 33 if (i == 0) { 34 width = mMeasuredDimension[0]; 35 } 36 } 37 } 38 switch (widthMode) { 39 case View.MeasureSpec.EXACTLY: 40 width = widthSize; 41 case View.MeasureSpec.AT_MOST: 42 case View.MeasureSpec.UNSPECIFIED: 43 } 44 45 switch (heightMode) { 46 case View.MeasureSpec.EXACTLY: 47 height = heightSize; 48 case View.MeasureSpec.AT_MOST: 49 case View.MeasureSpec.UNSPECIFIED: 50 } 51 52 setMeasuredDimension(width, height); 53 } 54 55 private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec, 56 int heightSpec, int[] measuredDimension) { 57 try { 58 View view = recycler.getViewForPosition(0);//fix 动态添加时报IndexOutOfBoundsException 59 60 if (view != null) { 61 RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams(); 62 63 int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, 64 getPaddingLeft() + getPaddingRight(), p.width); 65 66 int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, 67 getPaddingTop() + getPaddingBottom(), p.height); 68 69 view.measure(childWidthSpec, childHeightSpec); 70 measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin; 71 measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin; 72 recycler.recycleView(view); 73 } 74 } catch (Exception e) { 75 e.printStackTrace(); 76 } finally { 77 } 78 }
1 private int[] mMeasuredDimension = new int[2]; 2 3 @Override 4 public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) { 5 final int widthMode = View.MeasureSpec.getMode(widthSpec); 6 final int heightMode = View.MeasureSpec.getMode(heightSpec); 7 final int widthSize = View.MeasureSpec.getSize(widthSpec); 8 final int heightSize = View.MeasureSpec.getSize(heightSpec); 9 10 int width = 0; 11 int height = 0; 12 int count = getItemCount(); 13 int span = getSpanCount(); 14 for (int i = 0; i < count; i++) { 15 measureScrapChild(recycler, i, 16 View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), 17 View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED), 18 mMeasuredDimension); 19 20 if (getOrientation() == HORIZONTAL) { 21 if (i % span == 0) { 22 width = width + mMeasuredDimension[0]; 23 } 24 if (i == 0) { 25 height = mMeasuredDimension[1]; 26 } 27 } else { 28 if (i % span == 0) { 29 height = height + mMeasuredDimension[1]; 30 } 31 if (i == 0) { 32 width = mMeasuredDimension[0]; 33 } 34 } 35 } 36 37 switch (widthMode) { 38 case View.MeasureSpec.EXACTLY: 39 width = widthSize; 40 case View.MeasureSpec.AT_MOST: 41 case View.MeasureSpec.UNSPECIFIED: 42 } 43 44 switch (heightMode) { 45 case View.MeasureSpec.EXACTLY: 46 height = heightSize; 47 case View.MeasureSpec.AT_MOST: 48 case View.MeasureSpec.UNSPECIFIED: 49 } 50 51 setMeasuredDimension(width, height); 52 } 53 54 private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec, 55 int heightSpec, int[] measuredDimension) { 56 if (position < getItemCount()) { 57 try { 58 View view = recycler.getViewForPosition(0);//fix 动态添加时报IndexOutOfBoundsException 59 if (view != null) { 60 RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams(); 61 int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, 62 getPaddingLeft() + getPaddingRight(), p.width); 63 int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, 64 getPaddingTop() + getPaddingBottom(), p.height); 65 view.measure(childWidthSpec, childHeightSpec); 66 measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin; 67 measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin; 68 recycler.recycleView(view); 69 } 70 } catch (Exception e) { 71 e.printStackTrace(); 72 } 73 } 74 }
##四、效果图如下:
Item默认动画效果
嵌套ScrollView效果