使用 ViewDragHelper 实现沿三个方向拖动的例子

1. 需求

先看一下效果图:



<script type="math/tex" id="MathJax-Element-5"> </script>
效果图

2. 需求分析

当手指拖动中间的原形图标(称为 home)时,只能向左,向右,向上三个方向拖动;
当 home 向左,向右,向上拖动时,home 的中心不能超过左,右,上三个图标的中心;
当 home 向左,向右,向上拖动时,若 home 的中心,和左,右,上三个图标的中心重合,就触发一定的操作,这里使用 toast 代替;
当 home 向左,向右,向上拖动时,若 home 的中心未达到左,右,上三个图标的中心并且手指松开,那么 home 会回到它原来的位置。

3. 实现

3.1 实现方案选择

我最开始接到的需求是只有左向和右向拖动,没有向上的拖动,采用的方法是监听 home 图标的触摸事件,在 onTouch(View v, MotionEvent event) 回调方法中再设置 home 图标的 setTranslationX(float translationX) 来实现的。
之后,新需求就增加了向上的拖动,在达到红包的中心时,就打开红包。采用的方法是在现有的代码基础上修改,也算完成了任务。但是里面的判断很多,实现起来很复杂。写完之后,就是感到这是堆出来的代码,磨出来的代码,总之,就是不好。这部分代码,就不提供了。
随后,在群里问了这个问题,有人说使用 ViewDragHelper 可以实现的。看一下,ViewDragHelper 的文档说明:

/**
 * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
 * of useful operations and state tracking for allowing a user to drag and reposition
 * views within their parent ViewGroup.
 */

ViewDragHelper 是一个工具类, 用来写自定义的 ViewGroup 。它提供了一些好用的操作和状态追踪,使用户可以在 view 的父容器里拖动和重新放置那些 view。

3.2 代码实现

自定义 ViewGroup

看了上面的文档说明,了解到需要把要拖动的 view,放在一个自定义的 ViewGroup 里面。

/**
 * 固定向拖动 ViewGroup
 * @author wzc
 * @date 2018/5/6
 */
public class DirectionDragLayout extends ConstraintLayout {

    private final ViewDragHelper mViewDragHelper;

    public DirectionDragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 1, 创建 ViewDragHelper 的实例
        // 参一 : 当前的ViewGroup对象 Parent view to monitor
        // 参二 : 灵敏度 Multiplier for how sensitive the helper should be about detecting
        // the start of a drag. Larger values are more sensitive. 1.0f is normal.
        // 参三 : 提供信息和接收事件的回调
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
    }

    // 2, 在onInterceptTouchEvent和onTouchEvent中调用VDH的方法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 通过使用mDragHelper.shouldInterceptTouchEvent(ev)来决定我们是否应该拦截当前的事件
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 通过mDragHelper.processTouchEvent(event)来处理事件
        mViewDragHelper.processTouchEvent(event);
        return true; // 返回 true,表示事件被处理了。
    }

    class ViewDragCallback extends ViewDragHelper.Callback {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return false;
        }
    }

}

在 Activity 的布局中使用自定义的 ViewGroup

