考拉Android全局滑动返回及联动效果的实现

前言

首次通过右滑来返回到上一个页面的操作是在 IOS7上出现。到目前android应用上支持这种操作的依然不多。分析其主要原因应该是android已有实体的返回按键,这样的功能变得不重要,但我觉得有这样的功能便于单手操作,能提升app的用户体验,特别是从ios转到android的用户。写这篇博文希望可以对大家有所帮助,希望自己的app上有滑动返回功能的可以参考下。

原理的简单描述

Android系统里有很多滑动相关的API和类,比如ViewDragHelper就是一个很好的滑动助手类。首先设置Window的背景为透明,再通过ViewDragHelper对Activity上DecorView的子view进行滑动,当滑动到一定距离,手指离开后就自动滑到最右侧,然后finish当前的activity,这样即可实现滑动返回效果。为了能够 “全局的”、“联动的” 实现滑动返回效果,在每个activity的DecorView下插入了SwipeBackLayout,当前activity滑动和下层activity的联动都在该类中完成。

效果图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

布局图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实现主要类:

SwipeBackActivity //滑动返回基类
SwipeBackLayout //滑动返回布局类
SwipeBackLayoutDragHelper //修改ViewDragHelper后助手类
TranslucentHelper //代码中修改透明或者不透明的助手类

##代码层面的讲解

一. 设置activity为透明、activity跳转动画(TranslucentHelper 讲解)

这个看起来很简单,但如果要兼容到API16及以下,会遇到过一个比较麻烦的页面切换动画问题:

1.1、通过activity的主题style进行设置

@color/transparent

**遇到问题:**如果在某个activity的主题style中设置了android:windowIsTranslucent属性为true,那么该activity切换动画与没设置之前是不同的,有些手机切换动画会变得非常跳。所以需要自定义activity的切换动画。
接下来我们会想到通过主题style里的windowAnimationStyle来设置切换动画

@anim/activity_open_enter @anim/activity_open_exit @anim/activity_close_enter @anim/activity_close_exit``` **实践证明:**当android:windowIsTranslucent为true时,以上几个属性是无效的,而下面两个属性还是可以用。但是这两个属性一个是窗口进来动画,一个是窗口退出动画,明显是不够。

@anim/***

结合overridePendingTransition(int enterAnim, int exitAnim)可以复写窗口进来动画和窗口退出动画,这种我觉得最终可能是可以实现的,不过控制起来比较复杂:
比如有A、B、C三个页面:
A跳到B,进场页面B动画从右进来,出场页面A动画从左出去,可以直接在style中写死

@anim/*** @anim/***```

如果B返回到A,进场页面A动画从左进来,出场页面B动画从右出去,此时需要通过复写onBackPressed() 方法,
在其中添加overridePendingTransition(int enterAnim, int exitAnim)方法来改变动画。

如果B是finish()后到A页面,在finish()后面加上overridePendingTransition ……
由于onBackPressed() 方法最终会调finish(),所以实际上只需要复写finish(),在其中添加overridePendingTransition……

但是假如B finish()后跳到C,则又不应该执行overridePendingTransition……,那么就需要判断finish执行后是否要加 overridePendingTransition……
对于一个较为庞大的项目,采取这种方法需要对每个页面进行排查,因此是不可行的,而对于刚刚起步的应用来说则是一个选择。

1.2、通过透明助手类(TranslucentHelper)进行设置

透明助手类(TranslucentHelper)里主要又有两个方法,一个是让activity变不透明,一个是让activity变透明,这两个都是通过反射来调用隐藏的系统api来实现的。因为较低的版本不支持代码中修改背景透明不透明,所以在类中有个静态变量mTranslucentState 来记录是否可以切换背景,这样低版本就不需要每次都反射通过捕获到的异常来做兼容方案。 另外:发现有些手机支持背景变黑,但不支持背景变透明(中兴z9 mini 5.0.2系统)

