使用 ViewDragHelper 实现滑动返回

在 上一篇文章 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容