動(dòng)手寫一個(gè)高仿微信的滑動(dòng)返回SwipeBackActivity

本文來(lái)源于這個(gè)開(kāi)源項(xiàng)目,由于作者只給出了用法和基本原理,因此才有了本文的產(chǎn)生。本文去除了原項(xiàng)目中較瑣碎和不必要的一些內(nèi)容,只實(shí)現(xiàn)了基本的Activity滑動(dòng)返回功能。

先看效果圖:

demo

效果還是挺不錯(cuò)的。

基本原理:利用Application類的registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks)方法,可以記錄全局所有Activity的生命周期,因此我們可以利用這點(diǎn)來(lái)存儲(chǔ)我們所有的Activity于一個(gè)棧中,每次滑動(dòng)返回時(shí)從棧中取出前一個(gè)Activtity,然后分離出其中id為Window.ID_ANDROID_CONTENT的FrameLayout,這個(gè)FrameLayout就是我們setContentView中的那個(gè)view的父view,利用這個(gè)FrameLayout就可以獲取Activity界面顯示的View。然后我們監(jiān)聽(tīng)手勢(shì)事件,在滑動(dòng)的時(shí)候?qū)⑶耙粋€(gè)Activity的View加載進(jìn)來(lái)并不斷更改其偏移量即可。

首先是實(shí)現(xiàn)ActivityLifecycleCallbacks接口,并在其中用一個(gè)棧存儲(chǔ)我們所有的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);
    }
}

看得出這個(gè)類很簡(jiǎn)單,只是在Activity創(chuàng)建的時(shí)候加入棧中,銷毀的時(shí)候移除。
然后就是在Application中調(diào)用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);
    }
}

然后定義一個(gè)最基本的SwipeBackActivity,當(dāng)然要繼承自AppCompactActivity,這個(gè)類我們要做的就是重寫它的dispatchTouchEvent()方法,這是因?yàn)槲覀円O(jiān)聽(tīng)邊界滑動(dòng)返回事件,肯定要攔截其中的一些觸摸事件。

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

這里有一個(gè)我們自己寫的類TouchHelper,具體的邏輯操作就在這里面實(shí)現(xiàn)了。接下來(lái)就是重點(diǎn)類TouchHelper了。
首先我們定義三個(gè)狀態(tài):

    private boolean isIdle=true;
    private boolean isSlinding=false;
    private boolean isAnimating=false;
  1. isIdle,表示當(dāng)前為靜止?fàn)顟B(tài)。
  2. isSliding,表示當(dāng)前用戶手指移動(dòng),我們的View隨之滑動(dòng)。
  3. isAnimating,表示用戶手指松開(kāi),View要么恢復(fù)原狀,要么移動(dòng)至最右并消失,這是一個(gè)Animation過(guò)程,isAnimating=true表示當(dāng)前處于這種動(dòng)畫過(guò)程中。

然后是幾個(gè)成員變量:

    private Window mWindow;
    private ViewGroup preContentView;
    private ViewGroup curContentView;
    private ViewGroup curView;
    private ViewGroup preView;
    private Activity preActivity; 
    
    //左邊觸發(fā)的寬度
    private int triggerWidth=50;
    //陰影寬度
    private int SHADOW_WIDTH=30;

mWindow用于初始化TouchHelper,并且這個(gè)window就包含了context,activity等信息。

curContentView、preContentView分別表示當(dāng)前、前一個(gè)Activity中外層的FrameLayout。

curView、preView分別表示當(dāng)前、前一個(gè)Activity的界面View。

然后就是處理手勢(shì)的代碼了:

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

在函數(shù)processTouchEvent()中所有要攔截的地方我們都return true,這樣子View就不會(huì)受到觸摸事件了,其余的則應(yīng)返回false,表示將觸摸事件分發(fā)給子View去處理。

其中狀態(tài)更改的代碼比較簡(jiǎn)單,就不解釋了。主要說(shuō)說(shuō)其中隨著狀態(tài)更改而進(jìn)行的幾個(gè)操作函數(shù):

  1. startSlide()
  2. sliding(x)
  3. startAnimating(width/x<=3,x)

startSlide(),顧名思義,開(kāi)始滑動(dòng),先看看代碼:

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()中,我們給幾個(gè)成員變量賦值,并且將preView添加到curContentView中,并賦予其一個(gè)初始偏移量。這里要特別注意addView(view,index)中的index參數(shù),index參數(shù)越大,代表越靠后繪制。這里添加preView時(shí)的index為0,表示最先繪制preView,否則preView會(huì)顯示在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);
    }

這個(gè)函數(shù)就簡(jiǎn)單多了,這是隨著用戶手指的位置動(dòng)態(tài)地更改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();
    }

當(dāng)用戶松開(kāi)手指時(shí)的位置的x坐標(biāo)小于屏幕寬度的1/3時(shí),恢復(fù)原狀,否則將preView完全顯示,這里利用ValueAnimator來(lái)實(shí)現(xiàn)動(dòng)畫。注意在動(dòng)畫完成后我們還要做一些收尾工作,就是方法doEndWorks():

private void doEndWorks(boolean isFinishing) {
        if(preActivity==null) return;
        if(isFinishing){
            //更改當(dāng)前activity的底view為preView,防止當(dāng)前activity finish時(shí)的白屏閃爍
            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;
    }

收尾工作中我們將狀態(tài)修正,該移除的View移除,該添加的View添加。若preView完全顯示,就finish當(dāng)前activity,注意還要利用((Activity)getContext()).overridePendingTransition(0,0)取消默認(rèn)的activity更換動(dòng)畫,這樣才能實(shí)現(xiàn)暗度陳倉(cāng)的目的。你應(yīng)該已經(jīng)看到了這里還有一個(gè)BackView,這個(gè)BackView其實(shí)就是preView的一個(gè)副本,我們將BackView添加到curContentView的最底部,覆蓋那個(gè)白色底部,否則動(dòng)畫完成后會(huì)有一個(gè)白屏閃爍現(xiàn)象。

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

這樣實(shí)現(xiàn)的效果就是如下:

demo

你應(yīng)該注意要這個(gè)和第一個(gè)demo顯示的不太一樣,因?yàn)檫@里沒(méi)有陰影效果,體現(xiàn)不出層次感,不夠美觀,那么接下來(lái)我們只需要在添加一點(diǎn)點(diǎn)代碼就可以添加這樣的一個(gè)陰影效果.

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

這個(gè)就是要繪制上去的陰影效果,很簡(jiǎn)單,將前面幾個(gè)方法中注釋的代碼部分還原即可。

這樣,就完成了我們所有的代碼~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容