public class TranslucentHelper {
private static final String TRANSLUCENT_STATE = “translucentState”;
private static final int INIT = 0;//表示初始
private static final int CHANGE_STATE_FAIL = INIT + 1;//表示确认不可以切换透明状态
private static final int CHANGE_STATE_SUCCEED = CHANGE_STATE_FAIL + 1;//表示确认可以切换透明状态
private static int mTranslucentState = INIT;

interface TranslucentListener {
void onTranslucent();
}

private static class MyInvocationHandler implements InvocationHandler {
private TranslucentListener listener;

MyInvocationHandler(TranslucentListener listener) {
this.listener = listener;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
boolean success = (boolean) args[0];
if (success && listener != null) {
listener.onTranslucent();
}
} catch (Exception ignored) {
}
return null;
}
}

static boolean convertActivityFromTranslucent(Activity activity) {
if (mTranslucentState == INIT) {
mTranslucentState = PreferencesUtils.getInt(TRANSLUCENT_STATE, INIT);
}
if (mTranslucentState == INIT) {
convertActivityToTranslucent(activity, null);
} else if (mTranslucentState == CHANGE_STATE_FAIL) {
return false;
}

try {
Method method = Activity.class.getDeclaredMethod(“convertFromTranslucent”);
method.setAccessible(true);
method.invoke(activity);
mTranslucentState = CHANGE_STATE_SUCCEED;
return true;
} catch (Throwable t) {
mTranslucentState = CHANGE_STATE_FAIL;
PreferencesUtils.saveInt(TRANSLUCENT_STATE, CHANGE_STATE_FAIL);
return false;
}
}

static void convertActivityToTranslucent(Activity activity, final TranslucentListener listener) {
if (mTranslucentState == CHANGE_STATE_FAIL) {
if (listener != null) {
listener.onTranslucent();
}
return;
}

try {
Class<?>[] classes = Activity.class.getDeclaredClasses(); Class<?> translucentConversionListenerClazz = null;
for (Class clazz : classes) {
if (clazz.getSimpleName().contains(“TranslucentConversionListener”)) {
translucentConversionListenerClazz = clazz;
}
}

MyInvocationHandler myInvocationHandler = new MyInvocationHandler(listener);
Object obj = Proxy.newProxyInstance(Activity.class.getClassLoader(),
new Class[] { translucentConversionListenerClazz }, myInvocationHandler);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Method getActivityOptions = Activity.class.getDeclaredMethod(“getActivityOptions”);
getActivityOptions.setAccessible(true);
Object options = getActivityOptions.invoke(activity);

Method method = Activity.class.getDeclaredMethod(“convertToTranslucent”,
translucentConversionListenerClazz, ActivityOptions.class);
method.setAccessible(true);
method.invoke(activity, obj, options);
} else {
Method method =
Activity.class.getDeclaredMethod(“convertToTranslucent”, translucentConversionListenerClazz);
method.setAccessible(true);
method.invoke(activity, obj);
}
mTranslucentState = CHANGE_STATE_SUCCEED;
} catch (Throwable t) {
mTranslucentState = CHANGE_STATE_FAIL;
PreferencesUtils.saveInt(TRANSLUCENT_STATE, CHANGE_STATE_FAIL);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (listener != null) {
listener.onTranslucent();
}
}
}, 100);
}
}
}

让activity变不透明的方法比较简单;让activity变透明的方法参数里传入了一个listener接口 ,主要是当antivity变透明后会回调,因为这个接口也在activity里,而且是私有的,所以我们只能通过动态代理去获取这个回调。最后如果版本大于等于5.0,还需要再传入一个ActivityOptions参数。

在实际开发中,这两个方法在android 5.0以上是有效的,在5.0以下需要当android:windowIsTranslucent为true时才有效,这样又回到了之前的问题activity切换动画异常。

**最终决解方法:**setContentView之前就调用 convertActivityFromTranslucent方法,让activity背景变黑,这样activity切换效果就正常。

**总结:**在style中设置android:windowIsTranslucent为true ,setContentView之前就调用 convertActivityFromTranslucent方法,当触发右滑时调用convertActivityToTranslucent,通过动态代理获取activity变透明后的回调,在回调后允许开始滑动。

二. 让BaseActivity继承SwipeBackActivity(SwipeBackActivity讲解)

先直接看代码,比较少

public abstract class SwipeBackActivity extends CoreBaseActivity {
/**

  • 滑动返回View
    */
    private SwipeBackLayout mSwipeBackLayout;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!isSwipeBackDisableForever()) {
TranslucentHelper.convertActivityFromTranslucent(this);
mSwipeBackLayout = new SwipeBackLayout(this);
}
}

