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

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,做联动操作
mTopActivity = activity;
Activity backActivity = ActivityUtils.getSecondTopActivity();
if (backActivity != null && backActivity instanceof SwipeBackActivity) {
if (!((SwipeBackActivity) backActivity).isSwipeBackDisableForever()) {
mBackActivityWeakRf = new WeakReference<>(backActivity);
}
}
}

/**

  • 设置是否可以滑动返回
    */
    public void setSwipeBackEnable(boolean enable) {
    mIsSwipeBackEnable = enable;
    }

public boolean isActivityTranslucent() {
return mIsActivityTranslucent;
}

/**

  • 启动进入动画
    */
    private void startEnterAnim() {
    if (mContentView != null) {
    ObjectAnimator anim =
    ObjectAnimator.ofFloat(mContentView, “TranslationX”, mContentView.getTranslationX(), 0f);
    anim.setDuration((long) (125 * mContentPercent));
    mEnterAnim = anim;
    mEnterAnim.start();
    }
    }

protected View getContentView() {
return mContentView;
}

private class ViewDragCallback extends SwipeBackLayoutDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
if (mIsSwipeBackEnable && mViewDragHelper.isEdgeTouched(SwipeBackLayoutDragHelper.EDGE_LEFT, pointerId)) {
TranslucentHelper.convertActivityToTranslucent(mTopActivity,
new TranslucentHelper.TranslucentListener() {
@Override
public void onTranslucent() {
if (mListener != null) {
mListener.onStart();
}
mIsActivityTranslucent = true;
}
});
return true;
}
return false;
}

@Override
public int getViewHorizontalDragRange(View child) {
return mIsSwipeBackEnable ? SwipeBackLayoutDragHelper.EDGE_LEFT : 0;
}

@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
if (changedView == mContentView) {
mScrollPercent = Math.abs((float) left / mContentView.getWidth());
mContentLeft = left;
//未执行动画就平移
if (!mIsEnterAnimRunning) {
moveBackActivity();
}
invalidate();
if (mScrollPercent >= 1 && !mTopActivity.isFinishing()) {
if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout().invalidate();
}
mTopActivity.finish();
mTopActivity.overridePendingTransition(0, 0);
}
}
}

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (xvel > DEFAULT_VELOCITY_THRESHOLD || mScrollPercent > DEFAULT_SCROLL_THRESHOLD) {
if (mIsActivityTranslucent) {
mViewDragHelper.settleCapturedViewAt(releasedChild.getWidth() + mShadowWidth, 0);
if (mContentPercent < 0.85f) {
startAnimOfBackActivity();
}
}
} else {
mViewDragHelper.settleCapturedViewAt(0, 0);
}
if (mListener != null) {
mListener.onEnd();
}
invalidate();
}

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return Math.min(child.getWidth(), Math.max(left, 0));
}

@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);

if (state == SwipeBackLayoutDragHelper.STATE_IDLE && mScrollPercent < 1f) {
TranslucentHelper.convertActivityFromTranslucent(mTopActivity);
mIsActivityTranslucent = false;
}
}

@Override
public boolean isTranslucent() {
return SwipeBackLayout.this.isActivityTranslucent();
}
}

/**

  • 背景Activity开始进入动画
    */
    private void startAnimOfBackActivity() {
    if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
    mIsEnterAnimRunning = true;
    SwipeBackLayout swipeBackLayout = ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout();
    swipeBackLayout.startEnterAnim();
    }
    }

/**

  • 移动背景Activity
    */
    private void moveBackActivity() {
    if (mBackActivityWeakRf != null && ActivityUtils.activityIsAlive(mBackActivityWeakRf.get())) {
    View view = ((SwipeBackActivity) mBackActivityWeakRf.get()).getSwipeBackLayout().getContentView();
    if (view != null) {
    int width = view.getWidth();
    view.setTranslationX(-width * 0.3f * Math.max(0f, mContentPercent - 0.15f));
    }
    }
    }

/**

  • 回复界面的平移到初始位置
    */
    public void recovery() {
    if (mEnterAnim != null && mEnterAnim.isRunning()) {
    mEnterAnim.end();
    } else {
    mContentView.setTranslationX(0);
    }
    }

interface onSwipeBackListener {
void onStart();

void onEnd();
}

