美文网首页高级UIrecycleview
RecyclerView基本使用

RecyclerView基本使用

作者: 者文_ | 来源:发表于2019-08-13 16:49 被阅读0次

    1. RecyclerView概述

    从Android 5.0开始,谷歌公司推出了一个用于大量数据展示的新控件RecylerView,可以用来代替传统的ListView,更加强大和灵活。

    在ListView中 改变列表某一个item数据,然后刷新列表,会回到最顶部,而RecyclerView可以保持原来滑动的位置不变。

    在使用RecyclerView时,可能会涉及到如下内容:

    • 想要控制其Item们的排列方式,使用布局管理器LayoutManager
    • 如果要创建一个适配器,请使用RecyclerView.Adapter
    • 想要控制Item间的间隔,请使用RecyclerView.ItemDecoration
    • 想要控制Item增删的动画,请使用RecyclerView.ItemAnimator
    • CardView扩展FrameLayout类并能够显示卡片内信息,这些信息在整个平台中拥有一致的呈现方式。CardView小部件可拥有阴影和圆角。

    上述功能主要涉及到的方法如下:

    mRecyclerView = findView(R.id.id_recyclerview);
    //设置布局管理器
    mRecyclerView.setLayoutManager(layout);
    //设置adapter
    mRecyclerView.setAdapter(adapter)
    //设置Item增加、移除动画
    mRecyclerView.setItemAnimator(new DefaultItemAnimator());
    //添加分割线
    mRecyclerView.addItemDecoration(new DividerItemDecoration(
                    getActivity(), DividerItemDecoration.HORIZONTAL_LIST));
    
    image

    如上图所示,如果要使用RecyclerView,必须指定一个Adapter和LayoutManager

    2. RecyclerView使用

    2.1 使用流程

    RecyclerView定义在support库当中,想要使用该控件,需要在项目的build.gradle中添加相应的依赖库。在app/build.gradle文件,在dependencies闭包中添加当下的版本:

     implementation 'com.android.support:recyclerview-v7:28.0.0'
    

    同步之后,就可以展开对RecyclerView的使用,其使用方式其实与ListView的三要素类似。

    在具体演示RecyclerView使用之前,左中了解一下其适配器:RecyclerView.Adapter

    2.2 RecyclerView.Adapter

    该适配器是一个抽象类,并支持泛型

    public abstract static class Adapter<VH extends RecyclerView.ViewHolder> {
       ...
    }
    

    如果创建一个适配器继承RecyclerView.Adapter,需要重写三个方法:

    • onCreateViewHolder()
    • onBindViewHolder()
    • getItemCount()

    指定泛型后,方法一与方法二会根据泛型改变。

    创建适配器时,一般会先定义一个ViewHolder内部类继承RecyclerView.ViewHolder,则指定的泛型即为该ViewHolder,可以在其内定义每个列表上的视图控件,并在onCreateViewHolder中初始化

    • onCreateViewHolder()示例
    @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
            ViewHolder holder = new ViewHolder(view);
            return holder;
        }
    

    该方法主要用来创建ViewHolder,可以根据需求的itemType,创建出多个ViewHolder。创建多个itemType时,需要getItemViewType(int position)方法配合

    • onBindViewHolder()示例
    @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            Fruit fruit = mFruitList.get(position);
            holder.fruitImage.setImageResource(fruit.getImageId());
            holder.fruitName.setText(fruit.getName());
        }
    

    该方法主要是将layout视图控件与ViewHolder中元件属性绑定。

    • getItemCount()示例
    @Override
        public int getItemCount() {
            return mFruitList.size();
        }
    

    这个方法的返回值,便是RecyclerView中实际item的数量。有些情况下,当增加了HeaderView或者FooterView后,需要注意考虑这个返回值

    介绍了RecyclerView.Adapter的这三个方法,现在简单示例RecyclerView的使用

    • 在xml中添加RecyclerView控件
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:divider="#ffff0000"
            android:dividerHeight="10dp" />
        
    </RelativeLayout>
    
    • 新建RecyclerView需要展示的界面xml布局
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp">
    
        <ImageView
            android:id="@+id/fruit_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"/>
    
        <TextView
            android:id="@+id/fruit_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="left"
            android:layout_marginTop="10dp" />
    
    </LinearLayout>
    
    • 定义数据类
    public class Fruit {
    
        private String name;
    
        private int imageId;
    
        public Fruit(String name, int imageId) {
            this.name = name;
            this.imageId = imageId;
        }
    
        public String getName() {
            return name;
        }
    
        public int getImageId() {
            return imageId;
        }
    
    }
    
    • 新建适配器,将其绑定到RecyclerView
    public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
    
        private List<Fruit> mFruitList;
    
        static class ViewHolder extends RecyclerView.ViewHolder {
            ImageView fruitImage;
            TextView fruitName;
    
            public ViewHolder(View view) {
                super(view);
                fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
                fruitName = (TextView) view.findViewById(R.id.fruit_name);
            }
        }
    
        public FruitAdapter(List<Fruit> fruitList) {
            mFruitList = fruitList;
        }
    
        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
            ViewHolder holder = new ViewHolder(view);
            return holder;
        }
    
        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            Fruit fruit = mFruitList.get(position);
            holder.fruitImage.setImageResource(fruit.getImageId());
            holder.fruitName.setText(fruit.getName());
        }
    
        @Override
        public int getItemCount() {
            return mFruitList.size();
        }
    }
    
    • 使用RecyclerView
    public class MainActivity extends AppCompatActivity {
    
        private List<Fruit> fruitList = new ArrayList<>();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initFruits();
            RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
            LinearLayoutManager layoutManager = new LinearLayoutManager(this);
            layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
            recyclerView.setLayoutManager(layoutManager);
            FruitAdapter adapter = new FruitAdapter(fruitList);
            recyclerView.setAdapter(adapter);
        }
    
        private void initFruits() {
            for (int i = 0; i < 2; i++) {
                Fruit apple = new Fruit(getRandomLengthName("Apple"), R.drawable.apple_pic);
                fruitList.add(apple);
                Fruit banana = new Fruit(getRandomLengthName("Banana"), R.drawable.banana_pic);
                fruitList.add(banana);
                Fruit orange = new Fruit(getRandomLengthName("Orange"), R.drawable.orange_pic);
                fruitList.add(orange);
                Fruit watermelon = new Fruit(getRandomLengthName("Watermelon"), R.drawable.watermelon_pic);
                fruitList.add(watermelon);
                Fruit pear = new Fruit(getRandomLengthName("Pear"), R.drawable.pear_pic);
                fruitList.add(pear);
                Fruit grape = new Fruit(getRandomLengthName("Grape"), R.drawable.grape_pic);
                fruitList.add(grape);
                Fruit pineapple = new Fruit(getRandomLengthName("Pineapple"), R.drawable.pineapple_pic);
                fruitList.add(pineapple);
                Fruit strawberry = new Fruit(getRandomLengthName("Strawberry"), R.drawable.strawberry_pic);
                fruitList.add(strawberry);
                Fruit cherry = new Fruit(getRandomLengthName("Cherry"), R.drawable.cherry_pic);
                fruitList.add(cherry);
                Fruit mango = new Fruit(getRandomLengthName("Mango"), R.drawable.mango_pic);
                fruitList.add(mango);
            }
        }
    
        private String getRandomLengthName(String name) {
            Random random = new Random();
            int length = random.nextInt(20) + 1;
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < length; i++){
                builder.append(name);
            }
            return builder.toString();
        }
    }
    

    除了除了LinearLayoutManager之外,还提供了GridLayoutManagerStaggeredGridLayoutManager这两种内置的布局排列方式。前者实现网格布局,后者可以用于实现瀑布流布局。

    备注

    在onCreateViewHolder()中,映射Layout必须为

    View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false);
    

    而不能是

    View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, null);
    

    2.3 万能适配器

    创建RecyclerView的Adapter时,发现实现过程大同小异,可以创建一个万能适配器,快捷创建Adapter。

    下面给出一个简易版万能适配器,学习了RecyclerView其他部分内容在给出完成版。

    public abstract class QuickAdapter<T> extends RecyclerView.Adapter<QuickAdapter.VH>{
    
         private List<T> mDatas;
    
         public QuickAdapter(List<T> datas){
             this.mDatas = datas;
         }
    
         public abstract int getLayoutId(int viewType);
    
         @Override
         public VH onCreateViewHolder(ViewGroup parent, int viewType) {
             return VH.get(parent,getLayoutId(viewType));
         }
    
         @Override
         public void onBindViewHolder(VH holder, int position) {
             convert(holder, mDatas.get(position), position);
         }
    
         @Override
         public int getItemCount() {
             return mDatas.size();
         }
    
         public abstract void convert(VH holder, T data, int position);
    
         static class VH extends RecyclerView.ViewHolder{...}
     }
    

    其中QuickAdapter.VH的实现如下:

    static class VH extends RecyclerView.ViewHolder{
         private SparseArray<View> mViews;
         private View mConvertView;
    
         private VH(View v){
             super(v);
             mConvertView = v;
             mViews = new SparseArray<>();
         }
    
         public static VH get(ViewGroup parent, int layoutId){
             View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
             return new VH(convertView);
         }
    
         public <T extends View> T getView(int id){
             View v = mViews.get(id);
             if(v == null){
                 v = mConvertView.findViewById(id);
                 mViews.put(id, v);
             }
             return (T)v;
         }
    
         public void setText(int id, String value){
             TextView view = getView(id);
             view.setText(value);
         }
     }
    

    其中:

    • getLayoutId(viewType)是根据viewType返回布局ID
    • convert()做具体Bind操作

    设置完上述的万能适配器,如果要创建一个Adapter时,只需:

    mAdapter = new QuickAdapter<Model>(data) {
         @Override
         public int getLayoutId(int viewType) {
             switch(viewType){
                 case TYPE_1:
                     return R.layout.item_1;
                 case TYPE_2:
                     return R.layout.item_2;
             }
         }
    
         public int getItemViewType(int position) {
             if(position % 2 == 0){
                 return TYPE_1;
             } else{
                 return TYPE_2;
             }
         }
    
         @Override
         public void convert(VH holder, Model data, int position) {
             int type = getItemViewType(position);
             switch(type){
                 case TYPE_1:
                     holder.setText(R.id.text, data.text);
                     break;
                 case TYPE_2:
                     holder.setImage(R.id.image, data.image);
                     break;
             }
         }
     };
    

    3. LayoutManager自定义与使用

    3.1 概述

    如果说Adapter负责提供View,而LayoutManger则负责它们在RecyclerView中摆放的位置以及在窗口中不可见之后的回收策略

    RecyclerView提供的布局管理器:

    • LinearLayoutManager 以垂直或水平滚动列表方式显示项目
    • GridLayoutManager 在网格中显示项目。
    • StaggeredGridLayoutManager 在分散对齐网格中显示项目。

    这里简单介绍LinearLayoutManager的几个重要方法,分析一下LayoutManger的实现

    • onLayoutChildren(): 对RecyclerView进行布局的入口方法。
    • fill(): 负责填充RecyclerView。
    • scrollVerticallyBy():根据手指的移动滑动一定距离,并调用fill()填充。
    • canScrollVertically()canScrollHorizontally(): 判断是否支持纵向滑动或横向滑动。

    其中onLayoutChildren()的核心实现如下:

    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
         detachAndScrapAttachedViews(recycler); //将原来所有的Item View全部放到Recycler的Scrap Heap或Recycle Pool
         fill(recycler, mLayoutState, state, false); //填充现在所有的Item View
     }
    

    RecyclerView的回收机制有个重要的概念,即将回收站分为Scrap HeapRecycle Pool,其中Scrap Heap的元素可以被直接复用,而不需要调用onBindViewHolder()。detachAndScrapAttachedViews()会根据情况,将原来的Item View放入Scrap Heap或Recycle Pool,从而在复用时提升效率

    fill()是对剩余空间不断地调用layoutChunk(),直到填充完为止。layoutChunk()的核心实现如下:

    public void layoutChunk() {
         View view = layoutState.next(recycler); //调用了getViewForPosition()
         addView(view);  //加入View
         measureChildWithMargins(view, 0, 0); //计算View的大小
         layoutDecoratedWithMargins(view, left, top, right, bottom); //布局View
     }
    

    其中next()调用了getViewForPosition(currentPosition),该方法是从RecyclerView的回收机制实现类Recycler中获取合适的View。

    在具体演示如何自定义LayoutManager之前,先介绍一个常用的API

    • recycler.getViewForPosition(position)

    • 获取位置为position的View。

    • getPosition(View view)

    • 获取view的位置。

    • measureChildWithMargins(View child, int widthUsed, int heightUsed)

    • 测量view的宽高,包括外边距。

    • layoutDecoratedWithMargins(View child, int left, int top, int right,int bottom)

    • 将child显示在RecyclerView上面,left,top,right,bottom规定了显示的区域。

    • detachView(View child)

    • 临时回收view。

    • attachView(View child)

    • detachView(View child)回收的view拿回来。

    • detachAndScrapAttachedViews(RecyclerView.Recycler recycler)

    • 用指定的recycler临时移除所有添加的views。

    • detachAndScrapView(View child, RecyclerView.Recycler recycler)

    • 用指定的recycler临时回收view。

    • removeAndRecycleAllViews(RecyclerView.Recycler recycler)

    • 移除所有的view并且用给的recycler回收。

    • removeAndRecycleView(View child, RecyclerView.Recycler recycler)

    • 移除指定的view并且用给的recycler回收。

    • offsetChildrenHorizontal(int dx)

    • 水平移动所有的view,同样也有offsetChildrenVertical(int dy)

    3.2 简单示例

    这里我们通过演示一个简单示例来介绍一下自定义一个LayoutManager需要实现的核心方法。

    • 首先生成一个类,例如CustomLayoutManager,派生自LayoutManager
    public class CustomLayoutManager extends LayoutManager {
        @Override
        public LayoutParams generateDefaultLayoutParams() {
            return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                    RecyclerView.LayoutParams.WRAP_CONTENT);
        }
    }
    

    派生自LayoutManager时,会强制让我们生成generateDefaultLayoutParams()方法,即RecyclerView Item的布局参数。无特殊要求,直接让子Item自己决定宽高即可

    • 添加onLayoutChild()
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        //定义竖直方向的偏移量
        int offsetY = 0;
        for (int i = 0; i < getItemCount(); i++) {
            View view = recycler.getViewForPosition(i);
            addView(view);
            measureChildWithMargins(view, 0, 0);
            int width = getDecoratedMeasuredWidth(view);
            int height = getDecoratedMeasuredHeight(view);
            layoutDecorated(view, 0, offsetY, width, offsetY + height);
            offsetY += height;
        }
    }
    

    ​ 这里做了两件事:

    • 把所有Item对应的view加载进来

    • 把所有Item摆在其应在的地方

    这个方法是LayoutManager的主入口,在view需要初始化布局时调用(适配器数据改变或适配器替换时再次被调用)。

    通常来说,在这个方法中你需要完成主要步骤如下:

    • 在滚动事件结束后检查所有附加视图当前的偏移位置。
    • 判断是否需要添加新视图填充由滚动屏幕产生的空白部分。并从 Recycler 中获取视图。
    • 判断当前视图是否不再显示。移除它们并放置到 Recycler 中。
    • 判断剩余视图是否需要整理。发生上述变化后可能 需要你修改视图的子索引来更好地和它们的适配器位置校准。

    这个示例较为简单,只是通过measureChildWithMargins(view, 0, 0);函数测量这个view,并且通过getDecoratedMeasuredWidth(view)得到测量出来的宽度,需要注意的是通过getDecoratedMeasuredWidth(view)得到的是item+decoration的总宽度。如果你只想得到view的测量宽度,通过view.getMeasuredWidth()就可以得到了

    然后通过layoutDecorated();函数将每个item摆放在对应的位置,每个Item的左右位置都是相同的,从左侧x=0开始摆放,只是y的点需要计算。所以这里有一个变量offsetY,用以累加当前Item之前所有item的高度。

    • 添加滑动效果
    @Override
    public boolean canScrollVertically() {
        return true;
    }
    
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 平移容器内的item
        offsetChildrenVertical(-dy);
        return dy;
    }
    

    通过在canScrollVertically()中return true;使LayoutManager具有垂直滚动的功能。然后在scrollVerticallyBy()中接收每次滚动的距离dy。

    其中dy表示手指在屏幕上每次滑动的位移:

    • 当手指由下往上滑时,dy>0
    • 当手指由上往下滑时,dy<0

    明显手指上滑,子Item上移,需要减去dy,让item移动-dy距离是合理的。可以通过offsetChildrenVertical()移动所有item

    • 添加异常判断

    前述代码实现滚动,但是Item到顶或者底仍可以继续滚动,需要加以判断是否到顶或者到底

    判断到顶了

    判断到顶只需把所有dy相加,如果小于0,表示到顶,不再移动

    private int mSumDy = 0;
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int travel = dy;
        //如果滑动到最顶部
        if (mSumDy + dy < 0) {
            travel = -mSumDy;
        }
        mSumDy += travel;
        // 平移容器内的item
        offsetChildrenVertical(-travel);
        return dy;
    }
    

    判断到底了

    判断到底的方法是用总高度减去最后一屏高度,即到底的偏移值。如果大于这个偏移值,则说明超过底部。

    首先要得到所有Item的总高度,在onLayoutChildren中会测量所有的item并对每一个item布局,只需在onLayoutChildren()中将所有的item高度相加即可得到所有Item的总高度。

    private int mTotalHeight = 0;
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        //定义竖直方向的偏移量
        int offsetY = 0;
        for (int i = 0; i < getItemCount(); i++) {
            View view = recycler.getViewForPosition(i);
            addView(view);
            measureChildWithMargins(view, 0, 0);
            int width = getDecoratedMeasuredWidth(view);
            int height = getDecoratedMeasuredHeight(view);
            layoutDecorated(view, 0, offsetY, width, offsetY + height);
            offsetY += height;
        }
        //如果所有子View的高度和没有填满RecyclerView的高度,
        // 则将高度设置为RecyclerView的高度
        mTotalHeight = Math.max(offsetY, getVerticalSpace());
    }
    private int getVerticalSpace() {
        return getHeight() - getPaddingBottom() - getPaddingTop();
    }
    

    其中getVerticalSpace()函数可以得到RecyclerView用于显示item的真实高度。接着在srcollVerticallyBy中判断到底与否并处理:

    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int travel = dy;
        //如果滑动到最顶部
        if (mSumDy + dy < 0) {
            travel = -mSumDy;
        } else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
            travel = mTotalHeight - getVerticalSpace() - mSumDy;
        }
    
        mSumDy += travel;
        // 平移容器内的item
        offsetChildrenVertical(-travel);
        return dy;
    }
    

    mSumDy + dy 表示当前的移动距离,mTotalHeight - getVerticalSpace()表示当滑动到底时滚动的总距离;

    3.3 LayoutManager附加特性

    LayoutManager中提供了一些辅助方法操作decoration(关于ItemDecoration在后面详细介绍):

    • getDecoratedLeft()代替child.getLeft()获取子视图的 left 边缘。
    • getDecoratedTop()代替getTop()获取子视图的 top 边缘。
    • getDecoratedRight()代替getRight()获取子视图的 right 边缘。
    • getDecoratedBottom()代替getBottom()获取子视图的 bottom 边缘。
    • 使用 measureChild()measureChildWithMargins() 代替child.measure() 测量来自 Recycler 的新视图。
    • 使用layoutDecorated() 代替 child.layout() 布局来自 Recycler 的新视图。
    • 使用 getDecoratedMeasuredWidth()getDecoratedMeasuredHeight() 代替 child.getMeasuredWidth()或 child.getMeasuredHeight()获取 子视图的测量数据

    3.3.1 数据集改变

    当使用 notifyDataSetChanged()触发 RecyclerView.Adapter 的更新操作时, LayoutManager 负责更新布局中的视图。这时,onLayoutChildren()会被再次调用。实现这个功能需要我们在onLayoutChildre()方法中判断出当前状态是生成一个新的视图还是adapter更新期间视图改变。

    。。。。。

    3.3.2 onAdapterChanged()

    这个方法提供了另一个重置布局的场所。设置新的 adapter 会触发这个事件。

    示例:

    @Override
    public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
        //Completely scrap the existing layout
        removeAllViews();
    }
    

    移除视图会触发一个新的布局过程,当 onLayoutChildren() 被再次调用时, 我们的代码会执行创建新视图的布局过程,因为现在没有 attched 的子视图。

    3.3.3 Scroll to Position

    其一个重要的特性就是给LayoutManager添加滚动到特定位置的功能。可以带有动画效果,也可以没有

    • scrollToPosition()

      layout将当前位置设为第一个可见Item时,调用RecyclerView的scrollToPosition()

    示例

    @Override
    public void scrollToPosition(int position) {
        if (position >= getItemCount()) {
            Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
            return;
        }
    
        //Ignore current scroll offset, snap to top-left
        mForceClearOffsets = true;
        //Set requested position as first visible
        mFirstVisiblePosition = position;
        //Trigger a new view layout
        requestLayout();
    }
    
    • smoothScrollToPosition()

    在带有动画的情况下,需要在该方法中创建一个RecyclerView.SmoothScroller实例,在方法返回前请求startSmoothScroll()启动动画

    RecyclerView.SmoothScroller 是提供 API 的抽象类,含有四个方法:

    1. onStart()

      当滑动动画开始时被触发

    2. onStop()

      当滑动动画停止时被触发

    3. onSeekTargetStep()

      当 scroller 搜索目标 view 时被重复调用,这个方法负责读取提供的 dx/dy ,然后更新应该在这两个方向移动的距离。

    4. onTragetFound()

      只在目标视图被 attach 后调用一次。 这是将目标视图要通过动画移动到准确位置最后的场所。

    4. ItemDecoration

    RecyclerView并没有支持divider这样的属性,我们可以自己定制分割线。

    RecyclerView添加分割线的方法是:mRecyclerView.addItemDecoration(),该方法的参数为RecyclerView.ItemDecoration,该类为抽象类

    查看一下其源码:

    public abstract static class ItemDecoration {
            public ItemDecoration() {
            }
    
            public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
                this.onDraw(c, parent);
            }
    
            /** @deprecated */
            @Deprecated
            public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
            }
    
            public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
                this.onDrawOver(c, parent);
            }
    
            /** @deprecated */
            @Deprecated
            public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
            }
    
            /** @deprecated */
            @Deprecated
            public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent) {
                outRect.set(0, 0, 0, 0);
            }
    
            public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
                this.getItemOffsets(outRect, ((RecyclerView.LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
            }
        }
    

    调用addItemDecoration()方法添加decoration的时候,会调用该类的onDraw()onDrawOver()方法

    • onDraw方法先于drawChildren
    • onDrawOver在drawChildren之后,一般我们选择复写其中一个即可。
    • getItemOffsets为每一个item设置一定的偏移量,主要用于绘制Decorator,设置分割线宽、高等

    简单示例:(这里LayoutManager为LinearLayoutManager)

    public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    
        private static final int[] ATTRS = new int[]{
                android.R.attr.listDivider
        };
    
        public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    
        public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
    
        private Drawable mDivider;
    
        private int mOrientation;
    
        public DividerItemDecoration(Context context, int orientation) {
            final TypedArray a = context.obtainStyledAttributes(ATTRS);
            mDivider = a.getDrawable(0);
            a.recycle();
            setOrientation(orientation);
        }
    
        public void setOrientation(int orientation) {
            if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
                throw new IllegalArgumentException("invalid orientation");
            }
            mOrientation = orientation;
        }
    
        @Override
        public void onDraw(Canvas c, RecyclerView parent) {
    
            if (mOrientation == VERTICAL_LIST) {
                drawVertical(c, parent);
            } else {
                drawHorizontal(c, parent);
            }
    
        }
    
    
        public void drawVertical(Canvas c, RecyclerView parent) {
            final int left = parent.getPaddingLeft();
            final int right = parent.getWidth() - parent.getPaddingRight();
    
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                android.support.v7.widget.RecyclerView v = new android.support.v7.widget.RecyclerView(parent.getContext());
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                        .getLayoutParams();
                final int top = child.getBottom() + params.bottomMargin;
                final int bottom = top + mDivider.getIntrinsicHeight();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
    
        public void drawHorizontal(Canvas c, RecyclerView parent) {
            final int top = parent.getPaddingTop();
            final int bottom = parent.getHeight() - parent.getPaddingBottom();
    
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                        .getLayoutParams();
                final int left = child.getRight() + params.rightMargin;
                final int right = left + mDivider.getIntrinsicHeight();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
    
        @Override
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            if (mOrientation == VERTICAL_LIST) {
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
            } else {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
            }
        }
    }
    

    该实现类可以看到通过读取系统主题中的 android.R.attr.listDivider作为Item间的分割线,并且支持横向和纵向

    然后在原来的代码中添加一句:

    mRecyclerView.addItemDecoration(new DividerItemDecoration(this,
    DividerItemDecoration.VERTICAL_LIST));
    

    4.1 ItemDecoration解析

    • 前述代码分割线是系统默认,你可以在theme.xml中找到该属性的使用情况,使用系统的listDivider可以方便我们随意改变,其属性声明:

      <!-- Application theme. -->
          <style name="AppTheme" parent="AppBaseTheme">
            <item name="android:listDivider">@drawable/divider_bg</item>  
          </style>
      

      当然可以自己写个Drawable,例如:

      <?xml version="1.0" encoding="utf-8"?>
      <shape xmlns:android="http://schemas.android.com/apk/res/android"
          android:shape="rectangle" >
      
          <gradient
              android:centerColor="#ff00ff00"
              android:endColor="#ff0000ff"
              android:startColor="#ffff0000"
              android:type="linear" />
          <size android:height="4dp"/>
      
      </shape>
      
    • 要使用ItemDecoration,必须先自定义,继承ItemDecoration,重写getItemOffsets()onDraw()

      这里先解释ItemDecoration含义。

      ItemDecoration即对Item起装饰作用

      image

      getItemOffsets()就是设置item周边的偏移量(也就是装饰区域的宽度),而onDraw()才是真正实现装饰的回调方法,该方法可以在装饰区域任意画画。

      前面ItemDecoration源码中可以看出,其主要有三个方法:

      OnDraw()
      onDrawOver()
      getItemOffsets()
      

      这三个方法的作用可以简单理解如下图

      image

      其中绿色表示内容,红色表示装饰:

      • 图1表示getItemOffsets(),实现类似padding效果
      • 图2表示onDraw(),实现类似绘制背景效果,内容在上
      • 图3表示onDrawOver(),可以绘制在内容上面,覆盖内容

      假设我们要实现线性列表的分割线:

      • 当线性列表是水平方向时,分割线竖直的;当线性列表是竖直方向时,分割线是水平的
      • 当画竖直分割线时,需要在item的右边偏移出一条线的宽度;当画水平分割线时,需要在item的下边偏移出一条线的高度
      /**
       * 画线
       */
      @Override
      public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
          super.onDraw(c, parent, state);
          if (orientation == RecyclerView.HORIZONTAL) {
              drawVertical(c, parent, state);
          } else if (orientation == RecyclerView.VERTICAL) {
              drawHorizontal(c, parent, state);
          }
      }
      
      
      /**
      * 设置条目周边的偏移量
      */
      @Override
      public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
          super.getItemOffsets(outRect, view, parent, state);
          if (orientation == RecyclerView.HORIZONTAL) {
              //画垂直线
              outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
          } else if (orientation == RecyclerView.VERTICAL) {
              //画水平线
              outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
          }
      }
      

      getItemOffsets()是相对每个item而言的,即每个item都会偏移出相同的装饰区域,而onDraw则不同,它是相对Canvas来说的,通俗的说就是要自己找到要画的线的位置

      /**
       * 在构造方法中加载系统自带的分割线(就是ListView用的那个分割线)
       */
      public MyDecorationOne(Context context, int orientation) {
          this.orientation = orientation;
          int[] attrs = new int[]{android.R.attr.listDivider};
          TypedArray a = context.obtainStyledAttributes(attrs);
          mDivider = a.getDrawable(0);
          a.recycle();
      }
      
      /**
       * 画竖直分割线
       */
      private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state) {
          int childCount = parent.getChildCount();
          for (int i = 0; i < childCount; i++) {
              View child = parent.getChildAt(i);
              RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
              int left = child.getRight() + params.rightMargin;
              int top = child.getTop() - params.topMargin;
              int right = left + mDivider.getIntrinsicWidth();
              int bottom = child.getBottom() + params.bottomMargin;
              mDivider.setBounds(left, top, right, bottom);
              mDivider.draw(c);
          }
      }
      
      /**
       * 画水平分割线
       */
      private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state) {
          int childCount = parent.getChildCount();
          for (int i = 0; i < childCount; i++) {
              View child = parent.getChildAt(i);
              RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
              int left = child.getLeft() - params.leftMargin;
              int top = child.getBottom() + params.bottomMargin;
              int right = child.getRight() + params.rightMargin;
              int bottom = top + mDivider.getIntrinsicHeight();
              mDivider.setBounds(left, top, right, bottom);
              mDivider.draw(c);
          }
      }
      
      image

    简单示例ItemDecoration的三个主要方法:点击查看原文阅读

    下面是没有添加任何ItemDecoration的界面:

    image
    1. 预计实现效果如下时:

      image

      从效果图看实现了分割线效果。根据前述知识,只需先用getItemOffset()方法在Item下方空出一定的高度控件,然后用onDraw()绘制这个空间即可

      public class SimpleDividerDecoration extends RecyclerView.ItemDecoration {
      
          private int dividerHeight;
          private Paint dividerPaint;
      
          public SimpleDividerDecoration(Context context) {
              dividerPaint = new Paint();
              dividerPaint.setColor(context.getResources().getColor(R.color.colorAccent));
              dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.divider_height);
          }
      
      
          @Override
          public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
              super.getItemOffsets(outRect, view, parent, state);
              outRect.bottom = dividerHeight;  //item下方空出一块空间
          }
      
          @Override
          public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
              int childCount = parent.getChildCount();
              int left = parent.getPaddingLeft();
              int right = parent.getWidth() - parent.getPaddingRight();
           //遍历item
              for (int i = 0; i < childCount - 1; i++) {
                  View view = parent.getChildAt(i);
                  float top = view.getBottom();
                  float bottom = view.getBottom() + dividerHeight;
                  c.drawRect(left, top, right, bottom, dividerPaint);
              }
          }
      }
      
    2. 预计实现效果如下图时:

      image

      这种效果有点像商品标签,覆盖在内容之上可以通过onDrawOver()实现

      public class LeftAndRightTagDecoration extends RecyclerView.ItemDecoration {
          private int tagWidth;
          private Paint leftPaint;
          private Paint rightPaint;
      
          public LeftAndRightTagDecoration(Context context) {
              leftPaint = new Paint();
              leftPaint.setColor(context.getResources().getColor(R.color.colorAccent));
              rightPaint = new Paint();
              rightPaint.setColor(context.getResources().getColor(R.color.colorPrimary));
              tagWidth = context.getResources().getDimensionPixelSize(R.dimen.tag_width);
          }
      
          @Override
          public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
              super.onDrawOver(c, parent, state);
              int childCount = parent.getChildCount();
              for (int i = 0; i < childCount; i++) {
                  View child = parent.getChildAt(i);
                  int pos = parent.getChildAdapterPosition(child);
                  boolean isLeft = pos % 2 == 0;
                  if (isLeft) {
                      float left = child.getLeft();
                      float right = left + tagWidth;
                      float top = child.getTop();
                      float bottom = child.getBottom();
                      c.drawRect(left, top, right, bottom, leftPaint);
                  } else {
                      float right = child.getRight();
                      float left = right - tagWidth;
                      float top = child.getTop();
                      float bottom = child.getBottom();
                      c.drawRect(left, top, right, bottom, rightPaint);
      
                  }
              }
          }
      }
      

      ItemDecoration效果是可以叠加的

      recyclerView.addItemDecoration(new LeftAndRightTagDecoration(this));
      recyclerView.addItemDecoration(new SimpleDividerDecoration(this));
      

      将上面两个ItemDecoration同时添加到RecyclerView时,效果如下:

      image
    3. 预计实现效果如下图时:

      image

      效果有点类似手机通讯录分组,这种效果就是有些Item添加分割线,有些没有而已。

      • 定义一个接口给Activity进行回调用来进行数据分组和获取首字母

        public interface DecorationCallback {
        
                long getGroupId(int position);
        
                String getGroupFirstLine(int position);
            }
        
      • 实现ItemDecoration

        public class SectionDecoration extends RecyclerView.ItemDecoration {
            private static final String TAG = "SectionDecoration";
        
            private DecorationCallback callback;   //回调接口
            private TextPaint textPaint;
            private Paint paint;
            private int topGap;
            private Paint.FontMetrics fontMetrics;
        
           //初始化
            public SectionDecoration(Context context, DecorationCallback decorationCallback) {
                Resources res = context.getResources();
                this.callback = decorationCallback;
        
                paint = new Paint();
                paint.setColor(res.getColor(R.color.colorAccent));
        
                textPaint = new TextPaint();
                textPaint.setTypeface(Typeface.DEFAULT_BOLD);
                textPaint.setAntiAlias(true);
                textPaint.setTextSize(80);
                textPaint.setColor(Color.BLACK);
                textPaint.getFontMetrics(fontMetrics);
                textPaint.setTextAlign(Paint.Align.LEFT);
                fontMetrics = new Paint.FontMetrics();
                topGap = res.getDimensionPixelSize(R.dimen.sectioned_top);//32dp
        
        
            }
        
        
            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
                super.getItemOffsets(outRect, view, parent, state);
                int pos = parent.getChildAdapterPosition(view);
                Log.i(TAG, "getItemOffsets:" + pos);
                long groupId = callback.getGroupId(pos);
                if (groupId < 0) return;
                if (pos == 0 || isFirstInGroup(pos)) {//同组的第一个才添加padding
                    outRect.top = topGap;
                } else {
                    outRect.top = 0;
                }
            }
        
            @Override
            public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
                super.onDraw(c, parent, state);
                int left = parent.getPaddingLeft();
                int right = parent.getWidth() - parent.getPaddingRight();
                int childCount = parent.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    View view = parent.getChildAt(i);
                    int position = parent.getChildAdapterPosition(view);
                    long groupId = callback.getGroupId(position);
                    if (groupId < 0) return;
                    String textLine = callback.getGroupFirstLine(position).toUpperCase();
                    //判断是否需要绘制分割线
                    if (position == 0 || isFirstInGroup(position)) {
                        float top = view.getTop() - topGap;
                        float bottom = view.getTop();
                        c.drawRect(left, top, right, bottom, paint);//绘制红色矩形
                        c.drawText(textLine, left, bottom, textPaint);//绘制文本
                    }
                }
            }
        
           
            private boolean isFirstInGroup(int pos) {
                if (pos == 0) {
                    return true;
                } else {
                    long prevGroupId = callback.getGroupId(pos - 1);
                    long groupId = callback.getGroupId(pos);
                    return prevGroupId != groupId;
                }
            }
        
            public interface DecorationCallback {
        
                long getGroupId(int position);
        
                String getGroupFirstLine(int position);
            }
        }
        
      • 在Activity中使用

        recyclerView.addItemDecoration(new SectionDecoration(this, new SectionDecoration.DecorationCallback() {
                    @Override
                    public long getGroupId(int position) {
                        return Character.toUpperCase(dataList.get(position).getName().charAt(0));
                    }
        
                    @Override
                    public String getGroupFirstLine(int position) {
                        return dataList.get(position).getName().substring(0, 1).toUpperCase();
                    }
                }));
        
    4. 预计实现效果如下时:

      [图片上传失败...(image-ca730d-1565686110808)]

      这种效果叫做粘性头部。

      Header不动则一定要绘制在Item内容之上,需要重写onDrawOver(),其他与前述代码一致

      public class PinnedSectionDecoration extends RecyclerView.ItemDecoration {
          private static final String TAG = "PinnedSectionDecoration";
      
          private DecorationCallback callback;
          private TextPaint textPaint;
          private Paint paint;
          private int topGap;
          private Paint.FontMetrics fontMetrics;
      
      
          public PinnedSectionDecoration(Context context, DecorationCallback decorationCallback) {
              Resources res = context.getResources();
              this.callback = decorationCallback;
      
              paint = new Paint();
              paint.setColor(res.getColor(R.color.colorAccent));
      
              textPaint = new TextPaint();
              textPaint.setTypeface(Typeface.DEFAULT_BOLD);
              textPaint.setAntiAlias(true);
              textPaint.setTextSize(80);
              textPaint.setColor(Color.BLACK);
              textPaint.getFontMetrics(fontMetrics);
              textPaint.setTextAlign(Paint.Align.LEFT);
              fontMetrics = new Paint.FontMetrics();
              topGap = res.getDimensionPixelSize(R.dimen.sectioned_top);
      
      
          }
      
      
          @Override
          public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
              super.getItemOffsets(outRect, view, parent, state);
              int pos = parent.getChildAdapterPosition(view);
              long groupId = callback.getGroupId(pos);
              if (groupId < 0) return;
              if (pos == 0 || isFirstInGroup(pos)) {
                  outRect.top = topGap;
              } else {
                  outRect.top = 0;
              }
          }
      
      
          @Override
          public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
              super.onDrawOver(c, parent, state);
              int itemCount = state.getItemCount();
              int childCount = parent.getChildCount();
              int left = parent.getPaddingLeft();
              int right = parent.getWidth() - parent.getPaddingRight();
              float lineHeight = textPaint.getTextSize() + fontMetrics.descent;
      
              long preGroupId, groupId = -1;
              for (int i = 0; i < childCount; i++) {
                  View view = parent.getChildAt(i);
                  int position = parent.getChildAdapterPosition(view);
      
                  preGroupId = groupId;
                  groupId = callback.getGroupId(position);
                  if (groupId < 0 || groupId == preGroupId) continue;
      
                  String textLine = callback.getGroupFirstLine(position).toUpperCase();
                  if (TextUtils.isEmpty(textLine)) continue;
      
                  int viewBottom = view.getBottom();
                  float textY = Math.max(topGap, view.getTop());
                  if (position + 1 < itemCount) { //下一个和当前不一样移动当前
                      long nextGroupId = callback.getGroupId(position + 1);
                      if (nextGroupId != groupId && viewBottom < textY ) {//组内最后一个view进入了header
                          textY = viewBottom;
                      }
                  }
                  c.drawRect(left, textY - topGap, right, textY, paint);
                  c.drawText(textLine, left, textY, textPaint);
              }
      
          }
      
      }
      

    5. Item Animator

    6. Item监听

    RecyclerView默认没有像ListView一样提供setOnItemClickListener()接口,需要我们自己去改造实现。

    6.1 常规实现

    关于RecyclerView的Item点击事件常规做法就是在Adapter中增加OnItemClickListener接口和setOnItemClickListener接口,为每个Item添加点击监听。

    代码示例:

    • 自定义Adapter适配器,添加item click接口,设置点击事件
    public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
    
        private LayoutInflater mInflater;
        private Context mContext;
        private List<String> mDatas;
        private int[] imgIds;
    
        private OnItemClickListener mOnItemClickListener;
    
        public MyAdapter(Context context, List<String> datas, int[] imgIds) {
            this.mContext = context;
            this.mDatas = datas;
            this.imgIds = imgIds;
            mInflater = LayoutInflater.from(context);
        }
    
        @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = mInflater.inflate(R.layout.item_single_textview, parent, false);
            MyViewHolder viewHolder = new MyViewHolder(view);
            return viewHolder;
        }
    
        @Override
        public void onBindViewHolder(final MyViewHolder holder, final int position) {
            holder.textView.setText(mDatas.get(position));
            holder.imageView.setBackgroundResource(imgIds[ position % imgIds.length]);
    
            // item click
            if (mOnItemClickListener != null) {
                holder.itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        mOnItemClickListener.onItemClick(holder.itemView, position);
                    }
                });
    
                // item long click
                holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View view) {
                        mOnItemClickListener.onItemLongClick(holder.itemView, position);
                        return true;
                    }
                });
            }
        }
    
        @Override
        public int getItemCount() {
            return mDatas.size();
        }
    
        public void setOnItemClickListener(OnItemClickListener listener) {
            this.mOnItemClickListener = listener;
        }
    
        public void addData(int pos) {
            mDatas.add(pos, "Add one");
            notifyItemInserted(pos);
        }
    
        public void deleteData(int pos) {
            mDatas.remove(pos);
            notifyItemRemoved(pos);
        }
    
        public interface OnItemClickListener {
            void onItemClick(View view, int position);
            void onItemLongClick(View view, int position);
        }
    }
    class MyViewHolder extends RecyclerView.ViewHolder {
    
        ImageView imageView;
        TextView textView;
    
        public MyViewHolder(View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.textView);
            imageView = itemView.findViewById(R.id.imageView);
        }
    }
    
    • 修改MainActivity
    mAdapter = new MyAdapter(this, mDatas, imgIds);
            recyclerView.setAdapter(mAdapter);
    
            //设置RecyclerView的布局管理
            LinearLayoutManager manager = new LinearLayoutManager(this,
                    LinearLayoutManager.VERTICAL, false);
            recyclerView.setLayoutManager(manager);
    
            recyclerView.setItemAnimator(new DefaultItemAnimator());
    
            mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
                @Override
                public void onItemClick(View view, int position) {
                    Toast.makeText(MainActivity.this, "clicked " + position,
                            Toast.LENGTH_SHORT).show();
                }
    
                @Override
                public void onItemLongClick(View view, int position) {
                    Toast.makeText(MainActivity.this, "long clicked " + position,
                            Toast.LENGTH_SHORT).show();
                }
            });
        }
    

    6.2 addOnItemTouchListener()

    相关文章

      网友评论

        本文标题:RecyclerView基本使用

        本文链接:https://www.haomeiwen.com/subject/voybjctx.html