Android UI进阶之旅2 Material Design之RecyclerView的使用

###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就可以发生拖拽了。

####思考

一个类如果想调用另外一个类的方法,但是那个类又不想直接持有另外一个类的对象的时候(比较大的类、业务分离),就可以通过抽取接口的形式来实现。

如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:

我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值