Android 仿全面屏侧滑返回功能
为啥要搞这个
最近,项目的线上崩溃统计中发现了由于 SwipeBackLayout 导致的闪退问题,在排查问题的时候发现这个库已经多年没有维护了,而随后找了其它的各种侧滑返回库也有着各式各样的问题,那么有没有一种侧滑返回适配简单没有那么多的问题呢?
市场调研
纵观现有的侧滑返回库,现有的都是仿 iOS 的效果,从左侧侧滑,当前 Activity 向右移动,能够看到下方的 Activity 布局,但是 Android 并没有对这个方式提供支持,这里我看到了 仿苹果手机的侧滑返回分析和实现 这篇文章,里面统计了当前各种侧滑返回的实现方案:
- 不透明方案 ,获取下层 Activity 布局,在当前 Activity 中绘制下层 Activity 布局或移动顶层 Activity 的 ContentView ,但是这种方法都有侧滑时显示的下层界面可能和实际不一致的问题。
- 透明方案 ,主题中设置窗口透明,监听侧滑事件移动顶层 Activity 的 ContentView ,但是这种方案会导致各种动画异常,并且下层 Activity 不会执行 onStop 方法,导致一系列的性能问题。
- 反射方案 ,通过反射,在侧滑时将 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 就是侧滑返回的源码啦。