RecyclerView 使用總結(jié)

主要是在使用 RecyclerView 過程中遇到的細(xì)碎問題和解決方案妻柒。

簡單使用

  • LinearLayoutManager 線性管理器(支持橫向茴她、縱向)
  • GridLayoutManager 網(wǎng)格布局管理器
  • StaggeredGridLayoutManager 瀑布流式布局管理器
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 如果可以確定每個 item 的高度是固定的鼠冕,設(shè)置這個選項可以提高性能
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() 方法,返回枚舉類型的序數(shù)
        // 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) {
        // 根據(jù)不同 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徐块,重寫回調(diào)方法

// 線性布局用
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);
    }

    // 設(shè)置屏幕的方向
    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());
        }
    }
}
// 網(wǎng)格或瀑布流布局用
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) {
        // 列數(shù)
        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 設(shè)置一個 selector硕舆,設(shè)置 android:state_focused 不同時不同的背景秽荞,關(guān)鍵是要在 item 根布局設(shè)置

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

item 有 EditText

TextWatcher 問題

EditText 添加 TextWatcher 后,每次執(zhí)行刷新抚官、添加數(shù)據(jù)之類的操作扬跋,即只要執(zhí)行 onBindViewHolder 就會進(jìn)入監(jiān)聽,導(dǎo)致數(shù)據(jù)錯亂凌节。

解決方法時一開始先移除監(jiān)聽:

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

然后做完數(shù)據(jù)操作后钦听,再添加:

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

自定義 TextWatcher,把一些 View 作為參數(shù)傳入倍奢,以免數(shù)據(jù)錯亂朴上,不然可能會傳到其它 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"); // 默認(rèn)的 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 默認(rèn) FocusableInTouchMode 為 false卒煞,需設(shè)置

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

是否到達(dá)最底部

方法一:

public static boolean isVisBottom(RecyclerView recyclerView){
    LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();  
    // 屏幕中最后一個可見子項的 position
    int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();  
    // 當(dāng)前屏幕所看到的子項個數(shù)
    int visibleItemCount = layoutManager.getChildCount();  
    // 當(dāng)前 RecyclerView 的所有子項個數(shù)
    int totalItemCount = layoutManager.getItemCount();  
    // RecyclerView 的滑動狀態(tài)
    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() 是當(dāng)前屏幕顯示的區(qū)域高度痪宰,computeVerticalScrollOffset() 是當(dāng)前屏幕之前滑過的距離,而 computeVerticalScrollRange() 是整個 View 控件的高度畔裕。

方法三:

  • RecyclerView.canScrollVertically(1) 的返回值表示是否能向上滾動衣撬,false 表示已經(jīng)滾動到底部
  • RecyclerView.canScrollVertically(-1) 的返回值表示是否能向下滾動,false 表示已經(jīng)滾動到頂部

跨列

復(fù)雜的不規(guī)則列(有的行顯示的列數(shù)多扮饶,有的行顯示的列數(shù)少具练,并且每列顯示的內(nèi)容頁不一樣),使用 GridLayoutManager.SpanSizeLookup 的相關(guān)功能實現(xiàn)甜无,新建 GridLayoutManager 的時候列數(shù)填寫所有可能列數(shù)的最小公倍數(shù)扛点。再結(jié)合 adapter 中的:

GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), 2);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        // 設(shè)置對應(yīng) position 位置的 item 的跨列數(shù)哥遮,比如第一行占兩列,其他行每個 Item 占一列
        return position == 0 ? 2 : 1;
    }
});
recyclerView.setLayoutManager(layoutManager);

SnapHelper

Android 24.2.0 的 support 包中添加了 SnapHelper占键,SnapHelper 是對 RecyclerView 的拓展昔善,旨在支持 RecyclerView 的對齊方式,也就是通過計算對齊 RecyclerView 中 TargetView 的指定點或者容器中的任何像素點畔乙。

