最近在使用IOS系統的時候,發現側滑關閉很實用,由於單手就能夠操做,不須要點擊左上角的回退按鈕、或者返回鍵了。android
因此打算在android上實現這個技術。git
需求:github
1:IOS只能在屏幕邊緣開始,往中間進行側滑才能關閉;咱們但願觸發點能夠在任意位置。canvas
2:對現有代碼入侵儘量下,簡單配置下就能夠實現這個功能。app
實戰參考:請參考本人的博客園項目框架
參考了GitHub上一個開源框架,優化後造成現有的框架ide
下面是其實現原理,總結的很到位,作了部分修改佈局
像fragment同樣,activity自己是不能夠滑動的,可是咱們能夠製造一個正在滑動activity的假象,使得看起來這個activity正在被手指滑動。優化
其原理其實很簡單,咱們滑動的實際上是activity裏面的可見view元素,而咱們將activity設置爲透明的,這樣當view滑過的時候,因爲activity的底部是透明的,咱們就能夠在滑動過程當中看到下面的activity,這樣看起來就是在滑動activity。動畫
設置透明: 很簡單,創建一個Style,在Style裏面添加下面兩行並將這個style應用在activity上就能夠了
<item name="android:windowBackground">@*android:color/transparent</item> <item name="android:windowIsTranslucent">true</item>
咱們用的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(); } } }