Android activity滑动返回

前言

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的滑动而滑动

整个过程入下图展示

《Android activity滑动返回》 初始状态
《Android activity滑动返回》 拖拽过程转台
《Android 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中设置了透明之后,不允许再设置该属性。

    原文作者:好多个胖子
    原文地址: https://www.jianshu.com/p/7b1c04b2e3cb
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