Android 平台滑动返回库对比

作者: 881ef7b85f62 | 来源:发表于2019-01-23 17:16 被阅读15次

Github 上滑动返回库比较多,由于实现思路甚至代码一部分库都差不多,所以只挑选了两个实现思路比较不同的库作为研究,分别是 SwipeBackLayout 和 and_swipeback。

除滑动返回功能外,本文还会围绕滑动过程中呈现前一个界面的方案对上述两个库展开分析,其他部分源码细节不予分析。

SwipeBackLayout

由于使用 SwipeBackLayout 库提供的滑动返回能力需要继承于 SwipeBackActivity,所以我们直接从 SwipeBackActivity 开始研究。

public class SwipeBackActivity extends AppCompatActivity implements SwipeBackActivityBase {
    private SwipeBackActivityHelper mHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHelper = new SwipeBackActivityHelper(this);
        mHelper.onActivityCreate();
    }
    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        mHelper.onPostCreate();
    }
    
    @Override
    public SwipeBackLayout getSwipeBackLayout() {
        return mHelper.getSwipeBackLayout();
    }
    @Override
    public void setSwipeBackEnable(boolean enable) {
        getSwipeBackLayout().setEnableGesture(enable);
    }
    
    @Override
    public void scrollToFinishActivity() {
        Utils.convertActivityToTranslucent(this);
        getSwipeBackLayout().scrollToFinishActivity();
    }
}

可以看出 SwipeBackActivity 实际上只是 SwipeBackActivityHelper 的代理。

public class SwipeBackActivityHelper {
    private Activity mActivity;
    private SwipeBackLayout mSwipeBackLayout;
    public SwipeBackActivityHelper(Activity activity) {
        mActivity = activity;
    }
    @SuppressWarnings("deprecation")
    public void onActivityCreate() {
        mActivity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        mActivity.getWindow().getDecorView().setBackgroundDrawable(null);
        mSwipeBackLayout = (SwipeBackLayout) LayoutInflater.from(mActivity).inflate(
                me.imid.swipebacklayout.lib.R.layout.swipeback_layout, null);
        // 先记住这里
        mSwipeBackLayout.addSwipeListener(new SwipeBackLayout.SwipeListener() {
            @Override
            public void onScrollStateChange(int state, float scrollPercent) {
            }
            @Override
            public void onEdgeTouch(int edgeFlag) {
                Utils.convertActivityToTranslucent(mActivity);
            }
            @Override
            public void onScrollOverThreshold() {
            }
        });
    }
    public void onPostCreate() {
        mSwipeBackLayout.attachToActivity(mActivity);
    }
    
    ...
    
    public SwipeBackLayout getSwipeBackLayout() {
        return mSwipeBackLayout;
    }
}

从 SwipeBackActivityHelper 的代码不难看出它主要的工作是在 Activity 创建的时候将 Activity 的背景修改为透明以及创建 SwipeBackLayout 并将其与 Activity 关联。再来看一下 mSwipeBackLayout.attachToActivity(mActivity) 的逻辑:

public void attachToActivity(Activity activity) {
    mActivity = activity;
    TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{
            android.R.attr.windowBackground
    });
    int background = a.getResourceId(0, 0);
    a.recycle();
    ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
    ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
    decorChild.setBackgroundResource(background);
    decor.removeView(decorChild);
    addView(decorChild);
    setContentView(decorChild);
    decor.addView(this);
}

SwipeBackLayout 首先通过 Activity 实例获取当前主题下 windowBackground 的色值,并将其设置为 ContentView 的背景色,然后将 ContentView 添加至 SwipeBackLayout 中并将 SwipeBackLayout 设为当前 Activity 的 ContentView。至此便完成了与 Activity 关联的工作。

滑动处理

开始滑动时,SwipeBackLayout 将滑动事件全部转发至 ViewDragHelper 处理,等待 SwipeBackLayout.ViewDragCallback 中接收到相应回调后执行相关逻辑。

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    ...
    try {
        return mDragHelper.shouldInterceptTouchEvent(event);
    } catch (ArrayIndexOutOfBoundsException e) {
        return false;
    }
}
@Override
public boolean onTouchEvent(MotionEvent event) {
    ...
    mDragHelper.processTouchEvent(event);
    return true;
}

当 ViewDragHelper 将当前状态修改为 STATE_DRAGGING 后,ViewDragHelper 通过调用 ViewDragCallback 的 clampViewPositionHorizontal 或 clampViewPositionVertical 回调方法设定当前页面的 ContentView 应移动的距离。

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    int ret = 0;
    if ((mTrackingEdge & EDGE_LEFT) != 0) {
        ret = Math.min(child.getWidth(), Math.max(left, 0));
    } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
        ret = Math.min(0, Math.max(left, -child.getWidth()));
    }
    return ret;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
    int ret = 0;
    if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
        ret = Math.min(0, Math.max(top, -child.getHeight()));
    }
    return ret;
}

