作者: 许方镇
前言
首次通过右滑来返回到上一个页面的操作是在 IOS7 上出现到目前 android 应用上支持这种操作的依然不多分析其主要原因应该是 android 已有实体的返回按键, 这样的功能变得不重要, 但我觉得有这样的功能便于单手操作, 能提升 app 的用户体验, 特别是从 ios 转到 android 的用户写这篇博文希望可以对大家有所帮助, 希望自己的 app 上有滑动返回功能的可以参考下
原理的简单描述
Android 系统里有很多滑动相关的 API 和类, 比如 ViewDragHelper 就是一个很好的滑动助手类首先设置 Window 的背景为透明, 再通过 ViewDragHelper 对 Activity 上 DecorView 的子 view 进行滑动, 当滑动到一定距离, 手指离开后就自动滑到最右侧, 然后 finish 当前的 activity, 这样即可实现滑动返回效果为了能够 全局的联动的 实现滑动返回效果, 在每个 activity 的 DecorView 下插入了 SwipeBackLayout, 当前 activity 滑动和下层 activity 的联动都在该类中完成
效果图
布局图
实现主要类:
- SwipeBackActivity // 滑动返回基类
- SwipeBackLayout // 滑动返回布局类
- SwipeBackLayoutDragHelper // 修改 ViewDragHelper 后助手类
- TranslucentHelper // 代码中修改透明或者不透明的助手类
- ##代码层面的讲解
一. 设置 activity 为透明 activity 跳转动画(TranslucentHelper 讲解)
这个看起来很简单, 但如果要兼容到 API16 及以下, 会遇到过一个比较麻烦的页面切换动画问题:
1.1 通过 activity 的主题 style 进行设置
- <item name="android:windowBackground">@color/transparent</item>
- <item name="android:windowIsTranslucent">true</item>```
** 遇到问题:** 如果在某个 activity 的主题 style 中设置了 android:windowIsTranslucent 属性为 true, 那么该 activity 切换动画与没设置之前是不同的, 有些手机切换动画会变得非常跳所以需要自定义 activity 的切换动画
接下来我们会想到通过主题 style 里的 windowAnimationStyle 来设置切换动画
@anim/activity_open_enter @anim/activity_open_exit @anim/activity_close_enter @anim/activity_close_exit``` ** 实践证明:** 当 android:windowIsTranslucent 为 true 时, 以上几个属性是无效的, 而下面两个属性还是可以用但是这两个属性一个是窗口进来动画, 一个是窗口退出动画, 明显是不够
- <item name="android:windowEnterAnimation">@anim/***</item>
- <item name="android:windowExitAnimation">@anim/***</item>```
- 结合 overridePendingTransition(int enterAnim, int exitAnim)可以复写窗口进来动画和窗口退出动画, 这种我觉得最终可能是可以实现的, 不过控制起来比较复杂:
- 比如有 ABC 三个页面:
- A 跳到 B, 进场页面 B 动画从右进来, 出场页面 A 动画从左出去, 可以直接在 style 中写死
- @anim/*** @anim/***```
- 如果 B 返回到 A, 进场页面 A 动画从左进来, 出场页面 B 动画从右出去, 此时需要通过复写 onBackPressed() 方法,
- 在其中添加 overridePendingTransition(int enterAnim, int exitAnim)方法来改变动画
- 如果 B 是 finish()后到 A 页面, 在 finish()后面加上 overridePendingTransition
- 由于 onBackPressed() 方法最终会调 finish(), 所以实际上只需要复写 finish(), 在其中添加 overridePendingTransition
- 但是假如 B finish()后跳到 C, 则又不应该执行 overridePendingTransition, 那么就需要判断 finish 执行后是否要加 overridePendingTransition
- 对于一个较为庞大的项目, 采取这种方法需要对每个页面进行排查, 因此是不可行的, 而对于刚刚起步的应用来说则是一个选择
- 1.2 通过透明助手类 (TranslucentHelper) 进行设置
- 透明助手类 (TranslucentHelper) 里主要又有两个方法, 一个是让 activity 变不透明, 一个是让 activity 变透明, 这两个都是通过反射来调用隐藏的系统 api 来实现的因为较低的版本不支持代码中修改背景透明不透明, 所以在类中有个静态变量 mTranslucentState 来记录是否可以切换背景, 这样低版本就不需要每次都反射通过捕获到的异常来做兼容方案 另外: 发现有些手机支持背景变黑, 但不支持背景变透明(中兴 z9 mini 5.0.2 系统)
- public class TranslucentHelper {
- private static final String TRANSLUCENT_STATE = "translucentState";
- private static final int INIT = 0; // 表示初始
- private static final int CHANGE_STATE_FAIL = INIT + 1; // 表示确认不可以切换透明状态
- private static final int CHANGE_STATE_SUCCEED = CHANGE_STATE_FAIL + 1; // 表示确认可以切换透明状态
- private static int mTranslucentState = INIT;
- interface TranslucentListener {
- void onTranslucent();
- }
- private static class MyInvocationHandler implements InvocationHandler {
- private TranslucentListener listener;
- MyInvocationHandler(TranslucentListener listener) {
- this.listener = listener;
- }@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- try {
- boolean success = (boolean) args[0];
- if (success && listener != null) {
- listener.onTranslucent();
- }
- } catch(Exception ignored) {}
- return null;
- }
- }
- static boolean convertActivityFromTranslucent(Activity activity) {
- if (mTranslucentState == INIT) {
- mTranslucentState = PreferencesUtils.getInt(TRANSLUCENT_STATE, INIT);
- }
- if (mTranslucentState == INIT) {
- convertActivityToTranslucent(activity, null);
- } else if (mTranslucentState == CHANGE_STATE_FAIL) {
- return false;
- }
- try {
- Method method = Activity.class.getDeclaredMethod("convertFromTranslucent");
- method.setAccessible(true);
- method.invoke(activity);
- mTranslucentState = CHANGE_STATE_SUCCEED;
- return true;
- } catch(Throwable t) {
- mTranslucentState = CHANGE_STATE_FAIL;
- PreferencesUtils.saveInt(TRANSLUCENT_STATE, CHANGE_STATE_FAIL);
- return false;
- }
- }
- static void convertActivityToTranslucent(Activity activity, final TranslucentListener listener) {
- if (mTranslucentState == CHANGE_STATE_FAIL) {
- if (listener != null) {
- listener.onTranslucent();
- }
- return;
- }
- try {
- Class < ?>[] classes = Activity.class.getDeclaredClasses();
- Class < ?>translucentConversionListenerClazz = null;
- for (Class clazz: classes) {
- if (clazz.getSimpleName().contains("TranslucentConversionListener")) {
- translucentConversionListenerClazz = clazz;
- }
- }
- MyInvocationHandler myInvocationHandler = new MyInvocationHandler(listener);
- Object obj = Proxy.newProxyInstance(Activity.class.getClassLoader(), new Class[] {
- translucentConversionListenerClazz
- },
- myInvocationHandler);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions");
- getActivityOptions.setAccessible(true);
- Object options = getActivityOptions.invoke(activity);
- Method method = Activity.class.getDeclaredMethod("convertToTranslucent", translucentConversionListenerClazz, ActivityOptions.class);
- method.setAccessible(true);
- method.invoke(activity, obj, options);
- } else {
- Method method = Activity.class.getDeclaredMethod("convertToTranslucent", translucentConversionListenerClazz);
- method.setAccessible(true);
- method.invoke(activity, obj);
- }
- mTranslucentState = CHANGE_STATE_SUCCEED;
- } catch(Throwable t) {
- mTranslucentState = CHANGE_STATE_FAIL;
- PreferencesUtils.saveInt(TRANSLUCENT_STATE, CHANGE_STATE_FAIL);
- new Handler().postDelayed(new Runnable() {@Override public void run() {
- if (listener != null) {
- listener.onTranslucent();
- }
- }
- },
- 100);
- }
- }
- }
- 让 activity 变不透明的方法比较简单; 让 activity 变透明的方法参数里传入了一个 listener 接口 , 主要是当 antivity 变透明后会回调, 因为这个接口也在 activity 里, 而且是私有的, 所以我们只能通过动态代理去获取这个回调最后如果版本大于等于 5.0, 还需要再传入一个 ActivityOptions 参数
- 在实际开发中, 这两个方法在 android 5.0 以上是有效的, 在 5.0 以下需要当 android:windowIsTranslucent 为 true 时才有效, 这样又回到了之前的问题 activity 切换动画异常
- ** 最终决解方法:**setContentView 之前就调用 convertActivityFromTranslucent 方法, 让 activity 背景变黑, 这样 activity 切换效果就正常
- ** 总结:** 在 style 中设置 android:windowIsTranslucent 为 true ,setContentView 之前就调用 convertActivityFromTranslucent 方法, 当触发右滑时调用 convertActivityToTranslucent, 通过动态代理获取 activity 变透明后的回调, 在回调后允许开始滑动
- 二. 让 BaseActivity 继承 SwipeBackActivity(SwipeBackActivity 讲解)
- 先直接看代码, 比较少
- public abstract class SwipeBackActivity extends CoreBaseActivity {
- /**
- * 滑动返回 View
- */
- private SwipeBackLayout mSwipeBackLayout;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (!isSwipeBackDisableForever()) {
- TranslucentHelper.convertActivityFromTranslucent(this);
- mSwipeBackLayout = new SwipeBackLayout(this);
- }
- }
- @Override
- protected void onPostCreate(Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
- if (!isSwipeBackDisableForever()) {
- mSwipeBackLayout.attachToActivity(this);
- mSwipeBackLayout.setOnSwipeBackListener(new SwipeBackLayout.onSwipeBackListener() {
- @Override
- public void onStart() {
- onSwipeBackStart();
- }
- @Override
- public void onEnd() {
- onSwipeBackEnd();
- }
- });
- }
- }
- @Override
- public void onWindowFocusChanged(boolean hasFocus) {
- super.onWindowFocusChanged(hasFocus);
- if (!isSwipeBackDisableForever() && hasFocus) {
- getSwipeBackLayout().recovery();
- }
- }
- /**
- * 滑动返回开始时的回调
- */
- protected void onSwipeBackStart() {}
- /**
- * 滑动返回结束时的回调
- */
- protected void onSwipeBackEnd() {}
- /**
- * 设置是否可以边缘滑动返回, 需要在 onCreate 方法调用
- */
- public void setSwipeBackEnable(boolean enable) {
- if (mSwipeBackLayout != null) {
- mSwipeBackLayout.setSwipeBackEnable(enable);
- }
- }
- public boolean isSwipeBackDisableForever() {
- return false;
- }
- public SwipeBackLayout getSwipeBackLayout() {
- return mSwipeBackLayout;
- }
- }
SwipeBackActivity 中包含了一个 SwipeBackLayout 对象
在 onCreate 方法中:
1.activity 转化为不透明
2.new 了一个 SwipeBackLayout
在 onPostCreate 方法中:
1.attachToActivity 主要是插入 SwipeBackLayout 窗口背景设置
2. 设置了滑动返回开始和结束的监听接口, 建议在滑动返回开始时, 把 PopupWindow 给 dismiss 掉
onWindowFocusChanged 方法中
如果是 hasFocus == true, 就 recovery()这个 SwipeBackLayout, 这个也是因为下层 activity 有联动效果而移动了 SwipeBackLayout, 所以需要 recovery()下, 防止异常情况
isSwipeBackDisableForever 方法是一个大开关, 默认返回 false, 在代码中复写后返回 true, 则相当于直接继承了 SwipeBackActivity 的父类
setSwipeBackEnable 方法是一个小开关, 设置了 false 之后就暂时不能滑动返回了, 可以在特定的时机设置为 true, 就恢复滑动返回的功能
** 总结说明:** 下层 activity 设置了 setSwipeBackEnable 为 false, 上层 activity 滑动时还是可以联动的, 比如 MainActivity 而 isSwipeBackDisableForever 返回 true 就不会联动了, 而且一些仿 PopupWindow 的 activity 需要复写这个方法, 因为 activity 需要透明
三滑动助手类的使用和滑动返回布局类的实现(SwipeBackLayout 讲解)
直接贴 SwipeBackLayout 源码:
- /**
- * 滑动返回容器类
- */
- public class SwipeBackLayout extends FrameLayout {
- /**
- * 滑动销毁距离比例界限, 滑动部分的比例超过这个就销毁
- */
- private static final float DEFAULT_SCROLL_THRESHOLD = 0.5f;
- /**
- * 滑动销毁速度界限, 超过这个速度就销毁
- */
- private static final float DEFAULT_VELOCITY_THRESHOLD = ScreenUtils.dpToPx(250);
- /**
- * 最小滑动速度
- */
- private static final int MIN_FLING_VELOCITY = ScreenUtils.dpToPx(200);
- /**
- * 左边移动的像素值
- */
- private int mContentLeft;
- /**
- * 左边移动的像素值 / (ContentView 的宽 + 阴影)
- */
- private float mScrollPercent;
- /**
- * (ContentView 可见部分 + 阴影)的比例 (即 1 - mScrollPercent)
- */
- private float mContentPercent;
- /**
- * 阴影图
- */
- private Drawable mShadowDrawable;
- /**
- * 阴影图的宽
- */
- private int mShadowWidth;
- /**
- * 内容 view,DecorView 的原第一个子 view
- */
- private View mContentView;
- /**
- * 用于记录 ContentView 所在的矩形
- */
- private Rect mContentViewRect = new Rect();
- /**
- * 设置是否可滑动
- */
- private boolean mIsSwipeBackEnable = true;
- /**
- * 是否正在放置
- */
- private boolean mIsLayout = true;
- /**
- * 判断背景 Activity 是否启动进入动画
- */
- private boolean mIsEnterAnimRunning = false;
- /**
- * 是否是透明的
- */
- private boolean mIsActivityTranslucent = false;
- /**
- * 进入动画(只在释放手指时使用)
- */
- private ObjectAnimator mEnterAnim;
- /**
- * 退拽助手类
- */
- private SwipeBackLayoutDragHelper mViewDragHelper;
- /**
- * 执行滑动时的最顶层 Activity
- */
- private Activity mTopActivity;
- /**
- * 后面的 Activity 的弱引用
- */
- private WeakReference<Activity> mBackActivityWeakRf;
- /**
- * 监听滑动开始和结束
- */
- private onSwipeBackListener mListener;
- public SwipeBackLayout(Context context) {
- super(context);
- init(context);
- }
- public SwipeBackLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
- public SwipeBackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init(context);
- }
- private void init(Context context) {
- mViewDragHelper = SwipeBackLayoutDragHelper.create(SwipeBackLayout.this, new ViewDragCallback());
- mViewDragHelper.setEdgeTrackingEnabled(SwipeBackLayoutDragHelper.EDGE_LEFT);
- mViewDragHelper.setMinVelocity(MIN_FLING_VELOCITY);
- mViewDragHelper.setMaxVelocity(MIN_FLING_VELOCITY * 2);
- try {
- mShadowDrawable = context.getResources().getDrawable(R.drawable.swipeback_shadow_left);
- } catch (Exception ignored) {
- }
- }
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- try {
- if (!mIsSwipeBackEnable) {
- super.onLayout(changed, left, top, right, bottom);
- return;
- }
- mIsLayout = true;
- if (mContentView != null) {
- mContentView.layout(mContentLeft, top, mContentLeft + mContentView.getMeasuredWidth(),
- mContentView.getMeasuredHeight());
- }
- mIsLayout = false;
- } catch (Exception e) {
- super.onLayout(changed, left, top, right, bottom);
- }
- }
- @Override
- public void requestLayout() {
- if (!mIsLayout || !mIsSwipeBackEnable) {
- super.requestLayout();
- }
- }
- @Override
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- try {
- // 绘制阴影
- if (mContentPercent > 0
- && mShadowDrawable != null
- && child == mContentView
- && mViewDragHelper.getViewDragState() != SwipeBackLayoutDragHelper.STATE_IDLE) {
- child.getHitRect(mContentViewRect);
- mShadowWidth = mShadowDrawable.getIntrinsicWidth();
- mShadowDrawable.setBounds(mContentViewRect.left - mShadowWidth, mContentViewRect.top,
- mContentViewRect.left, mContentViewRect.bottom);
- mShadowDrawable.draw(canvas);
- }
- return super.drawChild(canvas, child, drawingTime);
- } catch (Exception e) {
- return super.drawChild(canvas, child, drawingTime);
- }
- }
- @Override
- public void computeScroll() {
- mContentPercent = 1 - mScrollPercent;
- if (mViewDragHelper.continueSettling(true)) {
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- if (!mIsSwipeBackEnable) {
- return false;
- }
- try {
- return mViewDragHelper.shouldInterceptTouchEvent(event);
- } catch (ArrayIndexOutOfBoundsException e) {
- return super.onInterceptTouchEvent(event);
- }
- }
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (!mIsSwipeBackEnable) {
- return false;
- }
- try {
- mViewDragHelper.processTouchEvent(event);
- return true;
- } catch (Exception e) {
- return super.onTouchEvent(event);
- }
- }
- /**
- * 将 View 添加到 Activity
- */
- public void attachToActivity(Activity activity) {
- // 插入 SwipeBackLayout
- ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
- ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
- decor.removeView(decorChild);
- if (getParent() != null) {
- decor.removeView(this);
- }
- decor.addView(this);
- this.removeAllViews();
- this.addView(decorChild);
- //mContentView 为 SwipeBackLayout 的直接子 view, 获取 window 背景色进行赋值
- activity.getWindow().setBackgroundDrawableResource(R.color.transparent_white);
- TypedArray a = activity.getTheme().obtainStyledAttributes(new int[] {
- android.R.attr.windowBackground
- });
- mContentView = decorChild;
- mContentView.setBackgroundResource(a.getResourceId(0, 0));
- a.recycle();
- // 拿到顶层 activity 和下层 activity, 做联动操作
- mTopActivity = activity;
- Activity backActivity = ActivityUtils.getSecondTopActivity();
- if (backActivity != null && backActivity instanceof SwipeBackActivity) {
- if (!((SwipeBackActivity) backActivity).isSwipeBackDisableForever()) {
- mBackActivityWeakRf = new WeakReference<>(backActivity);
- }
- }
- }
- /**
- * 设置是否可以滑动返回
- */
- public void setSwipeBackEnable(boolean enable) {
- mIsSwipeBackEnable = enable;
- }
- public boolean isActivityTranslucent() {
- return mIsActivityTranslucent;
- }
- /**
- * 启动进入动画
- */
- private void startEnterAnim() {
- if (mContentView != null) {
- ObjectAnimator anim =
- ObjectAnimator.ofFloat(mContentView, "TranslationX", mContentView.getTranslationX(), 0f);
- anim.setDuration((long) (125 * mContentPercent));
- mEnterAnim = anim;
- mEnterAnim.start();
- }
- }
- protected View getContentView() {
- return mContentView;
- }
- private class ViewDragCallback extends SwipeBackLayoutDragHelper.Callback {
- @Override
- public boolean tryCaptureView(View child, int pointerId) {
- if (mIsSwipeBackEnable && mViewDragHelper.isEdgeTouched(SwipeBackLayoutDragHelper.EDGE_LEFT, pointerId)) {
- TranslucentHelper.convertActivityToTranslucent(mTopActivity,
- new TranslucentHelper.TranslucentListener() {
- @Override
- public void onTranslucent() {
- if (mListener != null) {
- mListener.onStart();
- }
- mIsActivityTranslucent = true;
- }
- });
- return true;
- }
- return false;
- }
- @Override
- public int getViewHorizontalDragRange(View child) {
- return mIsSwipeBackEnable ? SwipeBackLayoutDragHelper.EDGE_LEFT : 0;
- }
- @Override
- public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
- super.onViewPositionChanged(changedView, left, top, dx, dy);
- if (changedView == mContentView) {
- mScrollPercent = Math.abs((float) left / mContentView.getWidth());
- mContentLeft = left;
- // 未执行动画就平移
- if (!mIsEnterAnimRunning) {
- moveBackActivity();
- }
- invalidate();
- if (mScrollPercent >= 1 && !mTopActivity.isFinishing()) {
- if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
- ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout().invalidate();
- }
- mTopActivity.finish();
- mTopActivity.overridePendingTransition(0, 0);
- }
- }
- }
- @Override
- public void onViewReleased(View releasedChild, float xvel, float yvel) {
- if (xvel > DEFAULT_VELOCITY_THRESHOLD || mScrollPercent > DEFAULT_SCROLL_THRESHOLD) {
- if (mIsActivityTranslucent) {
- mViewDragHelper.settleCapturedViewAt(releasedChild.getWidth() + mShadowWidth, 0);
- if (mContentPercent < 0.85f) {
- startAnimOfBackActivity();
- }
- }
- } else {
- mViewDragHelper.settleCapturedViewAt(0, 0);
- }
- if (mListener != null) {
- mListener.onEnd();
- }
- invalidate();
- }
- @Override
- public int clampViewPositionHorizontal(View child, int left, int dx) {
- return Math.min(child.getWidth(), Math.max(left, 0));
- }
- @Override
- public void onViewDragStateChanged(int state) {
- super.onViewDragStateChanged(state);
- if (state == SwipeBackLayoutDragHelper.STATE_IDLE && mScrollPercent < 1f) {
- TranslucentHelper.convertActivityFromTranslucent(mTopActivity);
- mIsActivityTranslucent = false;
- }
- }
- @Override
- public boolean isTranslucent() {
- return SwipeBackLayout.this.isActivityTranslucent();
- }
- }
- /**
- * 背景 Activity 开始进入动画
- */
- private void startAnimOfBackActivity() {
- if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
- mIsEnterAnimRunning = true;
- SwipeBackLayout swipeBackLayout = ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout();
- swipeBackLayout.startEnterAnim();
- }
- }
- /**
- * 移动背景 Activity
- */
- private void moveBackActivity() {
- if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
- View view = ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout().getContentView();
- if (view != null) {
- int width = view.getWidth();
- view.setTranslationX(-width * 0.3f * Math.max(0f, mContentPercent - 0.15f));
- }
- }
- }
- /**
- * 回复界面的平移到初始位置
- */
- public void recovery() {
- if (mEnterAnim != null && mEnterAnim.isRunning()) {
- mEnterAnim.end();
- } else {
- mContentView.setTranslationX(0);
- }
- }
- interface onSwipeBackListener {
- void onStart();
- void onEnd();
- }
- public void setOnSwipeBackListener(onSwipeBackListener listener) {
- mListener = listener;
- }
- }
- attachToActivity
上面讲到 SwipeBackLayout 是在 activity 的 onCreate 时被创建, 在 onPostCreate 是插入到 DecorView 里, 主要是因为 DecorView 是在 setContentView 时与 Window 关联起来插入 SwipeBackLayout 方法如代码所示, 不难, 然后是设置了 window 的背景色为透明色, mContentView 为 SwipeBackLayout 的直接子 view, 获取 window 背景色进行赋值由于考拉项目已经有很多 activity, 而这些 activity 中 android:windowBackground 设置的颜色大部分是白色, 少部分是灰色和透明的, 所以需要在代码中设置统一设置一遍透明的, 原来的背景色则赋值给 SwipeBackLayout 的子 View 就可以达到最少修改代码的目的最后拿到顶层 activity 和下层 activity, 做联动操作
SwipeBackLayoutDragHelper 和 ViewDragCallback
SwipeBackLayout 中包含了一个滑动助手类 (SwipeBackLayoutDragHelper) 的对象, 该类是在 ViewDragHelper 的基础上进行修改得来的
修改点:
1. 右侧触发区域 EDGE_SIZE 由 20dp 改到 25dp
2. 提供滑动最大速度 的设置方法
3. 在 ViewDragHelper 的内部类 Callback 方法中提供是否 activity 为透明的回调接口
4. 在最终调用滑动的方法 dragTo 中添加判断逻辑, activity 为透明时才支持滑动
SwipeBackLayoutDragHelper 在 init 方法中初始化, 通过 onInterceptTouchEvent 和 onTouchEvent 拿到滑动事件, 通过 ViewDragCallback 的一些方法返回相应的滑动回调, ViewDragCallback 实现了 SwipeBackLayoutDragHelper.Callback 里的以下几个接口, 其中其中 isTranslucent()是自己添加进去的
tryCaptureView 方法当触摸到 SwipeBackLayout 里的子 View 时触发的, 当返回 true, 表示捕捉成功, 否则失败判断条件是如果支持滑动返回并且是左侧边距被触摸时才可以, 我们知道这个时候的的背景色是不透明的, 如果直接开始滑动则是黑色的, 所以需要在这里背景色改成透明的, 如果直接调用 TranslucentHelper.convertActivityToTranslucent(mTopActivity, null)后直接返回 true, 会出现一个异常情况, 就是滑动过快时会导致背景还来不及变成黑色就滑动出来了, 之后才变成透明的, 从而导致了会从黑色到透明的一个闪烁现象, 解决的办法是在代码中用了一个回调和标记, 当变成透明后设置了 mIsActivityTranslucent = true; 通过 mIsActivityTranslucent 这个变量来判断是否进行移动的操作由于修改 activity 变透明的方法是通过反射的, 不能简单的设置一个接口后进行回调, 而是通过动态代理的方式来实现的 (InvocationHandler), 在 convertToTranslucent 方法的第一个参数刚好是一个判断 activity 是否已经变成透明的回调, 看下面代码中 if 语句里的注释和回调, 如果窗口已经变成透明的话, 就传了一个 drawComplete (true) 通过动态代理, 将 translucentConversionListenerClazz 执行其方法 onTranslucentConversionComplete 的替换成 myInvocationHandler 中执行 invoke 方法其中赋值给 success 的 args[0]正是 drawComplete
isTranslucent 是自己添加了一个方法, 主要是返回 activity 是否是透明的默认为 true, 在 SwipeBackLayout 重写后将 mIsActivityTranslucent 返回仔细看 SwipeBackLayoutDragHelper 方法的话, 会发现最后通过 dragTo 方法对 view 进行移动, 因此在进行水平移动前判断下是否是透明的, 只有透明了才能移动
onViewPositionChanged view 移动过程中会持续调用, 这里面的逻辑主要有这几个: 1. 实时计算滑动了多少距离, 用于绘制左侧阴影等
2. 使下面的 activity 进行移动 moveBackActivity();
3. 当 view 完全移出屏幕后, 销毁当前的 activity
onViewReleased 是手指释放后触发的一个方法如果滑动速度大于最大速度或者滑动的距离大于设定的阈值距离, 则直接移到屏幕外, 同时触发下层 activity 的复位动画, 否则移会到原来位置
onViewDragStateChanged 当滑动的状态发生改变时的回调, 主要是停止滑动后, 将背景改成不透明, 这样跳到别的页面是动画就是正常的
clampViewPositionHorizontal 返回水平移动距离, 防止滑出父 view
getViewHorizontalDragRange 对于 clickable=true 的子 view, 需要返回大于 0 的数字才能正常捕获
其他方法都较为简单, 注释也写了, 就不多说了, 最后毫不吝啬的贴上 SwipeBackLayoutDragHelper 的 dragTo 代码, 就多了 if (mCallback.isTranslucent())
- private void dragTo(int left, int top, int dx, int dy) {
- int clampedX = left;
- int clampedY = top;
- final int oldLeft = mCapturedView.getLeft();
- final int oldTop = mCapturedView.getTop();
- if (dx != 0) {
- clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
- if (mCallback.isTranslucent()) {
- ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
- }
- }
- if (dy != 0) {
- clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
- ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
- }
- if (dx != 0 || dy != 0) {
- final int clampedDx = clampedX - oldLeft;
- final int clampedDy = clampedY - oldTop;
- if (mCallback.isTranslucent()) {
- mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy);
- }
- }
- }
来源: https://juejin.im/post/5a7919ce5188257a8929915a