###RecyclerView基本介紹
特點:
- 谷歌在高階版本提出一個新的替代ListView、GridView的控制元件。
- 高度解耦,但是用起來會比較難用,而且條目點選也需要自己處理。
- 自帶了效能優化。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);
複製程式碼
其中:
- 寫法一般情況下是沒有問題的,但是當我們在onBindViewHolder中拿到佈局中TextView的LayoutParams的時候,就有可能返回空。
- 寫法二直接Crash,因為ItemView佈局已經有一個Parent了(Inflate的時候把ItemView新增到Recycleview了),不能再新增一個Parent(Recycleview再次新增ItemView)。
- 寫法三是一、二的兩種相容方案,推薦這種寫法。
####關於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 {
}
}
}
複製程式碼
在這裡我們就知道原因:
- 寫法一之所以在onBindViewHolder中拿不到佈局中TextView的LayoutParams,是因為root我們傳了null,那麼就不會呼叫generateLayoutParams了。
- 寫法二報錯是因為,渲染的時候,我們實際傳進來的root != null && attachToRoot成立,那麼就會呼叫root.addView(temp, params);把當前的View(ItemView)新增到root(Recycleview)中。在Recycleview執行的時候,內部又會把ItemView新增進來一次,那麼就會報錯。
- 寫法三沒毛病,哈哈。
上面這個問題除了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兩個方法。
其中:
- getItemOffsets是返回條目之間的間隔,例如我們想仿照ListView一樣新增分割線,那麼就需要設定outRect的下邊距。
- 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);
}
}
複製程式碼
}
程式碼分析:
- 提供構造方法,載入Drawable,設定當前的佈局方向。
- getItemOffsets中,根據佈局方向設定outRect矩形區域。
- 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);
}
}
複製程式碼
下面介紹幾個需要重寫的方法:
- getMovementFlags,返回需要監聽的方向,其中包括拖拽以及滑動。這裡我監聽了上下拖拽以及左右的滑動。
- isLongPressDragEnabled,返回true代表當長按條目的時候開啟拖拽效果,當然我們也可以指定觸控一個View開始拖拽。
- onMoveon、Swiped的時候,需要回撥Adapter中的notify方法,因此我自定義了一個介面。
- onSelectedChanged,在條目被長按觸發滑動或者拖拽效果的時候(不是ACTION_STATE_IDLE狀態),方便使用者設定一個屬性,例如背景色。clearView是動畫結束的時候的回撥,為了就是清除一些屬性,不然的話由於ItemView的複用會導致BUG。
- 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就可以發生拖拽了。
####思考
一個類如果想呼叫另外一個類的方法,但是那個類又不想直接持有另外一個類的物件的時候(比較大的類、業務分離),就可以通過抽取介面的形式來實現。
如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:
我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)。