【android】側滑關閉activity


最近在使用IOS系統的時候,發現側滑關閉很實用,因為單手就可以操作,不需要點擊左上角的回退按鈕、或者返回鍵了。

所以打算在android上實現這個技術。

需求:

1:IOS只能在屏幕邊緣開始,往中間進行側滑才能關閉;我們希望觸發點可以在任意位置。

2:對現有代碼入侵盡可能下,簡單配置下就可以實現這個功能。

實戰參考:請參考本人的博客園項目

參考了GitHub上一個開源框架,優化后形成現有的框架

 

下面是其實現原理,總結的很到位,做了部分修改

Android activity滑動返回原理

像fragment一樣,activity本身是不可以滑動的,但是我們可以制造一個正在滑動activity的假象,使得看起來這個activity正在被手指滑動。

其原理其實很簡單,我們滑動的其實是activity里面的可見view元素,而我們將activity設置為透明的,這樣當view滑過的時候,由於activity的底部是透明的,我們就可以在滑動過程中看到下面的activity,這樣看起來就是在滑動activity。

所以activity滑動效果分兩步,1,設置透明,2,滑動view

設置透明: 很簡單,建立一個Style,在Style里面添加下面兩行並將這個style應用在activity上就可以了

<item name="android:windowBackground">@*android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>

 

先看看activity的層次結構:

我們用的activity的xml的根view並不是activity的根view,在它上面還有一個父view,id是android.R.id.content,再向上一層,還有一個view,它是一個LinearLayout,

它除了放置我們創建的view之外,還放置我們的xml之外的一些東西比如放ActionBar什么的。而再往上一級,就到了activity的根view——DecorView。

如下圖

要做到像iOS那樣可以滑動整個activity,只滑動我們在xml里面創建的view顯然是不對的

因為我們還有ActionBar什么的,所以我們要滑動的應該是DecorView或者倒數第二層的那個view

而要滑動view的話,我們要重寫其父窗口的onInterceptTouchEvent以及onTouchEvent【當然使用setOnTouchListener不是不可能,但是如果子view里面有一個消費了onTouch事件,那么也就接收不到了】,但是窗口的創建過程不是我們能控制的,DecorView的創建都不是我們能干預的。

解決辦法就是,我們自己創建一個SwipeLayout,然后人為地插入頂層view中,放置在DecorView和其下面的LinearLayout中間,隨着手指的滑動,不斷改變SwipeLayout的子view——曾經是DecorView的子view——的位置

