Android 拖拽滑动(OnDragListener和ViewDragHelper)

说到拖拽,Google提供了两种方式:OnDragListenerViewDragHelper,通过这两个工具我们可以非常方便的实现控件的拖拽效果。在这里不会过多的讲解两种工具的具体怎么使用,因为网上很多文章已经有讲解,还需要提出疑问:为什么拖拽工具Google要提供两种?只用一种不行吗?各自的使用场景是如何?这将是要具体分析的问题。

1 OnDragListener

1.1 OnDragListener的使用

OnDragListener 作为一个接口,它被定义在 View 源码中:

/**
 * Interface definition for a callback to be invoked when a drag is being dispatched
 * to this view.  The callback will be invoked before the hosting view's own
 * onDrag(event) method.  If the listener wants to fall back to the hosting view's
 * onDrag(event) behavior, it should return 'false' from this callback.
 *
 * <div class="special reference">
 * <h3>Developer Guides</h3>
 * <p>For a guide to implementing drag and drop features, read the
 * <a href="{@docRoot}guide/topics/ui/drag-drop.html">Drag and Drop</a> developer guide.</p>
 * </div>
 */
public interface OnDragListener {
    /**
     * Called when a drag event is dispatched to a view. This allows listeners
     * to get a chance to override base View behavior.
     *
     * @param v The View that received the drag event.
     * @param event The {@link android.view.DragEvent} object for the drag event.
     * @return {@code true} if the drag event was handled successfully, or {@code false}
     * if the drag event was not handled. Note that {@code false} will trigger the View
     * to call its {@link #onDragEvent(DragEvent) onDragEvent()} handler.
     */
    boolean onDrag(View v, DragEvent event);
}

/**
 * Register a drag event listener callback object for this View. The parameter is
 * an implementation of {@link android.view.View.OnDragListener}. To send a drag event to a
 * View, the system calls the
 * {@link android.view.View.OnDragListener#onDrag(View,DragEvent)} method.
 * @param l An implementation of {@link android.view.View.OnDragListener}.
 */
public void setOnDragListener(OnDragListener l) {
    getListenerInfo().mOnDragListener = l;
}

该接口会回调两个参数:

  • View:被拖拽的View对象

  • DragEvent:View的拖拽状态,比较常用的有以下几种状态:

    • DragEvent.ACTION_DRAG_STARTED:开始拖拽,在调用 view.startDrag() 时回调

    • DragEvent.ACTION_DRAG_ENTERED:当拖拽触摸到了被拖拽的那个View的区域内就会回调

    • DragEvent.ACTION_DRAG_ENDED:已经松手结束拖拽

    • DragEvent.ACTION_DROP:拖拽结束松手了

OnDragListener 接口只是拖拽的回调监听,具体实现对View的拖拽,需要调用View的 startDrag()

/**
 * @deprecated Use {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)
 * startDragAndDrop()} for newer platform versions.
 */
@Deprecated
public final boolean startDrag(ClipData data, DragShadowBuilder shadowBuilder,
                               Object myLocalState, int flags) {
    return startDragAndDrop(data, shadowBuilder, myLocalState, flags);
}

public final boolean startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder, Object myLocalState, int flags) {}

startDrag() 是在API 11的时候提供,如果需要在低版本也实现 OnDragListener 的拖拽效果,可以使用 ViewCompat.startDragAndDrop()

startDrag() 需要传递四个参数:

  • ClipData:拖拽时用于传递的数据,会在 DragEvent.ACTION_DROP 拖拽结束松手时才能获取到 ClipData 的数据

  • DragShadowBuilder:在拖拽时生成View的半透明像素,可以观察跟随手指的拖拽状态

  • myLocalState:可以用它传递本地数据,监听 DragEvent.ACTION_DRAG_STARTEDDragEvent.ACTION_DRAG_ENTEREDDragEvent.ACTION_DRAG_ENDED 等拖拽状态时通过 DragEvent.getLocalState() 随时获取该本地数据。但需要注意的是,如果是跨进程的Activity之间通信,DragEvent.getLocalState() 会返回null

  • flags:控制拖拽时的操作,一般传递0即可

ClipDatamyLocalState 是比较重要的两个参数,后续在场景分析时会详细讲解他们。

