仿苹果手机的侧滑返回分析和实现-郭霖

今日快讯
近日,阿里巴巴集团(下称“阿里”)安全部介绍了AI鉴黄的有关工作:当用户输入一张图片,AI将返回一个0-100之间的分值。这个分值非线性地标示了图片含色情内容的概率。阿里安全部高级算法专家介绍,假设一天要审核4亿张图片忒伊亚,单纯由人工来审,如果一人一天审1万张,就需要4万人。而经由AI鉴黄后需要交由人工审核的量大约只需20万张,这样只需要20人不良主母 ,大大节省了人力。
作者简介
大家周一好,新的一周继续加油哦!
本篇来自SimonLeeeeeeeee的投稿,分享了Android侧滑的相关知识,一起来看看!希望大家喜欢。
SimonLeeeeeeeee的博客地址:
https://www.jianshu.com/u/c35bd597dafb
前言
不久前淘汰了用了三年多的iPhone6Plus,换了部三星S9+。流畅的吃鸡体验,丝滑的屏幕,超高的性价比(港行还另打了9折),真喜欢的不行。不过从IOS切换到Android,还是不太适应,首当其冲就是 没!有!侧!滑!返!回! 每天蚂蚁森林偷个能量要点无数遍返回键,简直崩溃!于是,热(喜)爱(欢)工(装)作(逼)的我,决定在自己的项目中一定要有爱的不行的侧滑功能。
本文demo效果如下所示:



