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

整个过程入下图展示

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

上图中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中设置了透明之后,不允许再设置该属性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容