根据上面简单的说明,使用 OnDragListener 实现对View的拖拽如下:

view.startDrag(null, new DragShadowBuilder(v), v, 0);

view.setOnDragListener(new OnDragListener() {
	@Override
	public boolean onDrag(View v, DragEvent event) {
		switch(event.getAction()) {
			case DragEvent.ACTION_DRAG_STARTED:
				break;
			case DragEvent.ACTION_DRAG_ENTERED:
				break;	
			case DragEvent.ACTION_DRAG_EXITED:
				break;	
			case DragEvent.ACTION_DRAG_ENDED:
				break;
			case DragEvent.ACTION_DROP:
				break;		
		}
	}
});

拖拽起一个View,其他View的onDragEvent()也会接收到拖拽监听

1.2 OnDragListener简单示例

先看一下 OnDragListener 实现的拖拽效果:
在这里插入图片描述
使用6个View排列成网格状,在长按的时候启动并跟随手指拖拽移动,拖拽到View的区域就开始重新排序。具体布局代码和布局如下:

<?xml version="1.0" encoding="utf-8"?>
<com.example.demo.DragListenerGridView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#EF5350" />

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#9C27B0" />

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#1E88E5" />

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00695C" />

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FDD835" />

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#546E7A" />

</com.example.demo.DragListenerGridView>

public class DragListenerGridView extends ViewGroup {
    private static final int COLUMNS = 2;
    private static final int ROWS = 3;

    private OnDragListener mOnDragListener = new TestDragListener();
    private View mDraggedView;
    private List<View> mOrderedChildren = new ArrayList<>();

    public DragListenerGridView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setChildrenDrawingOrderEnabled(true);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 初始化位置
            mOrderedChildren.add(child); 
            // 长按启动拖拽
            child.setOnLongClickListener(new OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    mDraggedView = v;
                    v.startDrag(null, new DragShadowBuilder(v), v, 0);
                    return false;
                }
            });
            // 设置OnDragListener拖拽监听
            child.setOnDragListener(mOnDragListener);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int specWidth = MeasureSpec.getSize(widthMeasureSpec);
        int specHeight = MeasureSpec.getSize(heightMeasureSpec);
        int childWidth = specWidth / COLUMNS;
        int childHeight = specHeight / ROWS;

        measureChildren(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));

        setMeasuredDimension(specWidth, specHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int childLeft;
        int childTop;
        int childWidth = getWidth() / COLUMNS;
        int childHeight = getHeight() / ROWS;
        // 先把childView摆放在同一个位置,然后再一个个进行偏移摆放为Grid
        // 这样处理主要是方便再拖拽时实现所有childView重新排列为Grid
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            childLeft = i % 2 * childWidth;
            childTop = i / 2 * childHeight;
            child.layout(0, 0, childWidth, childHeight);
            child.setTranslationX(childLeft);
            child.setTranslationY(childTop);
        }
    }

    private class TestDragListener implements OnDragListener {

        @Override
        public boolean onDrag(View v, DragEvent event) {
            switch (event.getAction()) {
                case DragEvent.ACTION_DRAG_STARTED:
                    if (event.getLocalState() == v) {
                   		// 启动拖拽时将拖拽的View隐藏
                        v.setVisibility(View.INVISIBLE); 
                    }
                    break;
                case DragEvent.ACTION_DRAG_ENTERED:
                	// 如果被拖拽的View进入了其他View的拖拽范围就触发重新排列
                    // 如果被拖拽的View是在自己的拖拽范围内,就不用重新排列
                    if (event.getLocalState() != v) {
                        sort(v);
                    }
                    break;
                case DragEvent.ACTION_DRAG_EXITED:
                    break;
                case DragEvent.ACTION_DRAG_ENDED:
                    if (event.getLocalState() == v) {
                    	// 结束拖拽时将拖拽的View显示
                        v.setVisibility(View.VISIBLE);
                    }
                    break;
                case DragEvent.ACTION_DROP:
                    break;
            }
            return true;
        }
    }

	// 子View重新排序
    private void sort(View targetView) {
        int draggedIndex = -1;
        int targetIndex = -1;
        for (int i = 0; i< getChildCount(); i++) {
            View child = mOrderedChildren.get(i);
            if (targetView == child) {
                targetIndex = i;
            } else if (mDraggedView == child) {
                draggedIndex = i;
            }
        }
        if (targetIndex < draggedIndex) {
            mOrderedChildren.remove(draggedIndex);
            mOrderedChildren.add(targetIndex, mDraggedView);
        } else if (targetIndex > draggedIndex) {
            mOrderedChildren.remove(draggedIndex);
            mOrderedChildren.add(targetIndex, mDraggedView);
        }
        int childLeft;
        int childTop;
        int childWidth = getWidth() / COLUMNS;
        int childHeight = getHeight() / ROWS;
        for (int i = 0; i < getChildCount(); i++) {
            View child = mOrderedChildren.get(i);
            childLeft = i % 2 * childWidth;
            childTop = i / 2 * childHeight;
            child.animate()
                    .translationX(childLeft)
                    .translationY(childTop)
                    .setDuration(150);
        }
    }
}

