资讯专栏INFORMATION COLUMN

RecyclerView用法和源码深度解析

ShowerSun / 1403人阅读

摘要:此方法应由实现使用,以获取视图来表示来自的数据。如果适配器没有指示给定位置上的数据已更改,则回收程序将尝试发回一个以前为该数据初始化的报废视图,而不进行重新绑定。如果它只附加了一个适配器,并且新适配器使用与不同的,则将清除其缓存。

目录介绍

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 {
    public static enum ITEM_TYPE {
        ITEM_TYPE_IMAGE,
        ITEM_TYPE_TEXT
    }
     
    private final LayoutInflater mLayoutInflater;
    private final Context mContext;
    private ArrayList 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 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 {
    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 extends RecyclerView.ViewHolder {


    // SparseArray 比 HashMap 更省内存,在某些条件下性能更好,只能存储 key 为 int 类型的数据,
    // 用来存放 View 以减少 findViewById 的次数

    private SparseArray 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     泛型
     * @return 将结果强转为 View 或 View 的子类型
     */
    @SuppressWarnings("unchecked")
    protected  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 
     * @return                  adapter
     */
    @Nullable
    private   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是一个的映射,mMaxScrap是一个的映射,这两个成员变量代表可复用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 mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    SparseArray 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 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 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 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 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...


    

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.关于博客汇总链接

1.技术博客汇总

2.开源项目汇总

3.生活博客汇总

4.喜马拉雅音频汇总

5.其他汇总

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...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77341.html

相关文章

  • SnapHelper源码深度解析

    摘要:为表示之前进行过滚动,为状态表示滚动结束停下来的抽象方法抽象方法计算最终对齐要移动的距离计算二个参数对应的当前的坐标与需要对齐的坐标之间的距离。抽象方法找到要对齐的该方法会找到当前上最接近对齐位置的那个,该称为,对应的称为。 目录介绍 01.SnapHelper简单介绍 1.1 SnapHelper作用 1.2 SnapHelper类分析 1.3 LinearSnapHelper...

    ThinkSNS 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<