高仿微信的滑动返回SwipeBackActivity

你。 提交于 2019-12-02 12:58:20

高仿微信的滑动返回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;
  1. isIdle,表示当前为静止状态
  2. isSliding,表示当前用户手指移动,我们的View随之滑动
  3. 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去处理。

其中状态更改的代码比较简单,就不解释了。主要说说其中随着状态更改而进行的几个操作函数:

  1. startSlide()
  2. sliding(x)
  3. 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);

只要这么一调用即可。这样的话,就大功告成了。

本文参考 http://www.jianshu.com/p/b194b0234d2c

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!