上面简单的示例有几个 OnDragListener 在使用上要留意的地方:

  • 因为其他的子View也监听了 OnDragListener,所以在回调 OnDrag 时,不仅是被拖拽的View接收到了拖拽回调,其他View同样也接收到,所以 DragEvent.ACTION_DRAG_STARTED 处理的不是该View被拖拽起来了,而是有个View被拖拽起来了

  • event.getLocalState() == v 和传递的View参数判断是否相同,在 v.startDrag() 方法传递了当前View对象为 myLocalState

2 ViewDragHelper

2.1 ViewDragHelper的使用

ViewDragHelper 的使用可以分成三个步骤:

  • 使用 ViewDragHelper.create() 创建ViewDragHelper对象

  • 将事件拦截 onInterceptTouchEvent()onTouchEvent() 交由 ViewDragHelper 接管

  • 提供 ViewDragHelper.Callback 处理View的拖拽,ViewGroup重写 computeScroll 处理拖拽动画

首先通过 ViewDragHelper.create() 创建一个 ViewDragHelper 对象,需要传入 ViewGroupViewDragHelper.Callback

public static ViewDragHelper create(@NonNull ViewGroup forParent, @NonNull ViewDragHelper.Callback cb) {
     return new ViewDragHelper(forParent.getContext(), forParent, cb);
 }

 public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity, @NonNull ViewDragHelper.Callback cb) {
     ViewDragHelper helper = create(forParent, cb);
     helper.mTouchSlop = (int)((float)helper.mTouchSlop * (1.0F / sensitivity));
     return helper;
 }

创建了 ViewDragHelper 后,触摸事件交由 ViewDragHelper 处理,具体实现是ViewGroup重写 onInterceptTouchEvent()onTouchEvent() 将事件交由 ViewDragHelper 处理。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
	return viewDragHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
	viewDragHelper.processTouchEvent(event);
	return true;
}

最后要具体对View进行拖拽操作,还需要实现 ViewDragHelper.Callback

class DragCallback extends ViewDragHelper.Callback {
	
    // 尝试抓住View,当手触摸到要拖拽的View时就会回调
    // 返回true表示要触发拖拽,但返回true还不能够实现拖拽
    // 还需要重写clampViewPositionHorizontal()或clampViewPositionVertical()
    // 返回false表示不拖拽
	@Override
	public boolean tryCaptureView(@NonNull View view, int pointerId) {
		return false;
	}

    // 限制View在拖拽时水平方向的偏移
    // 如果只有只重写了clampViewPositionHorizontal,View的拖拽只能在水平方向移动
    /**
     * @param left 手指拖动View的水平距离,重写该方法返回该参数表示水平方向拖动不干预限制拖拽
     */
    @Override
    public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
        return left;
    }

  	// 限制View在拖拽时垂直方向的偏移
    // 如果只有重写了clampViewPositionVertical,View的拖拽只能在垂直方向移动
    /**
     * @param top 手指拖动View的垂直距离,重写该方法返回该参数表示垂直方向拖动不干预限制拖拽
     */
    @Override
    public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
        return top;
    }

	// 当View被拖拽起来的时候回调
	@Override
	public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
	}
	
	// 当View被移动的时候回调
	@Override
	public void onViewPositionChanged(@NonNull View changeView, int left, int top, int dx, int dy) {
	}

	// 当松手的时候会回调
	@Override
	public void onReleased(@NonNull View releasedChild, float xvel, float yvel) {
	}
	
	@Override
	public void onViewDragStateChanged(int state) {
	}
}