当滑动事件结束后(onViewReleased),SwipeBackLayout 通过判断此前在 onViewPositionChanged 中计算好的滑动率是否达到阈值以执行回弹或退出动画,这样就完成了一次完整的滑动过程。

@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    if ((mTrackingEdge & EDGE_LEFT) != 0) {
        mScrollPercent = Math.abs((float) left
                / (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth()));
    } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
        mScrollPercent = Math.abs((float) left
                / (mContentView.getWidth() + mShadowRight.getIntrinsicWidth()));
    } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
        mScrollPercent = Math.abs((float) top
                / (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight()));
    }
    ...
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    final int childWidth = releasedChild.getWidth();
    final int childHeight = releasedChild.getHeight();
    int left = 0, top = 0;
    if ((mTrackingEdge & EDGE_LEFT) != 0) {
        // 是否达到阈值
        left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
                + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0;
    } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
        left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth
                + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0;
    } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
        top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight
                + mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0;
    }
    // 回弹或滑到最后
    mDragHelper.settleCapturedViewAt(left, top);
    invalidate();
}

显示前一个页面

为了显示前一个 Activity 界面,SwipeBackActivityHelper 在 Activity onCreate 时就将当前 Window 设置为透明背景。

mActivity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
mActivity.getWindow().getDecorView().setBackgroundDrawable(null);

而单单设置为透明背景并不足以显示上一个 Activity。为了解决这个问题,SwipeBackActivityHelper 在接收到 SwipeListener 的 onEdgeTouch 回调后调用 Utils.convertActivityToTranslucent(mActivity) 来让前一个 Activity 显示出来。

@Override
public void onEdgeTouch(int edgeFlag) {
    Utils.convertActivityToTranslucent(mActivity);
}

其中 onEdgeTouch 是在 ViewDragCallback 的 tryCaptureView 回调中被调用的。

@Override
public boolean tryCaptureView(View view, int i) {
    boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, i);
    if (ret) {
        if (mDragHelper.isEdgeTouched(EDGE_LEFT, i)) {
            mTrackingEdge = EDGE_LEFT;
        } else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, i)) {
            mTrackingEdge = EDGE_RIGHT;
        } else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, i)) {
            mTrackingEdge = EDGE_BOTTOM;
        }
        if (mListeners != null && !mListeners.isEmpty()) {
            for (SwipeListener listener : mListeners) {
                // 可触发滑动的边缘区域接收到触摸事件
                listener.onEdgeTouch(mTrackingEdge);
            }
        }
        mIsScrollOverValid = true;
    }
    ...
}

那么为什么 Utils.convertActivityToTranslucent(mActivity) 可以让前一个 Activity 显示呢?我们来看一下相关的逻辑:

public static void convertActivityToTranslucent(Activity activity) {
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
       convertActivityToTranslucentAfterL(activity);
   } else {
       convertActivityToTranslucentBeforeL(activity);
   }
}
public static void convertActivityToTranslucentBeforeL(Activity activity) {
   try {
       Class<?>[] classes = Activity.class.getDeclaredClasses();
       Class<?> translucentConversionListenerClazz = null;
       for (Class clazz : classes) {
           if (clazz.getSimpleName().contains("TranslucentConversionListener")) {
               translucentConversionListenerClazz = clazz;
           }
       }
       Method method = Activity.class.getDeclaredMethod("convertToTranslucent",
               translucentConversionListenerClazz);
       method.setAccessible(true);
       method.invoke(activity, new Object[] {
           null
       });
   } catch (Throwable t) {
   }
}
private static void convertActivityToTranslucentAfterL(Activity activity) {
   try {
       Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions");
       getActivityOptions.setAccessible(true);
       Object options = getActivityOptions.invoke(activity);
       Class<?>[] classes = Activity.class.getDeclaredClasses();
       Class<?> translucentConversionListenerClazz = null;
       for (Class clazz : classes) {
           if (clazz.getSimpleName().contains("TranslucentConversionListener")) {
               translucentConversionListenerClazz = clazz;
           }
       }
       Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent",
               translucentConversionListenerClazz, ActivityOptions.class);
       convertToTranslucent.setAccessible(true);
       convertToTranslucent.invoke(activity, null, options);
   } catch (Throwable t) {
   }
}

