目录介绍
- 1.RecycleView的结构
-
2.Adapter
- 2.1 RecyclerView.Adapter扮演的角色
- 2.2 重写的方法
- 2.3 notifyDataSetChanged()刷新数据
-
2.4 数据变更通知之观察者模式
- a.首先看.notifyDataSetChanged()源码
- b.接着查看.notifyChanged()源码
- c.接着查看setAdapter()源码中的setAdapterInternal(adapter, false, true)方法
- d.notify……方法被调用,刷新数据
-
3.ViewHolder
- 3.1 ViewHolder的作用
- 3.2 ViewHolder与复用
- 3.3 ViewHolder简单封装
-
4.LayoutManager
- 4.1 作用
- 4.2 LayoutManager样式
- 4.3 LayoutManager当前有且仅有一个抽象函数
- 4.4 setLayoutManager(LayoutManager layout)源码
-
5.ItemDecoration
- 5.1 作用
- 5.2 RecyclerView.ItemDecoration是一个抽象类
-
5.3 addItemDecoration()源码分析
- a.首先看addItemDecoration源码
- b.接着看下markItemDecorInsetsDirty这个方法
- c.接着看下mRecycler.markItemDecorInsetsDirty();这个方法
- d.回过头在看看addItemDecoration中requestLayout方法
- e.在 RecyclerView 中搜索 mItemDecorations 集合
-
6.ItemAnimator
- 6.1 作用
- 6.2 触发的三种事件
-
7.其他知识点
- 7.1 Recycler && RecycledViewPool
- 7.2 Recyclerview.getLayoutPosition()区别
-
8.RecyclerView嵌套方案滑动冲突解决方案
- 8.1 如何判断RecyclerView控件滑动到顶部和底部
- 8.2 RecyclerView嵌套RecyclerView 条目自动上滚的Bug
- 8.3 ScrollView嵌套RecyclerView滑动冲突
- 8.4 ViewPager嵌套水平RecyclerView横向滑动到底后不滑动ViewPager
-
9.RecyclerView复杂布局封装库案例
- 9.1 能够实现业务的需求和功能
- 9.2 具备的优势分析
- 10.针对阿里VLayout代码分析
-
11.版本更新说明
- v1.0.0 2016年5月5日
- v1.1.0 更新于2017年2月1日
- v1.1.1 更新于2017年6月9日
- v2.0.0 更新于2018年8月21日
- v2.1.0 更新于2018年9月29日
好消息
- 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
- 链接地址:https://github.com/yangchong2...
- 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!
1.RecycleView的结构
-
关于RecyclerView,大家都已经很熟悉了,用途十分广泛,大概结构如下所示
- RecyclerView.Adapter - 处理数据集合并负责绑定视图
- ViewHolder - 持有所有的用于绑定数据或者需要操作的View
- LayoutManager - 负责摆放视图等相关操作
- ItemDecoration - 负责绘制Item附近的分割线
- ItemAnimator - 为Item的一般操作添加动画效果,如,增删条目等
-
如图所示,直观展示结构
-
针对上面几个属性,最简单用法如下所示
recyclerView = (RecyclerView) findViewById(R.id.recyclerView); LinearLayoutManager layoutManager = new LinearLayoutManager(this); //设置layoutManager recyclerView.setLayoutManager(layoutManager); final RecycleViewItemLine line = new RecycleViewItemLine(this, LinearLayout.HORIZONTAL,1,this.getResources().getColor(R.color.colorAccent)); //设置添加分割线 recyclerView.addItemDecoration(line); adapter = new MultipleItemAdapter(this); //设置adapter recyclerView.setAdapter(adapter); //添加数据并且刷新adapter adapter.addAll(list); adapter.notifyDataSetChanged(); //adapter //onCreateViewHolder(ViewGroup parent, int viewType)这里的第二个参数就是View的类型,可以根据这个类型判断去创建不同item的ViewHolder public class MultipleItemAdapter extends RecyclerView.Adapter<recyclerview.viewholder> { public static enum ITEM_TYPE { ITEM_TYPE_IMAGE, ITEM_TYPE_TEXT } private final LayoutInflater mLayoutInflater; private final Context mContext; private ArrayList<String> mTitles; public MultipleItemAdapter(Context context) { mContext = context; mLayoutInflater = LayoutInflater.from(context); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal()) { return new ImageViewHolder(mLayoutInflater.inflate(R.layout.item_image, parent, false)); } else { return new TextViewHolder(mLayoutInflater.inflate(R.layout.item_text, parent, false)); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (holder instanceof TextViewHolder) { ((TextViewHolder) holder).mTextView.setText(mTitles[position]); } else if (holder instanceof ImageViewHolder) { ((ImageViewHolder) holder).mTextView.setText(mTitles[position]); } } @Override public int getItemViewType(int position) { return position % 2 == 0 ? ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal() : ITEM_TYPE.ITEM_TYPE_TEXT.ordinal(); } @Override public int getItemCount() { return mTitles == null ? 0 : mTitles.length; } public void addAll(ArrayList<String> list){ if(mTitles!=null){ mTitles.clear(); }else { mTitles = new ArrayList<>(); } mTitles.addAll(list); } public static class TextViewHolder extends RecyclerView.ViewHolder { @InjectView(R.id.text_view) TextView mTextView; TextViewHolder(View view) { super(view); ButterKnife.inject(this, view); } } public static class ImageViewHolder extends RecyclerView.ViewHolder { @InjectView(R.id.text_view) TextView mTextView; @InjectView(R.id.image_view) ImageView mImageView; ImageViewHolder(View view) { super(view); ButterKnife.inject(this, view); } } }
2.Adapter
2.1 RecyclerView.Adapter扮演的角色
- 一是,根据不同ViewType创建与之相应的的Item-Layout
- 二是,访问数据集合并将数据绑定到正确的View上
2.2 重写的方法
-
一般常用的重写方法有以下这么几个:
public VH onCreateViewHolder(ViewGroup parent, int viewType) 创建Item视图,并返回相应的ViewHolder public void onBindViewHolder(VH holder, int position) 绑定数据到正确的Item视图上。 public int getItemCount() 返回该Adapter所持有的Itme数量 public int getItemViewType(int position) 用来获取当前项Item(position参数)是哪种类型的布局
2.3 notifyDataSetChanged()刷新数据
-
当时据集合发生改变时,我们通过调用.notifyDataSetChanged(),来刷新列表,因为这样做会触发列表的重绘,所以并不会出现任何动画效果,因此需要调用一些以notifyItem*()作为前缀的特殊方法,比如:
- public final void notifyItemInserted(int position) 向指定位置插入Item
- public final void notifyItemRemoved(int position) 移除指定位置Item
- public final void notifyItemChanged(int position) 更新指定位置Item
2.4 数据变更通知之观察者模式
-
a.首先看.notifyDataSetChanged()源码
/** @see #notifyItemChanged(int) * @see #notifyItemInserted(int) * @see #notifyItemRemoved(int) * @see #notifyItemRangeChanged(int, int) * @see #notifyItemRangeInserted(int, int)
*/
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
```
-
b.接着查看.notifyChanged();源码
- 被观察者AdapterDataObservable,内部持有观察者AdapterDataObserver集合
static class AdapterDataObservable extends Observable<AdapterDataObserver> { public boolean hasObservers() { return !mObservers.isEmpty(); } public void notifyChanged() { for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onChanged(); } } public void notifyItemRangeChanged(int positionStart, int itemCount) { notifyItemRangeChanged(positionStart, itemCount, null); } public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) { for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload); } } public void notifyItemRangeInserted(int positionStart, int itemCount) { for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onItemRangeInserted(positionStart, itemCount); } } }
- 观察者AdapterDataObserver,具体实现为RecyclerViewDataObserver,当数据源发生变更时,及时响应界面变化
public static abstract class AdapterDataObserver { public void onChanged() { // Do nothing } public void onItemRangeChanged(int positionStart, int itemCount) { // do nothing } public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { onItemRangeChanged(positionStart, itemCount); } }
-
c.接着查看setAdapter()源码中的setAdapterInternal(adapter, false, true)方法
- setAdapter源码
public void setAdapter(Adapter adapter) { // bail out if layout is frozen setLayoutFrozen(false); setAdapterInternal(adapter, false, true); requestLayout(); }
- setAdapterInternal(adapter, false, true)源码
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) { if (mAdapter != null) { mAdapter.unregisterAdapterDataObserver(mObserver); mAdapter.onDetachedFromRecyclerView(this); } if (!compatibleWithPrevious || removeAndRecycleViews) { removeAndRecycleViews(); } mAdapterHelper.reset(); final Adapter oldAdapter = mAdapter; mAdapter = adapter; if (adapter != null) { //注册一个观察者RecyclerViewDataObserver adapter.registerAdapterDataObserver(mObserver); adapter.onAttachedToRecyclerView(this); } if (mLayout != null) { mLayout.onAdapterChanged(oldAdapter, mAdapter); } mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious); mState.mStructureChanged = true; markKnownViewsInvalid(); }
-
d.notify……方法被调用,刷新数据
- 当数据变更时,调用notify**方法时,Adapter内部的被观察者会遍历通知已经注册的观察者的对应方法,这时界面就会响应变更。
3.ViewHolder
3.1 ViewHolder的作用
-
ViewHolder作用大概有这些:
- adapter应当拥有ViewHolder的子类,并且ViewHolder内部应当存储一些子view,避免时间代价很大的findViewById操作
- 其RecyclerView内部定义的ViewHolder类包含很多复杂的属性,内部使用场景也有很多,而我们经常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一个新类型。item的ViewHolder时调用来创建一个ViewHolder,而onBindViewHolder()方法则当RecyclerView需要在特定位置的item展示数据时调用。
3.2 ViewHolder与复用
-
在复写RecyclerView.Adapter的时候,需要我们复写两个方法:
- onCreateViewHolder
- onBindViewHolder
- 这两个方法从字面上看就是创建ViewHolder和绑定ViewHolder的意思
-
复用机制是怎样的?
- 模拟场景:只有一种ViewType,上下滑动的时候需要的ViewHolder种类是只有一种,但是需要的ViewHolder对象数量并不止一个。所以在后面创建了5个ViewHolder之后,需要的数量够了,无论怎么滑动,都只需要复用以前创建的对象就行了。那么逗比程序员们思考一下,为什么会出现这种情况呢
- 看到了下面log之后,第一反应是在这个ViewHolder对象的数量“够用”之后就停止调用onCreateViewHolder方法,但是onBindViewHolder方法每次都会调用的
-
查看一下createViewHolder源代码
- 发现这里并没有限制
public final VH createViewHolder(ViewGroup parent, int viewType) { TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG); final VH holder = onCreateViewHolder(parent, viewType); holder.mItemViewType = viewType; TraceCompat.endSection(); return holder; }
-
对于ViewHolder对象的数量“够用”之后就停止调用onCreateViewHolder方法,可以查看
- 获取为给定位置初始化的视图。
- 此方法应由{@link LayoutManager}实现使用,以获取视图来表示来自{@LinkAdapter}的数据。
- 如果共享池可用于正确的视图类型,则回收程序可以重用共享池中的废视图或分离视图。如果适配器没有指示给定位置上的数据已更改,则回收程序将尝试发回一个以前为该数据初始化的报废视图,而不进行重新绑定。
public View getViewForPosition(int position) { return getViewForPosition(position, false); } View getViewForPosition(int position, boolean dryRun) { return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; } @Nullable ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) { //代码省略了,有需要的小伙伴可以自己看看,这里面逻辑实在太复杂呢 }
3.3 ViewHolder简单封装
-
关于ViewHolder简单的封装代码如下所示:
public abstract class BaseMViewHolder<M> extends RecyclerView.ViewHolder { // SparseArray 比 HashMap 更省内存,在某些条件下性能更好,只能存储 key 为 int 类型的数据, // 用来存放 View 以减少 findViewById 的次数 private SparseArray<View> viewSparseArray; BaseMViewHolder(View itemView) { super(itemView); if(viewSparseArray==null){ viewSparseArray = new SparseArray<>(); } } public BaseMViewHolder(ViewGroup parent, @LayoutRes int res) { super(LayoutInflater.from(parent.getContext()).inflate(res, parent, false)); if(viewSparseArray==null){ viewSparseArray = new SparseArray<>(); } } /** * 子类设置数据方法
*/
public void setData(M data) {}
/**
* 第二种findViewById方式
* 根据 ID 来获取 View
* @param viewId viewID
* @param <T> 泛型
* @return 将结果强转为 View 或 View 的子类型
*/
@SuppressWarnings("unchecked")
protected <T extends View> T getView(int viewId) {
// 先从缓存中找,找打的话则直接返回
// 如果找不到则 findViewById ,再把结果存入缓存中
View view = viewSparseArray.get(viewId);
if (view == null) {
view = itemView.findViewById(viewId);
viewSparseArray.put(viewId, view);
}
return (T) view;
}
/**
* 获取上下文context
* @return context
*/
protected Context getContext(){
return itemView.getContext();
}
/**
* 获取数据索引的位置
* @return position
*/
protected int getDataPosition(){
RecyclerView.Adapter adapter = getOwnerAdapter();
if (adapter!=null && adapter instanceof RecyclerArrayAdapter){
return getAdapterPosition() - ((RecyclerArrayAdapter) adapter).getHeaderCount();
}
return getAdapterPosition();
}
/**
* 获取adapter对象
* @param <T>
* @return adapter
*/
@Nullable
private <T extends RecyclerView.Adapter> T getOwnerAdapter(){
RecyclerView recyclerView = getOwnerRecyclerView();
//noinspection unchecked
return recyclerView != null ? (T) recyclerView.getAdapter() : null;
}
@Nullable
private RecyclerView getOwnerRecyclerView(){
try {
Field field = RecyclerView.ViewHolder.class.getDeclaredField("mOwnerRecyclerView");
field.setAccessible(true);
return (RecyclerView) field.get(this);
} catch (NoSuchFieldException ignored) {
ignored.printStackTrace();
} catch (IllegalAccessException ignored) {
ignored.printStackTrace();
}
return null;
}
/**
* 添加子控件的点击事件
* @param viewId 控件id
*/
protected void addOnClickListener(@IdRes final int viewId) {
final View view = getView(viewId);
if (view != null) {
if (!view.isClickable()) {
view.setClickable(true);
}
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(getOwnerAdapter()!=null){
if (((RecyclerArrayAdapter)getOwnerAdapter()).getOnItemChildClickListener() != null) {
((RecyclerArrayAdapter)getOwnerAdapter()).getOnItemChildClickListener()
.onItemChildClick(v, getDataPosition());
}
}
}
});
}
}
//省略部分代码
//关于adapter封装可以查看我的开源adpater封装库:https://github.com/yangchong211/YCBaseAdapter
//关于recyclerView封装库,可以查看我的开源库:https://github.com/yangchong211/YCRefreshView
}
```
4.LayoutManager
4.1 作用
- LayoutManager的职责是摆放Item的位置,并且负责决定何时回收和重用Item。
- RecyclerView 允许自定义规则去放置子 view,这个规则的控制者就是 LayoutManager。一个 RecyclerView 如果想展示内容,就必须设置一个 LayoutManager
4.2 LayoutManager样式
- LinearLayoutManager 水平或者垂直的Item视图。
- GridLayoutManager 网格Item视图。
- StaggeredGridLayoutManager 交错的网格Item视图。
4.3 LayoutManager当前有且仅有一个抽象函数
-
具体如下:
public LayoutParams generateDefaultLayoutParams()
4.4 setLayoutManager(LayoutManager layout)源码
-
a.setLayoutManager入口源码
- 分析:当之前设置过 LayoutManager 时,移除之前的视图,并缓存视图在 Recycler 中,将新的 mLayout 对象与 RecyclerView 绑定,更新缓存 View 的数量。最后去调用 requestLayout ,重新请求 measure、layout、draw。
public void setLayoutManager(LayoutManager layout) { if (layout == mLayout) { return; } // 停止滑动 stopScroll(); if (mLayout != null) { // 如果有动画,则停止所有的动画 if (mItemAnimator != null) { mItemAnimator.endAnimations(); } // 移除并回收视图 mLayout.removeAndRecycleAllViews(mRecycler); // 回收废弃视图 mLayout.removeAndRecycleScrapInt(mRecycler); //清除mRecycler mRecycler.clear(); if (mIsAttached) { mLayout.dispatchDetachedFromWindow(this, mRecycler); } mLayout.setRecyclerView(null); mLayout = null; } else { mRecycler.clear(); } mChildHelper.removeAllViewsUnfiltered(); mLayout = layout; if (layout != null) { if (layout.mRecyclerView != null) { throw new IllegalArgumentException("LayoutManager " + layout + " is already attached to a RecyclerView: " + layout.mRecyclerView); } mLayout.setRecyclerView(this); if (mIsAttached) { mLayout.dispatchAttachedToWindow(this); } } //更新新的缓存数据 mRecycler.updateViewCacheSize(); //重新请求 View 的测量、布局、绘制 requestLayout(); }
5.ItemDecoration
5.1 作用
- 通过设置recyclerView.addItemDecoration(new DividerDecoration(this));来改变Item之间的偏移量或者对Item进行装饰。
- 当然,你也可以对RecyclerView设置多个ItemDecoration,列表展示的时候会遍历所有的ItemDecoration并调用里面的绘制方法,对Item进行装饰。
5.2 RecyclerView.ItemDecoration是一个抽象类
-
该抽象类常见的方法如下所示:
public void onDraw(Canvas c, RecyclerView parent) 装饰的绘制在Item条目绘制之前调用,所以这有可能被Item的内容所遮挡 public void onDrawOver(Canvas c, RecyclerView parent) 装饰的绘制在Item条目绘制之后调用,因此装饰将浮于Item之上 public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) 与padding或margin类似,LayoutManager在测量阶段会调用该方法,计算出每一个Item的正确尺寸并设置偏移量。
5.3 .addItemDecoration()源码分析
-
a.通过下面代码可知,mItemDecorations是一个ArrayList,我们将ItemDecoration也就是分割线对象,添加到其中。
- 可以看到,当通过这个方法添加分割线后,会指定添加分割线在集合中的索引,然后再重新请求 View 的测量、布局、(绘制)。注意: requestLayout会调用onMeasure和onLayout,不一定调用onDraw!
- 关于View自定义控件源码分析,可以参考我的其他博客:https://github.com/yangchong2...
public void addItemDecoration(ItemDecoration decor) { addItemDecoration(decor, -1); } //主要看这个方法,我的GitHub:https://github.com/yangchong211/YCBlogs public void addItemDecoration(ItemDecoration decor, int index) { if (mLayout != null) { mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or" + " layout"); } if (mItemDecorations.isEmpty()) { setWillNotDraw(false); } if (index < 0) { mItemDecorations.add(decor); } else { // 指定添加分割线在集合中的索引 mItemDecorations.add(index, decor); } markItemDecorInsetsDirty(); // 重新请求 View 的测量、布局、绘制 requestLayout(); }
-
b.接着看下markItemDecorInsetsDirty这个方法做了些什么
- 这个方法先获取所有子View的数量,然后遍历了 RecyclerView 和 LayoutManager 的所有子 View,再将其子 View 的 LayoutParams 中的 mInsetsDirty 属性置为 true,最后调用了 mRecycler.markItemDecorInsetsDirty()方法处理复用逻辑。
void markItemDecorInsetsDirty() { final int childCount = mChildHelper.getUnfilteredChildCount(); //先遍历了 RecyclerView 和 LayoutManager 的所有子 View for (int i = 0; i < childCount; i++) { final View child = mChildHelper.getUnfilteredChildAt(i); //将其子 View 的 LayoutParams 中的 mInsetsDirty 属性置为 true ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; } //调用了 mRecycler.markItemDecorInsetsDirty(), //Recycler 是 RecyclerView 的一个内部类,就是它管理着 RecyclerView 的复用逻辑 mRecycler.markItemDecorInsetsDirty(); }
-
c.接着看下markItemDecorInsetsDirty()这个方法
- 该方法就是获取RecyclerView 缓存的集合,然后遍历集合得到RecyclerView 的缓存单位是 ViewHolder,获取缓存对象,在获取到layoutParams,并且将其 mInsetsDirty 字段一样置为 true
void markItemDecorInsetsDirty() { //就是 RecyclerView 缓存的集合 final int cachedCount = mCachedViews.size(); for (int i = 0; i < cachedCount; i++) { //RecyclerView 的缓存单位是 ViewHolder,获取缓存对象 final ViewHolder holder = mCachedViews.get(i); //获得 LayoutParams LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams(); if (layoutParams != null) { //将其 mInsetsDirty 字段一样置为 true layoutParams.mInsetsDirty = true; } } }
-
d.回过头在看看addItemDecoration中requestLayout方法
- requestLayout 方法用一种责任链的方式,层层向上传递,最后传递到 ViewRootImpl,然后重新调用 view 的 measure、layout、draw 方法来展示布局
@CallSuper public void requestLayout() { if (mMeasureCache != null) mMeasureCache.clear(); if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) { // Only trigger request-during-layout logic if this is the view requesting it, // not the views in its parent hierarchy ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null && viewRoot.isInLayout()) { if (!viewRoot.requestLayoutDuringLayout(this)) { return; } } mAttachInfo.mViewRequestingLayout = this; } mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); } if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) { mAttachInfo.mViewRequestingLayout = null; } }
-
e.在 RecyclerView 中搜索 mItemDecorations 集合
- 在onDraw中
@Override public void onDraw(Canvas c) { super.onDraw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } }
- 在draw方法中
@Override public void draw(Canvas c) { super.draw(c); final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDrawOver(c, this, mState); } //省略部分代码 }
-
总结概括
- 可以看到在 View 的以上两个方法中,分别调用了 ItemDecoration 对象的 onDraw onDrawOver 方法。
- 这两个抽象方法,由我们继承 ItemDecoration 来自己实现,他们区别就是 onDraw 在 item view 绘制之前调用,onDrawOver 在 item view 绘制之后调用。
- 所以绘制顺序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。
6.ItemAnimator
6.1 作用
- ItemAnimator能够帮助Item实现独立的动画
6.2 触发的三种事件
- 某条数据被插入到数据集合中
- 从数据集合中移除某条数据
- 更改数据集合中的某条数据
7.其他知识点
7.1 Recycler && RecycledViewPool
-
RecycledViewPool
- RecyclerViewPool用于多个RecyclerView之间共享View。只需要创建一个RecyclerViewPool实例,然后调用RecyclerView的setRecycledViewPool(RecycledViewPool)方法即可。RecyclerView默认会创建一个RecyclerViewPool实例。
- 下列源码,是我借助于有道词典翻译部分注释内容……
- 看出mScrap是一个<viewType, List>的映射,mMaxScrap是一个<viewType, maxNum>的映射,这两个成员变量代表可复用View池的基本信息。调用setMaxRecycledViews(int viewType, int max)时,当用于复用的mScrap中viewType对应的ViewHolder个数超过maxNum时,会从列表末尾开始丢弃超过的部分。调用getRecycledView(int viewType)方法时从mScrap中移除并返回viewType对应的List的末尾项
public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; static class ScrapData { final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); int mMaxScrap = DEFAULT_MAX_SCRAP; long mCreateRunningAverageNs = 0; long mBindRunningAverageNs = 0; } SparseArray<ScrapData> mScrap = new SparseArray<>(); private int mAttachCount = 0; //丢弃所有视图 public void clear() { for (int i = 0; i < mScrap.size(); i++) { ScrapData data = mScrap.valueAt(i); data.mScrapHeap.clear(); } } //设置丢弃前要在池中持有的视图持有人的最大数量 public void setMaxRecycledViews(int viewType, int max) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mMaxScrap = max; final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; while (scrapHeap.size() > max) { scrapHeap.remove(scrapHeap.size() - 1); } } //返回给定视图类型的RecycledViewPool所持有的当前视图数 public int getRecycledViewCount(int viewType) { return getScrapDataForType(viewType).mScrapHeap.size(); } //从池中获取指定类型的ViewHolder,如果没有指定类型的ViewHolder,则获取{@Codenull} @Nullable public ViewHolder getRecycledView(int viewType) { final ScrapData scrapData = mScrap.get(viewType); if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; return scrapHeap.remove(scrapHeap.size() - 1); } return null; } //池持有的视图持有者总数 int size() { int count = 0; for (int i = 0; i < mScrap.size(); i++) { ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i).mScrapHeap; if (viewHolders != null) { count += viewHolders.size(); } } return count; } //向池中添加一个废视图保存器。 //如果那个ViewHolder类型的池已经满了,它将立即被丢弃。 public void putRecycledView(ViewHolder scrap) { final int viewType = scrap.getItemViewType(); final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap; if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { return; } if (DEBUG && scrapHeap.contains(scrap)) { throw new IllegalArgumentException("this scrap item already exists"); } scrap.resetInternal(); scrapHeap.add(scrap); } long runningAverage(long oldAverage, long newValue) { if (oldAverage == 0) { return newValue; } return (oldAverage / 4 * 3) + (newValue / 4); } void factorInCreateTime(int viewType, long createTimeNs) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mCreateRunningAverageNs = runningAverage( scrapData.mCreateRunningAverageNs, createTimeNs); } void factorInBindTime(int viewType, long bindTimeNs) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mBindRunningAverageNs = runningAverage( scrapData.mBindRunningAverageNs, bindTimeNs); } boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) { long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs; return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); } boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) { long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs; return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); } void attach(Adapter adapter) { mAttachCount++; } void detach() { mAttachCount--; } //分离旧适配器并附加新适配器。如果它只附加了一个适配器,并且新适配器使用与oldAdapter不同的ViewHolder, //则RecycledViewPool将清除其缓存。 void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,boolean compatibleWithPrevious) { if (oldAdapter != null) { detach(); } if (!compatibleWithPrevious && mAttachCount == 0) { clear(); } if (newAdapter != null) { attach(newAdapter); } } private ScrapData getScrapDataForType(int viewType) { ScrapData scrapData = mScrap.get(viewType); if (scrapData == null) { scrapData = new ScrapData(); mScrap.put(viewType, scrapData); } return scrapData; } }
-
ViewCacheExtension
- ViewCacheExtension是一个由开发者控制的可以作为View缓存的帮助类。调用Recycler.getViewForPosition(int)方法获取View时,Recycler先检查attachedscrap和一级缓存,如果没有则检查ViewCacheExtension.getViewForPositionAndType(Recycler, int, int),如果没有则检查RecyclerViewPool。注意:Recycler不会在这个类中做缓存View的操作,是否缓存View完全由开发者控制。
public abstract static class ViewCacheExtension { abstract public View getViewForPositionAndType(Recycler recycler, int position, int type); }
-
Recycler
- 后续再深入分析
7.2 Recyclerview.getLayoutPosition()问题
-
在RecycleView中的相关方法中,有两种类型的位置
-
布局位置:从LayoutManager的角度看,条目在最新布局计算中的位置。
- 返回布局位置的方法使用最近一次布局运算后的位置,如getLayoutPosition()和findViewHolderForLayoutPosition(int)。这些位置包含了最近一次布局运算后的变化。你可以根据这些位置来与用户正在屏幕上看到的保持一致。比如,你有一个条目列表,当用户请求第5个条目时,你可以使用这些方法来匹配用户看到的。
-
适配器位置:从适配器的角度看,条目在是适配器中的位置。
- 另外一系列方法与AdapterPosition关联,比如getAdapterPosition()和findViewHolderForAdapterPosition(int)。当你想获得条目在更新后的适配器中的位置使用这些方法,即使这些位置变化还没反映到布局中。比如,你想访问适配器中条目的位置时,就应该使用getAdapterPosition()。注意,notifyDataSetChanged()已经被调用而且还没计算新布局,这些方法或许不能够计算适配器位置。所以,你要小心处理这些方法返回NO_POSITION和null的情况。
- 注意: 这两种类型的位置是等同的,除非在分发adapter.notify*事件和更新布局时。
-
-
关于两者的区别
- 网上查了一些资料,发现相关内容很少,最后在stackoverflow上终于看到有大神这样解释两者的区别
-
具体区别就是adapter和layout的位置会有时间差(<16ms), 如果你改变了Adapter的数据然后刷新视图, layout需要过一段时间才会更新视图, 在这段时间里面, 这两个方法返回的position会不一样。
- 在notifyDataSetChanged之后并不能马上获取Adapter中的position, 要等布局结束之后才能获取到
- 在notifyItemInserted之后,Layout不能马上获取到新的position,因为布局还没更新(需要<16ms的时间刷新视图), 所以只能获取到旧的,但是Adapter中的position就可以马上获取到最新的position。
public final int getAdapterPosition() {
if (mOwnerRecyclerView == null) {
return NO_POSITION;
}
return mOwnerRecyclerView.getAdapterPositionFor(this);
}
public final int getLayoutPosition() {
return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}
-
可能会导致的错误
- 这种情况有点难以复现,在 ViewHolder 中处理 item 的点击事件的时候,发现多个 item 同时点击就会出现闪退,debug 看到 position = -1
- 解决办法:使用 ViewHolder#getLayoutPosition() 获取 position,而不要通过 ViewHolder#getAdapterPosition() 来获取 position 的
8.RecyclerView嵌套方案滑动冲突解决方案
8.1 如何判断RecyclerView控件滑动到顶部和底部
-
有一种使用场景,购物商城的购物车页面,当RecyclerView滑动到顶部时,让刷新控件消费事件;当RecyclerView滑动到底部时,让下一页控件[猜你喜欢]消费事件。
- 代码如下所示:
public class VerticalRecyclerView extends RecyclerView { private float downX; private float downY; /** 第一个可见的item的位置 */ private int firstVisibleItemPosition; /** 第一个的位置 */ private int[] firstPositions; /** 最后一个可见的item的位置 */ private int lastVisibleItemPosition; /** 最后一个的位置 */ private int[] lastPositions; private boolean isTop; private boolean isBottom; public VerticalRecyclerView(Context context) { this(context, null); } public VerticalRecyclerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public VerticalRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { LayoutManager layoutManager = getLayoutManager(); if (layoutManager != null) { if (layoutManager instanceof GridLayoutManager) { lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); firstVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition(); } else if (layoutManager instanceof LinearLayoutManager) { lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; if (lastPositions == null) { lastPositions = new int[staggeredGridLayoutManager.getSpanCount()]; firstPositions = new int[staggeredGridLayoutManager.getSpanCount()]; } staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions); staggeredGridLayoutManager.findFirstVisibleItemPositions(firstPositions); lastVisibleItemPosition = findMax(lastPositions); firstVisibleItemPosition = findMin(firstPositions); } } else { throw new RuntimeException("Unsupported LayoutManager used. Valid ones are LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager"); } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: downX = ev.getX(); downY = ev.getY(); //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许 getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: float dx = ev.getX() - downX; float dy = ev.getY() - downY; boolean allowParentTouchEvent; if (Math.abs(dy) > Math.abs(dx)) { if (dy > 0) { //位于顶部时下拉,让父View消费事件 allowParentTouchEvent = isTop = firstVisibleItemPosition == 0 && getChildAt(0).getTop() >= 0; } else { //位于底部时上拉,让父View消费事件 int visibleItemCount = layoutManager.getChildCount(); int totalItemCount = layoutManager.getItemCount(); allowParentTouchEvent = isBottom = visibleItemCount > 0 && (lastVisibleItemPosition) >= totalItemCount - 1 && getChildAt(getChildCount() - 1).getBottom() <= getHeight(); } } else { //水平方向滑动 allowParentTouchEvent = true; } getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent); } return super.dispatchTouchEvent(ev); } private int findMax(int[] lastPositions) { int max = lastPositions[0]; for (int value : lastPositions) { if (value >= max) { max = value; } } return max; } private int findMin(int[] firstPositions) { int min = firstPositions[0]; for (int value : firstPositions) { if (value < min) { min = value; } } return min; } public boolean isTop() { return isTop; } public boolean isBottom() { return isBottom; } }
8.2 RecyclerView嵌套RecyclerView条目自动上滚的Bug
-
RecyclerViewA嵌套RecyclerViewB 进入页面自动跳转到RecyclerViewB上面页面会自动滚动。
- 两种解决办法
-
一,recyclerview去除焦点
- recyclerview.setFocusableInTouchMode(false);
- recyclerview.requestFocus();
-
二,在代码里面 让处于ScrollView或者RecyclerView1 顶端的某个控件获得焦点即可
- 比如顶部的一个textview
- tv.setFocusableInTouchMode(true);
- tv.requestFocus();
8.3 ScrollView嵌套RecyclerView滑动冲突
-
第一种方式:
- 重写父控件,让父控件 ScrollView 直接拦截滑动事件,不向下分发给 RecyclerView,具体是定义一个ScrollView子类,重写其 onInterceptTouchEvent()方法
public class NoNestedScrollview extends NestedScrollView { private int downX; private int downY; private int mTouchSlop; public NoNestedScrollview(Context context) { super(context); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } public NoNestedScrollview(Context context, AttributeSet attrs) { super(context, attrs); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } public NoNestedScrollview(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } @Override public boolean onInterceptTouchEvent(MotionEvent e) { int action = e.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: downX = (int) e.getRawX(); downY = (int) e.getRawY(); break; case MotionEvent.ACTION_MOVE: //判断是否滑动,若滑动就拦截事件 int moveY = (int) e.getRawY(); if (Math.abs(moveY - downY) > mTouchSlop) { return true; } break; default: break; } return super.onInterceptTouchEvent(e); } }
-
第二种解决方式
- a.禁止RecyclerView滑动
recyclerView.setLayoutManager(new GridLayoutManager(mContext,2){ @Override public boolean canScrollVertically() { return false; } @Override public boolean canScrollHorizontally() { return super.canScrollHorizontally(); } }); recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayout.VERTICAL,false){ @Override public boolean canScrollVertically() { return false; } });
-
b.重写LayoutManager
- 代码设置LayoutManager.setScrollEnabled(false);
public class ScrollLayoutManager extends LinearLayoutManager {
private boolean isScrollEnable = true;
public ScrollLayoutManager(Context context) {
super(context);
}
public ScrollLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public ScrollLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean canScrollVertically() {
return isScrollEnable && super.canScrollVertically();
}
/**
* 设置 RecyclerView 是否可以垂直滑动
* @param isEnable
*/
public void setScrollEnable(boolean isEnable) {
this.isScrollEnable = isEnable;
}
}
-
可能会出现的问题
- 虽然上面两种方式解决了滑动冲突,但是有的手机上出现了RecyclerView会出现显示不全的情况。
-
针对这种情形,使用网上的方法一种是使用 RelativeLayout 包裹 RecyclerView 并设置属性:android:descendantFocusability="blocksDescendants"
- android:descendantFocusability="blocksDescendants",该属>性是当一个view 获取焦点时,定义 ViewGroup 和其子控件直接的关系,常用来>解决父控件的焦点或者点击事件被子空间获取。
- beforeDescendants: ViewGroup会优先其子控件获取焦点
- afterDescendants: ViewGroup只有当其子控件不需要获取焦点时才获取焦点
- blocksDescendants: ViewGroup会覆盖子类控件而直接获得焦点
- 相关代码案例:https://github.com/yangchong2...
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:descendantFocusability="blocksDescendants"> <android.support.v7.widget.RecyclerView android:id="@+id/rv_hot_review" android:layout_width="match_parent" android:layout_height="wrap_content" android:foregroundGravity="center" /> </RelativeLayout>
8.4 viewPager嵌套水平RecyclerView横向滑动到底后不滑动ViewPager
-
继承RecyclerView,重写dispatchTouchEvent,根据ACTION_MOVE的方向判断是否调用getParent().requestDisallowInterceptTouchEvent去阻止父view拦截点击事件
@Override public boolean dispatchTouchEvent(MotionEvent ev) { /*---解决垂ViewPager嵌套直RecyclerView嵌套水平RecyclerView横向滑动到底后不滑动ViewPager start ---*/ ViewParent parent = this; while(!((parent = parent.getParent()) instanceof ViewPager)); // 循环查找viewPager parent.requestDisallowInterceptTouchEvent(true); return super.dispatchTouchEvent(ev); }
9.RecyclerView复杂布局封装库案例
- 开源项目库的地址:https://github.com/yangchong2...
9.1 能够实现业务的需求和功能
- 1.1 支持上拉加载,下拉刷新,可以自定义foot底部布局,支持添加多个自定义header头部布局。
- 1.2 支持切换不同的状态,比如加载中[目前是ProgressBar,加载成功,加载失败,加载错误等不同布局状态。当然也可以自定义这些状态的布局
- 1.3 支持复杂界面使用,比如有的页面包含有轮播图,按钮组合,横向滑动,还有复杂list,那么用这个控件就可以搞定。
- 1.4 已经用于实际开发项目投资界,新芽,沙丘大学中……
- 1.5 轻量级侧滑删除菜单,直接嵌套item布局即可使用,使用十分简单。
- 1.6 支持插入或者删除某条数据,支持CoordinatorLayout炫酷的效果
- 1.7 支持粘贴头部的需求效果
- 1.8 RecyclerView实现条目Item拖拽排序与滑动删除
9.2 具备的优势分析
- 自定义支持上拉加载更多,下拉刷新,支持自由切换状态【加载中,加载成功,加载失败,没网络等状态】的控件,拓展功能[支持长按拖拽,侧滑删除]可以选择性添加 。具体使用方法,可以直接参考demo。
- 轻量级侧滑删除菜单,支持recyclerView,listView,直接嵌套item布局即可使用,整个侧滑菜单思路是:跟随手势将item向左滑动
10.针对阿里VLayout代码分析
- 关于Vlayout的使用和相关介绍的博客有许多。具体可以看这篇博客:https://blog.csdn.net/m0_3770...
- 关于使用Vlayout实现复杂页面的案例有:https://github.com/yangchong2...,https://github.com/yangchong211/YCVideoPlayer
- 实现的复杂界面效果展示:
11.版本更新说明
- v1.0.0 2016年5月5日
- v1.1.0 更新于2017年2月1日
- v1.1.1 更新于2017年6月9日
- v2.0.0 更新于2018年9月26日
关于其他内容介绍
01.关于博客汇总链接
02.关于我的博客
- 我的个人站点:www.yczbj.org,www.ycbjie.cn
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/...
- 简书:http://www.jianshu.com/u/b7b2...
- csdn:http://my.csdn.net/m0_37700275
- 喜马拉雅听书:http://www.ximalaya.com/zhubo...
- 开源中国:https://my.oschina.net/zbj161...
- 泡在网上的日子:http://www.jcodecraeer.com/me...
- 邮箱:yangchong211@163.com
- 阿里云博客:https://yq.aliyun.com/users/a... 239.headeruserinfo.3.dT4bcV
- segmentfault头条:https://segmentfault.com/u/xi...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。