ViewDragHelper 和对应回调只会处理View的拖拽移动,具体被拖拽的View的移动处理是由我们控制,需要重写ViewGroup的 computeScroll()

@Override
public void computeScroll() {
	if (viewDragHelper.continueSettling(true)) {
		ViewCompat.postInvalidateOnAnimation(this);
	}
}

2.2 ViewDragHelper简单实例

先看一下 ViewDragHelper 实现的拖拽效果:
在这里插入图片描述
OnDragListener 一样也是网格排列,不过这里为了演示方便就不实现重排序了。具体布局和代码如下:

<?xml version="1.0" encoding="utf-8"?>
<com.example.demo.DragHelperGridView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#EF5350"/>

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#9C27B0"/>

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#1E88E5"/>

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00695C"/>

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FDD835"/>

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#546E7A"/>

</com.example.demo.DragHelperGridView>

public class DragHelperGridView extends ViewGroup {
    private static final int COLUMNS = 2;
    private static final int ROWS = 3;

    private ViewDragHelper mViewDragHelper;

    public DragHelperGridView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mViewDragHelper = ViewDragHelper.create(this, new DragCallback());
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int specWidth = MeasureSpec.getSize(widthMeasureSpec);
        int specHeight = MeasureSpec.getSize(heightMeasureSpec);
        int childWidth = specWidth / COLUMNS;
        int childHeight = specHeight / ROWS;

        measureChildren(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));

        setMeasuredDimension(specWidth, specHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int childLeft;
        int childTop;
        int childWidth = getWidth() / COLUMNS;
        int childHeight = getHeight() / ROWS;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            childLeft = i % 2 * childWidth;
            childTop = i / 2 * childHeight;
            child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    private class DragCallback extends ViewDragHelper.Callback {
        private float captureLeft;
        private float captureTop;

        @Override
        public boolean tryCaptureView(@NonNull View view, int pointerId) {
            return true;
        }

        @Override
        public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
            return left;
        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            return top;
        }

        @Override
        public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                capturedChild.setElevation(getElevation() + 1);
            }
            // 记录被拖拽起来时View的位置,松手时onViewRelease()返回到原始位置
            captureLeft = capturedChild.getLeft();
            captureTop = capturedChild.getTop();
        }

        @Override
        public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
        }

        @Override
        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
            // 将View移动回拖拽起来时的开始位置
            // settleCapturedViewAt()可以认为只是一个计算器,不是实际执行移动的方法
            mViewDragHelper.settleCapturedViewAt((int) captureLeft, (int) captureTop);
            postInvalidateOnAnimation(); // 通知以动画的防止刷新返回,回调computeScroll
        }

        @Override
        public void onViewDragStateChanged(int state) {
            if (state == ViewDragHelper.STATE_IDLE) {
                View capturedView = mViewDragHelper.getCapturedView();
                if (capturedView != null) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        capturedView.setElevation(capturedView.getElevation() - 1);
                    }
                }
            }
        }
    }

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
}

3 OnDragListener和ViewDragHelper使用场景分析

3.1 OnDragListener使用场景

上面讲解 OnDragListener 使用时我们发现,OnDragListener 在拖拽时并不是直接将View拖拽起来,而是生成了一个和被拖拽的View大小相同的像素进行拖拽,为什么要这样设计呢?这需要说到两个参数:ClipDatamyLocalState,这两个参数在调用 startDrag() 时需要我们传入:

/**
 * @deprecated Use {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)
 * startDragAndDrop()} for newer platform versions.
 */
@Deprecated
public final boolean startDrag(ClipData data, DragShadowBuilder shadowBuilder,
                               Object myLocalState, int flags) {
    return startDragAndDrop(data, shadowBuilder, myLocalState, flags);
}
  • ClipData:拖拽时用于传递的数据,会在 DragEvent.ACTION_DROP 拖拽结束松手时才能获取到 ClipData 的数据

  • myLocalState:可以用它传递本地数据,监听 DragEvent.ACTION_DRAG_STARTEDDragEvent.ACTION_DRAG_ENTEREDDragEvent.ACTION_DRAG_ENDED 等拖拽状态时通过 DragEvent.getLocalState() 随时获取该本地数据