activity_directiondrag.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="#44000000"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.wzc.t20_vdh.DirectionDragLayout
        android:layout_alignParentBottom="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--阅读-->
        <ImageView
            android:id="@+id/iv_read"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="@+id/iv_home"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/iv_arrow_left"
            app:layout_constraintTop_toTopOf="@+id/iv_home"
            app:srcCompat="@drawable/read"/>

        <!--箭头-->
        <ImageView
            android:id="@+id/iv_arrow_left"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:rotation="180"
            android:src="@drawable/lock_slide"
            app:layout_constraintBottom_toBottomOf="@+id/iv_home"
            app:layout_constraintLeft_toRightOf="@+id/iv_read"
            app:layout_constraintRight_toLeftOf="@+id/iv_home"
            app:layout_constraintTop_toTopOf="@+id/iv_home"/>

        <!--箭头-->
        <ImageView
            android:id="@+id/iv_arrow_right"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/lock_slide"
            app:layout_constraintBottom_toBottomOf="@+id/iv_home"
            app:layout_constraintLeft_toRightOf="@+id/iv_home"
            app:layout_constraintRight_toLeftOf="@+id/iv_unlock"
            app:layout_constraintTop_toTopOf="@+id/iv_home"/>

        <!--解锁-->
        <ImageView
            android:id="@+id/iv_unlock"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="@+id/iv_home"
            app:layout_constraintLeft_toRightOf="@+id/iv_arrow_right"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/iv_home"
            app:srcCompat="@drawable/unlock"/>

        <!--箭头-->
        <ImageView
            android:id="@+id/iv_arrow_top"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:src="@drawable/lock_slide_up"
            app:layout_constraintBottom_toTopOf="@+id/iv_home"
            app:layout_constraintLeft_toLeftOf="@+id/iv_home"
            app:layout_constraintRight_toRightOf="@+id/iv_home"/>

        <!--红包-->
        <ImageView
            android:id="@+id/iv_redbag"
            android:layout_width="44dp"
            android:layout_height="44dp"
            android:layout_marginBottom="16dp"
            android:layout_marginTop="16dp"
            app:layout_constraintBottom_toTopOf="@+id/iv_arrow_top"
            app:layout_constraintLeft_toLeftOf="@+id/iv_home"
            app:layout_constraintRight_toRightOf="@+id/iv_home"
            app:srcCompat="@drawable/ic_lock_redbag"/>

        <!--滑动的原点-->
        <ImageView
            android:id="@+id/iv_home"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/iv_arrow_left"
            app:layout_constraintRight_toLeftOf="@+id/iv_arrow_right"
            android:layout_marginBottom="16dp"
            app:srcCompat="@drawable/circle"/>
    </com.wzc.t20_vdh.DirectionDragLayout>
</RelativeLayout>

预览图和gif效果图开始部分是一样的。

在 Activity 中使用这个布局:

/**
 * 固定向拖动页面
 * @author wzc
 * @date 2018/5/6
 */
public class DirectionDragActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_directiondrag);
    }
}

DirectionDragActivity 中的代码就完成了,之后不会在这里添加任何代码了。余下的任务都会在 DirectionDragLayout 中完成。

完善 DirectionDragLayout 中的代码
  • 随着手指拖动:
/**
 * 固定向拖动 ViewGroup
 * @author wzc
 * @date 2018/5/6
 */
public class DirectionDragLayout extends ConstraintLayout {

    private final ViewDragHelper mViewDragHelper;
    /**
     * 左边的View
     */
    private View mReadView;
    /**
     * 右边的View
     */
    private View mUnlockView;
    /**
     * 上边的View
     */
    private View mRedbagView;
    /**
     * 中间的View,就是要拖动的View
     */
    private View mHomeView;

    public DirectionDragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 1, 创建 ViewDragHelper 的实例
        // 参一 : 当前的ViewGroup对象 Parent view to monitor
        // 参二 : 灵敏度 Multiplier for how sensitive the helper should be about detecting
        // the start of a drag. Larger values are more sensitive. 1.0f is normal.
        // 参三 : 提供信息和接收事件的回调
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 3,获取View
        mReadView = findViewById(R.id.iv_read);
        mUnlockView = findViewById(R.id.iv_unlock);
        mRedbagView = findViewById(R.id.iv_redbag);
        mHomeView = findViewById(R.id.iv_home);
    }

    // 2, 在onInterceptTouchEvent和onTouchEvent中调用VDH的方法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 通过使用mDragHelper.shouldInterceptTouchEvent(ev)来决定我们是否应该拦截当前的事件
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 通过mDragHelper.processTouchEvent(event)来处理事件
        mViewDragHelper.processTouchEvent(event);
        return true; // 返回 true,表示事件被处理了。
    }

    class ViewDragCallback extends ViewDragHelper.Callback {
        // 4, 这个是必须重写的方法,
        // 返回true,表示允许捕获该子view
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            // 当 child 是 mHomeView 时,才允许捕获。
            return child == mHomeView;
        }
        // 5, 限制被拖拽的子view沿纵轴的运动
        // 如果不重写,就不能实现纵向的拖动
        // 参一:child 表示正在拖拽的 view
        // 参二:top Attempted motion along the Y axis 理解为拖动的那个view想要到达位置的top值
        // 参三:增量,变化量
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return top;
        }
        // 6, 限制被拖拽的子view沿横轴的运动
        // 如果不重写,就不能实现横向的拖动
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }
    }

}

