Android系统中SwipeDismissLayout(右滑退出)

背景

最近在做一个手表项目, Android 7.1.1系统, 系统中有个全局从左向右滑动退出当前Activity功能, 本以为是哪位同事添加的功能, 后来看了下代码才发现是Android系统本身就有的功能(Android 5.0加入的), 使用也非常方便, 下面就来讲一下这个功能如何启用和基本原理.

右滑退出原理

右滑退出基本原理很简单, 在某个ViewGroup中, 拦截onTouch事件(onInterceptTouchEvent()), 根据滑动手势改变View或者Window的偏移量, 在达到某个阈值后, 判定当前手势为退出, 调用Activity退出方法(finish() onBackPressed())即可.

但是如果你只是这样操作的话,会发现滑动过程中的背景是黑的, 而不是显示当前Activity后面的Activity内容, 这是因为, Activity执行onStop()后, 处于一种不可见状态, 要想让当前Activity后面的Activity被绘制出来, 需要用到Activity的两个函数: convertFromTranslucent() 和 convertToTranslucent()
我们来看下对应的函数解释:
frameworks/base/core/java/android/app/Activity.java

/**
 * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} to a
 * fullscreen opaque Activity.
 * <p>
 * Call this whenever the background of a translucent Activity has changed to become opaque.
 * Doing so will allow the {@link android.view.Surface} of the Activity behind to be released.
 * <p>
 * This call has no effect on non-translucent activities or on activities with the
 * {@link android.R.attr#windowIsFloating} attribute.
 *
 * @see #convertToTranslucent(android.app.Activity.TranslucentConversionListener,
 * ActivityOptions)
 * @see TranslucentConversionListener
 *
 * @hide
 */
@SystemApi
public void convertFromTranslucent() {
    ......
}

/**
 * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} back from
 * opaque to translucent following a call to {@link #convertFromTranslucent()}.
 * <p>
 * Calling this allows the Activity behind this one to be seen again. Once all such Activities
 * have been redrawn {@link TranslucentConversionListener#onTranslucentConversionComplete} will
 * be called indicating that it is safe to make this activity translucent again. Until
 * {@link TranslucentConversionListener#onTranslucentConversionComplete} is called the image
 * behind the frontmost Activity will be indeterminate.
 * <p>
 * This call has no effect on non-translucent activities or on activities with the
 * {@link android.R.attr#windowIsFloating} attribute.
 *
 * @param callback the method to call when all visible Activities behind this one have been
 * drawn and it is safe to make this Activity translucent again.
 * @param options activity options delivered to the activity below this one. The options
 * are retrieved using {@link #getActivityOptions}.
 * @return <code>true</code> if Window was opaque and will become translucent or
 * <code>false</code> if window was translucent and no change needed to be made.
 *
 * @see #convertFromTranslucent()
 * @see TranslucentConversionListener
 *
 * @hide
 */
@SystemApi
public boolean convertToTranslucent(TranslucentConversionListener callback,
        ActivityOptions options) {
    ......
}

简单解释就是调用当前Activity的convertToTranslucent(), 会导致其后面的Activity变为可见, 这正是我们想要的效果, convertFromTranslucent()则相反, 让后面Activity不可见.
知道这些内容以后, 我们就可以在滑动开始的时候调用convertToTranslucent()来让后面的Activity可见. 基本原理了解后,下面看下具体代码实现.

代码实现

frameworks/base/core/java/com/android/internal/widget/SwipeDismissLayout.java
首先在SwipeDismissLayout.java中拦截触摸事件:

    @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            resetMembers();
            mDownX = ev.getRawX();
            mDownY = ev.getRawY();
            mActiveTouchId = ev.getPointerId(0);
            mVelocityTracker = VelocityTracker.obtain();
            mVelocityTracker.addMovement(ev);
            break;
         //部分代码省略...
        case MotionEvent.ACTION_POINTER_UP:
            actionIndex = ev.getActionIndex();
            int pointerId = ev.getPointerId(actionIndex);
            if (pointerId == mActiveTouchId) {
                // This was our active pointer going up. Choose a new active pointer.
                int newActionIndex = actionIndex == 0 ? 1 : 0;
                mActiveTouchId = ev.getPointerId(newActionIndex);
            }
            break;
        case MotionEvent.ACTION_MOVE:
            //部分代码省略...
            float dx = ev.getRawX() - mDownX;
            float x = ev.getX(pointerIndex);
            float y = ev.getY(pointerIndex);
            if (dx != 0 && canScroll(this, false, dx, x, y)) {
                mDiscardIntercept = true;
                break;
            }
            updateSwiping(ev);
            break;
    }

    return !mDiscardIntercept && mSwiping;
}

