zoukankan      html  css  js  c++  java
  • 【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();
            }
        }
    }
  • 相关阅读:
    Python全栈开发之21、django
    Python全栈开发之17、tornado和web基础知识
    Python全栈开发之18、cookies、session和ajax等相关知识
    jquery之别踩白块游戏的实现
    Python全栈开发之16、jquery
    Python全栈开发之15、DOM
    Python全栈开发之13、CSS
    Python全栈开发之12、html
    Go语言学习之路-2-变量与常量
    Go语言学习之路-1-Go语言环境搭建
  • 原文地址:https://www.cnblogs.com/kimmy/p/5199279.html
Copyright © 2011-2022 走看看