Android 仿全面屏侧滑返回功能

4,838 阅读4分钟

Android 仿全面屏侧滑返回功能

为啥要搞这个

最近,项目的线上崩溃统计中发现了由于 SwipeBackLayout 导致的闪退问题,在排查问题的时候发现这个库已经多年没有维护了,而随后找了其它的各种侧滑返回库也有着各式各样的问题,那么有没有一种侧滑返回适配简单没有那么多的问题呢?

市场调研

纵观现有的侧滑返回库,现有的都是仿 iOS 的效果,从左侧侧滑,当前 Activity 向右移动,能够看到下方的 Activity 布局,但是 Android 并没有对这个方式提供支持,这里我看到了 仿苹果手机的侧滑返回分析和实现 这篇文章,里面统计了当前各种侧滑返回的实现方案:

  1. 不透明方案 ,获取下层 Activity 布局,在当前 Activity 中绘制下层 Activity 布局或移动顶层 Activity 的 ContentView ,但是这种方法都有侧滑时显示的下层界面可能和实际不一致的问题。
  2. 透明方案 ,主题中设置窗口透明,监听侧滑事件移动顶层 Activity 的 ContentView ,但是这种方案会导致各种动画异常,并且下层 Activity 不会执行 onStop 方法,导致一系列的性能问题。
  3. 反射方案 ,通过反射,在侧滑时将 Activity 设置为透明,侧滑结束恢复成不透明,这是文章作者的实现方案,可惜当前 star 比较少,有没有问题还待观察。

那么问题来了,为什么我们做 Android 应用非要照着 iOS 的来呢?Android 全面屏的侧滑返回不香吗?没错,这正是我打算做的 ( ̄3 ̄)a

实现思路

现在的 Android 全面屏手机一般都有自带的侧滑返回功能了,其原理就是拦截了侧滑事件,并显示一个侧滑的浮窗,但是这个功能只有全面屏手机才有啊,我们要如何在其他手机上页实现相同的功能呢?

我第一个想到是悬浮窗,但是使用全局的悬浮窗是需要用户手动去开启悬浮窗权限的,显然,这并不是一个好的解决方案,不使用全局悬浮窗就没办法达到全面屏手机原生的效果了,没办法,只好退而求其次,给每个 Activity 单独加上吧。

核心功能只有一个类:

class SwipeBackHelper(private val mActivity: Activity) {

    /** 侧滑返回 View */
    private val slideBackView: SlideBackView = SlideBackView(mActivity)

    init {
        // 将侧滑返回 View 添加到 Activity
        (mActivity.window.decorView as FrameLayout).addView(
                slideBackView,
                FrameLayout.LayoutParams(
                        WindowManager.LayoutParams.WRAP_CONTENT,
                        WindowManager.LayoutParams.MATCH_PARENT
                )
        )
    }

    /** 屏幕宽度 */
    private val screenWidth = mActivity.resources.displayMetrics.widthPixels

    /** 标记 - 是否是从左侧侧滑 */
    private var slideFromStartSide = false
    /** 标记 - 是否是从右侧侧滑 */
    private var slideFromEndSide = false

    /** 按下 x 轴位置 */
    private var mTouchStartX = 0f

    /** 标记 - 是否允许侧滑返回 */
    var swipeBackEnable = true

    /**
     * 拦截[Activity]事件分发并处理侧滑返回
     *
     * @param ev 事件对象
     *
     * @return 是否拦截
     */
    fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        if (!swipeBackEnable) {
            return false
        }
        return when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 按下
                mTouchStartX = ev.rawX
                if (ev.rawX < screenWidth * SwipeBackConfig.sensorPercent) {
                    // 边缘侧滑
                    show(true, ev.rawY)
                    slideFromStartSide = true
                } else if (ev.rawX > screenWidth * (1 - SwipeBackConfig.sensorPercent)) {
                    show(false, ev.rawY)
                    slideFromEndSide = true
                }
                slideFromStartSide || slideFromEndSide
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                // 抬起,取消
                if (slideFromStartSide || slideFromEndSide) {
                    hide()
                    slideFromStartSide = false
                    slideFromEndSide = false
                }
                false
            }
            MotionEvent.ACTION_MOVE -> {
                // 移动
                if (slideFromStartSide || slideFromEndSide) {
                    val x = ev.rawX
                    val moveSize = (x - mTouchStartX).absoluteValue * SwipeBackConfig.slippageFactor
                    val progress = if (moveSize >= SwipeBackConfig.moveMax) 1f else (moveSize / SwipeBackConfig.moveMax)
                    setProgress(progress)
                    true
                } else {
                    false
                }
            }
            else -> {
                // 其他
                false
            }
        }
    }

    /**
     * 显示侧滑 View
     *
     * @param start 是否从左侧侧滑
     * @param y 手指按下 y 轴位置
     */
    private fun show(start: Boolean, y: Float) {
        slideBackView.visibility = View.VISIBLE
        slideBackView.rotation = if (start) 0f else 180f
        slideBackView.translationY = y - (slideBackView.height) / 2
        slideBackView.layoutParams = (slideBackView.layoutParams as FrameLayout.LayoutParams).apply {
            gravity = if (start) Gravity.START else Gravity.END
        }
    }

    /**
     * 隐藏侧滑 View
     * - 滑动距离超过一半时调用[Activity.onBackPressed]
     */
    private fun hide() {
        if (slideBackView.mProgress >= 0.5f) {
            // 滑动距离超过一半
            mActivity.onBackPressed()
            if (mActivity.isFinishing) {
                slideBackView.visibility = View.GONE
            }
        }
        slideBackView.release()
    }

    /**
     * 设置滑动进度
     *
     * @param progress 进度, 0~1
     */
    private fun setProgress(progress: Float) {
        slideBackView.setProgress(progress)
    }
}

/**
 * 拦截[Activity]事件分发
 *
 * @param ev 事件对象
 * @param activityDispatch 处理正常分发代码块
 *
 * @return 是否拦截事件
 */
fun SwipeBackHelper?.dispatchTouchEvent(ev: MotionEvent, activityDispatch: () -> Boolean): Boolean {
    return if (null != this && dispatchTouchEvent(ev)) {
        true
    } else {
        activityDispatch.invoke()
    }
}

使用起来也很简单哒:

abstract class BaseActivity: AppCompatActivity() {

    protected var mSwipeBackHelper: SwipeBackHelper? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mSwipeBackHelper = SwipeBackHelper(this)
    }

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean = mSwipeBackHelper.dispatchTouchEvent(ev) {
        super.dispatchTouchEvent(ev)
    }
}

实现重点就在于接管 Activity 的事件分发,当按下时在屏幕边缘时判断为侧滑返回,拦截不向下分发,显示侧滑返回的 View ,并根据滑动距离更新 View 的显示,在松开手指时判断滑动距离是否达到指定距离,并调用 Activity 的 onBackPressed() 方法。

最终效果

侧滑返回效果

  • 图片可能有点大,请耐心等待~~

感谢

写下这篇文章其中参考了 仿苹果手机的侧滑返回分析和实现 中提供的数据,并且侧滑返回的 View 参照了Android开发之使用贝塞尔曲线实现黏性水珠下拉效果 ,感谢各位大佬的无私分享。

项目地址 其中的lib_swipe_back 就是侧滑返回的源码啦。