Android activity滑动返回

作者: 好多个胖子 | 来源:发表于2018-06-01 15:22 被阅读975次

    前言

    activity的滑动返回也是个常用的功能,网上有很多介绍怎么去实现的库,我测过其中几个,包括star比较多的

    1. SwipeBackLayout
    2. BGASwipeBackLayout-Android

    但是这两个库各有各的缺点,SwipeBackLayout没有实现上一个activity跟随滑动的效果,这也是网上大多数介绍滑动返回文章的问题,只是粗略实现了当前界面滑动后 finish的效果。BGASwipeBackLayout-Android实现了跟随滑动的效果,但是其一是跟随滑动不流畅,并且在将activity的主题设置为透明之后,将activity主题设置为透明后,会有bug(前一个activity在滑动的过程中,底部会有黑背景);第二个问题是不够轻量,仔细去看源代码,有很多不需要的代码逻辑,包括measure和layout等不需要的设计。

    于是我决定在前人的基础上重写一个滑动返回的控件

    思路

    • Android的activity滑动的实现的设计思路大部分都是借助ViewDragHelper这个类实现的,因为这个类可以帮我们处理很多的手势检测和尺寸计算。
    • 上个activity跟随滑动的实现,当手指开始从左侧边缘滑动的时候,通过将上个activity的contentView暂时添加到当前activity的contentView下方,通过属性动画让它跟随当前activity的滑动而滑动

    整个过程入下图展示

    初始状态 拖拽过程转台 结束拖拽

    上图中viewDragHelper其实是一个包含viewDragHelperFramLayout,我取名为SlidebackLayout

    这里之所以不是直接添加preContentView而是用一个preWrapper来包装,是因为不能在SlidebackLayout初始化的时候去获取preContentView,因为这个时候activity整window没有绘制,获取的preContentView会是一片空白,所以是在activity初始化好展示给用户,用户要开始拖拽的时候才去添加preContentView。因此我们在SlidebackLayout初始化的时候用一个preWrapper来占位,它存在于当前contentView的下方.

    代码

    SlideBackLayout

    这是整个拖拽的核心,他包含一个viewDragHelperviewDragHelper的使用创建方法不多做解释,我们主要看一下它的ViewDragHelper.Callback,因为主要的处理逻辑都在这里

        public SlideBackLayout(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            this.mShawDrawable = ContextCompat.getDrawable(getContext(), R.drawable.bga_sbl_shadow);
            mViewDragHelper = ViewDragHelper.create(this, 1.0f, mDragCallback);
             //支持左侧边缘滑动
            mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
        }
    
    
    private ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return false;
            }
    
            @Override
            public void onEdgeDragStarted(int edgeFlags, int pointerId) {
                super.onEdgeDragStarted(edgeFlags, pointerId);
                mViewDragHelper.captureChildView(mCurrentContentView, pointerId);
                if (!mPreContentViewWrapper.isBindPreActivity())
                    mPreContentViewWrapper.bindPreActivity(mCurrentActivity);
                if (mSlideListener != null)
                    mSlideListener.onSlideStart();
            }
    
            @Override
            public void onViewDragStateChanged(int state) {
                if (state == ViewDragHelper.STATE_IDLE) {
                    //返回上个界面
                    if (mCurrentContentView.getLeft() >= mWidth) {
                        if (mSlideListener != null) {
                            mSlideListener.onSlideComplete();
                        }
                        mCurrentActivity.finish();
                        mCurrentActivity.overridePendingTransition(0,0);
                        mCurrentActivity.getWindow().getDecorView().setVisibility(GONE);
                        removeView(mPreContentViewWrapper);
                        mPreContentViewWrapper.unBindPreActivity();
                    } else {
                        //返回当前界面
                        if (mSlideListener != null)
                            mSlideListener.onSlideCancel();
                    }
                }
            }
    
            @Override
            public void onViewCaptured(View capturedChild, int activePointerId) {
                super.onViewCaptured(capturedChild, activePointerId);
                mDragLeftX = capturedChild.getLeft();
                mDragTopY = capturedChild.getTop();
            }
    
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return left < 0 ? 0 : left;
            }
    
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel);
                if (releasedChild.getLeft() > mWidth * BACK_THRESHOLD_RATIO) {
                    mViewDragHelper.settleCapturedViewAt(mWidth, mDragTopY);
                } else {
                    mViewDragHelper.settleCapturedViewAt(mDragLeftX, mDragTopY);
                }
                invalidate();
            }
    
            @Override
            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
                super.onViewPositionChanged(changedView, left, top, dx, dy);
                if (mPreContentViewWrapper != null && mPreContentViewWrapper.isBindPreActivity()) {
                    float ratio = left * 1.0f / mWidth;
                    mPreContentViewWrapper.onSlideChange(ratio);
                    mShawDrawable.setBounds(left - SHADOW_WIDTH, 0, left, mHeight);
                    if (mSlideListener != null)
                        mSlideListener.onSliding(ratio);
                    invalidate();
                }
            }
        };
    

    注意到上述代码中有个mPreContentViewWrapper,这个就是上面我们提到的包装preContentView的容器。我将它抽取成一个自定义view,看下它的代码

     /*============================上一个界面的容器=============================*/
        public static class PreContentViewWrapper extends FrameLayout {
    
            private static final float TRANSLATE_X_RATIO = 0.3f;//当前页面在滑动的时候,前一个界面初始被隐藏的宽度为0.3*width
    
            private WeakReference<Activity> mPreActivityRef;
            private ViewGroup mPreDecorView;
            private ViewGroup mPreContentView;
    
            private ViewGroup.LayoutParams mPreLayoutParams;
    
            private boolean isBindPreActivity;
            private int mHideWidth;
    
            public PreContentViewWrapper(Context context) {
                this(context, null);
            }
    
            public PreContentViewWrapper(Context context, @Nullable AttributeSet attrs) {
                super(context, attrs);
            }
    
            @Override
            protected void onSizeChanged(int w, int h, int oldw, int oldh) {
                super.onSizeChanged(w, h, oldw, oldh);
                mHideWidth = (int) (TRANSLATE_X_RATIO * w);
            }
    
            /**
             * 绑定上一个activity的ContenView
             * @param currentActivity
             */
            public void bindPreActivity(Activity currentActivity) {
                Activity preActivity = ActivityStackUtil.getInstance().getPreActivity(currentActivity);
                if (!preActivity.isDestroyed() && !preActivity.isFinishing()) {
                    //创建一个软连接指向上个activity
                    mPreActivityRef = new WeakReference<Activity>(preActivity);
    
                    mPreDecorView = (ViewGroup) preActivity.getWindow().getDecorView();
                    mPreContentView = (ViewGroup) mPreDecorView.getChildAt(0);
                    mPreLayoutParams = mPreContentView.getLayoutParams();
                    mPreDecorView.removeView(mPreContentView);
                    addView(mPreContentView, 0, mPreLayoutParams);
                    this.isBindPreActivity = true;
                }
            }
    
    
            /**
             * 解除绑定,将preContentView归还给上个activity
             */
            public void unBindPreActivity() {
                if (!isBindPreActivity) return;
                if (mPreActivityRef == null || mPreActivityRef.get() == null) return;
                if (mPreContentView != null && mPreDecorView != null) {
                    this.removeView(mPreContentView);
                    mPreDecorView.addView(mPreContentView, 0, mPreLayoutParams);
                    mPreContentView = null;
                    mPreActivityRef.clear();
                    mPreActivityRef = null;
                }
                this.isBindPreActivity = false;
            }
    
            @Override
            protected void dispatchDraw(Canvas canvas) {
                super.dispatchDraw(canvas);
                if (mPreDecorView != null && mPreContentView == null) {
                    mPreDecorView.draw(canvas);
                }
            }
    
            /**
             * 前一个界面跟随当前页面滑动而滑动
             *
             * @param ratio
             */
            public void onSlideChange(float ratio) {
                this.setTranslationX(mHideWidth * (ratio - 1));
            }
    
    
            public boolean isBindPreActivity() {
                return isBindPreActivity;
            }
        }
    

    ActivityStackUtil类用于获取当前activity的前一个activity,需要在application中调用ActivityStackUtil getInstance().init(this);

    package lu.basetool.util;
    
    import android.app.Activity;
    import android.app.Application;
    import android.os.Bundle;
    
    import java.util.Stack;
    
    /**
     * @Author: luqihua
     * @Time: 2018/5/29
     * @Description: ActivityUtil
     */
    
    public class ActivityStackUtil implements Application.ActivityLifecycleCallbacks {
    
        private Stack<Activity> mActivityStack = new Stack<>();
    
        private static class Holder {
            private static ActivityStackUtil sInstance = new ActivityStackUtil();
        }
    
        public static ActivityStackUtil getInstance() {
            return Holder.sInstance;
        }
    
    
        public void init(Application application) {
            application.registerActivityLifecycleCallbacks(this);
        }
    
        @Override
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            mActivityStack.push(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 outState) {
    
        }
    
        @Override
        public void onActivityDestroyed(Activity activity) {
            mActivityStack.remove(activity);
        }
    
    
        /**
         * 获取相对于当前activity前一个activity
         *
         * @param mCurrentActivity
         * @return
         */
        public Activity getPreActivity(Activity mCurrentActivity) {
            Activity preActivity = null;
            if (mActivityStack.size() > 1) {
                int index = mActivityStack.lastIndexOf(mCurrentActivity);
                if (index > 0) {
                    preActivity = mActivityStack.get(index - 1);
                } else {
                    preActivity = mActivityStack.lastElement();
                }
            }
            return preActivity;
        }
    }
    
    

    callback的onViewDragStateChanged方法中处理滑动结束的操作,除了mCurrentActivity.finish()结束当前activity之外,还处理滑动退出后闪屏的几个点

    //1.由于我们使用了滑动退出,因此不需要activity之间默认的切换动画
    
      mCurrentActivity.overridePendingTransition(0,0);
    //2. 由于尽管取消了activity切换动画,但是activity的消失可能任然会有闪一下的可能,于是我们干脆把当前的视图隐藏
    mCurrentActivity.getWindow().getDecorView().setVisibility(GONE);
    removeView(mPreContentViewWrapper);
    
    

    3.将当前mCurrentActivity的主题设置为透明

    在style.xml中新建个主题,并在AndroidManifest.xml中设置给需要滑动返回的activity

      <style name="AppTheme.transparent" parent="AppTheme">
            <!-- Customize your theme here. -->
            <item name="android:windowIsTranslucent">true</item>
            <item name="android:windowBackground">@android:color/transparent</item>
      </style>
    

    使用 代码git地址

    编写好的代码有2个类,ActivityStackUtilSlideBackLayout

    //1.在application中初始化ActivityStackUtil
    
      ActivityStackUtil.getInstance().init(this);
      
    //2.给需要滑动返回的activity的(style.xml)theme添加如下两行代码
       <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
    
    //3.在需要滑动返回的activity的onCreate()方法中调用
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
        //在 super.onCreate(savedInstanceState);之前调用此方法
      //第二个参数是一个滑动的监听,一般情况下设置为null即可
            new SlideBackLayout(this).attach2Activity(this, null);
            super.onCreate(savedInstanceState);
        }
    
    

    可能出现的错误:

    ViewDragHelper在处理动态变化的子view的时候,可能会出现已经拖拽的子view自动回到原位,所谓动态变化的子view例如:轮播图动画等,所以尽量确保在启动可以滑动返回的activity之后,上一个activity的一些定时改变视图(例如轮播图定时翻页)的效果暂停掉。

    [异常]Only fullscreen opaque activities can request orientation;出现该异常的话,将activity中的android:screenOrientation=""属性去掉。因为当activity的theme中设置了透明之后,不允许再设置该属性。

    相关文章

      网友评论

      • mzqcreate::joy: 我这整个activity,没有内容的下半部分完全透明:joy:

      本文标题:Android activity滑动返回

      本文链接:https://www.haomeiwen.com/subject/vchksftx.html