RecyclerView 使用总结

主要是在使用 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 条。

2018_08_30_15_02_05.gif

ItemTouchHelper 侧滑与拖动

ItemTouchHelper.Callback

是 ItemTouchHelper 的内部静态抽象类。其中三个抽象方法:

/**
 * 指定可以拖拽(drag)的方向
 *      ItemTouchHelper.UP、ItemTouchHelper.DOWN
 *      ItemTouchHelper.LEFT、ItemTouchHelper.RIGHT
 *      列表只有 UP 和 DOWN,网格还有 LEFT 和 RIGHT
 * 滑动(swipe)的方向
 *      ItemTouchHelper.START、ItemTouchHelper.END
 *      
 * 若返回 0,表示不触发滑动 or 拖拽
 */
public abstract int getMovementFlags(RecyclerView recyclerView,
        ViewHolder viewHolder);
        
/**
 * 拖拽到新位置时回调
 * @param viewHolder 拖动的 ViewHolder
 * @param target     目标位置的 ViewHolder
 */
public abstract boolean onMove(RecyclerView recyclerView,
        ViewHolder viewHolder, ViewHolder target);
        
/**
 * 针对swipe状态,swipe 到达滑动消失的距离回调函数,一般在这个函数里面处理删除 item 的逻辑。确切的来讲是 swipe item 滑出屏幕动画结束的时候调用
 * @param viewHolder 滑动的 ViewHolder
 * @param direction  滑动的方向
 */
public abstract void onSwiped(ViewHolder viewHolder, int direction);

两个方法控制是否支持:

/**
 * 是否支持长按拖拽,默认为 true,表示支持长按拖拽。
 * 也可以返回 false,手动调用 startDrag() 方法启动拖拽
 */
public boolean isLongPressDragEnabled() {
    return true;
}

/**
 * 是否支持任意位置触摸事件发生时启用滑动操作,默认为 true,表示支持滑动 
 * 也可以返回 false,手动调用 startSwipe() 方法启动滑动
 */
public boolean isItemViewSwipeEnabled() {
    return true;
}

其它一些方法:

/**
 * 针对 swipe 和 drag 状态,当 swipe 或者 drag 对应的 ViewHolder 改变的时候调用。可以改变样式之类
 * 可以通过重写这个函数获取到 swipe、drag 开始和结束时机,viewHolder 不为空的时候是开始,空的时候是结束
 * @param actionState 
 *         ACTION_STATE_IDLE:闲置状态
 *         ACTION_STATE_SWIPE:滑动状态
 *         ACTION_STATE_DRAG:拖拽状态
 */
public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
    if (viewHolder != null) {
        sUICallback.onSelected(viewHolder.itemView);
    }
}

/**
 * 针对 swipe 和 drag 状态,当一个 item view 在 swipe、drag 状态结束的时候调用,可以恢复原来的状态
 * drag 状态:当手指释放的时候会调用
 * swipe 状态:当 item 从 RecyclerView 中删除的时候调用,一般会在 onSwiped() 函数里面删除掉指定的 item view
 */
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
    sUICallback.clearView(viewHolder.itemView);
}

/**
 * 针对 drag 状态,当前 target 对应的 item 是否允许移动
 */
public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
        ViewHolder target) {
    return true;
}

/**
 * 针对 drag 状态,当 drag ItemView 和底下 ItemView 重叠时,可以给drag ItemView 设置一个 Margin 值
 * 让重叠不容易发生,相当于增大了 drag Item 的区域
 */
public int getBoundingBoxMargin() {
    return 0;
}

/**
 * 针对 swipe 状态,swipe 滑动的位置超过了百分之多少就消失
 */
public float getSwipeThreshold(ViewHolder viewHolder) {
    return .5f;
}

/**
 * 针对 drag 状态,当滑动超过多少就可以触发 onMove() 方法
 */
public float getMoveThreshold(ViewHolder viewHolder) {
    return .5f;
}

/**
 * 针对 swipe 状态,swipe 的逃逸速度,换句话说就算没达到getSwipeThreshold 设置的距离,达到了这个逃逸速度 item 也会触发 onSwiped
 */
public float getSwipeEscapeVelocity(float defaultValue) {
    return defaultValue;
}

使用

class ItemTouchHelperActivity : AppCompatActivity() {

    lateinit var list : MutableList<Int>
    lateinit var adapter : MyAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_item_touch_helper)

        list = mutableListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        recycler.layoutManager = LinearLayoutManager(this)
        adapter = MyAdapter(list)
        recycler.adapter = adapter
        recycler.addItemDecoration(DividerItemDecoration(ctx, DividerItemDecoration.VERTICAL))

        val callback = object : ItemTouchHelper.Callback() {
            override fun getMovementFlags(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?): Int {
                val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN //允许上下的拖动
                val swipeFlags = ItemTouchHelper.LEFT //只允许从右向左侧滑
                return makeMovementFlags(dragFlags, swipeFlags)
            }

            override fun onMove(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
                adapter.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition)
                return true
            }

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                val position = viewHolder.adapterPosition
                list.removeAt(position)
                adapter.notifyItemRemoved(position)
            }

        }
        ItemTouchHelper(callback).attachToRecyclerView(recycler)
    }

    class MyAdapter(val list : MutableList<Int>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
        override fun getItemCount(): Int = list.size

        override fun onBindViewHolder(holder: MyAdapter.ViewHolder, position: Int) {
            holder.tv.text = "值=${list[position]},position=$position"
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyAdapter.ViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.item_touch_helper, parent, false)
            return ViewHolder(view)
        }

        class ViewHolder(view : View) : RecyclerView.ViewHolder(view) {
            val tv = view as TextView
        }
    }
}

ItemTouchHelper.SimpleCallback

同样是抽象类,继承了 Callback。应用方法类似:

val callback2 = object : ItemTouchHelper.SimpleCallback(
        ItemTouchHelper.UP or ItemTouchHelper.DOWN,
        ItemTouchHelper.LEFT) {
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        val position = viewHolder.adapterPosition
        list.removeAt(position)
        adapter.notifyItemRemoved(position)
    }

    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        adapter.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition)
        return true
    }

}
ItemTouchHelper(callback2).attachToRecyclerView(recycler)

参考:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容