最近在使用IOS系统的时候,发现侧滑关闭很实用,因为单手就可以操作,不需要点击左上角的回退按钮、或者返回键了。
所以打算在android上实现这个技术。
需求:
1:IOS只能在屏幕边缘开始,往中间进行侧滑才能关闭;我们希望触发点可以在任意位置。
2:对现有代码入侵尽可能下,简单配置下就可以实现这个功能。
实战参考:请参考本人的博客园项目
参考了GitHub上一个开源框架,优化后形成现有的框架
下面是其实现原理,总结的很到位,做了部分修改
像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(); } } }