SnapHelper 繼承自 RecyclerView.OnFlingListener君仆,并實現(xiàn)抽象方法 onFling,支持 SnapHelper 的 RecyclerView.LayoutManager 必須實現(xiàn) RecyclerView.SmoothScroller.ScrollVectorProvider 接口牲距,或者自己實現(xiàn) onFling 方法手動處理返咱。SnapHelper 有以下幾個重要方法:

  • attachToRecyclerView: 將 SnapHelper attach 到指定的 RecyclerView 上。
  • calculateDistanceToFinalSnap: 復(fù)寫這個方法計算對齊到 TargetView 或容器指定點的距離牍鞠,這是一個抽象方法咖摹,由子類自己實現(xiàn),返回的是一個長度為 2 的 int 數(shù)組 out难述,out[0] 是 x 方向?qū)R要移動的距離萤晴,out[1] 是 y 方向?qū)R要移動的距離。
  • calculateScrollDistance: 根據(jù)每個方向給定的速度估算滑動的距離胁后,用于 Fling 操作店读。
  • findSnapView: 提供一個指定的目標(biāo) View 來對齊,是抽象方法攀芯,需要子類實現(xiàn)屯断。
  • findTargetSnapPosition: 提供一個用于對齊的 Adapter 目標(biāo) position,是抽象方法侣诺,需要子類實現(xiàn)殖演。
  • onFling: 根據(jù)給定的 x 和 y 軸上的速度處理 Fling刊侯。

LinearSnapHelper鲁纠,PagerSnapHelper

SnapHelper 是一個抽象類,要使用 SnapHelper俯在,需要實現(xiàn)它的幾個方法搔确。而 Google 內(nèi)置了兩個默認(rèn)實現(xiàn)類朋鞍,LinearSnapHelper 和 PagerSnapHelper,LinearSnapHelper 可以使 RecyclerView 的當(dāng)前 Item 居中顯示(橫向和豎向都支持)妥箕,PagerSnapHelper 使 RecyclerView 像 ViewPager 一樣滥酥,每次只能滑動一頁(LinearSnapHelper 支持快速滑動),PagerSnapHelper 也是 Item 居中對齊畦幢。

