Drag and Swipe with RecyclerView Part Two

原文

RecyclerView的拖放和滑动特效


第二部分:手柄、网格与自定义动画

2015年7月22日

第一部分中,我们研究了ItemTouchHelper,并通过实现ItemTouchHelper.Callback接口,为RecyclerView线性列表添加了基本的“拖放”和“滑动消失”效果。本文就这个例子扩展,增加了对网格布局,触摸“手柄”拖拽,显示所选择的视图和自定义滑动动画的支持。

拖拽手柄

当设计一个支持拖放的列表,一般要有一个启动触摸拖动的提示。这有助于用户发现和使用功能,材料教程也推荐列表在“编辑模式”下采用这种设计。接下来我们为前面的例子做一些微小的改动,以生成“手柄”控件,或者“可重新排序的列表控件”。

资料来源:google.com/design

首先,修改List子项布局文件(item_main.xml)

<?xml ersion="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/item"
        android:layout_width="match_parent"
        android:layout_height="?listPreferredItemHeight"
        android:clickable="true"
        android:focusable="true"
        android:foreground="?selectableItemBackground">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="16dp"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <ImageView
        android:id="@+id/handle"
        android:layout_width="?listPreferredItemHeight"
        android:layout_height="match_parent"
        android:layout_gravity="center_vertical|right"
        android:scaleType="center"
        android:src="@drawable/ic_reorder_grey_500_24dp" />
    </FrameLayout>

view rawitem_main.xml hosted with ❤ by GitHub

拖拽手柄的图标可以在Github项目“Material Design Icons”里找到,并通过“Android Material Design Icon Generator Plugin”插件添加到项目中。

Part One曾简单提到,可通过编程方式使用ItemTouchHelper.startDrag(ViewHolder)启动一个拖拽事件。所以,我们所要做的就是修改ViewHolder以包含新的手柄图标,并设置一个触发startDrag()调用的简单的触摸事件侦听器。

我们需要一个接口来传递事件链:

public interface OnStartDragListener {

    /**
     * Called when a view is requesting a start of a drag.
     *
     * @param viewHolder The holder of the view to drag.
     */
    void onStartDrag(RecyclerView.ViewHolder viewHolder);
}

然后,实例化ItemViewHolder中的手柄视图:

public final ImageView handleView;
public ItemViewHolder(View itemView) {
    super(itemView);
    // ...
    handleView = (ImageView) itemView.findViewById(R.id.handle);
}

然后修改RecyclerListAdapter

private final OnStartDragListener mDragStartListener;

public RecyclerListAdapter(OnStartDragListener dragStartListener) {
    mDragStartListener = dragStartListener;
    // ...
}
@Override
public void onBindViewHolder(final ItemViewHolder holder, 
int position) {
    // ...
    holder.handleView.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (MotionEventCompat.getActionMasked(event) == 
                MotionEvent.ACTION_DOWN) {
            mDragStartListener.onStartDrag(holder);
            }
            return false;
        }
    });
}

完整的RecyclerListAdapter类现在应该是这个样子

接下来要做的就是在Fragment中添加OnStartDragListener的实现了:

public class RecyclerListFragment extends Fragment implements 
    OnStartDragListener {

    // ...
    @Override
    public void onViewCreated(View view, Bundle icicle) {
        super.onViewCreated(view, icicle);

        RecyclerListAdapter a = new RecyclerListAdapter(this);
        // ...
    }
    @Override
    public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
        mItemTouchHelper.startDrag(viewHolder);
    }
}

整个RecyclerListFragment类现在应该是这样的。运行项目,你现在应该能够通过触摸手柄进行拖拽。

指示所选视图

在我们的基本实例中,还没有视觉效果指示被拖拽的视图就是实际被选中的List子Item。显而易见,这还达不到我们的目的,但是也很容易修复。事实上,通过ItemTouchHelper的帮助类,你可以很方便的设置ViewHolder Item子项的背景来达到这种效果。对于Android 5.0版本,以至于更高的版本,可以为Item视图在被拖拽和滑动时设置更大的elevation属性值;在早期版本中,你可以为滑动设置一个基本的淡出效果。

通过使用我们现有的例子也可以实现这一效果,只需为item_main.xml文件的FrameLayout根布局添加一个背景,或在RecyclerListAdapter.ItemViewHolder的构造函数中通过代码来设置。实际效果如下图:

看起来很不错,但你可能希望有更多的控制。要做到这一点的方法就是让ViewHolder可以处理“选中”或“清除”手势的转变。对于这一点,ItemTouchHelper.Callback提供两个回调。

  • onSelectedChanged(ViewHolder, int),每次ViewHolder的状态变为拖拽(ACTION_STATE_DRAG)或滑动(ACTION_STATE_SWIPE)时被调用。这是改变Item视图状态到“激活”状态的理想位置。

  • clearView(RecyclerView, ViewHolder),拖拽的视图被放下,以及滑动手势被取消或完成(ACTION_STATE_IDLE)时调用。在这里,通常会恢复Item视图的“空闲”状态。

接下来我们将上述方法融合到一起。

