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 可以跟着手指拖动了。
- 边界控制
边界控制是在 clampViewPositionVertical
和 clampViewPositionHorizontal
中完成。
进行边界控制需要一些数据:左边界,右边界,上边界,下边界。
以算出左边界为例,其它的计算可以类推:
设左边界为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
时,调用 ViewDragHelper
的 settleCapturedViewAt(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