@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
if (!isSwipeBackDisableForever()) {
mSwipeBackLayout.attachToActivity(this);
mSwipeBackLayout.setOnSwipeBackListener(new SwipeBackLayout.onSwipeBackListener() {
@Override
public void onStart() {
onSwipeBackStart();
}

@Override
public void onEnd() {
onSwipeBackEnd();
}
});
}
}

@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (!isSwipeBackDisableForever() && hasFocus) {
getSwipeBackLayout().recovery();
}
}

/**

  • 滑动返回开始时的回调
    */
    protected void onSwipeBackStart() {}

/**

  • 滑动返回结束时的回调
    */
    protected void onSwipeBackEnd() {}

/**

  • 设置是否可以边缘滑动返回,需要在onCreate方法调用
    */
    public void setSwipeBackEnable(boolean enable) {
    if (mSwipeBackLayout != null) {
    mSwipeBackLayout.setSwipeBackEnable(enable);
    }
    }

public boolean isSwipeBackDisableForever() {
return false;
}

public SwipeBackLayout getSwipeBackLayout() {
return mSwipeBackLayout;
}
}

SwipeBackActivity中包含了一个SwipeBackLayout对象

在onCreate方法中:
1.activity转化为不透明。
2.new了一个SwipeBackLayout。

在onPostCreate方法中:
1.attachToActivity主要是插入SwipeBackLayout、窗口背景设置……
2.设置了滑动返回开始和结束的监听接口,建议在滑动返回开始时,把PopupWindow给dismiss掉。

onWindowFocusChanged 方法中
如果是hasFocus == true,就recovery()这个SwipeBackLayout,这个也是因为下层activity有联动效果而移动了SwipeBackLayout,所以需要recovery()下,防止异常情况。

isSwipeBackDisableForever 方法是一个大开关,默认返回false,在代码中复写后返回 true,则相当于直接继承了SwipeBackActivity的父类。

setSwipeBackEnable 方法是一个小开关,设置了false之后就暂时不能滑动返回了,可以在特定的时机设置为true,就恢复滑动返回的功能。

**总结说明:**下层activity设置了setSwipeBackEnable 为 false,上层activity滑动时还是可以联动的,比如MainActivity。而isSwipeBackDisableForever 返回true就不会联动了,而且一些仿PopupWindow的activity需要复写这个方法,因为activity需要透明。

三、滑动助手类的使用和滑动返回布局类的实现(SwipeBackLayout讲解)

直接贴SwipeBackLayout源码:

/**

  • 滑动返回容器类
    /
    public class SwipeBackLayout extends FrameLayout {
    /
    *
  • 滑动销毁距离比例界限,滑动部分的比例超过这个就销毁
    /
    private static final float DEFAULT_SCROLL_THRESHOLD = 0.5f;
    /
    *
  • 滑动销毁速度界限,超过这个速度就销毁
    /
    private static final float DEFAULT_VELOCITY_THRESHOLD = ScreenUtils.dpToPx(250);
    /
    *
  • 最小滑动速度
    /
    private static final int MIN_FLING_VELOCITY = ScreenUtils.dpToPx(200);
    /
    *
  • 左边移动的像素值
    /
    private int mContentLeft;
    /
    *
  • 左边移动的像素值 / (ContentView的宽+阴影)
    /
    private float mScrollPercent;
    /
    *
  • (ContentView可见部分+阴影)的比例 (即1 - mScrollPercent)
    /
    private float mContentPercent;
    /
    *
  • 阴影图
    /
    private Drawable mShadowDrawable;
    /
    *
  • 阴影图的宽
    /
    private int mShadowWidth;
    /
    *
  • 内容view,DecorView的原第一个子view
    /
    private View mContentView;
    /
    *
  • 用于记录ContentView所在的矩形
    /
    private Rect mContentViewRect = new Rect();
    /
    *
  • 设置是否可滑动
    /
    private boolean mIsSwipeBackEnable = true;
    /
    *
  • 是否正在放置
    /
    private boolean mIsLayout = true;
    /
    *
  • 判断背景Activity是否启动进入动画
    /
    private boolean mIsEnterAnimRunning = false;
    /
    *
  • 是否是透明的
    /
    private boolean mIsActivityTranslucent = false;
    /
    *
  • 进入动画(只在释放手指时使用)
    /
    private ObjectAnimator mEnterAnim;
    /
    *
  • 退拽助手类
    /
    private SwipeBackLayoutDragHelper mViewDragHelper;
    /
    *
  • 执行滑动时的最顶层Activity
    /
    private Activity mTopActivity;
    /
    *
  • 后面的Activity的弱引用
    /
    private WeakReference mBackActivityWeakRf;
    /
    *
  • 监听滑动开始和结束
    */
    private onSwipeBackListener mListener;