這樣我們就可以控制activity的滑動啦。我們在activity的onPostCreate方法中調用swipeLayout.replaceLayer替換我們的SwipeLayout,代碼如下

 /**
     * 將本view注入到decorView的子view上
     * 在{@link Activity#onPostCreate(Bundle)}里使用本方法注入
     */
    public void injectWindow() {
        if (mIsInjected)
            return;

        final ViewGroup root = (ViewGroup) mActivity.getWindow().getDecorView();
        mContent = root.getChildAt(0);
        root.removeView(mContent);
        this.addView(mContent, new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        root.addView(this);
        mIsInjected = true;
    }

然后我們把這些寫成一個SwipeActivity,其它activity只要繼承這個SwipeActivity就可以實現滑動返回功能(當然Style仍然要設置的) 這里只說滑動activity的原理,剩下的都是控制滑動的事了,詳見代碼

BTW,滑動Fragment原理其實一樣,只不過更加簡單,Fragment在view樹中就是它inflate的元素,用fragment.getView可以取得,滑動fragment其實滑動的就是fragment.getView。只要把滑動方法寫在它父view中就可以了

 

在實際使用中,我們發現,當你把Activity背景色設置為透明之后,原先設置的Activity進入、退出動畫效果就消失了

原因是因為透明背景色、Translucent的Activity,它的動畫體系和有背景色的Activity是不同的,看下面代碼的parent部分

 

  <!-- 日間模式,透明 -->
    <style name="AppTheme.day.transparent" parent="AppTheme.day">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowAnimationStyle">@style/transparentAnimation</item>
    </style>

    <!--普通有底色的Activity動畫-->
    <style name="normalAnimation" parent="@android:style/Animation.Activity">
        <item name="android:activityOpenEnterAnimation">@anim/slide_right_in</item>
        <item name="android:activityOpenExitAnimation">@anim/slide_left_out</item>
        <item name="android:activityCloseEnterAnimation">@anim/slide_left_in</item>
        <item name="android:activityCloseExitAnimation">@anim/slide_right_out</item>
    </style>
    <!--透明的Activity動畫-->
    <style name="transparentAnimation" parent="@android:style/Animation.Translucent">
        <item name="android:windowEnterAnimation">@anim/slide_right_in</item>
        <item name="android:windowExitAnimation">@anim/slide_right_out</item>
    </style>

其他也沒啥好說的了,直接看代碼吧

package zhexian.learn.cnblogs.ui;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;

import zhexian.learn.cnblogs.R;

/**
 * 側滑關閉的布局,使用方式
 * 在目標容器的onCreate里面創建本布局 {@link #SwipeCloseLayout(Context)}
 * 在目標容器的onPostCreate里面將本布局掛載到decorView下{@link #injectWindow()}
 * Created by 陳俊傑 on 2016/2/16.
 */
public class SwipeCloseLayout extends FrameLayout {
    private static final int ANIMATION_DURATION = 200;

    /**
     * 是否可以滑動關閉頁面
     */
    private boolean mSwipeEnabled = true;
    private boolean mIsAnimationFinished = true;
    private boolean mCanSwipe = false;
    private boolean mIgnoreSwipe = false;
    private boolean mHasIgnoreFirstMove;

    private Activity mActivity;
    private VelocityTracker tracker;
    private ObjectAnimator mAnimator;
    private Drawable mLeftShadow;
    private View mContent;
    private int mScreenWidth;
    private int touchSlopLength;
    private float mDownX;
    private float mDownY;
    private float mLastX;
    private float mCurrentX;
    private int mPullMaxLength;
    private boolean mIsInjected;


    public SwipeCloseLayout(Context context) {
        this(context, null, 0);
    }

    public SwipeCloseLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SwipeCloseLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mActivity = (Activity) context;
        mLeftShadow = context.getResources().getDrawable(R.drawable.left_shadow);
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        touchSlopLength = (int) (20 * displayMetrics.density);
        touchSlopLength *= touchSlopLength;
        mScreenWidth = displayMetrics.widthPixels;
        mPullMaxLength = (int) (mScreenWidth * 0.33f);
        setClickable(true);
    }

    /**
     * 將本view注入到decorView的子view上
     * 在{@link Activity#onPostCreate(Bundle)}里使用本方法注入
     */
    public void injectWindow() {
        if (mIsInjected)
            return;

        final ViewGroup root = (ViewGroup) mActivity.getWindow().getDecorView();
        mContent = root.getChildAt(0);
        root.removeView(mContent);
        this.addView(mContent, new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        root.addView(this);
        mIsInjected = true;
    }

    public boolean isSwipeEnabled() {
        return mSwipeEnabled;
    }

    public void setSwipeEnabled(boolean swipeEnabled) {
        this.mSwipeEnabled = swipeEnabled;
    }

    @Override
    protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
        boolean result = super.drawChild(canvas, child, drawingTime);
        final int shadowWidth = mLeftShadow.getIntrinsicWidth();
        int left = (int) (getContentX()) - shadowWidth;
        mLeftShadow.setBounds(left, child.getTop(), left + shadowWidth, child.getBottom());
        mLeftShadow.draw(canvas);
        return result;
    }

    @Override
    public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
        if (mSwipeEnabled && !mCanSwipe && !mIgnoreSwipe) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mDownX = ev.getX();
                    mDownY = ev.getY();
                    mCurrentX = mDownX;
                    mLastX = mDownX;
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dx = ev.getX() - mDownX;
                    float dy = ev.getY() - mDownY;
                    if (dx * dx + dy * dy > touchSlopLength) {
                        if (dy == 0f || Math.abs(dx / dy) > 1) {
                            mDownX = ev.getX();
                            mDownY = ev.getY();
                            mCurrentX = mDownX;
                            mLastX = mDownX;
                            mCanSwipe = true;
                            tracker = VelocityTracker.obtain();
                            return true;
                        } else {
                            mIgnoreSwipe = true;
                        }
                    }
                    break;
            }
        }
        if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
            mIgnoreSwipe = false;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mCanSwipe || super.onInterceptTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        if (mCanSwipe) {
            tracker.addMovement(event);
            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mDownX = event.getX();
                    mCurrentX = mDownX;
                    mLastX = mDownX;
                    break;
                case MotionEvent.ACTION_MOVE:
                    mCurrentX = event.getX();
                    float dx = mCurrentX - mLastX;
                    if (dx != 0f && !mHasIgnoreFirstMove) {
                        mHasIgnoreFirstMove = true;
                        dx = dx / dx;
                    }
                    if (getContentX() + dx < 0) {
                        setContentX(0);
                    } else {
                        setContentX(getContentX() + dx);
                    }
                    mLastX = mCurrentX;
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    tracker.computeCurrentVelocity(10000);
                    tracker.computeCurrentVelocity(1000, 20000);
                    mCanSwipe = false;
                    mHasIgnoreFirstMove = false;
                    int mv = mScreenWidth * 3;
                    if (Math.abs(tracker.getXVelocity()) > mv) {
                        animateFromVelocity(tracker.getXVelocity());
                    } else {
                        if (getContentX() > mPullMaxLength) {
                            animateFinish(false);
                        } else {
                            animateBack(false);
                        }
                    }
                    tracker.recycle();
                    break;
                default:
                    break;
            }
        }
        return super.onTouchEvent(event);
    }


    public void cancelPotentialAnimation() {
        if (mAnimator != null) {
            mAnimator.removeAllListeners();
            mAnimator.cancel();
        }
    }

    public float getContentX() {
        return mContent.getX();
    }

    private void setContentX(float x) {
        mContent.setX(x);
        invalidate();
    }

    public boolean isAnimationFinished() {
        return mIsAnimationFinished;
    }

    /**
     * 彈回,不關閉,因為left是0,所以setX和setTranslationX效果是一樣的
     *
     * @param withVel 使用計算出來的時間
     */
    private void animateBack(boolean withVel) {
        cancelPotentialAnimation();
        mAnimator = ObjectAnimator.ofFloat(this, "contentX", getContentX(), 0);
        int tmpDuration = withVel ? ((int) (ANIMATION_DURATION * getContentX() / mScreenWidth)) : ANIMATION_DURATION;
        if (tmpDuration < 100) {
            tmpDuration = 100;
        }
        mAnimator.setDuration(tmpDuration);
        mAnimator.setInterpolator(new DecelerateInterpolator());
        mAnimator.start();
    }

    private void animateFinish(boolean withVel) {
        cancelPotentialAnimation();
        mAnimator = ObjectAnimator.ofFloat(this, "contentX", getContentX(), mScreenWidth);
        int tmpDuration = withVel ? ((int) (ANIMATION_DURATION * (mScreenWidth - getContentX()) / mScreenWidth)) : ANIMATION_DURATION;
        if (tmpDuration < 100) {
            tmpDuration = 100;
        }
        mAnimator.setDuration(tmpDuration);
        mAnimator.setInterpolator(new DecelerateInterpolator());
        mAnimator.addListener(new Animator.AnimatorListener() {

            @Override
            public void onAnimationStart(Animator animation) {
                mIsAnimationFinished = false;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                mIsAnimationFinished = true;
                if (!mActivity.isFinishing()) {
                    mActivity.finish();
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                mIsAnimationFinished = true;
            }
        });
        mAnimator.start();
    }


    private void animateFromVelocity(float v) {
        int currentX = (int) getContentX();
        if (v > 0) {
            if (currentX < mPullMaxLength && v * ANIMATION_DURATION / 1000 + currentX < mPullMaxLength) {
                animateBack(false);
            } else {
                animateFinish(true);
            }
        } else {
            if (currentX > mPullMaxLength / 3 && v * ANIMATION_DURATION / 1000 + currentX > mPullMaxLength) {
                animateFinish(false);
            } else {
                animateBack(true);
            }
        }
    }

    public void finish() {
        if (!isAnimationFinished()) {
            cancelPotentialAnimation();
        }
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM