Android UI進階之旅2 Material Design之RecyclerView的使用

小楠總發表於2017-12-21

###RecyclerView基本介紹

特點:

  1. 谷歌在高階版本提出一個新的替代ListView、GridView的控制元件。
  2. 高度解耦,但是用起來會比較難用,而且條目點選也需要自己處理。
  3. 自帶了效能優化。ViewHolder。

需要注意的是:RecyclerView沒有條目點選事件,需要自己寫。

######Tips:軟體的一個很重要的概念:低耦合高內聚。

###基本使用

由於這個控制元件大家用得比較多,這裡只是簡單回顧一下使用的步驟。

寫條目佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:orientation="vertical">

    <TextView
        android:id="@+id/tv_item"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>
複製程式碼

寫Adapter以及其內部類自定義的ViewHolder:

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.MyViewHolder> {

    private List<String> mDatas;
    private Context mContext;

    public MyRecyclerViewAdapter(Context context, List<String> datas) {
        mContext = context;
        mDatas = datas;
    }

    //自定義ViewHolder
    class MyViewHolder extends RecyclerView.ViewHolder {

        TextView tv_item;

        MyViewHolder(View itemView) {
            super(itemView);
            tv_item = (TextView) itemView.findViewById(R.id.tv_item);
        }
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //建立ViewHolder
        View itemView = View.inflate(parent.getContext(), R.layout.item_list, null);
        return new MyViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, final int position) {
        //資料繫結
        holder.tv_item.setText(mDatas.get(position));
        //設定點選監聽
        holder.tv_item.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, mDatas.get(position), Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    public int getItemCount() {
        //資料集大小
        return mDatas.size();
    }

}
複製程式碼

在Activity中的使用,通過設定不同的LayoutManager就可以實現不同的佈局效果:

public class MDRecyclerViewActivity extends AppCompatActivity {

    private RecyclerView rv_list;
    private MyRecyclerViewAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_md_recyclerview);

        rv_list = (RecyclerView) findViewById(R.id.rv_list);

        List<String> datas = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            datas.add("第" + i + "個資料");
        }

        mAdapter = new MyRecyclerViewAdapter(this, datas);
        //豎直線性,不反轉佈局
//        rv_list.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        //表格佈局
//        rv_list.setLayoutManager(new GridLayoutManager(this, 3));
        //瀑布流佈局
        rv_list.setLayoutManager(new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL));
        rv_list.setAdapter(mAdapter);

    }
}
複製程式碼

###RecyclerView的一個坑以及Inflate原始碼分析

在Adapter中的onCreateViewHolder,我們需要Inflate佈局檔案,這裡有三種寫法:

View itemView = View.inflate(parent.getContext(), R.layout.item_list, null);
View itemView = View.inflate(parent.getContext(), R.layout.item_list, parent);
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list, parent, false);
複製程式碼

其中:

  1. 寫法一般情況下是沒有問題的,但是當我們在onBindViewHolder中拿到佈局中TextView的LayoutParams的時候,就有可能返回空。
  2. 寫法二直接Crash,因為ItemView佈局已經有一個Parent了(Inflate的時候把ItemView新增到Recycleview了),不能再新增一個Parent(Recycleview再次新增ItemView)。
  3. 寫法三是一、二的兩種相容方案,推薦這種寫法。

####關於Inflate的簡單原始碼分析

我們先看View.inflate(parent.getContext(), R.layout.item_list, null)方法:

public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
    LayoutInflater factory = LayoutInflater.from(context);
    return factory.inflate(resource, root);
}
複製程式碼

實際上這裡還是會呼叫LayoutInflater的inflate方法:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root{
	//attachToRoot為真
    return inflate(resource, root, root != null);
}
複製程式碼

繼續深入分析呼叫關係:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                + Integer.toHexString(resource) + ")");
    }

    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}
複製程式碼

佈局的渲染是通過解析器解析,然後不斷反射來生成View的。我們繼續深入try中的inflate方法(這裡只給出省略之後的核心程式碼):

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        try {
            if (TAG_MERGE.equals(name)) {
            } else {

				//如果發現root不為null,那麼就會為當前渲染的View建立LayoutParams
                if (root != null) {
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }

                //如果root不為null而且attachToRoot為真,那麼把當前渲染的View新增到root中
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
            }

        } catch (XmlPullParserException e) {
        } catch (Exception e) {
        } finally {
        }
    }
}
複製程式碼

在這裡我們就知道原因:

  1. 寫法一之所以在onBindViewHolder中拿不到佈局中TextView的LayoutParams,是因為root我們傳了null,那麼就不會呼叫generateLayoutParams了。
  2. 寫法二報錯是因為,渲染的時候,我們實際傳進來的root != null && attachToRoot成立,那麼就會呼叫root.addView(temp, params);把當前的View(ItemView)新增到root(Recycleview)中。在Recycleview執行的時候,內部又會把ItemView新增進來一次,那麼就會報錯。
  3. 寫法三沒毛病,哈哈。

上面這個問題除了RecyclerView,以前的AbsListView(ListView、GridView等)也存在。

######擴充:Fragment的onCreateView渲染布局的注意事項

Fragment中,我們也是最好使用下面這種形式:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    return inflater.inflate(resID, container, false);
}
複製程式碼

###新增增刪介面

在Adapter中新增以及刪除的介面:

//條目的增刪
public void addItem(String data, int position) {
    mDatas.add(position, data);
    notifyItemInserted(position);
}

public void removeItem(int position) {
    mDatas.remove(position);
    notifyItemRemoved(position);
}
複製程式碼

注意如果你想使用RecyclerView提供的增刪動畫,那麼就需要使用新增的notify方法。

###新增條目點選監聽

我們自定義一個點選回撥介面:

//條目點選
ItemClickListener mItemClickListener;

public interface ItemClickListener {
    void onclick(int position, String data);
}

public void setItemClickListener(ItemClickListener listener) {
    mItemClickListener = listener;
}

public abstract class ItemClickListenerPosition implements View.OnClickListener {

    private int mPosition;

    public ItemClickListenerPosition(int position) {
        mPosition = position;
    }

    public int getPosition() {
        return mPosition;
    }
}
複製程式碼

其中,ItemClickListenerPosition是一個自定義的OnClickListener,目的就是為了把Position和監聽繫結在一起,同時也使用了getLayoutPosition方法。防止了點選Position錯亂的問題。

(onBindViewHolder() 方法中的位置引數 position 不是實時更新的,例如在我們刪除元素後,item 的 position 並沒有改變。)

然後在onBindViewHolder裡面進行監聽:

@Override
public void onBindViewHolder(final MyViewHolder holder, int position) {

    //資料繫結
   
    //設定條目監聽
    holder.itemView.setOnClickListener(new ItemClickListenerPosition(holder.getLayoutPosition()) {
        @Override
        public void onClick(View v) {
            if (mItemClickListener != null) {
                mItemClickListener.onclick(getPosition(), mDatas.get(getPosition()));
            }
        }
    });
}
複製程式碼

另外,長按的使用也是類似。我們當然也可以直接為每一個條目上面的某一個控制元件設定監聽,實現思路跟這個一樣,就不介紹了。

###分割線處理

為了實現普通的分割線,我們需要自定義類,繼承RecyclerView.ItemDecoration,並且實現getItemOffsets、onDraw兩個方法。

其中:

  1. getItemOffsets是返回條目之間的間隔,例如我們想仿照ListView一樣新增分割線,那麼就需要設定outRect的下邊距。
  2. onDraw方法就是自己畫需要的分割線。

####例子:畫橫縱向ListView分割線

首先,我們需要有一個分割線Drawable:

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

    <solid android:color="#ccc"/>

</shape>
複製程式碼

其中,這裡指定的寬高值是指橫向或者縱向的時候的分割線寬度。

然後,建立一個類,繼承RecyclerView.ItemDecoration,並且實現getItemOffsets、onDraw兩個方法:

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

//佈局方向
private int mOrientation = LinearLayoutManager.VERTICAL;
private final Drawable mDivider;

public DividerItemDecoration(Context context, int orientation) {

	//獲取自定義Drawable
    mDivider = context.getResources().getDrawable(R.drawable.item_divider);

    //設定方向
    setOrientation(orientation);
}

//1.呼叫此方法(首先會先獲取條目之間的間隙高度---Rect矩形區域)
// 獲得條目的偏移量(所有的條目都回撥用一次該方法)
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    if (mOrientation == LinearLayoutManager.VERTICAL) {//垂直
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    } else {//水平
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    }
}

//2.呼叫這個繪製方法, RecyclerView會毀掉該繪製方法,需要你自己去繪製條目的間隔線
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    if (mOrientation == LinearLayoutManager.VERTICAL) {//垂直
        drawVertical(c, parent);
    } else {//水平
        drawHorizontal(c, parent);
    }
}

public void setOrientation(int orientation) {
    if (orientation != LinearLayoutManager.HORIZONTAL && orientation != LinearLayoutManager.VERTICAL) {
        throw new IllegalArgumentException("非水平或者豎直方向的列舉型別");
    }
    this.mOrientation = orientation;
}

private void drawVertical(Canvas c, RecyclerView parent) {
    // 畫水平線
    int left = parent.getPaddingLeft();
    int right = parent.getWidth() - parent.getPaddingRight();
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);

        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
        int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child));
        int bottom = top + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}

private void drawHorizontal(Canvas c, RecyclerView parent) {
    int top = parent.getPaddingTop();
    int bottom = parent.getHeight() - parent.getPaddingBottom();
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);

        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
        int left = child.getRight() + params.rightMargin + Math.round(ViewCompat.getTranslationX(child));
        int right = left + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}
複製程式碼

}

程式碼分析:

  1. 提供構造方法,載入Drawable,設定當前的佈局方向。
  2. getItemOffsets中,根據佈局方向設定outRect矩形區域。
  3. onDraw方法裡面進行Drawable的繪製。

當然,我們也可以使用系統自帶的分割線:

int[] attrs = new int[]{
        android.R.attr.listDivider
};
TypedArray typedArray = context.obtainStyledAttributes(attrs);
mDivider = typedArray.getDrawable(0);
typedArray.recycle();
複製程式碼

如果你不喜歡,也可以在style檔案裡面修改:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:listDivider">@drawable/item_divider</item>
</style>
複製程式碼

####進階例子---網格佈局分割線

public class DividerGridViewItemDecoration extends ItemDecoration {

    private Drawable mDivider;
    private int[] attrs = new int[]{
            android.R.attr.listDivider
    };

    public DividerGridViewItemDecoration(Context context) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs);
        mDivider = typedArray.getDrawable(0);
        typedArray.recycle();
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, State state) {
        drawVertical(c, parent);
        drawHorizontal(c, parent);
    }

    private void drawHorizontal(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            int left = child.getLeft() - params.leftMargin;
            int right = child.getRight() + params.rightMargin;
            int top = child.getBottom() + params.bottomMargin;
            int bottom = top + mDivider.getIntrinsicHeight();

            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    private void drawVertical(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            int left = child.getRight() + params.rightMargin;
            int right = left + mDivider.getIntrinsicWidth();
            int top = child.getTop() - params.topMargin;
            int bottom = child.getBottom() + params.bottomMargin;

            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    @Deprecated
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        int right = mDivider.getIntrinsicWidth();
        int bottom = mDivider.getIntrinsicHeight();
        if (isLastColum(itemPosition, parent)) {
            right = 0;
        }
        if (isLastRow(itemPosition, parent)) {
            bottom = 0;
        }
        outRect.set(0, 0, right, bottom);

    }

    private boolean isLastRow(int itemPosition, RecyclerView parent) {
        int spanCount = getSpanCount(parent);
        LayoutManager layoutManager = parent.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            int childCount = parent.getAdapter().getItemCount();
            int lastRowCount = childCount % spanCount;
            if (lastRowCount == 0 || lastRowCount < spanCount) {
                return true;
            }
        }
        return false;
    }

    private boolean isLastColum(int itemPosition, RecyclerView parent) {
        LayoutManager layoutManager = parent.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            int spanCount = getSpanCount(parent);
            if ((itemPosition + 1) % spanCount == 0) {
                return true;
            }
        }
        return false;
    }

    private int getSpanCount(RecyclerView parent) {
        LayoutManager layoutManager = parent.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            GridLayoutManager lm = (GridLayoutManager) layoutManager;
            int spanCount = lm.getSpanCount();
            return spanCount;
        }
        return 0;
    }

}
複製程式碼

道理都一樣,只不過畫的時候水平豎直方向都需要畫而已。

####擴充--原始碼分析

在RecyclerView裡面,我們看看原始碼是怎麼把我們自己的Decoration畫上去的,

@Override
public void onDraw(Canvas c) {
    super.onDraw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}
複製程式碼

從RecyclerView的繪製可以看出,在RecyclerView繪製的時候,是通過迴圈不斷回撥Decoration的onDraw(自己實現)進行繪製的。

###新增頭部和尾部

頭部和尾部需要我們自己處理,我們可以參考ListView的實現:

addHeaderView(){
	 if (mAdapter != null) {
            if (!(mAdapter instanceof HeaderViewListAdapter)) {
                mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
            }
}

setAdapter(){
	if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }
}
複製程式碼

我們通過看ListView的原始碼,可以知道,ListView在新增頭部(尾部)、setAdapter的時候,內部其實是利用裝飾者設計模式,利用一個HeaderViewListAdapter來包裝我們自己的Adapter,因此我們可以模仿這樣的實現,自定義一個RecyclerView:

public class WrapRecyclerView extends RecyclerView {
	
	//頭部尾部資訊
    private ArrayList<View> mHeaderViewInfos = new ArrayList<>();
    private ArrayList<View> mFooterViewInfos = new ArrayList<>();
    private Adapter mAdapter;

    public WrapRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void addHeaderView(View v) {
        mHeaderViewInfos.add(v);
        if (mAdapter != null) {
            if (!(mAdapter instanceof HeaderViewRecyclerAdapter)) {
                mAdapter = new HeaderViewRecyclerAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
            }
        }
    }

    public void addFooterView(View v) {
        mFooterViewInfos.add(v);
        if (mAdapter != null) {
            if (!(mAdapter instanceof HeaderViewRecyclerAdapter)) {
                mAdapter = new HeaderViewRecyclerAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
            }
        }
    }

    @Override
    public void setAdapter(Adapter adapter) {
        if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) {
            mAdapter = new HeaderViewRecyclerAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }
        super.setAdapter(mAdapter);
    }

}
複製程式碼

作為包裝類的Adapter如下:

public class HeaderViewRecyclerAdapter extends Adapter {

    private static final int VIEW_TYPE_HEADER = 0;
    private static final int VIEW_TYPE_FOOTER = 1;
    private static final int VIEW_TYPE_ITEM = 2;

    private Adapter mAdapter;

    ArrayList<View> mHeaderViewInfos;
    ArrayList<View> mFooterViewInfos;

    public HeaderViewRecyclerAdapter(ArrayList<View> headerViewInfos, ArrayList<View> footerViewInfos, Adapter adapter) {
        mAdapter = adapter;

        if (headerViewInfos == null) {
            mHeaderViewInfos = new ArrayList<>();
        } else {
            mHeaderViewInfos = headerViewInfos;
        }

        if (footerViewInfos == null) {
            mFooterViewInfos = new ArrayList<>();
        } else {
            mFooterViewInfos = footerViewInfos;
        }
    }

    @Override
    public int getItemCount() {
        if (mAdapter != null) {
            return getFootersCount() + getHeadersCount() + mAdapter.getItemCount();
        } else {
            return getFootersCount() + getHeadersCount();
        }
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return;
        }
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getItemCount();
            if (adjPosition < adapterCount) {
                mAdapter.onBindViewHolder(holder, adjPosition);
                return;
            }
        }
    }

    @Override
    public int getItemViewType(int position) {
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            return VIEW_TYPE_HEADER;
        }
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getItemCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }
        return VIEW_TYPE_FOOTER;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == VIEW_TYPE_HEADER) {
            return new HeaderViewHolder(mHeaderViewInfos.get(0));
        } else if (viewType == VIEW_TYPE_FOOTER) {
            return new HeaderViewHolder(mFooterViewInfos.get(0));
        }
        return mAdapter.onCreateViewHolder(parent, viewType);
    }

    public int getHeadersCount() {
        return mHeaderViewInfos.size();
    }

    public int getFootersCount() {
        return mFooterViewInfos.size();
    }

    private static class HeaderViewHolder extends ViewHolder {

        public HeaderViewHolder(View view) {
            super(view);
        }
    }

}
複製程式碼

主要就是根據getItemViewType返回不同型別的佈局,然後在對應的方法進行了一次分發。

###Item互動動畫效果

接下來我們實現條目的拖拽效果以及條目的橫向滑動刪除效果,主要用到的是ItemTouchHelper這個類。

新建一個ItemTouchHelper物件,並且繫結到RecyclerView。

mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelperCallback(mQqAdapter));
mItemTouchHelper.attachToRecyclerView(rv_list);
複製程式碼

接下來介紹在建立ItemTouchHelper的時候需要傳入的Callback,這是自定義的一個類,繼承了ItemTouchHelper.Callback。

public class ItemTouchHelperCallback extends ItemTouchHelper.Callback {

    private ItemMovedListener mItemMovedListener;

