动手写一个高仿微信的滑动返回SwipeBackActivity

本文来源于这个开源项目,由于作者只给出了用法和基本原理,因此才有了本文的产生。本文去除了原项目中较琐碎和不必要的一些内容,只实现了基本的Activity滑动返回功能。

先看效果图:

demo

效果还是挺不错的。

基本原理:利用Application类的registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks)方法,可以记录全局所有Activity的生命周期,因此我们可以利用这点来存储我们所有的Activity于一个栈中,每次滑动返回时从栈中取出前一个Activtity,然后分离出其中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创建的时候加入栈中,销毁的时候移除。
然后就是在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()方法,这是因为我们要监听边界滑动返回事件,肯定要拦截其中的一些触摸事件。

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;
        //return super.dispatchTouchEvent(ev)||mTouchHelepr.processTouchEvent(ev);
    }
}

这里有一个我们自己写的类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 int triggerWidth=50;
    //阴影宽度
    private int SHADOW_WIDTH=30;

mWindow用于初始化TouchHelper,并且这个window就包含了context,activity等信息。

curContentView、preContentView分别表示当前、前一个Activity中外层的FrameLayout。

curView、preView分别表示当前、前一个Activity的界面View。

然后就是处理手势的代码了:

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){
                    isIdle=false;
                    isSlinding=true;
                    startSlide();
                    return true;
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                if(isSlinding) return true;
                break;
            case MotionEvent.ACTION_MOVE:
                if(isSlinding){
                    if(event.getActionIndex()!=0) return true;
                    sliding(x);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if(!isSlinding) return false;
                int width=getContext().getResources().getDisplayMetrics().widthPixels;
                isAnimating=true;
                isSlinding=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/x<=3,x)

startSlide(),顾名思义,开始滑动,先看看代码:

private 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()中,我们给几个成员变量赋值,并且将preView添加到curContentView中,并赋予其一个初始偏移量。这里要特别注意addView(view,index)中的index参数,index参数越大,代表越靠后绘制。这里添加preView时的index为0,表示最先绘制preView,否则preView会显示在curView的上面,这样就不正确了。注释部分稍后再讲。

然后看看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而已。注释稍后讲。

然后是startAnimating()方法:

private 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 valueAnimator) {
                sliding((Float) valueAnimator.getAnimatedValue());
            }
        });
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                doEndWorks(isFinishing);
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
        animator.start();
    }

当用户松开手指时的位置的x坐标小于屏幕宽度的1/3时,恢复原状,否则将preView完全显示,这里利用ValueAnimator来实现动画。注意在动画完成后我们还要做一些收尾工作,就是方法doEndWorks():

private void doEndWorks(boolean isFinishing) {
        if(preActivity==null) return;
        if(isFinishing){
            //更改当前activity的底view为preView,防止当前activity finish时的白屏闪烁
            BackView view=new BackView(getContext());
            view.cacheView(preView);
            curContentView.addView(view,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;
        isSlinding=false;
        isIdle=true;
        preView=null;
        curView=null;
    }

收尾工作中我们将状态修正,该移除的View移除,该添加的View添加。若preView完全显示,就finish当前activity,注意还要利用((Activity)getContext()).overridePendingTransition(0,0)取消默认的activity更换动画,这样才能实现暗度陈仓的目的。你应该已经看到了这里还有一个BackView,这个BackView其实就是preView的一个副本,我们将BackView添加到curContentView的最底部,覆盖那个白色底部,否则动画完成后会有一个白屏闪烁现象。

//用于防止白屏闪烁
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;
        }
    }
}

这样实现的效果就是如下:

demo

你应该注意要这个和第一个demo显示的不太一样,因为这里没有阴影效果,体现不出层次感,不够美观,那么接下来我们只需要在添加一点点代码就可以添加这样的一个阴影效果.

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);
    }
}

这个就是要绘制上去的阴影效果,很简单,将前面几个方法中注释的代码部分还原即可。

这样,就完成了我们所有的代码~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容