增加的代码是第3,4,5,6处,运行一下,效果是 home 可以跟着手指拖动了。

  • 边界控制

边界控制是在 clampViewPositionVerticalclampViewPositionHorizontal 中完成。
进行边界控制需要一些数据:左边界,右边界,上边界,下边界。
以算出左边界为例,其它的计算可以类推:
设左边界为leftBound,home 的宽度为 homeWidth,左边 view 的中心点 x 坐标为 leftViewCenterX。
那么,当 home 达到左边界时,home 的中心和左边 view 的中心是重合的,即:
leftBound + homeWidth / 2 = leftViewCenterX;
leftBound = leftViewCenterX - homeWidth / 2;

onLayout() 方法中获取需要的边界值:

    Point mHomeCenterPoint = new Point();
    Point mReadCenterPoint = new Point();
    Point mUnlockCenterPoint = new Point();
    Point mRedbagCenterPoint = new Point();
    Point mHomeOriginalPoint = new Point();

    private int mTopBound;
    private int mBottomBound;
    private int mLeftBound;
    private int mRightBound;
    // 7, 计算边界
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        mHomeOriginalPoint.x = mHomeView.getLeft();
        mHomeOriginalPoint.y = mHomeView.getTop();

        mHomeCenterPoint.x = mHomeView.getLeft() + mHomeView.getMeasuredWidth() / 2;
        mHomeCenterPoint.y = mHomeView.getTop() + mHomeView.getMeasuredHeight() / 2;

        mReadCenterPoint.x = mReadView.getLeft() + mReadView.getMeasuredWidth() / 2;
        mReadCenterPoint.y = mReadView.getTop() + mReadView.getMeasuredHeight() / 2;

        mUnlockCenterPoint.x = mUnlockView.getLeft() + mUnlockView.getMeasuredWidth() / 2;
        mUnlockCenterPoint.y = mUnlockView.getTop() + mUnlockView.getMeasuredHeight() / 2;

        mRedbagCenterPoint.x = mRedbagView.getLeft() + mRedbagView.getMeasuredWidth() / 2;
        mRedbagCenterPoint.y = mRedbagView.getTop() + mRedbagView.getMeasuredHeight() / 2;

        mTopBound = mRedbagCenterPoint.y - mHomeView.getMeasuredHeight() / 2;
        mBottomBound = mHomeCenterPoint.y - mHomeView.getMeasuredHeight() / 2;
        mLeftBound = mReadCenterPoint.x - mHomeView.getMeasuredWidth() / 2;
        mRightBound = mUnlockCenterPoint.x - mHomeView.getMeasuredWidth() / 2;
    }

上下左右边界控制

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            // 8, 上下边界控制
            final int newTop = Math.min(Math.max(mTopBound, top), mBottomBound);
            return newTop;
        }
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            // 9, 左右边界控制
            final int newLeft = Math.min(Math.max(mLeftBound, left), mRightBound);
            return newLeft;
        }

运行一下程序,可以看到实现了边界控制。

  • 松手返回起始点

重写 onViewReleased() 方法,这个方法在释放拖拽的 view 时,会回调。

    class ViewDragCallback extends ViewDragHelper.Callback {
        // 10, 松手返回起始点
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            // 判断释放的 view 是不是 mHomeView
            if (releasedChild == mHomeView) {
                // 让释放的 view 停在给定的位置
                mViewDragHelper.settleCapturedViewAt(mHomeOriginalPoint.x, mHomeOriginalPoint.y);
                invalidate();
            }
        }
    }
    // 10, 松手返回起始点
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mViewDragHelper.continueSettling(true)) {
            invalidate();
        }
    }

