在 上一篇文章 View touch event 分发机制 中介绍了 View 的 Touch event 的传递过程,并且实现了一个简单的右滑返回控件。不过那个控件实在太简陋,于是继续研究,发现了 ViewDragHelper 这个类,这个类的使用方法非常的简单,并且不需要我们去管理事件分发和处理滑动冲突,就能实现 View 的拖动等效果。
首先我们先来看它的构造方法
create(ViewGroup forParent, float sensitivity, ViewDragHelper.Callback cb)
需要三个参数:
- 第一个 ViewGroup 是承载可以被拖动的控件的
- 第二个是灵敏度
- 第三个是拖动回调。我们需要实现拖动效果,主要就是依靠重写回调方法实现。
ViewDragHelper.Callback 中 boolean tryCaptureView(View child, int pointerId)
需要实现,表明 child
是否可以被拖动,可以返回 true ,否则反之。
对于需要实现滑动返回效果来说,还有余下方法需要实现:
-
int clampViewPositionHorizontal(View child, int left, int dx)
表明child
横向上可以拖动的范围,与之对应的是clampViewPositionVertical(View child, int top, int dy)
。第二个参数表示 child view 尝试的移动到位置,返回值为实际应该在的位置。 -
void onViewReleased(View releasedChild, float xvel, float yvel)
方法在用户拖拽完成、放手的时候会被调用,需要在这个方法中判断是否拖动大于屏幕距离的一半,大于就执行 * 返回逻辑 *,否则反之。 -
void onEdgeDragStarted(int edgeFlags, int pointerId)
方法在用户从边缘发起拖动时会触发,因为我的目的是只有从边界处拖动才能触发效果,所以需要实现。
之后就是设置 ViewDragHelper 监视边缘拖拽,并且将事件拦截和事件处理交由 ViewDragHelper 即可。代码如下:
private void init() {
mHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 默认不扑获 View
return false;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 拖动限制(大于左边界)
return Math.max(0, left);
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 拖动距离大于屏幕的一半右移,拖动距离小于屏幕的一半左移
int left = releasedChild.getLeft();
if (left > getWidth() / 2) {
mActivity.finish();
} else {
mHelper.settleCapturedViewAt(0, 0);
}
invalidate();
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
// 捕获子 View
mHelper.captureChildView(mView, pointerId);
}
});
// 跟踪左边界拖动
mHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 拦截代理
return mHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Touch Event 代理
mHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
// 子 View 做动画
if (mHelper.continueSettling(true)) {
invalidate();
}
}
因为我们需要在拖动后看到上一个 Activity 的内容,所以需要将屏幕最顶层的 View 背景设置为透明,在 style 中设置:
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
注:也有用 Java 设置的方法,但是亲测无效,会导致背景全黑,猜测是因为使用 AppCompatActivity
的原因。
之后,最为重要的一步就是将我们的自定义布局插入顶层 ViewGroup 和我们显示的内容中间,在我们定义的 ViewGroup 中的代码如下:
public void attachActivity(Activity activity) {
mActivity = activity;
ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
View content = decor.getChildAt(0);
decor.removeView(content);
mView = content;
addView(content);
decor.addView(this);
}
其中 DecorView 就是最顶层的 ViewGroup。然后在 Activity#void onPostCreate(@Nullable Bundle savedInstanceState)
中调用即可,因为此时 View 已经创建完成。
到这一步,我们的主要逻辑已经全部完成,下面我放出完整的代码,分别是 SwipeBackLayout 和 SwipeBackActivity。
public class SwipeBackLayout extends FrameLayout {
private ViewDragHelper mHelper;
private View mView;
private Activity mActivity;
public SwipeBackLayout(Context context) {
super(context);
init();
}
private void init() {
mHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 默认不扑获 View
return false;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 拖动限制(大于左边界)
return Math.max(0, left);
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 拖动距离大于屏幕的一半右移,拖动距离小于屏幕的一半左移
int left = releasedChild.getLeft();
if (left > getWidth() / 2) {
mActivity.finish();
} else {
mHelper.settleCapturedViewAt(0, 0);
}
invalidate();
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
// 移动子 View
mHelper.captureChildView(mView, pointerId);
}
});
// 跟踪左边界拖动
mHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 拦截代理
return mHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Touch Event 代理
mHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
// 子 View 需要更新状态
if (mHelper.continueSettling(true)) {
invalidate();
}
}
/**
* 绑定 Activity
*
* @param activity 容器 Activity
*/
public void attachActivity(Activity activity) {
mActivity = activity;
ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
View content = decor.getChildAt(0);
decor.removeView(content);
mView = content;
addView(content);
decor.addView(this);
}
}
public class SwipeBackActivity extends AppCompatActivity {
private SwipeBackLayout mSwipeBackLayout;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSwipeBackLayout = new SwipeBackLayout(this);
}
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
mSwipeBackLayout.attachActivity(this);
}
@Override
public void finish() {
super.finish();
overridePendingTransition(0, R.anim.right_out);
}
}
顺带我们需要在 Activity 退出时是从右边退出的,所以添加了退出动画:
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromXDelta="0"
android:toXDelta="100%" />
</set>
全部代码就已经完成了,我们只需要继承 SwipeBackActivity 就能自动实现滑动返回了。总结一下要点:
- 利用 ViewDragHelper 实现界面拖动效果
- 设置顶层 ViewGroup 背景透明,能看见上一层内容
- 将 SwipeBackLayout 插入顶层 ViewGroup 和 ContentView 中间,拦截手势。
最后放出一张效果图:
网友评论