这里面就是一些滑动逻辑判断, 主要判断是否是右滑, 如果是就拦截当前事件, 这样后续事件的onTouchEvent()就不会传到子View中, 而是在当前View中的onTouchEvent()中进行处理.

onTouchEvent():

@Override
public boolean onTouchEvent(MotionEvent ev) {
    // 部分代码省略...
    ev.offsetLocation(mTranslationX, 0);
    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_UP:
            updateDismiss(ev);
            //判断当前动作是取消右滑还是结束Activity
            if (mDismissed) {
                dismiss();
            } else if (mSwiping) {
                cancel();
            }
            resetMembers();
            break;
        // 部分代码省略....
        case MotionEvent.ACTION_MOVE:
            mVelocityTracker.addMovement(ev);
            mLastX = ev.getRawX();
            updateSwiping(ev);
            if (mSwiping) {
                if (mUseDynamicTranslucency && getContext() instanceof Activity) {
                    //如果是右滑并且是Activity, 调用convertToTranslucent() 让后面Activity可见
                    ((Activity) getContext()).convertToTranslucent(null, null);
                }
                //此处会调用到PhoneWindow.java中, 来让Window偏移
                setProgress(ev.getRawX() - mDownX);
                break;
            }
    }
    return true;
}

此部分代码主要包括右滑动作,取消右滑(cancel())以及右滑手势完成后结束Activity(dismiss()). 同时, 如果开始右滑, 则调用 convertToTranslucent(), 让后面Activity可见, 这样当前Activity向右偏移后, 才能正常看到后面的Activity内容. 滑动过程中Activity的偏移, 结束, 是否启用右滑等代码的实现在PhoneWindow.java中,下面继续看源码.

启用/禁用右滑退出功能

SwipeDismissLayout是在什么时候被加载的呢? 这部分是在调用setContentView()之后的流程中来实现的. Activity的setContentView()最终会调用到PhoneWindow中, 在PhoneWindow中的generateLayout()函数中, 会根据一些条件, 来决定加载那个布局, 代码如下:
frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java


protected ViewGroup generateLayout(DecorView decor) {
    //部分代码省略...
    //如果主题中windowSwipeToDismiss为true, 添加FEATURE_SWIPE_TO_DISMISS
    if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
        requestFeature(FEATURE_SWIPE_TO_DISMISS);
    }
    //部分代码省略...
    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        // 如果包含 FEATURE_SWIPE_TO_DISMISS,则加载的布局是screen_swipe_dismiss.xml
        layoutResource = R.layout.screen_swipe_dismiss;
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        // XXX Remove this once action bar supports these features.
        removeFeature(FEATURE_ACTION_BAR);
        // System.out.println("Title Icons!");
    }
    //部分代码省略...
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        //注册滑动相关回调
        registerSwipeCallbacks();
    }
    //部分代码省略...
    return contentParent;
}

这个函数内容很多, 我只挑了关键代码, 可以看到, 关键点即加载对应的layout文件:

layoutResource = R.layout.screen_swipe_dismiss;

screen_swipe_dismiss.xml的路径为:
frameworks/base/core/res/res/layout/screen_swipe_dismiss.xml

内容就一个SwipeDismissLayout布局:

<com.android.internal.widget.SwipeDismissLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/content"
    android:fitsSystemWindows="true"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

使用这个布局后, setContentView的内容就会加入到此布局之中.

因此,启用右滑功能可以通过两种方式实现:

  1. 调用函数方式(Activity中) : getWindow().requestFeature(Window.FEATURE_SWIPE_TO_DISMISS);, 必须在setContentView()之前进行设置
  2. 通过主题配置, 在主题样式中加入:<item name="android:windowSwipeToDismiss">true</item>