其中反射调用的 convertToTranslucent 方法的 Android 官方注释中写到 “Calling this allows the Activity behind this one to be seen again” 。所以,由于 convertActivityToTranslucent 通过反射调用 Activity 的 convertToTranslucent 方法将 Activity 转为 Translucent(相当于 windowIsTranslucent 值为 true),配合之前的透明背景,那么前一个 Activity 也就能显示出来了。

以上即为 SwipeBackLayout 库为了显示上一界面所做的操作,然而这种方法有其弊端。由于被设为透明背景的 Activity 的前一个 Activity 无法进入 onStop(),从而导致 Activity 的 Window 不能释放部分资源。一旦多个透明背景的 Activity 叠加会出现明显的卡顿现象(毕竟这么多 View 需要绘制)。

and_swipeback

and_swipeback 与 SwipeBackLayout 一样提供了自定义的 Activity 让用户直接继承以集成滑动返回功能。

public class SwipeBackActivity extends AppCompatActivity implements SwipeBackHelper.SlideBackManager {
    private SwipeBackHelper mSwipeBackHelper;
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mSwipeBackHelper == null) {
            mSwipeBackHelper = new SwipeBackHelper(this);
        }
        return mSwipeBackHelper.processTouchEvent(ev) || super.dispatchTouchEvent(ev);
    }
    
    @Override
    public void finish() {
        if (mSwipeBackHelper != null) {
            mSwipeBackHelper.finishSwipeImmediately();
            mSwipeBackHelper = null;
        }
        super.finish();
    }
}

可以看到 SwipeBackActivity 先将触摸事件转发给 SwipeBackHelper 处理,如未被消费再交由自身处理。

滑动处理

在触摸事件处理上 and_swipeback 并没有使用 ViewDragHelper,而是让 SwipeBackHelper 直接消费触摸事件。

public class SwipeBackHelper extends Handler {
    public boolean processTouchEvent(MotionEvent ev) {
        if (!mIsSupportSlideBack) { //不支持滑动返回,则手势事件交给View处理
            return false;
        }
    
        if (mIsSlideAnimPlaying) {  //正在滑动动画播放中,直接消费手势事件
            return true;
        }
    
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        if (action == MotionEvent.ACTION_DOWN) {
            mLastPointX = ev.getRawX();
            mIsInThresholdArea = mLastPointX >= 0 && mLastPointX <= mEdgeSize;
        }
    
        if (!mIsInThresholdArea) {  //不满足滑动区域,不做处理
            return false;
        }
    
        final int actionIndex = ev.getActionIndex();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                sendEmptyMessage(MSG_ACTION_DOWN);
                break;
    
            case MotionEvent.ACTION_POINTER_DOWN:
                if (mIsSliding) {  //有第二个手势事件加入,而且正在滑动事件中,则直接消费事件
                    return true;
                }
                break;
    
            case MotionEvent.ACTION_MOVE:
                //一旦触发滑动机制,拦截所有其他手指的滑动事件
                if (actionIndex != 0) {
                    return mIsSliding;
                }
    
                final float curPointX = ev.getRawX();
    
                boolean isSliding = mIsSliding;
                if (!isSliding) {
                    if (Math.abs(curPointX - mLastPointX) < mTouchSlop) { //判断是否满足滑动
                        return false;
                    } else {
                        mIsSliding = true;
                    }
                }
    
                Bundle bundle = new Bundle();
                bundle.putFloat(CURRENT_POINT_X, curPointX);
                Message message = obtainMessage();
                message.what = MSG_ACTION_MOVE;
                message.setData(bundle);
                sendMessage(message);
    
                if (isSliding == mIsSliding) {
                    return true;
                } else {
                    MotionEvent cancelEvent = MotionEvent.obtain(ev); //首次判定为滑动需要修正事件:手动修改事件为 ACTION_CANCEL,并通知底层View
                    cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
                    mActivity.getWindow().superDispatchTouchEvent(cancelEvent);
                    return true;
                }
    
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_POINTER_UP:
            case MotionEvent.ACTION_OUTSIDE:
                if (mDistanceX == 0) { //没有进行滑动
                    mIsSliding = false;
                    sendEmptyMessage(MSG_ACTION_UP);
                    return false;
                }
    
                if (mIsSliding && actionIndex == 0) { // 取消滑动 或 手势抬起 ,而且手势事件是第一手势,开始滑动动画
                    mIsSliding = false;
                    sendEmptyMessage(MSG_ACTION_UP);
                    return true;
                } else if (mIsSliding && actionIndex != 0) {
                    return true;
                }
                break;
            default:
                mIsSliding = false;
                break;
        }
        return false;
    }
}