    public ItemTouchHelperCallback(ItemMovedListener itemMovedListener) {
        mItemMovedListener = itemMovedListener;
    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {

        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;

        return makeMovementFlags(dragFlags, swipeFlags);
    }

    @Override
    public boolean isLongPressDragEnabled() {
        return true;
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder fromHolder, RecyclerView.ViewHolder toHolder) {
        if (fromHolder.getItemViewType() != toHolder.getItemViewType()) {
            return false;
        }
        if (mItemMovedListener != null) {
            mItemMovedListener.onItemMoved(fromHolder.getAdapterPosition(), toHolder.getAdapterPosition());
        }
        return true;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        if (mItemMovedListener != null) {
            mItemMovedListener.onItemRemoved(viewHolder.getAdapterPosition());
        }
    }

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
            viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext().getResources().getColor(R.color.grey));
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext().getResources().getColor(R.color.white));

        viewHolder.itemView.setAlpha(1);//1~0
        viewHolder.itemView.setScaleX(1);//1~0
        viewHolder.itemView.setScaleY(1);//1~0
        super.clearView(recyclerView, viewHolder);
    }

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {

        //dX:水平方向移動的增量(負:往左;正:往右)範圍:0~View.getWidth  0~1
        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            //透明度動畫
            float alpha = 1 - Math.abs(dX) / viewHolder.itemView.getWidth();
            viewHolder.itemView.setAlpha(alpha);//1~0
            viewHolder.itemView.setScaleX(alpha);//1~0
            viewHolder.itemView.setScaleY(alpha);//1~0
        }

        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }
}
複製程式碼

下面介紹幾個需要重寫的方法:

  1. getMovementFlags,返回需要監聽的方向,其中包括拖拽以及滑動。這裡我監聽了上下拖拽以及左右的滑動。
  2. isLongPressDragEnabled,返回true代表當長按條目的時候開啟拖拽效果,當然我們也可以指定觸控一個View開始拖拽。
  3. onMoveon、Swiped的時候,需要回撥Adapter中的notify方法,因此我自定義了一個介面。
  4. onSelectedChanged,在條目被長按觸發滑動或者拖拽效果的時候(不是ACTION_STATE_IDLE狀態),方便使用者設定一個屬性,例如背景色。clearView是動畫結束的時候的回撥,為了就是清除一些屬性,不然的話由於ItemView的複用會導致BUG。
  5. onChildDraw方法就是在滑動的時候(ACTION_STATE_SWIPE狀態),實現一個平移、縮放、漸變等動畫。當然,在這個方法裡面還可以實現類似QQ的滑動刪除效果,具體實現效果網上很多,這裡就不再贅述了。

其中,ItemMovedListener的定義如下:

public interface ItemMovedListener {

    void onItemMoved(int fromPosition, int toPosition);

    void onItemRemoved(int position);

}
複製程式碼

ItemMovedListener由Adapter實現:

@Override
public void onItemMoved(int fromPosition, int toPosition) {
    Collections.swap(list, fromPosition, toPosition);
    notifyItemMoved(fromPosition, toPosition);
}

@Override
public void onItemRemoved(int position) {
    list.remove(position);
    notifyItemRemoved(position);
}
複製程式碼

在進行了拖拽或者滑動的時候,一定要進行相應的資料處理以及notify。

除了長按觸發拖拽,ItemTouchHelper有一個onStartDrag(RecyclerView.ViewHolder holder)方法也可以觸發拖拽。因此我們又可以抽取一個介面:

public interface StartDragListener {

    void onStartDrag(RecyclerView.ViewHolder holder);

}
複製程式碼

StartDragListener由Activity實現:

@Override
public void onStartDrag(RecyclerView.ViewHolder holder) {
    mItemTouchHelper.startDrag(holder);
}
複製程式碼

並且在Adapter構造的時候傳進去,在onBindViewHolder的時候就可以進行回撥:

@Override
public void onBindViewHolder(final MyViewHolder holder, int location) {

    holder.iv_logo.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                if (mStartDragListener != null) {
                    mStartDragListener.onStartDrag(holder);
                }
            }
            return false;
        }
    });
}
複製程式碼

這裡是觸控一個ImageView就可以發生拖拽了。

####思考

一個類如果想呼叫另外一個類的方法,但是那個類又不想直接持有另外一個類的物件的時候(比較大的類、業務分離),就可以通過抽取介面的形式來實現。

如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:

公眾號:Android開發進階

我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)

相關文章