首先,创建一个接口供ViewHolder视图实现:

/**
 * Notifies a View Holder of relevant callbacks from 
 * {@link ItemTouchHelper.Callback}.
 */
public interface ItemTouchHelperViewHolder {

    /**
     * Called when the {@link ItemTouchHelper} first registers an 
     * item as being moved or swiped.
     * Implementations should update the item view to indicate 
     * it's active state.
     */
    void onItemSelected();


    /**
     * Called when the {@link ItemTouchHelper} has completed the 
     * move or swipe, and the active item state should be cleared.
     */
    void onItemClear();
}

然后,让SimpleItemTouchHelperCallback触发相应的回调:

@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, 
        int actionState) {
   // We only want the active item
   if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
        if (viewHolder instanceof ItemTouchHelperViewHolder) {
            ItemTouchHelperViewHolder itemViewHolder = 
                    (ItemTouchHelperViewHolder) viewHolder;
            itemViewHolder.onItemSelected();
        }
    }

    super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(RecyclerView recyclerView, 
        RecyclerView.ViewHolder viewHolder) {
    super.clearView(recyclerView, viewHolder);

    if (viewHolder instanceof ItemTouchHelperViewHolder) {
        ItemTouchHelperViewHolder itemViewHolder = 
                (ItemTouchHelperViewHolder) viewHolder;
        itemViewHolder.onItemClear();
    }
}

最后,唯一要做的就是使RecyclerListAdapter.ItemViewHolder实现ItemTouchHelperViewHolder接口:

public class ItemViewHolder extends RecyclerView.ViewHolder 
        implements ItemTouchHelperViewHolder {

    // ...
    @Override
    public void onItemSelected() {
        itemView.setBackgroundColor(Color.LTGRAY);
    }

    @Override
    public void onItemClear() {
        itemView.setBackgroundColor(0);
    }
}

在这个例子中,当视图处于活动状态时我们只是简单地增加一个灰色的背景时,并在视图清除时移除这个背景。如果你的ItemTouchHelper和适配器是紧耦合的,你可以很容易跳过这一步,并直接在ItemTouchHelper.Callback切换视图状态。

网格布局

如果你试图改变这个项目以使用GridLayout管理器,你很快就会发现,项目没有正常工作。究其原因和修复方法都很简单:我们需要ItemTouchHelper支持左右拖拽。在SimpleItemTouchHelperCallback,我们已经指定:

@Override
public int getMovementFlags(RecyclerView recyclerView, 
        RecyclerView.ViewHolder viewHolder) {
    int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
    int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
    return makeMovementFlags(dragFlags, swipeFlags);
}

支持网格布局所需的唯一改变是为dragFlags标识添加左右方向:

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

然而,对于网格布局来说滑动消失不能算是一个合理的动画模式,所以最终的结果可能会是这样:

@Override
public int getMovementFlags(RecyclerView recyclerView, 
        RecyclerView.ViewHolder viewHolder) {
    int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | 
                    ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
    int swipeFlags = 0;
    return makeMovementFlags(dragFlags, swipeFlags);
}

要查看一个能工作的GridLayoutManager示例,请参见RecyclerGridFragment。运行时效果如下图:

自定义滑动动画

ItemTouchHelper.Callback为我们提供了一个非常方便的方式去完全控制拖拽和滑动时的视图动画效果。因为ItemTouchHelper辅助类是RecyclerView.ItemDecoration的子类,我们完全可以对有类似情况的视图进行同样的处理。在接下来的部分,我们将在此基础上进一步扩展范围,但这里我们仅提供一个简单实例,通过重写默认滑动特效来显示线性淡出效果。

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

    if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
        float width = (float) viewHolder.itemView.getWidth();
        float alpha = 1.0f - Math.abs(dX) / width;
        viewHolder.itemView.setAlpha(alpha);
        viewHolder.itemView.setTranslationX(dX);    
    } else {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, 
                actionState, isCurrentlyActive);
    }
}

参数dXdY表示所选视图的位置变化:

  • -1.0f,表示从ItemTouchHelper.ENDItemTouchHelper.START的滑动

  • 1.0f,表示从ItemTouchHelper.STARTItemTouchHelper.END的滑动

对于不需要处理的任何actionState,调用super方法尤为重要,这样其他的默认动画效果才能运行。

下一部分将包括一个可以控制拖拽绘图的例子。

结论

我们刚刚了解了如何定制ItemTouchHelper类的核心部分。我原本希望在本文提供更多内容,但考虑到文章长度的限制,我决定将其分割成。第二部分和第三部分之间的时间将大大缩短。

文章发表之前GitHub的项目可能要提前更新,所以,如果您无需本文指导,可以直接参看项目仓库查看更新。

源码

本系列文章有相应的GitHub项目,Android-ItemTouchHelper-Demo。这一部分的引用代码请参考cimmit提交ef8f149d164fba

Follow me on Google+ and Twitter

© 2015 Paul Burke
All code appearing in this article is licensed under Apache 2.0

Some rights reserved by the author.

Android App Development Android

I Don't Want Your Money, I Want Aragaki Yui.