LinearSnapHelper 使當(dāng)前 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,將原來設(shè)置的回調(diào)全部 remove 或者設(shè)置為 null
     * 3. Attach 的 RecyclerView 不為 null际看,先設(shè)置滑動的回調(diào)和 Fling 操作的回調(diào)咸产,然后
     *    初始化一個 Scroller 用于后面做滑動處理,然后調(diào)用 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仲闽,或者當(dāng) Scroll 被觸發(fā)的時候和 Fling 操作的時候?qū)R TargetView脑溢。在 attachToRecyclerView 和 onScrollStateChanged 中都調(diào)用了這個方法

    /**
     * 1. 判斷 RecyclerView 和 LayoutManager 是否為 null
     * 2. 調(diào)用 findSnapView 方法來獲取需要對齊的目標(biāo) View(這是個抽象方法,需要子類實現(xiàn))
     * 3. 通過 calculateDistanceToFinalSnap 獲取 x 方向和 y 方向?qū)R需要移動的距離
     * 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赖欣,實現(xiàn)了 onFling 方法進(jìn)行對齊

    /**
     * 真正的對齊邏輯在 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 沒有實現(xiàn) ScrollVectorProvider 接口直接返回
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return false;
        }
        // 創(chuàng)建一個 SmoothScroller 用來做滑動到指定位置
        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }
        // 根據(jù) x 和 y 方向的速度來獲取需要對齊的 View 的位置屑彻,需要子類實現(xiàn)。
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
        // 最終通過 SmoothScroller 來滑動到指定位置
        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }
    

通過上面三個方法就實現(xiàn)了 SnapHelper 的對齊顶吮,只是有幾個抽象方法是沒有實現(xiàn)的社牲,具體的對齊規(guī)則交給子類去實現(xiàn)。

下面看一下 LinearSnapHelper 是怎么實現(xiàn)居中對齊的云矫。

  1. calculateDistanceToFinalSnap: 計算最終對齊要移動的距離,返回一個長度為 2 的 int 數(shù)組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

    // 找到要對齊的目標(biāo) View, 最終的邏輯在 findCenterView 方法里
    // 規(guī)則是:遍歷 LayoutManager 的所有子元素巡揍,計算每個 childView 的
    //中點距離 Parent 的中點,找到距離最近的一個菌瘪,就是需要居中對齊的目標(biāo) 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: 找到需要對齊的目標(biāo) View 的 Position

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
        ...
    
        int vDeltaJump, hDeltaJump;
        // 如果是水平方向滾動的列表腮敌,估算出水平方向 SnapHelper 響應(yīng) fling
        // 對齊要滑動的 position 和當(dāng)前 position 的差,否則水平方向滾動的差值為 0
        if (layoutManager.canScrollHorizontally()) {
            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0);
            if (vectorForEnd.x < 0) {
                hDeltaJump = -hDeltaJump;
            }
        } else {
            hDeltaJump = 0;
        }
        // 如果是豎直方向滾動的列表俏扩,估算出豎直方向 SnapHelper 響應(yīng) fling
        // 對齊要滑動的 position 和當(dāng)前 position 的差糜工,否則豎直方向滾動的差值為 0
        if (layoutManager.canScrollVertically()) {
            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getVerticalHelper(layoutManager), 0, velocityY);
            if (vectorForEnd.y < 0) {
                vDeltaJump = -vDeltaJump;
            }
        } else {
            vDeltaJump = 0;
        }
    
        // 最終要滑動的 position 就是當(dāng)前的 Position 加上上面算出來的差值。
    
        ...
    }
    

自定義 SnapHelper

更改 LinearSnapHelper 的對齊規(guī)則录淡,更改為開始對齊(計算目標(biāo) 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嫉戚,如果是最后一個則不讓對齊刨裆,以免出現(xiàn)最后一個顯示不完全澈圈。
            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 是全量的刷新,且無法應(yīng)用 ItemAnimator帆啃,而 notifyItemXXX 之類的方法使用場景有限瞬女,不適合整體的數(shù)據(jù)更新。

support-v7:24.2.0 中新增了工具類 DiffUtil努潘,用來比較兩個數(shù)據(jù)集诽偷,尋找出舊數(shù)據(jù)集/新數(shù)據(jù)集的最小變化量。

它會自動計算新老數(shù)據(jù)集的差異慈俯,并根據(jù)差異情況渤刃,自動調(diào)用 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 調(diào)用它來判斷兩個 Item 是否相等
         * 如果明確兩個 Item 不一樣贴膘,直接返回 false卖子,比如多布局,布局都變了肯定不一樣
         */
        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            // 具體情況用具體的邏輯
            return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName());
        }
    
        /**
         * areItemsTheSame() 返回 true 時才調(diào)用
         *
         * DiffUtil 調(diào)用它來檢查兩個 Item 是否含有相同的數(shù)據(jù)
         * 用返回值來檢測當(dāng)前 Item 的內(nèi)容是否發(fā)生了變化刑峡,根據(jù) UI 需求來改變它的返回值
         * 如果用 RecyclerView.Adapter 配合 DiffUtil 使用洋闽,需要返回 Item 的視覺表現(xiàn)是否相同
         * 如果返回 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; // 如果有內(nèi)容不同突梦,就返回 false
            }
            if (beanOld.getPic() != beanNew.getPic()) {
                return false; // 如果有內(nèi)容不同诫舅,就返回 false
            }
            return true; // 默認(rèn)兩個 data 內(nèi)容是相同的
        }
    }
    
  2. 刷新數(shù)據(jù)

    // 計算新舊數(shù)據(jù)的差異,第二個參數(shù)表示是否檢測 Item 的移動
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
    
    // 新的刷新方法宫患,內(nèi)部調(diào)用了 notifyItemXXX 之類的方法
    diffResult.dispatchUpdatesTo(mAdapter);
    
    // 更新數(shù)據(jù)
    mDatas = newDatas;
    

進(jìn)階用法:

  1. 實現(xiàn) DiffUtil.Callback 的 getChangePayload

    payload 是一個用來描述 Item 變化的對象刊懈,即 Item 發(fā)生了哪些變化,這些變化就封裝成一個 payload娃闲。

    /**
     * 當(dāng) areItemsTheSame 返回 true 且 areContentsTheSame 返回 false 會調(diào)用這個方法
     * 表示數(shù)據(jù)有局部變化虚汛,所以刷新時也局部刷新
     * 刷新會使用 notifyItemChanged 或 notifyItemRangeChanged
     * DiffUtils 就會調(diào)用這個方法,假如配合 RecyclerView皇帮,可以返回這個 Item 改變的那些字段
     * 然后 RecyclerView 的 ItemAnimator 會用這些信息執(zhí)行正確的動畫
     * 默認(rèn)返回 null卷哩,自定義返回代表新舊 Item 改變的內(nèi)容的 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 這個內(nèi)容變了,加到 payload 中
            payload.putString("KEY_DESC", newBean.getDesc());
        }
        if (oldBean.getPic() != newBean.getPic()) {
            // pic 這個內(nèi)容變了属拾,加到 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 不為空,那么當(dāng)前綁定了舊數(shù)據(jù)的 ViewHolder 和 Adapter 使用 payload 進(jìn)行局部更新渐白。如果 payload 為空尊浓,Adapter 則進(jìn)行一次完整的更新(調(diào)用兩參方法)。

    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 里的數(shù)據(jù),不過 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 是在算法的結(jié)果后再進(jìn)行一次移動檢查。檢測元素移動會使時間復(fù)雜度變成平方級刨疼,所以如果集合本身就已經(jīng)排好序泉唁,可以不進(jìn)行移動的檢測提升效率。

    DiffUtil.calculateDiff 的第二個參數(shù)表示檢測移動揩慕,當(dāng)數(shù)據(jù)很多時亭畜,開啟這個會很耗費性能。所以數(shù)據(jù)量很大時迎卤,將這個方法放入子線程

    // 這句放到子線程執(zhí)行
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true);
    // 利用 Handler 之類拴鸵,將 diffResult 發(fā)送到主線程
    

    然后在主線程執(zhí)行刷新操作

    diffResult.dispatchUpdatesTo(mAdapter);
    mDatas = mNewDatas;
    

Paging

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

DataSource

數(shù)據(jù)源,可以從網(wǎng)絡(luò)獲取或從本地獲取要顯示的數(shù)據(jù)蜗搔。

DataSource 有三個抽象子類 ItemKeyedDataSource劲藐、PageKeyedDataSource、PositionalDataSource樟凄,它們分別有一個子類 WrapperItemKeyedDataSource聘芜、WrapperPageKeyedDataSource、WrapperPositionalDataSource缝龄。

  • ItemKeyedDataSource<Key, Value>:適用于目標(biāo)數(shù)據(jù)的加載依賴特定 item 的信息汰现,比如需要根據(jù)第 N 項的信息加載第 N+1 項的數(shù)據(jù),Key 中包含了第 N 項的信息叔壤。
  • PageKeyedDataSource<Key, Value>:適用于目標(biāo)數(shù)據(jù)根據(jù)頁信息請求數(shù)據(jù)的場景瞎饲,即 Key 字段是頁相關(guān)的信息,而不是前一個 item 的信息百新。
  • PositionalDataSource<T>:適用于目標(biāo)數(shù)據(jù)總數(shù)固定企软,通過特定的位置加載數(shù)據(jù)的情況庐扫,T 是 Value饭望,隱含的 Key 就是位置信息。

以 Wrap 開頭的三個類形庭,從名字和源碼可以看出就是一個裝飾铅辞,構(gòu)造方法傳入被裝飾的那個類和一個 Function,除了加載數(shù)據(jù)的方法外都是直接委托給被裝飾類萨醒,加載數(shù)據(jù)的方法將 Value 經(jīng)過 Function 進(jìn)行一種轉(zhuǎn)換再調(diào)用被裝飾類的方法斟珊。

class MyDataSource : PageKeyedDataSource<Int, String>() {

    // 開始加載的數(shù)據(jù)
    override fun loadInitial(params: PageKeyedDataSource.LoadInitialParams<Int>, callback: PageKeyedDataSource.LoadInitialCallback<Int, String>) {
        // 沒有前一頁,所以第二個參數(shù)為 null富纸,第三個參數(shù)指定下一頁的 key
        callback.onResult(getData(0, params.requestedLoadSize), null, 1)
    }

    // 加載上一頁的數(shù)據(jù)
    override fun loadBefore(params: PageKeyedDataSource.LoadParams<Int>, callback: PageKeyedDataSource.LoadCallback<Int, String>) {
        callback.onResult(getData(params.key, params.requestedLoadSize), params.key - 1)
    }

    // 加載下一頁的數(shù)據(jù)
    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() 方法的第一個參數(shù) LoadInitialParams 內(nèi)有兩個屬性:

  • requestedLoadSize 要加載的數(shù)據(jù)個數(shù)
  • placeholdersEnabled 沒有數(shù)據(jù)時是否允許顯示占位

LoadParams 的 key 屬性在這里就代表頁囤踩,也是 callback.onResult 的最后一個參數(shù)傳進(jìn)去的旨椒。

PageList

負(fù)責(zé)從 DataSource 取出數(shù)據(jù),第一次顯示多少數(shù)據(jù)堵漱,之后每次加載多少數(shù)據(jù)综慎。

class MyDataSourceFactory : DataSource.Factory<Int, String>() {
    // 通過工廠生產(chǎn)一個數(shù)據(jù)源對象
    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 每頁加載數(shù)目
  • mPrefetchDistance 距底部還有多少距離開始下一次加載
  • mInitialLoadSizeHint 第一次加載多少數(shù)據(jù),LoadInitialParams 的 requestedLoadSize
  • mEnablePlaceholders 數(shù)據(jù)為空時是否顯示占位勤庐,默認(rèn) true

PagedListAdapter 刷新數(shù)據(jù)

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 中調(diào)用

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)
})

結(jié)果顯示第一頁顯示 20 條示惊,以后每頁加載 10 條。

2018_08_30_15_02_05.gif

ItemTouchHelper 側(cè)滑與拖動

ItemTouchHelper.Callback

是 ItemTouchHelper 的內(nèi)部靜態(tài)抽象類愉镰。其中三個抽象方法:

/**
 * 指定可以拖拽(drag)的方向
 *      ItemTouchHelper.UP米罚、ItemTouchHelper.DOWN
 *      ItemTouchHelper.LEFT、ItemTouchHelper.RIGHT
 *      列表只有 UP 和 DOWN丈探,網(wǎng)格還有 LEFT 和 RIGHT
 * 滑動(swipe)的方向
 *      ItemTouchHelper.START录择、ItemTouchHelper.END
 *      
 * 若返回 0,表示不觸發(fā)滑動 or 拖拽
 */
public abstract int getMovementFlags(RecyclerView recyclerView,
        ViewHolder viewHolder);
        
/**
 * 拖拽到新位置時回調(diào)
 * @param viewHolder 拖動的 ViewHolder
 * @param target     目標(biāo)位置的 ViewHolder
 */
public abstract boolean onMove(RecyclerView recyclerView,
        ViewHolder viewHolder, ViewHolder target);
        
/**
 * 針對swipe狀態(tài)类嗤,swipe 到達(dá)滑動消失的距離回調(diào)函數(shù),一般在這個函數(shù)里面處理刪除 item 的邏輯糊肠。確切的來講是 swipe item 滑出屏幕動畫結(jié)束的時候調(diào)用
 * @param viewHolder 滑動的 ViewHolder
 * @param direction  滑動的方向
 */
public abstract void onSwiped(ViewHolder viewHolder, int direction);

兩個方法控制是否支持:

/**
 * 是否支持長按拖拽,默認(rèn)為 true遗锣,表示支持長按拖拽货裹。
 * 也可以返回 false,手動調(diào)用 startDrag() 方法啟動拖拽
 */
public boolean isLongPressDragEnabled() {
    return true;
}

/**
 * 是否支持任意位置觸摸事件發(fā)生時啟用滑動操作精偿,默認(rèn)為 true弧圆,表示支持滑動 
 * 也可以返回 false,手動調(diào)用 startSwipe() 方法啟動滑動
 */