这两个参数最大的区别在于:ClipData 是可以跨进程的,而 myLocalState 不能。如果是跨进程的Activity之间通信,DragEvent.getLocalState() 会返回null。

Google将 OnDragListener 设计为拖拽时生成像素而不是直接拖拽View,假设目前有一种场景:在一个图片库app有一张图片,我们可能会希望直接将这张图片在分屏情况下拖拽到另外一个应用然后发送或上传这张图片。既然这张图片它是其他应用的,如果我们直接拖拽View,在这种跨进程的场景就会令用户感到疑惑。所以Google这种设计是合理的。

OnDragListener 更偏向于对内容数据的操作而不是对View的操作,它的拖拽是与界面无关的,不需要自定义View就可以实现拖拽,可以跨进程通信传输数据。

下面使用 OnDragListener 实现拖拽View通过 ClipData 传输数据的例子:

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

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/iv_icon"
            android:layout_width="0dp"
            android:layout_height="120dp"
            android:layout_weight="1"
            android:contentDescription="icon"
            android:src="@mipmap/ic_launcher" />

        <ImageView
            android:id="@+id/iv_logo"
            android:layout_width="0dp"
            android:layout_height="120dp"
            android:layout_weight="1"
            android:contentDescription="logo"
            android:src="@mipmap/ic_launcher" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/collect_layout"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:layout_gravity="bottom"
        android:background="#78909C"
        android:orientation="horizontal" />

</FrameLayout>

public class MainActivity extends AppCompatActivity {

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

        ImageView ivIcon = findViewById(R.id.iv_icon);
        ImageView ivLogo = findViewById(R.id.iv_logo);
        LinearLayout collectLayout = findViewById(R.id.collect_layout);

        ivIcon.setOnLongClickListener(mDragStarter);
        ivLogo.setOnLongClickListener(mDragStarter);
        collectLayout.setOnDragListener(mOnDragListener);
    }


    private View.OnLongClickListener mDragStarter = new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            ClipData imageData = ClipData.newPlainText("name", v.getContentDescription());
            // DragShadowBuilder(v)就是拖拽时那个半透明的View
            // OnDragListener拖拽起来的半透明View是显示在上层的,说明它是全局的,而且是跨界面跨进程的
            return ViewCompat.startDragAndDrop(v, imageData, new View.DragShadowBuilder(v), v, 0);
        }
    };

    private View.OnDragListener mOnDragListener = new CollectListener();

    private class CollectListener implements View.OnDragListener {

        @Override
        public boolean onDrag(View v, DragEvent event) {
            // event.getLocalState()和event.getClipData()都是可以装载数据的,那为什么要提供两种?
            // 最主要的区别在于event.getClipData()是可以跨进程的,可以实现跨进程的拖拽获取数据
            if (event.getAction() == DragEvent.ACTION_DROP) {
                if (v instanceof LinearLayout) {
                    LinearLayout layout = (LinearLayout) v;
                    TextView textView = new TextView(MainActivity.this);
                    textView.setTextSize(16);
                    textView.setText(event.getClipData().getItemAt(0).getText());
                    layout.addView(textView);
                }
            }
            return true;
        }
    }
}

3.2 ViewDragHelper使用场景

ViewDragHelper 是一个工具类,使用它需要我们自定义ViewGroup,从 ViewDragHelper.create() 和后续需要 ViewDragHelper 接管触摸事件重写 onInterceptTouchEventonTouchEvent() 就能了解到:

public static ViewDragHelper create(@NonNull ViewGroup forParent, @NonNull ViewDragHelper.Callback cb) {
    return new ViewDragHelper(forParent.getContext(), forParent, cb);
}

使用 ViewDragHelper 的目的在于我们想拖拽某个ViewGroup下的子View,对View的操作是限定在ViewGroup的,它与界面有关。

相比 OnDragListenerViewDragHelper 的使用场景会更加常见,比如Launcher桌面的上拉类似抽屉的View展示更多的应用,侧滑退出界面 SwipeBackLayout 也是使用的 ViewDragHelper 实现的。