public SwipeBackLayout(Context context) {
super(context);
init(context);
}

public SwipeBackLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}

public SwipeBackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

private void init(Context context) {
mViewDragHelper = SwipeBackLayoutDragHelper.create(SwipeBackLayout.this, new ViewDragCallback());
mViewDragHelper.setEdgeTrackingEnabled(SwipeBackLayoutDragHelper.EDGE_LEFT);
mViewDragHelper.setMinVelocity(MIN_FLING_VELOCITY);
mViewDragHelper.setMaxVelocity(MIN_FLING_VELOCITY * 2);
try {
mShadowDrawable = context.getResources().getDrawable(R.drawable.swipeback_shadow_left);
} catch (Exception ignored) {
}
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
try {
if (!mIsSwipeBackEnable) {
super.onLayout(changed, left, top, right, bottom);
return;
}
mIsLayout = true;
if (mContentView != null) {
mContentView.layout(mContentLeft, top, mContentLeft + mContentView.getMeasuredWidth(),
mContentView.getMeasuredHeight());
}
mIsLayout = false;
} catch (Exception e) {
super.onLayout(changed, left, top, right, bottom);
}
}

@Override
public void requestLayout() {
if (!mIsLayout || !mIsSwipeBackEnable) {
super.requestLayout();
}
}

@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
try {
//绘制阴影
if (mContentPercent > 0
&& mShadowDrawable != null
&& child == mContentView
&& mViewDragHelper.getViewDragState() != SwipeBackLayoutDragHelper.STATE_IDLE) {
child.getHitRect(mContentViewRect);
mShadowWidth = mShadowDrawable.getIntrinsicWidth();
mShadowDrawable.setBounds(mContentViewRect.left - mShadowWidth, mContentViewRect.top,
mContentViewRect.left, mContentViewRect.bottom);
mShadowDrawable.draw(canvas);
}
return super.drawChild(canvas, child, drawingTime);
} catch (Exception e) {
return super.drawChild(canvas, child, drawingTime);
}
}

@Override
public void computeScroll() {
mContentPercent = 1 - mScrollPercent;
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!mIsSwipeBackEnable) {
return false;
}
try {
return mViewDragHelper.shouldInterceptTouchEvent(event);
} catch (ArrayIndexOutOfBoundsException e) {
return super.onInterceptTouchEvent(event);
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mIsSwipeBackEnable) {
return false;
}
try {
mViewDragHelper.processTouchEvent(event);
return true;
} catch (Exception e) {
return super.onTouchEvent(event);
}
}

/**

  • 将View添加到Activity
    */
    public void attachToActivity(Activity activity) {
    //插入SwipeBackLayout
    ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
    ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
    decor.removeView(decorChild);
    if (getParent() != null) {
    decor.removeView(this);
    }
    decor.addView(this);
    this.removeAllViews();
    this.addView(decorChild);

//mContentView为SwipeBackLayout的直接子view,获取window背景色进行赋值
activity.getWindow().setBackgroundDrawableResource(R.color.transparent_white);
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[] {
android.R.attr.windowBackground
});
mContentView = decorChild;
mContentView.setBackgroundResource(a.getResourceId(0, 0));
a.recycle();

//拿到顶层activity和下层activity,做联动操作

更多学习和讨论,欢迎加入我们!

有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

这里有2000+小伙伴,让你的学习不寂寞~·
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
obtainStyledAttributes(new int[] {
android.R.attr.windowBackground
});
mContentView = decorChild;
mContentView.setBackgroundResource(a.getResourceId(0, 0));
a.recycle();

//拿到顶层activity和下层activity,做联动操作

[外链图片转存中…(img-syJoMNji-1715358677153)]

更多学习和讨论,欢迎加入我们!

有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

这里有2000+小伙伴,让你的学习不寂寞~·
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值