public void setOnSwipeBackListener(onSwipeBackListener listener) {
mListener = listener;
}
}

  • attachToActivity
    上面讲到SwipeBackLayout是在activity的onCreate时被创建,在onPostCreate是插入到DecorView里,主要是因为DecorView是在setContentView时与Window关联起来插入SwipeBackLayout方法如代码所示,不难,然后是设置了window的背景色为透明色,mContentView为SwipeBackLayout的直接子view,获取window背景色进行赋值。由于考拉项目已经有很多activity,而这些activity中android:windowBackground设置的颜色大部分是白色,少部分是灰色和透明的,所以需要在代码中设置统一设置一遍透明的,原来的背景色则赋值给SwipeBackLayout的子View就可以达到最少修改代码的目的。最后拿到顶层activity和下层activity,做联动操作

  • SwipeBackLayoutDragHelper 和 ViewDragCallback
    SwipeBackLayout中包含了一个滑动助手类(SwipeBackLayoutDragHelper)的对象,该类是在ViewDragHelper的基础上进行修改得来的。
    修改点:
    1.右侧触发区域 EDGE_SIZE由 20dp 改到 25dp
    2.提供滑动最大速度 的设置方法
    3.在ViewDragHelper 的内部类Callback方法中提供是否activity为透明的回调接口
    4.在最终调用滑动的方法dragTo中添加判断逻辑,activity为透明时才支持滑动
    SwipeBackLayoutDragHelper 在init 方法中初始化,通过onInterceptTouchEvent和onTouchEvent拿到滑动事件,通过ViewDragCallback的一些方法返回相应的滑动回调, ViewDragCallback实现了SwipeBackLayoutDragHelper.Callback里的以下几个接口,其中其中isTranslucent()是自己添加进去的。

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

  • tryCaptureView方法当触摸到SwipeBackLayout里的子View时触发的,当返回true,表示捕捉成功,否则失败。判断条件是如果支持滑动返回并且是左侧边距被触摸时才可以,我们知道这个时候的的背景色是不透明的,如果直接开始滑动则是黑色的,所以需要在这里背景色改成透明的,如果直接调用 TranslucentHelper.convertActivityToTranslucent(mTopActivity, null)后直接返回true,会出现一个异常情况,就是滑动过快时会导致背景还来不及变成黑色就滑动出来了,之后才变成透明的,从而导致了会从黑色到透明的一个闪烁现象,解决的办法是在代码中用了一个回调和标记,当变成透明后设置了mIsActivityTranslucent = true;通过mIsActivityTranslucent 这个变量来判断是否进行移动的操作。由于修改activity变透明的方法是通过反射的,不能简单的设置一个接口后进行回调,而是通过动态代理的方式来实现的(InvocationHandler),在convertToTranslucent方法的第一个参数刚好是一个判断activity是否已经变成透明的回调,看下面代码中 if 语句里的注释和回调,如果窗口已经变成透明的话,就传了一个drawComplete (true)。通过动态代理,将translucentConversionListenerClazz 执行其方法onTranslucentConversionComplete的替换成myInvocationHandler中执行invoke方法。其中赋值给success的args[0]正是 drawComplete。

  • isTranslucent是自己添加了一个方法,主要是返回activity是否是透明的默认为true,在SwipeBackLayout重写后将mIsActivityTranslucent返回。仔细看SwipeBackLayoutDragHelper方法的话,会发现最后通过dragTo方法对view进行移动,因此在进行水平移动前判断下是否是透明的,只有透明了才能移动

  • onViewPositionChanged view移动过程中会持续调用,这里面的逻辑主要有这几个: 1.实时计算滑动了多少距离,用于绘制左侧阴影等
    2.使下面的activity进行移动moveBackActivity();
    3.当view完全移出屏幕后,销毁当前的activity

  • onViewReleased是手指释放后触发的一个方法。如果滑动速度大于最大速度或者滑动的距离大于设定的阈值距离,则直接移到屏幕外,同时触发下层activity的复位动画,否则移会到原来位置 。

  • onViewDragStateChanged当滑动的状态发生改变时的回调,主要是停止滑动后,将背景改成不透明,这样跳到别的页面是动画就是正常的。

  • clampViewPositionHorizontal 返回水平移动距离,防止滑出父 view。

  • getViewHorizontalDragRange对于clickable=true的子view,需要返回大于0的数字才能正常捕获。

其他方法都较为简单,注释也写了,就不多说了,最后毫不吝啬的贴上SwipeBackLayoutDragHelper的dragTo代码,就多了if (mCallback.isTranslucent())

最后

这里我特地整理了一份《Android开发核心知识点笔记》,里面就包含了自定义View相关的内容

除了这份笔记,还给大家分享 Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料这几块的内容。非常适合近期有面试和想在技术道路上继续精进的朋友。

分享上面这些资源,希望可以帮助到大家提升进阶,如果你觉得还算有用的话,不妨把它们推荐给你的朋友~

喜欢本文的话,给我点个小赞、评论区留言或者转发支持一下呗~
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
gHelper的dragTo代码,就多了if (mCallback.isTranslucent())

最后

这里我特地整理了一份《Android开发核心知识点笔记》,里面就包含了自定义View相关的内容

[外链图片转存中…(img-oVw1TU3e-1715358643276)]

除了这份笔记,还给大家分享 Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料这几块的内容。非常适合近期有面试和想在技术道路上继续精进的朋友。

[外链图片转存中…(img-AEDCL9Vo-1715358643277)]

分享上面这些资源,希望可以帮助到大家提升进阶,如果你觉得还算有用的话,不妨把它们推荐给你的朋友~

喜欢本文的话,给我点个小赞、评论区留言或者转发支持一下呗~
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值