onViewReleased() 方法中,判断释放的 view 是 mHomeView 时,调用 ViewDragHelpersettleCapturedViewAt(int finalLeft, int finalTop) 方法,让 view 停在给定的位置。这个方法内部是使用 Scroller ,所以记得紧接着这行代码需要调用 invalidate(); , 并且重写 computeScroll() 方法。

运行一下程序,达到了效果。

  • 固定向拖拽

声明两个标记,用来记录当前是在进行哪种拖拽,再在 clampViewPositionVertical()clampViewPositionHorizontal() 方法中根据 dx,dy 的绝对值是否大于 0,来决定首先进行哪种拖拽。

一旦一种拖拽先决定好,那么另外一种拖拽,在本次拖拽过程中就不会生效了。通过这种方法,实现了固定向拖拽的效果。

    /**
     * 当前正在水平拖拽的标记
     */
    private boolean mIsHorizontalDrag = false;
    /**
     * 当前正在竖直拖拽的标记
     */
    private boolean mIsVerticalDrag = false;

    class ViewDragCallback extends ViewDragHelper.Callback {

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            // 11, 固定向拖拽
            if (mIsHorizontalDrag) {
                return mHomeView.getTop();
            }
            if (Math.abs(dy) > 0) {
                mIsVerticalDrag = true;
            }
            // 8, 上下边界控制
            final int newTop = Math.min(Math.max(mTopBound, top), mBottomBound);
            return newTop;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            // 11, 固定向拖拽
            if (mIsVerticalDrag) {
                return mHomeView.getLeft();
            }
            if (Math.abs(dx) > 0) {
                mIsHorizontalDrag = true;
            }
            // 9, 左右边界控制
            final int newLeft = Math.min(Math.max(mLeftBound, left), mRightBound);
            return newLeft;
        }
    }

需要注意的是,在新的拖拽发生前,清除掉拖拽标记的值。注意清除标记要在拦截事件前,而不是在 onTouchEvent() 中。写在后者中,是没有任何效果的。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 在 VDH 拦截事件前,记得重置拖拽标记
        resetFlags();
        // 通过使用mDragHelper.shouldInterceptTouchEvent(ev)来决定我们是否应该拦截当前的事件
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    private void resetFlags() {
        mIsHorizontalDrag = false;
        mIsVerticalDrag = false;
    }
  • 到达边界时触发操作

onViewPositionChanged() 中进行,这个方法当拖拽的 view 位置发生变化时被回调。

    /**
     * 是否到达边界的标记
     */
    private boolean mIsReachBound;
    class ViewDragCallback extends ViewDragHelper.Callback {

        // 当拖拽的View的位置发生变化的时候回调(特指capturedview)
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            // 12, 到达边界时触发操作
            if (mIsReachBound) {
                return;
            }
            if (left <= mLeftBound) {
                mIsReachBound = true;
                Toast.makeText(getContext(), "到达左边界", Toast.LENGTH_SHORT).show();
            }
            if (left >= mRightBound) {
                mIsReachBound = true;
                Toast.makeText(getContext(), "到达右边界", Toast.LENGTH_SHORT).show();
            }
            if (top <= mTopBound) {
                mIsReachBound = true;
                Toast.makeText(getContext(), "到达上边界", Toast.LENGTH_SHORT).show();
            }
        }
    }

这里使用标记 mIsReachBound , 为了防止到达边界时,多次触发操作。
同样地,记得在拦截事件前,把此标记的值清除掉。

    private void resetFlags() {
        mIsHorizontalDrag = false;
        mIsVerticalDrag = false;
        mIsReachBound = false;
    }

运行一下程序,实现了全部的需求。

源码位置:https://github.com/jhwsx/AndroidTraining/tree/master/t20_vdh

参考

Android ViewDragHelper完全解析 自定义ViewGroup神器
Each Navigation Drawer Hides a ViewDragHelper

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值