RecyclerView 使用总结

作者: 三流之路 | 来源:发表于2018-09-08 19:32 被阅读22次

    主要是在使用 RecyclerView 过程中遇到的细碎问题和解决方案。

    简单使用

    • LinearLayoutManager 线性管理器(支持横向、纵向)
    • GridLayoutManager 网格布局管理器
    • StaggeredGridLayoutManager 瀑布流式布局管理器
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    // 如果可以确定每个 item 的高度是固定的,设置这个选项可以提高性能
    recyclerView.setHasFixedSize(true);
    // item 显示的动画
    recyclerView.setItemAnimator(new DefaultItemAnimator());  
    recyclerView.setAdapter(new MyAdapter());
    
    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
        public List<String> datas;
        public MyAdapter(List<String> datas) {
            this.datas = datas;
        }
    
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
            return new ViewHolder(view);
        }
    
        @Override
        public void onBindViewHolder(ViewHolder viewHolder, int position) {
            viewHolder.mTextView.setText(datas.get(position));
        }
    
        @Override
        public int getItemCount() {
            return datas.size();
        }
    
        public static class ViewHolder extends RecyclerView.ViewHolder {
            public TextView mTextView;
            public ViewHolder(View view){
                super(view);
                mTextView = (TextView) view.findViewById(R.id.text);
            }
        }
    }
    

    多种布局

    public class MyAdapter extends RecyclerView.Adapter {
        public List<String> datas;
        public MyAdapter(List<String> datas) {
            this.datas = datas;
        }
    
        public enum ITEM_TYPE {
            ITEM1, ITEM2
        }
    
        @Override
        public int getItemViewType(int position) {
            // Enum 类提供了一个 ordinal() 方法,返回枚举类型的序数
            // ITEM_TYPE.ITEM1.ordinal() 代表0, ITEM_TYPE.ITEM2.ordinal() 代表1
            return position % 2 == 0 ? ITEM_TYPE.ITEM1.ordinal() : ITEM_TYPE.ITEM2.ordinal();
        }
    
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            // 根据不同 viewType 加载不同的布局
            if (viewType == ITEM_TYPE.ITEM1.ordinal()) {
                View view1 = LayoutInflater.from(parent.getContext()).inflate(R.layout.item1, parent, false);
                return new ViewHolder1(view1);
            } else if (viewType == ITEM_TYPE.ITEM2.ordinal()) {
                View view2 = LayoutInflater.from(parent.getContext()).inflate(R.layout.item2, parent, false);
                return new ViewHolder2(view2);
            }
        }
    
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
            if (holder instanceof ViewHolder1) {
                ((ViewHolder1) holder).mTextView.setText(datas.get(position));
            } else if (holder instanceof ViewHolder2) {
                ((ViewHolder2) holder).mTextView.setText(datas.get(position)+"abc");
            }
        }
    
        @Override
        public int getItemCount() {
            return datas.size();
        }
    
        public static class ViewHolder1 extends RecyclerView.ViewHolder {
            public TextView mTextView;
            public ViewHolder(View view){
                super(view);
                mTextView = (TextView) view.findViewById(R.id.text);
            }
        }
    
        public static class ViewHolder2 extends RecyclerView.ViewHolder {
            public TextView mTextView;
            public ViewHolder(View view){
                super(view);
                mTextView = (TextView) view.findViewById(R.id.big_text);
            }
        }
    }
    

    分隔线 ItemDecoration

    自定义分隔线

    定义分隔线 divider.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <solid android:color="#7b7a7a"/>
        <size android:height="1dp"/>
    </shape>
    

    自定义类继承 RecyclerView.ItemDecoration,重写回调方法

    // 线性布局用
    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Canvas;
    import android.graphics.Rect;
    import android.graphics.drawable.Drawable;
    import android.support.v7.widget.LinearLayoutManager;
    import android.support.v7.widget.RecyclerView;
    import android.util.Log;
    import android.view.View;
    
    public class MyDecoration extends RecyclerView.ItemDecoration{
    
        private Context mContext;
        private Drawable mDivider;
        private int mOrientation;
        public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
        public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
    
        public MyDecoration(Context context, int orientation) {
            this.mContext = context;
            mDivider = context.getResources().getDrawable(R.drawable.divider);
            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, RecyclerView.State state) {
            if (mOrientation == HORIZONTAL_LIST){
                drawVerticalLine(c, parent, state);
            }else {
                drawHorizontalLine(c, parent, state);
            }
        }
    
        // 画竖线, 这里的 parent 其实是显示在屏幕显示的这部分
        public void drawVerticalLine(Canvas c, RecyclerView parent, RecyclerView.State state){
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++){
                final View child = parent.getChildAt(i);
    
                // 获得 child 的布局信息
                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 drawHorizontalLine(Canvas c, RecyclerView parent, RecyclerView.State state){
            int top = parent.getPaddingTop();
            int bottom = parent.getHeight() - parent.getPaddingBottom();
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++){
                final View child = parent.getChildAt(i);
    
               // 获得 child 的布局信息
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)child.getLayoutParams();
                final int left = child.getRight() + params.rightMargin;
                final int right = left + mDivider.getIntrinsicWidth();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
    
        // 由于 Divider 也有长宽高,每一个 Item 需要向下或者向右偏移
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            if(mOrientation == HORIZONTAL_LIST){
                // 画竖线,就是往右偏移一个分割线的宽度
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
            } else {
                // 画横线,就是往下偏移一个分割线的高度
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
            }
        }
    }
    
    // 网格或瀑布流布局用
    public class DividerGridItemDecoration extends RecyclerView.ItemDecoration {
        private static final int[] ATTRS = new int[] { android.R.attr.listDivider };
        private Drawable mDivider;
    
        public DividerGridItemDecoration(Context context) {
            final TypedArray a = context.obtainStyledAttributes(ATTRS);
            mDivider = a.getDrawable(0);
            a.recycle();
        }
    
        @Override
        public void onDraw(Canvas c, RecyclerView parent, State state) {
            drawHorizontal(c, parent);
            drawVertical(c, parent);
        }
    
        private int getSpanCount(RecyclerView parent) {
            // 列数
            int spanCount = -1;
            LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                spanCount = ((StaggeredGridLayoutManager) layoutManager)
                        .getSpanCount();
            }
            return spanCount;
        }
    
        public void drawHorizontal(Canvas c, RecyclerView parent) {
            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.getLeft() - params.leftMargin;
                final int right = child.getRight() + params.rightMargin
                        + mDivider.getIntrinsicWidth();
                final int top = child.getBottom() + params.bottomMargin;
                final int bottom = top + mDivider.getIntrinsicHeight();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
    
        public void drawVertical(Canvas c, RecyclerView parent) {
            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 top = child.getTop() - params.topMargin;
                final int bottom = child.getBottom() + params.bottomMargin;
                final int left = child.getRight() + params.rightMargin;
                final int right = left + mDivider.getIntrinsicWidth();
    
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
    
        private boolean isLastColum(RecyclerView parent, int pos, int spanCount, int childCount) {
            LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                if ((pos + 1) % spanCount == 0) { // 如果是最后一列,则不需要绘制右边
                    return true;
                }
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                int orientation = ((StaggeredGridLayoutManager) layoutManager)
                        .getOrientation();
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    if ((pos + 1) % spanCount == 0) { // 如果是最后一列,则不需要绘制右边              
                        return true;
                    }
                } else {
                    childCount = childCount - childCount % spanCount;
                    if (pos >= childCount) // 如果是最后一列,则不需要绘制右边
                        return true;
                }
            }
            return false;
        }
    
        private boolean isLastRaw(RecyclerView parent, int pos, int spanCount, int childCount) {
            LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                childCount = childCount - childCount % spanCount;
                if (pos >= childCount) // 如果是最后一行,则不需要绘制底部
                    return true;
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                int orientation = ((StaggeredGridLayoutManager) layoutManager)
                        .getOrientation();
                // StaggeredGridLayoutManager 且纵向滚动
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    childCount = childCount - childCount % spanCount;
                    // 如果是最后一行,则不需要绘制底部
                    if (pos >= childCount)
                        return true;
                } else { // StaggeredGridLayoutManager 且横向滚动
                    // 如果是最后一行,则不需要绘制底部
                    if ((pos + 1) % spanCount == 0) {
                        return true;
                    }
                }
            }
            return false;
        }
    
        @Override
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            int spanCount = getSpanCount(parent);
            int childCount = parent.getAdapter().getItemCount();
            if (isLastRaw(parent, itemPosition, spanCount, childCount)) { // 如果是最后一行,则不需要绘制底部  
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
            } else if (isLastColum(parent, itemPosition, spanCount, childCount)) { // 如果是最后一列,则不需要绘制右边
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
            } else {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
            }
        }
    }
    

    使用自定义分隔线:

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

    使用自带的分隔线

    mRecyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.HORIZONTAL));
    

    可以更改自带分隔线的样式:

    <style name="AppTheme" parent="AppBaseTheme">
        <item name="android:listDivider">@drawable/divider_bg</item>  
    </style>
    

    可以自己画个分隔线 shape。

    item 选择器

    给 item 设置一个 selector,设置 android:state_focused 不同时不同的背景,关键是要在 item 根布局设置

    android:focusable="true"
    android:clickable="true"
    android:focusableInTouchMode="true"
    

    item 有 EditText

    TextWatcher 问题

    EditText 添加 TextWatcher 后,每次执行刷新、添加数据之类的操作,即只要执行 onBindViewHolder 就会进入监听,导致数据错乱。

    解决方法时一开始先移除监听:

    holder.tvOrderGoodsQuantity.removeTextChangedListener((QunaitityWatcher) holder.tvOrderGoodsQuantity.getTag());
    

    然后做完数据操作后,再添加:

    QunaitityWatcher watcher = new QunaitityWatcher(holder.tvOrderGoodsQuantity, holder.tvOrderGoodsQuantity, mData, holder.tvOrderGoodsAmount);
    holder.tvOrderGoodsQuantity.addTextChangedListener(watcher);
    

    自定义 TextWatcher,把一些 View 作为参数传入,以免数据错乱,不然可能会传到其它 item 的 View 上。

    private class QunaitityWatcher implements TextWatcher {
        private EditText editText;
        private GoodModel model;
        private TextView tv;
        private EditText et;
        public QunaitityWatcher(EditText et, EditText editText, GoodModel model, TextView tv) {
            this.et = et;
            this.editText = editText;
            this.model = model;
            this.tv = tv;
    
            et.setOnFocusChangeListener(new View.OnFocusChangeListener() {
                @Override
                public void onFocusChange(View v, boolean hasFocus) {
                    if (!hasFocus)
                        et.setText(Utils.subZeroAndDot(model.getScatterGoodQuantity()));
                }
            });
        }
    
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    
        }
    
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
    
        }
    
        @Override
        public void afterTextChanged(Editable s) {
            String text = s.toString();
            if (!TextUtils.isEmpty(text) && Utils.isDouble(text) && !text.startsWith(".")) {
                double d = Double.parseDouble(text);
                if (d <= 0) {
                    editText.setText("1"); // 默认的 1 件
                    d = 1;
                } else {
                    int dotindex = text.indexOf(".");
                    if (dotindex > 0 && text.length() > dotindex + 3) {
                        s.delete(dotindex + 3, text.length());
                    } else {
                        d = Double.parseDouble(s.toString());
                    }
                }
                tv.setText("¥" + Utils.subZeroAndDot(d * model.getPrice()));
                model.setScatterGoodQuantity(d);
                EventBus.getDefault().post(new ChangeGoodQuantityEvent());
                refreshQuantityAndPrice();
            }
        }
    }
    

    Edittext 获取不到焦点

    因为 EditText 默认 FocusableInTouchMode 为 false,需设置

    et.setFocusableInTouchMode(true);
    et.requestFocus();
    

    是否到达最底部

    方法一:

    public static boolean isVisBottom(RecyclerView recyclerView){
        LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();  
        // 屏幕中最后一个可见子项的 position
        int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();  
        // 当前屏幕所看到的子项个数
        int visibleItemCount = layoutManager.getChildCount();  
        // 当前 RecyclerView 的所有子项个数
        int totalItemCount = layoutManager.getItemCount();  
        // RecyclerView 的滑动状态
        int state = recyclerView.getScrollState();  
        if(visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1 && state == recyclerView.SCROLL_STATE_IDLE){
            return true;
        } else {   
            return false;  
        }
    }
    

    方法二:

    public static boolean isSlideToBottom(RecyclerView recyclerView) {    
       if (recyclerView == null) return false;
       if (recyclerView.computeVerticalScrollExtent() + recyclerView.computeVerticalScrollOffset()
            >= recyclerView.computeVerticalScrollRange())   
         return true;  
       return false;
    }
    

    computeVerticalScrollExtent() 是当前屏幕显示的区域高度,computeVerticalScrollOffset() 是当前屏幕之前滑过的距离,而 computeVerticalScrollRange() 是整个 View 控件的高度。

    方法三:

    • RecyclerView.canScrollVertically(1) 的返回值表示是否能向上滚动,false 表示已经滚动到底部
    • RecyclerView.canScrollVertically(-1) 的返回值表示是否能向下滚动,false 表示已经滚动到顶部

    跨列

    复杂的不规则列(有的行显示的列数多,有的行显示的列数少,并且每列显示的内容页不一样),使用 GridLayoutManager.SpanSizeLookup 的相关功能实现,新建 GridLayoutManager 的时候列数填写所有可能列数的最小公倍数。再结合 adapter 中的:

    GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), 2);
    layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            // 设置对应 position 位置的 item 的跨列数,比如第一行占两列,其他行每个 Item 占一列
            return position == 0 ? 2 : 1;
        }
    });
    recyclerView.setLayoutManager(layoutManager);
    

    SnapHelper

    Android 24.2.0 的 support 包中添加了 SnapHelper,SnapHelper 是对 RecyclerView 的拓展,旨在支持 RecyclerView 的对齐方式,也就是通过计算对齐 RecyclerView 中 TargetView 的指定点或者容器中的任何像素点。

    SnapHelper 继承自 RecyclerView.OnFlingListener,并实现抽象方法 onFling,支持 SnapHelper 的 RecyclerView.LayoutManager 必须实现 RecyclerView.SmoothScroller.ScrollVectorProvider 接口,或者自己实现 onFling 方法手动处理。SnapHelper 有以下几个重要方法:

    • attachToRecyclerView: 将 SnapHelper attach 到指定的 RecyclerView 上。
    • calculateDistanceToFinalSnap: 复写这个方法计算对齐到 TargetView 或容器指定点的距离,这是一个抽象方法,由子类自己实现,返回的是一个长度为 2 的 int 数组 out,out[0] 是 x 方向对齐要移动的距离,out[1] 是 y 方向对齐要移动的距离。
    • calculateScrollDistance: 根据每个方向给定的速度估算滑动的距离,用于 Fling 操作。
    • findSnapView: 提供一个指定的目标 View 来对齐,是抽象方法,需要子类实现。
    • findTargetSnapPosition: 提供一个用于对齐的 Adapter 目标 position,是抽象方法,需要子类实现。
    • onFling: 根据给定的 x 和 y 轴上的速度处理 Fling。

    LinearSnapHelper,PagerSnapHelper

    SnapHelper 是一个抽象类,要使用 SnapHelper,需要实现它的几个方法。而 Google 内置了两个默认实现类,LinearSnapHelper 和 PagerSnapHelper,LinearSnapHelper 可以使 RecyclerView 的当前 Item 居中显示(横向和竖向都支持),PagerSnapHelper 使 RecyclerView 像 ViewPager 一样,每次只能滑动一页(LinearSnapHelper 支持快速滑动),PagerSnapHelper 也是 Item 居中对齐。

    LinearSnapHelper 使当前 Item 居中显示,并且显示前一页和后一页的部分。常用场景是横向的 RecyclerView,类似 ViewPager 效果,但是又可以快速滑动(滑动多页)。

    LinearLayoutManager manager = new LinearLayoutManager(getContext());
    manager.setOrientation(LinearLayoutManager.HORIZONTAL);
    mRecyclerView.setLayoutManager(manager);
    // 将 SnapHelper attach 到 RecyclrView
    LinearSnapHelper snapHelper = new LinearSnapHelper();
    snapHelper.attachToRecyclerView(mRecyclerView);
    

    PagerSnapHelper 是 Android 25.1.0 support 包加入的,展示效果和 LineSnapHelper 一样,只是 PagerSnapHelper 限制一次只能滑动一页,不能快速滑动。

    PagerSnapHelper snapHelper = new PagerSnapHelper();
    snapHelper.attachToRecyclerView(mRecyclerView);
    

    SnapHelper 分析

    1. attachToRecyclerView:将 SnapHelper attach 到 RecyclerView

      /**
       *
       * 1. 首先判断 attach 的 RecyclerView 和原来是否是一样的,一样则返回,不一样则替换
       * 2. 如果不是同一个 RecyclerView,将原来设置的回调全部 remove 或者设置为 null
       * 3. Attach 的 RecyclerView 不为 null,先设置滑动的回调和 Fling 操作的回调,然后
       *    初始化一个 Scroller 用于后面做滑动处理,然后调用 snapToTargetExistingView
       */
      public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
          throws IllegalStateException {
          if (mRecyclerView == recyclerView) {
              return; // nothing to do
          }
          if (mRecyclerView != null) {
              destroyCallbacks();
          }
          mRecyclerView = recyclerView;
          if (mRecyclerView != null) {
              setupCallbacks();
              mGravityScroller = new Scroller(mRecyclerView.getContext(),
                  new DecelerateInterpolator());
              snapToTargetExistingView();
          }
      }
      
    2. snapToTargetExistingView:用于第一次 Attach 到 RecyclerView 时对齐 TargetView,或者当 Scroll 被触发的时候和 Fling 操作的时候对齐 TargetView。在 attachToRecyclerView 和 onScrollStateChanged 中都调用了这个方法

      /**
       * 1. 判断 RecyclerView 和 LayoutManager 是否为 null
       * 2. 调用 findSnapView 方法来获取需要对齐的目标 View(这是个抽象方法,需要子类实现)
       * 3. 通过 calculateDistanceToFinalSnap 获取 x 方向和 y 方向对齐需要移动的距离
       * 4. 最后通过 RecyclerView 的 smoothScrollBy 来移动对齐
       */
      void snapToTargetExistingView() {
          if (mRecyclerView == null) {
              return;
          }
          LayoutManager layoutManager = mRecyclerView.getLayoutManager();
          if (layoutManager == null) {
              return;
          }
          View snapView = findSnapView(layoutManager);
          if (snapView == null) {
              return;
          }
          int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
          if (snapDistance[0] != 0 || snapDistance[1] != 0) {
              mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
          }
      }
      
    3. onFling:SnapHelper 继承了 RecyclerView.OnFlingListener,实现了 onFling 方法进行对齐

      /**
       * 真正的对齐逻辑在 snapFromFling 里
       */
      @Override
      public boolean onFling(int velocityX, int velocityY) {
          LayoutManager layoutManager = mRecyclerView.getLayoutManager();
          if (layoutManager == null) {
              return false;
          }
          RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
          if (adapter == null) {
              return false;
          }
          int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
          return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                  && snapFromFling(layoutManager, velocityX, velocityY);
      }
      
      private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) {
          // 如果 LayoutManager 没有实现 ScrollVectorProvider 接口直接返回
          if (!(layoutManager instanceof ScrollVectorProvider)) {
              return false;
          }
          // 创建一个 SmoothScroller 用来做滑动到指定位置
          RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
          if (smoothScroller == null) {
              return false;
          }
          // 根据 x 和 y 方向的速度来获取需要对齐的 View 的位置,需要子类实现。
          int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
          if (targetPosition == RecyclerView.NO_POSITION) {
              return false;
          }
          // 最终通过 SmoothScroller 来滑动到指定位置
          smoothScroller.setTargetPosition(targetPosition);
          layoutManager.startSmoothScroll(smoothScroller);
          return true;
      }
      

    通过上面三个方法就实现了 SnapHelper 的对齐,只是有几个抽象方法是没有实现的,具体的对齐规则交给子类去实现。

    下面看一下 LinearSnapHelper 是怎么实现居中对齐的。

    1. calculateDistanceToFinalSnap: 计算最终对齐要移动的距离,返回一个长度为 2 的 int 数组out

      @Override
      public int[] calculateDistanceToFinalSnap(
              @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
          int[] out = new int[2];
          // 如果是水平方向滚动的,则计算水平方向需要移动的距离,否则水平方向的移动距离为 0
          if (layoutManager.canScrollHorizontally()) {
              out[0] = distanceToCenter(layoutManager, targetView,
                      getHorizontalHelper(layoutManager));
          } else {
              out[0] = 0;
          }
          // 如果是竖直方向滚动的,则计算竖直方向需要移动的距离,否则竖直方向的移动距离为 0
          if (layoutManager.canScrollVertically()) {
              out[1] = distanceToCenter(layoutManager, targetView,
                      getVerticalHelper(layoutManager));
          } else {
              out[1] = 0;
          }
          return out;
      }
      
      // 计算水平或者竖直方向需要移动的距离
      private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
              @NonNull View targetView, OrientationHelper helper) {
          final int childCenter = helper.getDecoratedStart(targetView) +
                  (helper.getDecoratedMeasurement(targetView) / 2);
          final int containerCenter;
          if (layoutManager.getClipToPadding()) {
              containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
          } else {
              containerCenter = helper.getEnd() / 2;
          }
          return childCenter - containerCenter;
      }
      
    2. findSnapView: 找到要对齐的 View

      // 找到要对齐的目标 View, 最终的逻辑在 findCenterView 方法里
      // 规则是:遍历 LayoutManager 的所有子元素,计算每个 childView 的
      //中点距离 Parent 的中点,找到距离最近的一个,就是需要居中对齐的目标 View
      @Override
      public View findSnapView(RecyclerView.LayoutManager layoutManager) {
          if (layoutManager.canScrollVertically()) {
              return findCenterView(layoutManager, getVerticalHelper(layoutManager));
          } else if (layoutManager.canScrollHorizontally()) {
              return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
          }
          return null;
      }     
      
    3. findTargetSnapPosition: 找到需要对齐的目标 View 的 Position

      @Override
      public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
          ...
      
          int vDeltaJump, hDeltaJump;
          // 如果是水平方向滚动的列表,估算出水平方向 SnapHelper 响应 fling
          // 对齐要滑动的 position 和当前 position 的差,否则水平方向滚动的差值为 0
          if (layoutManager.canScrollHorizontally()) {
              hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                      getHorizontalHelper(layoutManager), velocityX, 0);
              if (vectorForEnd.x < 0) {
                  hDeltaJump = -hDeltaJump;
              }
          } else {
              hDeltaJump = 0;
          }
          // 如果是竖直方向滚动的列表,估算出竖直方向 SnapHelper 响应 fling
          // 对齐要滑动的 position 和当前 position 的差,否则竖直方向滚动的差值为 0
          if (layoutManager.canScrollVertically()) {
              vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                      getVerticalHelper(layoutManager), 0, velocityY);
              if (vectorForEnd.y < 0) {
                  vDeltaJump = -vDeltaJump;
              }
          } else {
              vDeltaJump = 0;
          }
      
          // 最终要滑动的 position 就是当前的 Position 加上上面算出来的差值。
      
          ...
      }
      

    自定义 SnapHelper

    更改 LinearSnapHelper 的对齐规则,更改为开始对齐(计算目标 View 到 Parent start 要滑动的距离),其他的逻辑和 LinearSnapHelper 一样的。

    public class StartSnapHelper extends LinearSnapHelper {
    
        private OrientationHelper mHorizontalHelper, mVerticalHelper;
    
        @Nullable
        @Override
        public int[] calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView) {
            int[] out = new int[2];
            if (layoutManager.canScrollHorizontally()) {
                // 不再是 distanceToCenter
                out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
            } else {
                out[0] = 0;
            }
            if (layoutManager.canScrollVertically()) {
                out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager));
            } else {
                out[1] = 0;
            }
            return out;
        }
    
        private int distanceToStart(View targetView, OrientationHelper helper) {
            return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
        }
    
        @Nullable
        @Override
        public View findSnapView(RecyclerView.LayoutManager layoutManager) {
            if (layoutManager instanceof LinearLayoutManager) {
    
                if (layoutManager.canScrollHorizontally()) {
                    return findStartView(layoutManager, getHorizontalHelper(layoutManager));
                } else {
                    return findStartView(layoutManager, getVerticalHelper(layoutManager));
                }
            }
            return super.findSnapView(layoutManager);
        }
    
        private View findStartView(RecyclerView.LayoutManager layoutManager,
                                  OrientationHelper helper) {
            if (layoutManager instanceof LinearLayoutManager) {
                int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
                // 需要判断是否是最后一个 Item,如果是最后一个则不让对齐,以免出现最后一个显示不完全。
                boolean isLastItem = ((LinearLayoutManager) layoutManager)
                        .findLastCompletelyVisibleItemPosition()
                        == layoutManager.getItemCount() - 1;
    
                if (firstChild == RecyclerView.NO_POSITION || isLastItem) {
                    return null;
                }
    
                View child = layoutManager.findViewByPosition(firstChild);
    
                if (helper.getDecoratedEnd(child) >= helper.getDecoratedMeasurement(child) / 2
                        && helper.getDecoratedEnd(child) > 0) {
                    return child;
                } else {
                    if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()
                            == layoutManager.getItemCount() - 1) {
                        return null;
                    } else {
                        return layoutManager.findViewByPosition(firstChild + 1);
                    }
                }
            }
            return super.findSnapView(layoutManager);
        }
    
        private OrientationHelper getHorizontalHelper(
                @NonNull RecyclerView.LayoutManager layoutManager) {
            if (mHorizontalHelper == null) {
                mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
            }
            return mHorizontalHelper;
        }
    
        private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
            if (mVerticalHelper == null) {
                mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
            }
            return mVerticalHelper;
        }
    }
    

    嵌套在 NestedScrollView

    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    layoutManager.setSmoothScrollbarEnabled(true);
    layoutManager.setAutoMeasureEnabled(true);
    recyclerView.setLayoutManager(layoutManager);
    recyclerView.setHasFixedSize(true);
    recyclerView.setNestedScrollingEnabled(false);
    

    DiffUtil

    notifyDataSetChanged 是全量的刷新,且无法应用 ItemAnimator,而 notifyItemXXX 之类的方法使用场景有限,不适合整体的数据更新。

    support-v7:24.2.0 中新增了工具类 DiffUtil,用来比较两个数据集,寻找出旧数据集/新数据集的最小变化量。

    它会自动计算新老数据集的差异,并根据差异情况,自动调用 notifyItemXXX 之类的方法。

    基本用法:

    1. 自定义 DiffUtil.Callback 的子类

      public class DiffCallBack extends DiffUtil.Callback {
          private List<TestBean> mOldDatas, mNewDatas;
      
          public DiffCallBack(List<TestBean> mOldDatas, List<TestBean> mNewDatas) {
              this.mOldDatas = mOldDatas;
              this.mNewDatas = mNewDatas;
          }
      
          @Override
          public int getOldListSize() {
              return mOldDatas != null ? mOldDatas.size() : 0;
          }
      
          @Override
          public int getNewListSize() {
              return mNewDatas != null ? mNewDatas.size() : 0;
          }
      
          /**
           * DiffUtil 调用它来判断两个 Item 是否相等
           * 如果明确两个 Item 不一样,直接返回 false,比如多布局,布局都变了肯定不一样
           */
          @Override
          public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
              // 具体情况用具体的逻辑
              return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName());
          }
      
          /**
           * areItemsTheSame() 返回 true 时才调用
           *
           * DiffUtil 调用它来检查两个 Item 是否含有相同的数据
           * 用返回值来检测当前 Item 的内容是否发生了变化,根据 UI 需求来改变它的返回值
           * 如果用 RecyclerView.Adapter 配合 DiffUtil 使用,需要返回 Item 的视觉表现是否相同
           * 如果返回 true,就不会刷新
           */
          @Override
          public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
              TestBean beanOld = mOldDatas.get(oldItemPosition);
              TestBean beanNew = mNewDatas.get(newItemPosition);
              if (!beanOld.getDesc().equals(beanNew.getDesc())) {
                  return false; // 如果有内容不同,就返回 false
              }
              if (beanOld.getPic() != beanNew.getPic()) {
                  return false; // 如果有内容不同,就返回 false
              }
              return true; // 默认两个 data 内容是相同的
          }
      }
      
    2. 刷新数据

      // 计算新旧数据的差异,第二个参数表示是否检测 Item 的移动
      DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
      
      // 新的刷新方法,内部调用了 notifyItemXXX 之类的方法
      diffResult.dispatchUpdatesTo(mAdapter);
      
      // 更新数据
      mDatas = newDatas;
      

    进阶用法:

    1. 实现 DiffUtil.Callback 的 getChangePayload

      payload 是一个用来描述 Item 变化的对象,即 Item 发生了哪些变化,这些变化就封装成一个 payload。

      /**
       * 当 areItemsTheSame 返回 true 且 areContentsTheSame 返回 false 会调用这个方法
       * 表示数据有局部变化,所以刷新时也局部刷新
       * 刷新会使用 notifyItemChanged 或 notifyItemRangeChanged
       * DiffUtils 就会调用这个方法,假如配合 RecyclerView,可以返回这个 Item 改变的那些字段
       * 然后 RecyclerView 的 ItemAnimator 会用这些信息执行正确的动画
       * 默认返回 null,自定义返回代表新旧 Item 改变的内容的 payload 对象
       */
      @Nullable
      @Override
      public Object getChangePayload(int oldItemPosition, int newItemPosition) {
          TestBean oldBean = mOldDatas.get(oldItemPosition);
          TestBean newBean = mNewDatas.get(newItemPosition);
      
          Bundle payload = new Bundle();
          if (!oldBean.getDesc().equals(newBean.getDesc())) {
              // desc 这个内容变了,加到 payload 中
              payload.putString("KEY_DESC", newBean.getDesc());
          }
          if (oldBean.getPic() != newBean.getPic()) {
              // pic 这个内容变了,加到 payload 中
              payload.putInt("KEY_PIC", newBean.getPic());
          }
      
          if (payload.size() == 0)
              return null;
          return payload;
      }
      

      RecyclerView 的 Adapter 有一个方法 onBindViewHolder(VH holder, int position, List<Object> payloads)

      如果 payloads 不为空,那么当前绑定了旧数据的 ViewHolder 和 Adapter 使用 payload 进行局部更新。如果 payload 为空,Adapter 则进行一次完整的更新(调用两参方法)。

      payloads 对象不会为 null,但可能是 empty,所以需要判断一下。

      @Override
      public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
          if (payloads.isEmpty()) { // 完整更新
              onBindViewHolder(holder, position);
          } else { // 局部更新
              // 取出 getChangePayload 方法里返回的 payload
              Bundle payload = (Bundle) payloads.get(0);
              TestBean bean = mDatas.get(position);
              for (String key : payload.keySet()) {
                  switch (key) {
                      case "KEY_DESC":
                          // 可以用 payload 里的数据,不过 data 也是新的,也可以用
                          holder.tv2.setText(bean.getDesc());
                          break;
                      case "KEY_PIC":
                          holder.iv.setImageResource(payload.getInt(key));
                          break;
                      default:
                          break;
                  }
              }
          }
      }
      
    2. DiffUtil.calculateDiff

      这个 DiffUtil 使用的是 Eugene Myers 的差别算法,这个算法本身不能检查到元素的移动,也就是移动只能被算作先删除、再增加,而 DiffUtil 是在算法的结果后再进行一次移动检查。检测元素移动会使时间复杂度变成平方级,所以如果集合本身就已经排好序,可以不进行移动的检测提升效率。

      DiffUtil.calculateDiff 的第二个参数表示检测移动,当数据很多时,开启这个会很耗费性能。所以数据量很大时,将这个方法放入子线程

      // 这句放到子线程执行
      DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true);
      // 利用 Handler 之类,将 diffResult 发送到主线程
      

      然后在主线程执行刷新操作

      diffResult.dispatchUpdatesTo(mAdapter);
      mDatas = mNewDatas;
      

    Paging

    添加依赖 implementation "android.arch.paging:runtime:1.0.1"

    DataSource

    数据源,可以从网络获取或从本地获取要显示的数据。

    DataSource 有三个抽象子类 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource,它们分别有一个子类 WrapperItemKeyedDataSource、WrapperPageKeyedDataSource、WrapperPositionalDataSource。

    • ItemKeyedDataSource<Key, Value>:适用于目标数据的加载依赖特定 item 的信息,比如需要根据第 N 项的信息加载第 N+1 项的数据,Key 中包含了第 N 项的信息。
    • PageKeyedDataSource<Key, Value>:适用于目标数据根据页信息请求数据的场景,即 Key 字段是页相关的信息,而不是前一个 item 的信息。
    • PositionalDataSource<T>:适用于目标数据总数固定,通过特定的位置加载数据的情况,T 是 Value,隐含的 Key 就是位置信息。

    以 Wrap 开头的三个类,从名字和源码可以看出就是一个装饰,构造方法传入被装饰的那个类和一个 Function,除了加载数据的方法外都是直接委托给被装饰类,加载数据的方法将 Value 经过 Function 进行一种转换再调用被装饰类的方法。

    class MyDataSource : PageKeyedDataSource<Int, String>() {
    
        // 开始加载的数据
        override fun loadInitial(params: PageKeyedDataSource.LoadInitialParams<Int>, callback: PageKeyedDataSource.LoadInitialCallback<Int, String>) {
            // 没有前一页,所以第二个参数为 null,第三个参数指定下一页的 key
            callback.onResult(getData(0, params.requestedLoadSize), null, 1)
        }
    
        // 加载上一页的数据
        override fun loadBefore(params: PageKeyedDataSource.LoadParams<Int>, callback: PageKeyedDataSource.LoadCallback<Int, String>) {
            callback.onResult(getData(params.key, params.requestedLoadSize), params.key - 1)
        }
    
        // 加载下一页的数据
        override fun loadAfter(params: PageKeyedDataSource.LoadParams<Int>, callback: PageKeyedDataSource.LoadCallback<Int, String>) {
            callback.onResult(getData(params.key, params.requestedLoadSize), params.key + 1)
        }
    
        private fun getData(page: Int, size: Int): List<String> {
            val data = ArrayList<String>()
            (0..size).forEach {
                data.add("page $page item $it")
            }
            return data
        }
    }
    

    loadInitial() 方法的第一个参数 LoadInitialParams 内有两个属性:

    • requestedLoadSize 要加载的数据个数
    • placeholdersEnabled 没有数据时是否允许显示占位

    LoadParams 的 key 属性在这里就代表页,也是 callback.onResult 的最后一个参数传进去的。

    PageList

    负责从 DataSource 取出数据,第一次显示多少数据,之后每次加载多少数据。

    class MyDataSourceFactory : DataSource.Factory<Int, String>() {
        // 通过工厂生产一个数据源对象
        override fun create(): DataSource<Int, String> = MyDataSource()
    }
    
    val data = LivePagedListBuilder(MyDataSourceFactory(), PagedList.Config.Builder()
                    .setPageSize(10) // 每页 10 条
                    .setEnablePlaceholders(true)
                    .setInitialLoadSizeHint(20) // 最初加载 20 条
                    .build()).build()
    

    PagedList.Config.Builder 里有四个属性:

    • mPageSize 每页加载数目
    • mPrefetchDistance 距底部还有多少距离开始下一次加载
    • mInitialLoadSizeHint 第一次加载多少数据,LoadInitialParams 的 requestedLoadSize
    • mEnablePlaceholders 数据为空时是否显示占位,默认 true

    PagedListAdapter 刷新数据

    class MyViewHolder(val view : View) : RecyclerView.ViewHolder(view) {
        val tv : TextView = view as TextView
    }
    
    class MyPagingAdapter : PagedListAdapter<String, MyViewHolder>(DIFF_CALLBACK) {
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val tv = TextView(parent.context)
            tv.textColor = Color.BLACK
            tv.textSize = 12f
    
            return MyViewHolder(tv)
        }
        
        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            // getItem 获取字符串
            holder.tv.text = getItem(position)
        }
    
        companion object {
            private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
                override fun areItemsTheSame(oldConcert: String, newConcert: String) = oldConcert == newConcert
                override fun areContentsTheSame(oldConcert: String, newConcert: String) = oldConcert == newConcert
            }
        }
    }
    

    Activity 中调用

    recyclerView.layoutManager = LinearLayoutManager(this)
    val adapter = MyPagingAdapter()
    recyclerView.addItemDecoration(DividerItemDecoration(ctx, DividerItemDecoration.VERTICAL))
    recyclerView.adapter = adapter
    
    val data = LivePagedListBuilder(MyDataSourceFactory(),
                PagedList.Config.Builder()
                        .setPageSize(10) // 每页 10 条
                        .setEnablePlaceholders(true)
                        .setInitialLoadSizeHint(20) // 最初加载 20 条
                        .build()
        ).build()
    
    data.observe(this, Observer {
        adapter.submitList(it)
    })
    

    结果显示第一页显示 20 条,以后没页加载 10 条。

    参考:

    相关文章

      网友评论

        本文标题:RecyclerView 使用总结

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