当 processTouchEvent 判定当前的触摸事件为滑动事件时,SwipeBackHelper 对自身发送了一个 MSG_ACTION_MOVE 的 Message。接收到该 Message 后,SwipeBackHelper 调用了 onSliding 方法以执行滑动动画。

@Override
public void handleMessage(Message msg) {
    switch (msg.what) {
        ...
        case MSG_ACTION_MOVE:
            final float curPointX = msg.getData().getFloat(CURRENT_POINT_X);
            onSliding(curPointX);
            break;
       ...
    }
}
private synchronized void onSliding(float curPointX) {
    final int width = mActivity.getResources().getDisplayMetrics().widthPixels;
    View previewActivityContentView = mViewManager.mPreviousContentView;
    View shadowView = mViewManager.mShadowView;
    View currentActivityContentView = mViewManager.getDisplayView();
    if (previewActivityContentView == null || currentActivityContentView == null || shadowView == null) {
        sendEmptyMessage(MSG_SLIDE_CANCELED);
        return;
    }
    final float distanceX = curPointX - mLastPointX;
    mLastPointX = curPointX;
    mDistanceX = mDistanceX + distanceX;
    if (mDistanceX < 0) {
        mDistanceX = 0;
    }
    previewActivityContentView.setX(-width / 3 + mDistanceX / 3);
    shadowView.setX(mDistanceX - SHADOW_WIDTH);
    currentActivityContentView.setX(mDistanceX);
}

与 ViewDragHelper 不同的是,SwipeBackHelper 通过调用 View 的 setX 方法来显示位移效果。最后,当手势结束时,SwipeBackHelper 在滑动距离不足时会执行回弹动画,在滑动距离充足时则会一直滑到尽头再结束当前页面,此处逻辑比较简单,不贴出代码。

显示前一个页面

and_swipeback 对前一个界面的管理主要放在 SwipeBackHelper.ViewManager 中,

class ViewManager {
    private Activity mPreviousActivity;
    private View mPreviousContentView;
    private View mShadowView;
    private boolean addViewFromPreviousActivity() {
        ...
        mPreviousActivity = ActivityLifecycleHelper.getPreviousActivity();
        ...
        ViewGroup previousActivityContainer = getContentView(mPreviousActivity);
        ...
        mPreviousContentView = previousActivityContainer.getChildAt(0);
        previousActivityContainer.removeView(mPreviousContentView);
        mCurrentContentView.addView(mPreviousContentView, 0);
        return true;
    }
    private void resetPreviousView() {
        if (mPreviousContentView == null)
            return;
        View view = mPreviousContentView;
        FrameLayout contentView = mCurrentContentView;
        view.setX(0);
        contentView.removeView(view);
        mPreviousContentView = null;
        if (mPreviousActivity == null || mPreviousActivity.isFinishing())
            return;
        Activity preActivity = mPreviousActivity;
        final ViewGroup previewContentView = getContentView(preActivity);
        previewContentView.addView(view);
        mPreviousActivity = null;
    }
    ...
    private void addCacheView() {
        final FrameLayout contentView = mCurrentContentView;
        final View previousView = mPreviousContentView;
        PreviousPageView previousPageView = new PreviousPageView(mActivity);
        contentView.addView(previousPageView, 0);
        previousPageView.cacheView(previousView);
    }
}

当调用 addViewFromPreviousActivity 时,ViewManager 从 ActivityLifecycleHelper 中获得上一个 Activity 的实例(其中 ActivityLifecycleHelper 实现了 Application.ActivityLifecycleCallbacks 并需在 Application 中注册监听,从而在 Activity 栈发生变化时能够获取到每个 Activity 的实例),并将该 Activity 的 ContentView 添加到当前 Activity 的底部。当滑动时,通过移开上层的 View 则可看到底部的 Previous ContentView。

相比 SwipeBackLayout,and_swipeback 避免了过多透明背景页面时所造成的卡顿感。

总结

在滑动处理上,两个库的能力其实都一样,也不存在与滑动控件冲突的情况。在显示前一个页面的方案上,从性能角度自然是 and_swipeback 更好,但实现逻辑上显然更加复杂。

在选择滑动返回库时,如果所需滑动返回的 Activity 叠加数量并不多,使用 SwipeBackLayout 或 and_swipeback 都差不多。而当带滑动返回功能的 Activity 数量较多时,如之前所述,透明背景会造成明显的卡顿感,此时应当使用 and_swipeback。

喜欢的话请帮忙转发一下能让更多有需要的人看到吧,有些技术上的问题大家可以多探讨一下。

以上Android资料以及更多Android相关资料及面试经验可在QQ群里获取:936903570。有加群的朋友请记得备注上简书,谢谢

相关文章

网友评论

    本文标题:Android 平台滑动返回库对比

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