美文网首页workAndroid Class
使用 ViewDragHelper 实现滑动返回

使用 ViewDragHelper 实现滑动返回

作者: chrnie | 来源:发表于2016-08-04 21:50 被阅读861次

    在 上一篇文章 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 ,否则反之。
    对于需要实现滑动返回效果来说,还有余下方法需要实现:

    1. int clampViewPositionHorizontal(View child, int left, int dx) 表明 child 横向上可以拖动的范围,与之对应的是 clampViewPositionVertical(View child, int top, int dy)。第二个参数表示 child view 尝试的移动到位置,返回值为实际应该在的位置。

    2. void onViewReleased(View releasedChild, float xvel, float yvel) 方法在用户拖拽完成、放手的时候会被调用,需要在这个方法中判断是否拖动大于屏幕距离的一半,大于就执行 * 返回逻辑 *,否则反之。

    3. 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 就能自动实现滑动返回了。总结一下要点:

    1. 利用 ViewDragHelper 实现界面拖动效果
    2. 设置顶层 ViewGroup 背景透明,能看见上一层内容
    3. 将 SwipeBackLayout 插入顶层 ViewGroup 和 ContentView 中间,拦截手势。
      最后放出一张效果图:
    效果图.gif

    相关文章

      网友评论

      本文标题:使用 ViewDragHelper 实现滑动返回

      本文链接:https://www.haomeiwen.com/subject/dexhsttx.html