Android开发最近需要做的安卓开发博客

Android系统中SwipeDismissLayout(右滑退

2018-09-08  本文已影响12人  幽客

背景

最近在做一个手表项目, 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时间只是简单判读是不是向右滑动了, 因此当App中有右滑的需求, 就会产生手势冲突, App的右滑事件会被拦截, 所以如果实际要用这个功能, 还需进行优化, 比如只在边缘向右滑动的时候才拦截事件, 这样就不会产生手势冲突了, 有兴趣的可以自己试一下.

上一篇下一篇

猜你喜欢

热点阅读