高仿微信的滑动返回SwipeBackActivity
仔细的同学可以发现,微信的滑动返回其实比一般的滑动返回要细致一点,返回的Activity会显示一部分,然后跟着联动,而以前一般的是直接两个相连的滑动。本文就讲一讲怎么高仿出一个微信的滑动返回功能。
滑动进入
如果只有滑动返回,用户是很难发现有这一功能的,但是如果新的Activity就是从屏幕的右侧向左滑动显示出来的,那么在屏幕左侧滑动能将该Activity滑出的感觉就很顺其自然,这就是一些交互方面的知识了,所以我们先要做出新Activity滑动进入的效果。
这一个效果非常好做,网上的方法基本就是一个使用 overridePendingTransition()
方法,然后使用已经编写好的项目,xml文件来加载出一个transition来,这就可以实现了。
@Override
public void startActivity(Intent intent) {
super.startActivity(intent);
overridePendingTransition(R.anim.in_right, R.anim.out_left);
}
R.anim.in_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%p" android:toXDelta="0"
android:duration="@android:integer/config_shortAnimTime" />
</set>
R.anim.out_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="-100%p"
android:duration="@android:integer/config_shortAnimTime" />
</set>
原理也非常简单,原Activity从屏幕的0位置移动到-100%p,-100%p就是一个屏幕的宽度,往负方向移动就是往左移动,所以就从左侧滑出了。而新Activity就从右侧100%p位置滑动到0位置,这样就从右侧滑入了。两个动画会同步进行,这样就能达到了滑动进入的效果了。
滑动返回
基本原理是:利用Application类的 registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks)
方法,可以记录全局所有Activity的生命周期,因此我们可以利用这点来储存我们所有的Activity与一个栈中,每次滑动返回时从栈中取出前一个Activity,然后分离出其中id为 Window.ID_ANDROID_CONTENT
的 FrameLayout,这个 FrameLayout 就是我们 setContentView
中的那个view的父view,利用这个 FrameLayout 就可以获取 Activity 界面显示的View。然后我们监听手势事件,在滑动的时候将前一个Activity的View加载进来并不断更改其偏移量即可。
首先是实现ActivityLifecycleCallbacks接口,并在其中用一个栈储存我们所有的Activity:
public class ActivityLifeCycleHelper implements Application.ActivityLifecycleCallbacks {
private Stack<Activity> mActivities;
public ActivityLifeCycleHelper(){
mActivities=new Stack<>();
}
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
mActivities.add(activity);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
}
@Override
public void onActivityDestroyed(Activity activity) {
mActivities.remove(activity);
}
public Activity getPreActivity(){
int size=mActivities.size();
if(size<2) return null;
else return mActivities.get(size-2);
}
public void removeActivity(Activity activity){
mActivities.remove(activity);
}
}
看的出来这个类很简单,只是在 Activity创建的时候加入栈,销毁的时候移除。但是这个类还可以拓展出很多其他功能,比如做一个任务管理器,一键清除所有Activity之类的,这里就不详细讲了。
然后就是在Application中调用 registerActivityLifecycleCallbacks()
方法了:
public class MyApplication extends Application {
public ActivityLifeCycleHelper getHelper() {
return mHelper;
}
private ActivityLifeCycleHelper mHelper;
@Override
public void onCreate() {
super.onCreate();
mHelper=new ActivityLifeCycleHelper();
//store all the activities
registerActivityLifecycleCallbacks(mHelper);
}
}
然后定义一个最基本的SwipeBackActivity,当然要继承自AppCompactActivity,这个类我们要做的就是重写它的dispatchTouchEvent()
方法,这是因为我们要监听边界滑动返回时间,肯定是要拦截其中的一些触摸事件的,否则如果还有一些recyclerview等控件的话,触摸事件会发生冲突,所以我们需要拦截一些触摸事件抢先处理了。
public class SwipeBackActivity extends AppCompatActivity {
private TouchHelepr mTouchHelepr;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(mTouchHelepr==null)
mTouchHelepr=new TouchHelepr(getWindow());
boolean consume=mTouchHelepr.processTouchEvent(ev);
if(!consume) return super.dispatchTouchEvent(ev);
return false;
}
}
这里有一个我们自己写的类 TouchHelper,具体的逻辑操作就在这里面实现了。接下来就是重点类TouchHelper了。
触摸事件处理
首先我们定义三个状态:
private boolean isIdle=true;
private boolean isSlinding=false;
private boolean isAnimating=false;
- isIdle,表示当前为静止状态
- isSliding,表示当前用户手指移动,我们的View随之滑动
- isAnimating,表示用户手指松开,View要么恢复原状,要么移动至最右并消失,这是一个Animation过程,isAnimating = true 表示当前处于这种动画过程中。
然后是几个成员变量
private Window mWindow;
private ViewGroup preContentView;
private ViewGroup curContentView;
private ViewGroup curView;
private ViewGroup preView;
private Activity preActivity;
//阴影类,稍后再讲
private ShadowView mShadowView;
//左边触发的宽度
private int triggerWidth=50;
//阴影宽度
private int SHADOW_WIDTH=30;
mWindow 用于初始化 TouchHelper,并且这个window包含了context,activity等信息。
curContentView、preContentView分别表示当前、前一个Activity中 外层的FrameLayout。
curView、preView 分别表示当前、前一个 Activity 的界面View。
然后就是处理手势的代码了:
public TouchHelper(Window window) {
mWindow = window;
}
private Context getContext() {
return mWindow.getContext();
}
public boolean processTouchEvent(MotionEvent event) {
if (isAnimating) {
return true;
}
float x = event.getRawX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (x <= triggerWidth) {
isIdler = false;
isSliding = true;
startSlide();
return true;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
if (isSliding) {
return true;
}
break;
case MotionEvent.ACTION_MOVE:
if (isSliding) {
if (event.getActionIndex() != 0) {
return true;
}
sliding(x);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (!isSliding) {
return false;
}
int width = getContext().getResources().getDisplayMetrics().widthPixels;
isAnimating = true;
isSliding = false;
startAnimating(width / x <= 3, x);
return true;
default:
break;
}
return false;
}
在函数 processTouchEvent() 中所有要拦截的地方我们都 return true,这样的话,子View就不会受到触摸事件了,其余的则应返回false,表示将触摸事件分发给子View去处理。
其中状态更改的代码比较简单,就不解释了。主要说说其中随着状态更改而进行的几个操作函数:
- startSlide()
- sliding(x)
- startAnimating(width / 3 <= 3, x)
startSlide()
顾名思义,开始滑动,先看看代码:
public void startSlide() {
preActivity = ((MyApplication) getContext().getApplicationContext()).getHelper().getPreActivity();
if (preActivity == null) {
return;
}
preContentView = (ViewGroup) preActivity.getWindow().findViewById(Window.ID_ANDROID_CONTENT);
preView = (ViewGroup) preContentView.getChildAt(0);
preContentView.removeView(preView);
curContentView=(ViewGroup) mWindow.findViewById(Window.ID_ANDROID_CONTENT);
curView= (ViewGroup) curContentView.getChildAt(0);
preView.setX(-preView.getWidth() / 3);
curContentView.addView(preView, 0);
if (mShadowView == null) {
mShadowView = new ShadowView(getContext());
}
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(SHADOW_WIDTH, FrameLayout.LayoutParams.MATCH_PARENT);
curContentView.addView(mShadowView, 1, params);
mShadowView.setX(-SHADOW_WIDTH);
}
在startSlide()中,我们给几个成员变量赋值,其实就是个初始化的函数,先获得前一个Activity,如果前面没有Activity的话,当然不能返回了,于是直接return;从preActivity中获取父View,然后再得到第一个子View就是我们需要的前一个Activity的View,再将preView添加到curContentView中,并赋予其一个初始偏移量,这里就可以做到联动效果的第一步。这里特别要注意 addView(view, index)
中的index蚕食,index参数值越大,代表越靠后绘制。这里添加 preView 时的index为0,表示最先绘制preView,否则preView会显示在curView的上面,这样就不正确了。而突然出现mShadowView,你会发现,微信滑动的话,边缘是有一层阴影,这里就是加上这层阴影。
sliding()
private void sliding(float rawX) {
if(preActivity==null) return;
curView.setX(rawX);
preView.setX(-preView.getWidth()/3+rawX/3);
mShadowView.setX(-SHADOW_WIDTH+rawX);
}
这个函数就简单多了,这是随着用户手指的位置动态地更改curView、preView与mShadowView而已。
startAnimating()
public void startAnimating(final boolean isFinishing, float x) {
int width = getContext().getResources().getDisplayMetrics().widthPixels;
ValueAnimator animator = ValueAnimator.ofFloat(x, isFinishing ? width : 0);
animator.setInterpolator(new DecelerateInterpolator());
animator.setDuration(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
sliding((Float) animation.getAnimatedValue());
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
doEndWorks(isFinishing);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
}
当用户松开手指时的位置的x坐标小于屏幕宽度的 1/3 时,恢复原态,否则将 preView 完全显示,这里利用 ValueAnimator 来实现动画。注意在动画完成之后我们还要做一些收尾工作,就是方法 doEndWorks()。
doEndWorks()
private void doEndWorks(boolean isFinishing) {
if (preActivity == null) {
return;
}
if (isFinishing) {
BackView view = new BackView(getContext());
view.cacheView(preView);
curContentView.addView(view, 0);
} else {
preView.setX(0);
}
curContentView.removeView(mShadowView);
if (curContentView == null || preContentView == null) {
return;
}
curContentView.removeView(preView);
preContentView.addView(preView);
if (isFinishing) {
((Activity) getContext()).finish();
((Activity) getContext()).overridePendingTransition(0, 0);
}
isAnimating=false;
isSliding=false;
isIdler=true;
preView=null;
curView=null;
}
收尾工作中我们将状态修正,该移除的View移除,该添加的View添加。若preView完全显示,就finish当前activity,注意还要利用((Activity)getContext()).overridePendingTransition(0,0)
取消默认的activity更换动画,这样才能实现暗度陈仓的目的。你应该已经看到了这里还有一个BackView,这个BackView其实就是preView的一个副本,我们将BackView添加到curContentView的最底部,覆盖那个白色底部,否则动画完成后会有一个白屏闪烁现象。
还有一种特殊的情况是,用户滑动了,然后点击了返回键返回的话,如果我们在这种情况下不做处理,我们会发现因为用户滑动的时候,我们将preView进行了移动,所以再点击返回键返回的时候,却又没有了联动的设置,这会导致preView就有偏移,所以我们要在这个函数中将preView修正为原来的位置,这样的话就可以避免,用户突然改主意使用返回键来返回。
//用于防止白屏闪烁
class BackView extends View{
private View mView;
public BackView(Context context) {
super(context);
}
public void cacheView(View view){
mView=view;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mView!=null){
mView.draw(canvas);
mView=null;
}
}
}
而一直卖关子到现在的阴影类,其实也就是一个我们自己绘制的Drawable而已。
class ShadowView extends View{
private Drawable mDrawable;
public ShadowView(Context context) {
super(context);
int[] colors=new int[]{0x00000000, 0x17000000, 0x43000000};
mDrawable=new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT,colors);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDrawable.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight());
mDrawable.draw(canvas);
}
}
这样的话,我们就完成了我们所有的代码。
配置
因为我们重写了Application类,所以需要在AndroidManifest中改用我们自己的Application,这样的话才能记录Activity到栈中。
对于能够实现滑动返回的Activity,我们只用继承SwipeBackActivity即可。这里其实还有一点可以改进,就是如果有返回按钮的,我们如果直接finish()的话,是没有滑动效果,而我们的SwipeBackActivity中其实也带有了finish(),所以我们可以利用一下,来实现,如果是点击返回按钮的话,来执行SwipeBackActivity中函数来间接finish掉。
this.getTouchHelper().startSlide();
this.getTouchHelper().startAnimating(true, 0);
只要这么一调用即可。这样的话,就大功告成了。
来源:CSDN
作者:陈靖靖靖靖
链接:https://blog.csdn.net/gu18168/article/details/60142394