正文
分析
搜一下“Android侧滑返回”,现在有很多很多的开源库作为选择被迫战斗。我几乎把每一种类型都尝试了一遍,发现了很多很多坑。按照实现方式的不同,我把它们大致归位两大类:
不透明方案
不透明方案通过注册ActivityLifecycleCallbacks回调来管理Activity栈,以获取下层Activity的ContentView,然后在上层Activity进行绘制。
不透明方案分支一
在顶层Activity的DecorView中插入一个Layout。监听侧滑事件,移动顶层Activity的ContentView同时,在该Layout的onDraw中调用View.draw(Canvas canvas)绘制下层Activity的ContentView。造成侧滑透视到下层Activity的假象。
存在问题:当布局变化或数据更新,如横竖屏切换、导航栏隐藏、窗口模式、分屏模式等,该假象始终如一不会有对应改变。
不透明方案分支二
在顶层Activity的DecorView中插入一个Layout。将下层Activity的ContentView移除,并添加到该Layout中。监听侧滑事件,钱景峰移动顶层Activity的ContentView,亦可造成侧滑透视到下层Activity的假象。此方案比方案一好在:可以适应部分布局变化。
存在问题:下层Activity有数据改变,无对应更新。当顶层Activity重建时(旋转屏幕、切换窗口模式等),会丢失ContentView中绑定的数据。旋转屏幕时,若下层Activity有对应两套布局,该假象露馅。
透明方案
通过设置窗口透明,真正透视到下层Activity的界面。
透明方案一
在styles中配置如下两条属性:
<itemname="android:windowBackground">@android:color/transparent</item><itemname="android:windowIsTranslucent">true</item>
然后监听侧滑事件,移动顶层Activity的ContentView,即可真正透视到下层Activity的界面。此时无论布局变化、数据更新,都没问题。BUT!该方案问题多如牛毛。萧山五中。。
存在问题:windowIsTranslucent为true会引起一系列的动画问题,如前后台切换动画、Activity回退动画等。网上有解决方案说设置"android:windowEnterAnimation"和"android:windowExitAnimation",经测试并无卵用。同时,在SDK26(Android8.0)及以上,会与固定屏幕方向冲突造成闪退。同时,下层的Activity只会进入onPause状态,不会onStop,当页面开启过多时,一定会让你崩溃。
透明方案二
如透明方案一,依旧在styles中配置那两条属性,在onPause中利用反射将窗口转为不透明,在onResume再利用反射将窗口转为透明。似乎酱紫很顺利地解决了下层以下的Activity不会onStop导致的性能问题。BUT!该方案问题依旧可怕。。。
存在问题:因顶层Activity透明,旋转屏幕时下层Activity会重建,然后在onResume中将窗口转为透明,然后下下层Activity也跟着复活了。。。一系列连锁反应,简直可怕!同时,windowIsTranslucent为true引起一系列的动画问题依然没有得到解决。实现
经以上可知,要想侧滑时看到的不是假象,窗口必须透明让下层的Activity接收布局变化和数据更新。但是窗口透明会影响动画效果,且和屏幕旋转产生冲突。那么是否可以只在侧滑时窗口保持透明?
ofcourse~我们可以在侧滑触发时利用反射将窗口转为透明,在侧滑结束时利用反射将窗口转为不透明。这样既可以在侧滑时一窥下层Activity真容三菱日蚀 ,又不会和屏幕旋转冲突,也不会影响到动画的使用。原理很简单,下面开始一步步实现。
Step.1 状态栏透明
既然要实现侧滑返回,状态栏必然要干掉,实现沉浸式体验。这里不多BB,直接上代码。
privatebooleansetStatusBarTransparent(booleandarkStatusBar){//SDK大于等于24,需要判断是否为窗口模式booleanisInMultiWindowMode=Build.VERSION.SDK_INT>=Build.VERSION_CODES.N&&mSwipeBackActivity.isInMultiWindowMode();//窗口模式或者SDK小于19,不设置状态栏透明if(isInMultiWindowMode||Build.VERSION.SDK_INT<Build.VERSION_CODES.KITKAT){returnfalse;}elseif(Build.VERSION.SDK_INT<Build.VERSION_CODES.LOLLIPOP){//SDK小于21mSwipeBackActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);}else{//SDK大于等于21intsystemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LAYOUT_STABLE;//SDK大于等于23支持翻转状态栏颜色if(darkStatusBar&&Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){//设置状态栏文字&图标暗色systemUiVisibility|=View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;}//去除状态栏背景mDecorView.setSystemUiVisibility(systemUiVisibility);//设置状态栏透明mSwipeBackActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);mSwipeBackActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);mSwipeBackActivity.getWindow().setStatusBarColor(Color.TRANSPARENT);}//监听DecorView的布局变化mDecorView.addOnLayoutChangeListener(mPrivateListener);returntrue;}
这里有几个要注意的地方。
SDK小于19是不支持状态栏透明的,SD21及以上的实现方式也有所不同。
SD23及以上支持状态栏颜色反转。
SD24及以上支持窗口模式,这里要进行判断,当窗口模式时,不要设置状态栏透明。
状态栏设置透明之后,输入法的adjustResize会失效。网传解决方案android:fitsSystemWindows="true"不推荐使用,因为这会导致无法在状态栏之下进行绘制。因此这里对DecorView布局变化进行监听,布局变化时动态调整子View的高度为DecorView的可见部分。贴一下代码:
publicvoidonLayoutChange(Viewv,intleft,inttop,intright,intbottom,intoldLeft吴倩莲资料,intoldTop,intoldRight,intoldBottom){//获取DecorView的可见区域RectvisibleDisplayRect=newRect();mDecorView.getWindowVisibleDisplayFrame(visibleDisplayRect);//状态栏透明情况下,输入法的adjustResize不会生效,这里手动调整View的高度以适配if(isStatusBarTransparent()){for(inti=0;i<mDecorView.getChildCount();i++){Viewchild=mDecorView.getChildAt(i);if(childinstanceofViewGroup){//获取DecorView的子ViewGroupViewGroup.LayoutParamschildLp=child.getLayoutParams();//调整子ViewGroup的paddingBottomintpaddingBottom=bottom-visibleDisplayRect.bottom;if(childLpinstanceofViewGroup.MarginLayoutParams){//此处减去bottomMargin,是考虑到导航栏的高度paddingBottom-=((ViewGroup.MarginLayoutParams)childLp).bottomMargin;}paddingBottom=Math.max(0,paddingBottom);if(paddingBottom!=child.getPaddingBottom()){//调整子ViewGroup的paddingBottom,以保证整个ViewGroup可见child.setPadding(child.getPaddingLeft(),child.getPaddingTop(),child.getPaddingRight(),paddingBottom);}break;}}}}
这里同样有两个小点需要注意:一个是paddingBottom的计算一定要考虑到导航栏高度的计算。还有就是paddingBottom不能为负值。
Step.2 支持侧滑
状态栏已经透明了,下一步就是让我们的界面可以滑动起来。这里我们在Activity的dispatchTouchEvent方法中实现。
首先失宠太子妃,在dispatchTouchEvent的ACTION_DOWN事件中判断按压区域是否为侧边,并进行标记。
然后,在dispatchTouchEvent的ACTION_MOVE事件中判断移动方向,并标记。如果是横向滑动,则对ContentView的父容器调用setTranslationX设置偏移值,让界面动起来。为什么是ContentView的父容器呢?因为ContentView不包含ActionBar,虽然不推荐使用ActionBar。。谭诗雨。
最后,在dispatchTouchEvent的ACTION_UP事件中进行距离判断,根据末速度和位移判断是否finish当前页面。
让页面滑动起来的基本思路就酱紫了。BUT,这其间还涉及到多点触摸、子View的Touch事件取消、末速度计算、松手后的动画处理等等。限于这块代码有点多也不是重点,这里就不贴出来了。有兴趣详细了解的同学请阅读源码,源码地址如下所示:
https://github.com/Simon-Leeeeeeeee/SLWidget/blob/master/swipeback/src/main/java/cn/simonlee/widget/swipeback/SwipeBackHelper.java
Step.3 窗口透明
到了这一步可能很多同学要问了,为毛我滑动之后底下黑黢黢的。别急,因为我们还没有甩出王炸。前面说了,我们需要在侧滑触发时利用反射将窗口转为透明,在侧滑结束时利用反射将窗口转为不透明。上一步已经讲解了如何让页面滑动起来,剩下的就好办了。请看王炸代码:
//将窗口转为透明privatevoidconvertToTranslucent(Activityactivity){if(activity.isTaskRoot())return;//栈底Activity不处理isTranslucentComplete=false;//转换完成标志try{//获取透明转换回调类的class对象if(mTranslucentConversionListenerClass==null){Class[]clazzArray=Activity.class.getDeclaredClasses();for(Classclazz:clazzArray){if(clazz.getSimpleName().contains("TranslucentConversionListener")){mTranslucentConversionListenerClass=clazz;}}}//代理透明转换回调if(mTranslucentConversionListener==null&&mTranslucentConversionListenerClass!=null){InvocationHandlerinvocationHandler=newInvocationHandler(){@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{isTranslucentComplete=true;returnnull;}};mTranslucentConversionListener=Proxy.newProxyInstance(mTranslucentConversionListenerClass.getClassLoader(),newClass[]{mTranslucentConversionListenerClass},invocationHandler);}//利用反射将窗口转为透明,注意SDK21及以上参数有所不同if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){Objectoptions=null;try{MethodgetActivityOptions=Activity.class.getDeclaredMethod("getActivityOptions");getActivityOptions.setAccessible(true);options=getActivityOptions.invoke(this);}catch(Exceptionignored){}MethodconvertToTranslucent=Activity.class.getDeclaredMethod("convertToTranslucent",mTranslucentConversionListenerClass,ActivityOptions.class);convertToTranslucent.setAccessible(true);convertToTranslucent.invoke(activity,mTranslucentConversionListener,options);}else{MethodconvertToTranslucent=Activity.class.getDeclaredMethod("convertToTranslucent",mTranslucentConversionListenerClass);convertToTranslucent.setAccessible(true);convertToTranslucent.invoke(activity,mTranslucentConversionListener);}}catch(Throwableignored){isTranslucentComplete=true;}if(mTranslucentConversionListener==null){isTranslucentComplete=true;}//去除窗口背景mSwipeBackActivity.getWindow().setBackgroundDrawable(null);}
//将窗口转为不透明privatevoidconvertFromTranslucent(Activityactivity){if(activity.isTaskRoot())return;//栈底Activity不处理try{MethodconvertFromTranslucent=Activity.class.getDeclaredMethod("convertFromTranslucent");convertFromTranslucent.setAccessible(true);convertFromTranslucent.invoke(activity);}catch(Throwablet){}}
代码有点长,不过很好理解。convertToTranslucent先获取透明转换回调类记李将军归来 ,然后代理透明转换回调,最后反射将窗口转为透明以及去掉窗口背景。convertFromTranslucent就是反射将窗口转为不透明。只需要在侧滑前调用convertToTranslucent即可将窗口转为透明,松手后调用convertFromTranslucent即可将窗口还原为不透明。 大家应该会注意到这里有个转换完成的标志,后面会解释它的作用。
Step.4 底层阴影
到了这里,已经基本实现了侧滑返回了,就三步走搞定。但是有些同学可能会觉得没个阴影不好看啊!这个简单,我们自定义一个ShadowView在侧滑时跟着调用setTranslationX即可。
publicViewgetShadowView(ViewGroupswipeBackView){if(mShadowView==null){mShadowView=newShadowView(mSwipeBackActivity);mShadowView.setTranslationX(-swipeBackView.getWidth());((ViewGroup)swipeBackView.getParent()).addView(mShadowView,0,newViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));}returnmShadowView;}
这里的swipeBackView即上文 Step.2 支持侧滑 提到的ContentView的父容器,将ShadowView插入到swipeBackView的父容器中。可能没有人注意到,这个getShadowView方法是public的,因为我这样想的,也许有人不喜欢我这个阴影偏偏要在侧滑的时候看到个皮卡丘呢?你说是吧。。。
另外到了这一步就不得不说,但凡是有几个人用的侧滑返回库,都支持微信那样下层Activity联动的,这里为了点题,咱们就不支持了。注意事项
经以上简单四步,基本上效果已经很棒了。不过还有一些需要特别注意的地方,以及前面占了两个坑,现在进行回填。
Tips.1
先填掉前面讲解DecorView的布局变化监听时占的坑。当布局变化时,我们通过调整DecorView子View的paddingBottom来达到适配输入法的adjustResize。这里就会导致一个问题,输入法的弹出有一个由下往上的动画,在动画这段时间内,这一块位置会显示窗口的颜色的—黑黢黢。这对于追求完美的人来说当然不能忍,我们的解决办法是new一个View堵住那块黑黢黢。是不是方法有点土。。。不过很凑效。。月季树 。
贴上前文onLayoutChange代码块中遗失的代码:
mWindowBackGroundView=getWindowBackGroundView(mDecorView);if(mWindowBackGroundView!=null){//堵住黑黢黢的那块mWindowBackGroundView.setTranslationY(visibleDisplayRect.bottom);}
Tips.2
在前面窗口透明处理中,也留了个坑:透明转换完成标志isTranslucentComplete。为什么要这个呢?因为将窗口转为透明需要约100ms左右的时间,如果在转换完成之前就移动了ContentView,你会看到底下又是一片黑黢黢。。。这当然非吾所愿,因此在移动之前判断若窗口还未转为透明,则不进行处理
privatevoidswipeBackEvent(inttranslation){if(!isTranslucentComplete)return;if(mShadowView.getBackground()!=null){intalpha=(int)((1F-1F*translation/mShadowView.getWidth())*255);alpha=Math.max(0,Math.min(255,alpha));mShadowView.getBackground().setAlpha(alpha);}mShadowView.setTranslationX(translation-mShadowView.getWidth());mSwipeBackView.setTranslationX(translation);}
这里可能有同学要说了,转换完成之前不处理,转换完成之后,这不是会突然跳一下么。比如从0突然跳到100的位置。思路很严谨,不过因为窗口转换100ms左右,除非是手速飞快,不然没多少距离,基本看不出来。如果手速飞快,变化太快也基本看不清前面到底是渐变还是突变浮髌试验。所以这样处理挺好的。。。
Tips.3
侧滑松手后会出现两种情况,其一回到左侧原点,其二继续滑动到右侧边界然后finish该Activity。前面提到侧滑松手后需要将窗口转为不透明。需要注意的是,如果会finish该Activity,请勿将窗口转为不透明。因为下层的Activity此时是透上来的,如果转为不透明,然后finish顶层Activity,会闪现一下黑色窗口峡江情歌 。另外finish之后要取消Activity的退出动画。
publicvoidonAnimationEnd(Animatoranimation){if(!isAnimationCancel){//最终移动距离位置超过半宽,结束当前Activityif(mShadowView.getWidth()+2*mShadowView.getTranslationX()>=0){mShadowView.setVisibility(View.GONE);mSwipeBackActivity.finish();mSwipeBackActivity.overridePendingTransition(-1,-1);//取消返回动画}else{mShadowView.setTranslationX(-mShadowView.getWidth());mSwipeBackView.setTranslationX(0);convertFromTranslucent(mSwipeBackActivity);}}}
Tips.4
侧滑的核心原理是利用反射转换窗口透明,在前面摸索透明方案中有提到,窗口透明会影响下层Activity的生命周期。当我们将窗口转为透明时,下层Activity会被唤醒,进入onStart状态,如果发生屏幕旋转,下层Activity还将会进行重建。当我们将窗口恢复为不透明,下层Activity会重新进入onStop状态恰克与飞鸟。因此如果你的Activity代码逻辑比较混乱,使用之前务必进行逻辑优化。
Tips.5
当顶层Activity方向与下层Activity方向不一致时侧滑会失效(下层方向未锁定除外),请关闭该层Activity侧滑功能。示例场景:竖屏界面点击视频,进入横屏播放。这个很好理解,例如顶层Activity横屏,下层锁定竖屏,当侧滑时,窗口到底是横屏还是竖屏?It's a question...
Tips.6
因为状态栏透明,布局会从屏幕顶端开始绘制m2神甲奇兵,Toolbar需要增加一个状态栏高度的paddingTop
//获取状态栏高度publicintgetStatusBarHeight(){intresourceId=getResources().getIdentifier("status_bar_height","dimen","android");try{returngetResources().getDimensionPixelSize(resourceId);}catch(Resources.NotFoundExceptione){return0;}}
Tips.7
如需动态支持横竖屏切换(比如APP中有“支持横屏”开关),屏幕方向需指定为behind跟随栈底Activity方向,同时在onCreate中进行判断,若不支持横竖屏切换则锁定屏幕方向(因为经测试SDK21中behind无效)。
Tips.8
可能有同学会发现,Styles中的"android:windowBackground"属性失效了巨野广电网 ,是因为需要透视到下层Activity所以去掉了这个背景。详见convertToTranslucent方法的最后一行:
privatevoidconvertToTranslucent(Activityactivity){if(activity.isTaskRoot())return;...//去除窗口背景mSwipeBackActivity.getWindow().setBackgroundDrawable(null);}
当然,对栈底Activity及未产生侧滑的Activity是不受影响的蕾雅赛杜 。另外在SDK21(Android5.0)以下必须指定<item name="android:windowIsTranslucent">true</item>,因为在SDK21(Android5.0)以下,反射调用的convertToTranslucent方法只能将【由convertFromTranslucent转换的不透明】转为透明,不能将原本就不透明的窗口转为透明。
总结
絮叨一通,全是大段文字。限于个人能力有限,难免存在些许疏忽失误,欢迎指正。如有更好的思路也请不吝赐教,此文权当抛砖引玉。
项目地址:
https://github.com/Simon-Leeeeeeeee/SLWidget/tree/master/swipeback
(含使用说明,欢迎Star,欢迎Fork)
Demo体验:
https://fir.im/SLWidget
欢迎长按下图->识别图中二维码
或者扫一扫关注我的公众号