加入这个Feature后, PhoneWindow.java中就会加载screen_swipe_dismiss.xml这个布局, APP布局的"content"就会成为SwipeDismissLayout的子View, 从而达到拦截事件以及实现右滑功能.

另外我们可以看到, 滑动的回调也在PhoneWindow中实现,代码如下:

private void registerSwipeCallbacks() {
    SwipeDismissLayout swipeDismiss =
            (SwipeDismissLayout) findViewById(R.id.content);
    swipeDismiss.setOnDismissedListener(new SwipeDismissLayout.OnDismissedListener() {
        @Override
        public void onDismissed(SwipeDismissLayout layout) {
            //此处最终会调到Activity的onBackPressed(), 从而结束当前Activity
            dispatchOnWindowDismissed(false /*finishTask*/);
        }
    });
    swipeDismiss.setOnSwipeProgressChangedListener(
            new SwipeDismissLayout.OnSwipeProgressChangedListener() {
                private static final float ALPHA_DECREASE = 0.5f;
                private boolean mIsTranslucent = false;
                @Override
                public void onSwipeProgressChanged(
                        SwipeDismissLayout layout, float progress, float translate) {
                    //通过设置WindowManager.LayoutParams来实现滑动偏移效果
                    WindowManager.LayoutParams newParams = getAttributes();
                    newParams.x = (int) translate;
                    newParams.alpha = 1 - (progress * ALPHA_DECREASE);
                    setAttributes(newParams);
                    //部分代码省略...
                }

                @Override
                public void onSwipeCancelled(SwipeDismissLayout layout) {
                    //取消滑动后重置相关参数
                    WindowManager.LayoutParams newParams = getAttributes();
                    newParams.x = 0;
                    newParams.alpha = 1;
                    setAttributes(newParams);
                    setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS);
                }
            });
}

逻辑也比较简单, 即Activity的关闭, 滑动偏移效果和取消滑动这三个关键逻辑,都是在这里实现的.

实际测试

既然是Android 5.0就加入的功能, 想必一般厂商不会没事去掉这个功能,我拿我手上的SONY Xperia Z5(Android 7.1.1)试了下, 随便写一个Activity进行测试:
setContentView()之前调用 getWindow().requestFeature(Window.FEATURE_SWIPE_TO_DISMISS);
或者在主题中加入 <item name="android:windowSwipeToDismiss">true</item>
都能实现右滑退出功.

swipe.png

但是由于本身实现逻辑问题, 取消滑动默认会进入全屏状态, 如果做系统开发的, 需要用这个功能的话, 可以根据需求进行修改.

总结

关于右滑退出这个系统功能, 关键点如下:

  1. 通过convertFromTranslucent() 和 convertToTranslucent()来实现让背后的Activity可见和不可见
  2. 在ViewGroup中拦截onTouchEvent事件, 通过手势实现右滑.
  3. 给Window添加Feature "FEATURE_SWIPE_TO_DISMISS", 会让系统加载SwipeDismissLayout来作为App布局的父View.
  4. 右滑偏移效果, 取消右滑, 关闭Activity都在PhoneWindow中进行处理

存在的问题:
从上面代码中可以看到, 拦截onTouch事件是判读是不是向右滑动了,并且会判断字View是否可以滑动, 如果不可以滑动, 右滑事件就会被拦截, 因此当App中有右滑的需求, 就会产生手势冲突, App的右滑事件会被拦截, 所以如果实际要用这个功能, 还需进行优化, 比如只在边缘向右滑动的时候才拦截事件, 这样就不会产生手势冲突了, 或者App自己处理这种类型的冲突, 调用requestDisallowInterceptTouchEvent(boolean disallowIntercept)根据需求禁用拦截.

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    wgl0419阅读 6,263评论 1 9
  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    passiontim阅读 15,394评论 2 45
  • 才一天不送我,我就有些难过,我也不清楚我到底在难过什么,或许是他昨天蹭答应我的没有办到?或许我习惯了有他的陪伴,可...
    嘴角上扬笑看世态炎凉阅读 204评论 0 0
  • 两天画了一枝花
    宛茹阅读 260评论 2 5