下面使用 ViewDragHelper 简单实现一个上下拖拽View有弹性回弹的效果:

<?xml version="1.0" encoding="utf-8"?>
<com.example.demo.DragUpDownLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="#388E3C"/>

</com.example.demo.DragUpDownLayout>

public class DragUpDownLayout extends FrameLayout {
    private View mView;
    private ViewDragHelper mViewDragHelper;
    private ViewConfiguration mViewConfiguration;

    public DragUpDownLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        mViewDragHelper = ViewDragHelper.create(this, new DragCallback());
        mViewConfiguration = ViewConfiguration.get(context);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mView = findViewById(R.id.view);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    private class DragCallback extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(@NonNull View view, int i) {
            return view == mView; // 触摸的是view则允许拖拽
        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            return top;
        }

        @Override
        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
            // 实现回弹
            if (Math.abs(yvel) > mViewConfiguration.getScaledMinimumFlingVelocity()) {
                if (yvel > 0) {
                    mViewDragHelper.settleCapturedViewAt(0, getHeight() - releasedChild.getHeight());
                } else {
                    mViewDragHelper.settleCapturedViewAt(0, 0);
                }
            } else {
                if (releasedChild.getTop() < getHeight() - releasedChild.getBottom()) {
                    mViewDragHelper.settleCapturedViewAt(0, 0);
                } else {
                    mViewDragHelper.settleCapturedViewAt(0, getHeight() - releasedChild.getHeight());
                }
            }
            postInvalidateOnAnimation();
        }
    }

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            postInvalidateOnAnimation();
        }
    }
}

3.3 OnDragListener和ViewDragHelper使用场景总结

  • OnDragListener 在API 11时加入的,它的重点在于内容的移动而不是控件的移动,可以不需要自己自定义View,只要实现 setOnDragListener() 即可,系统会帮你生成一个可拖拽的像素,该图像像素和界面是无关的,而且可以附加拖拽时的数据,能够跨进程传数据

  • ViewDragHelper 是一个工具类,用户要拖动某个ViewGroup里面的某个子View时使用(tryCaptureView() 判断一个或多个子View),它需要自定义View,需要让 ViewDragHelper 接管ViewGroup的触摸事件,可以实时的拖拽移动手动修改子View的位置,使用 ViewDragHelper 是在界面的操作

  • 6
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果你指的是 Android 中的滑动条(SeekBar),可以通过设置 OnSeekBarChangeListener 来监听滑动事件。例如: ```java SeekBar seekBar = findViewById(R.id.seekBar); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // 当滑动条改变时调用,可以在这里更新 UI 或执行其他操作 } @Override public void onStartTrackingTouch(SeekBar seekBar) { // 当用户开始拖动滑动条时调用 } @Override public void onStopTrackingTouch(SeekBar seekBar) { // 当用户停止拖动滑动条时调用 } }); ``` 如果你想要实现一个快速滑动条(即用户快速滑动时进度会快速变化),可以考虑使用 AccelerateInterpolator 或 DecelerateInterpolator 来实现加速或减速效果。例如: ```java SeekBar seekBar = findViewById(R.id.seekBar); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { private ValueAnimator animator; @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // 当滑动条改变时调用,可以在这里更新 UI 或执行其他操作 } @Override public void onStartTrackingTouch(SeekBar seekBar) { // 当用户开始拖动滑动条时调用 animator = ValueAnimator.ofInt(seekBar.getProgress(), seekBar.getMax()); animator.setDuration(2000); // 设置动画持续时间为 2 秒 animator.setInterpolator(new AccelerateInterpolator()); // 设置加速效果 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int progress = (int) animation.getAnimatedValue(); seekBar.setProgress(progress); } }); animator.start(); } @Override public void onStopTrackingTouch(SeekBar seekBar) { // 当用户停止拖动滑动条时调用 if (animator != null) { animator.cancel(); // 取消动画 } } }); ``` 这段代码会在用户开始拖动滑动条时启动一个动画,将滑动条的进度从当前值加速变化到最大值,动画持续时间为 2 秒。当用户停止拖动滑动条时取消动画,滑动条的进度将停止变化。你可以根据自己的需求调整动画的参数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值