public boolean isItemViewSwipeEnabled() {
    return true;
}

其它一些方法:

/**
 * 針對 swipe 和 drag 狀態(tài)笔咽,當(dāng) swipe 或者 drag 對應(yīng)的 ViewHolder 改變的時候調(diào)用搔预。可以改變樣式之類
 * 可以通過重寫這個函數(shù)獲取到 swipe叶组、drag 開始和結(jié)束時機拯田,viewHolder 不為空的時候是開始,空的時候是結(jié)束
 * @param actionState 
 *         ACTION_STATE_IDLE:閑置狀態(tài)
 *         ACTION_STATE_SWIPE:滑動狀態(tài)
 *         ACTION_STATE_DRAG:拖拽狀態(tài)
 */
public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
    if (viewHolder != null) {
        sUICallback.onSelected(viewHolder.itemView);
    }
}

/**
 * 針對 swipe 和 drag 狀態(tài)甩十,當(dāng)一個 item view 在 swipe船庇、drag 狀態(tài)結(jié)束的時候調(diào)用,可以恢復(fù)原來的狀態(tài)
 * drag 狀態(tài):當(dāng)手指釋放的時候會調(diào)用
 * swipe 狀態(tài):當(dāng) item 從 RecyclerView 中刪除的時候調(diào)用侣监,一般會在 onSwiped() 函數(shù)里面刪除掉指定的 item view
 */
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
    sUICallback.clearView(viewHolder.itemView);
}

/**
 * 針對 drag 狀態(tài)鸭轮,當(dāng)前 target 對應(yīng)的 item 是否允許移動
 */
public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
        ViewHolder target) {
    return true;
}

/**
 * 針對 drag 狀態(tài),當(dāng) drag ItemView 和底下 ItemView 重疊時橄霉,可以給drag ItemView 設(shè)置一個 Margin 值
 * 讓重疊不容易發(fā)生窃爷,相當(dāng)于增大了 drag Item 的區(qū)域
 */
public int getBoundingBoxMargin() {
    return 0;
}

/**
 * 針對 swipe 狀態(tài),swipe 滑動的位置超過了百分之多少就消失
 */
public float getSwipeThreshold(ViewHolder viewHolder) {
    return .5f;
}

/**
 * 針對 drag 狀態(tài),當(dāng)滑動超過多少就可以觸發(fā) onMove() 方法
 */
public float getMoveThreshold(ViewHolder viewHolder) {
    return .5f;
}

/**
 * 針對 swipe 狀態(tài)按厘,swipe 的逃逸速度医吊,換句話說就算沒達(dá)到getSwipeThreshold 設(shè)置的距離,達(dá)到了這個逃逸速度 item 也會觸發(fā) 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 //只允許從右向左側(cè)滑
                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遮咖。應(yīng)用方法類似:

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)

參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市造虏,隨后出現(xiàn)的幾起案子御吞,更是在濱河造成了極大的恐慌,老刑警劉巖漓藕,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陶珠,死亡現(xiàn)場離奇詭異,居然都是意外死亡享钞,警方通過查閱死者的電腦和手機揍诽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來栗竖,“玉大人暑脆,你說我怎么就攤上這事『” “怎么了添吗?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長份名。 經(jīng)常有香客問我碟联,道長,這世上最難降的妖魔是什么僵腺? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任鲤孵,我火速辦了婚禮,結(jié)果婚禮上辰如,老公的妹妹穿的比我還像新娘普监。我一直安慰自己,他們只是感情好琉兜,可當(dāng)我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布凯正。 她就那樣靜靜地躺著,像睡著了一般呕童。 火紅的嫁衣襯著肌膚如雪漆际。 梳的紋絲不亂的頭發(fā)上淆珊,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天夺饲,我揣著相機與錄音,去河邊找鬼。 笑死往声,一個胖子當(dāng)著我的面吹牛擂找,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播浩销,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼贯涎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了慢洋?” 一聲冷哼從身側(cè)響起塘雳,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎普筹,沒想到半個月后败明,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡太防,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年妻顶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜒车。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡讳嘱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出酿愧,到底是詐尸還是另有隱情沥潭,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布嬉挡,位于F島的核電站叛氨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏棘伴。R本人自食惡果不足惜寞埠,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望焊夸。 院中可真熱鬧仁连,春花似錦、人聲如沸阱穗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽揪阶。三九已至昌抠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鲁僚,已是汗流浹背炊苫。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工裁厅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侨艾。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓执虹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親唠梨。 傳聞我的和親對象是個殘疾皇子袋励,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 簡介 RecyclerView在24.2.0版本中新增了SnapHelper這個輔助類,用于輔助RecyclerV...
    辰之貓閱讀 154,094評論 65 617
  • 前言 學(xué)習(xí)使用RecyclerView也有一段時間当叭,一直都沒有時間做個總結(jié)茬故,就抽空想把RecyclerView的一...
    CaostGrace閱讀 21,966評論 3 17
  • 【Android 控件 RecyclerView】 概述 RecyclerView是什么 從Android 5.0...
    Rtia閱讀 307,381評論 27 439
  • 簡介: 提供一個讓有限的窗口變成一個大數(shù)據(jù)集的靈活視圖。 術(shù)語表: Adapter:RecyclerView的子類...
    酷泡泡閱讀 5,140評論 0 16
  • 今天這一篇蚁鳖,談?wù)勑省?最近效率實在太低均牢,工作白天不怎么做,晚上一直嚷嚷著要加班到最后時間也沒多少才睹。拖延癥也是越來...
    liyumy閱讀 186評論 0 1