ResideMenu 源码解析

发布于 2025-01-26 15:42:41 字数 9338 浏览 14 评论 0

1. 功能介绍

特性:

通过手势滑动缩放主界面展现的侧边栏,类似 QQ 5.0+ 版本侧滑菜单的出现方式,支持左右双侧边栏。

动效文字比较难以描述,请以演示效果 Gif 为例:

demo gif

优势:

  • 性能优异,几乎没有额外的重绘和性能损耗。
  • 良好的结构设计,易于扩展和改写。
  • DrawerLayoutSlidingMenu 相比,完全是另一种风格,第一次见到时给人眼前一亮的感觉。
  • 事件分发做了很好的处理,可以方便的与其它控件集成。

2. 总体设计

2.1 View 层次结构分析

View Tree:

view-tree-after-init

2.2 动画原理简单介绍:

从视觉效果上来看,可能会有人以为 Menu 展开过程是个平移+缩小的效果,但是实际上这里只使用了一个 Scale 动画,并没有使用任何平移动画。

Scale Animation

注意,缩放的中心点在屏幕外。

3. 事件分发流程图

dispatchTouchEvent

4. 详细设计

4.1 核心类功能介绍


4.1.1 ResideMenu

核心类

  • private void setScaleDirection(int direction)

本方法是该效果实现核心的部分,通过该方法配置了缩放动画的中心点。

private void setScaleDirection(int direction){
    int screenWidth = getScreenWidth();
    float pivotX;
    float pivotY = getScreenHeight() * 0.5f;

    if (direction == DIRECTION_LEFT){
        scrollViewMenu = scrollViewLeftMenu;
        pivotX  = screenWidth * 1.5f;
    }else{
        scrollViewMenu = scrollViewRightMenu;
        pivotX  = screenWidth * -0.5f;
    }

    ViewHelper.setPivotX(viewActivity, pivotX);
    ViewHelper.setPivotY(viewActivity, pivotY);
    ViewHelper.setPivotX(imageViewShadow, pivotX);
    ViewHelper.setPivotY(imageViewShadow, pivotY);
    scaleDirection = direction;
}

通过代码可以看到,

当屏幕左滑时,缩放中心是 (-0.5 * width, 0.5 * height)

当屏幕右滑时,缩放中心是 (1.5 * width, 0.5 * height)

这和平时我们使用的缩放中心 (0.5 * width, 0.5 * height)

效果上有些不同,请结合 2.2 动画效果示意图

  • private AnimatorSet buildScaleDownAnimation(View target,float targetScaleX,float targetScaleY)

    构造主界面的缩小动画.这里的 target 也就是上面的 viewActivity,注意其缩放中心并不是常见的 View 中心点。

  • private AnimatorSet buildScaleUpAnimation(View target,float targetScaleX,float targetScaleY)

    构造主界面的放大动画.这里的 target 也就是上面的 viewActivity,注意其缩放中心并不是常见的 View 中心点。

  • private AnimatorSet buildMenuAnimation(View target, float alpha)

    构造 Menu 显示/消失时的渐隐动画。

  • private void initValue(Activity activity)

实例化 TouchDisableView ,并替换 Activity 中的 DecorView .

private void initValue(Activity activity){
    this.activity   = activity;
    ...
    viewDecor = (ViewGroup) activity.getWindow().getDecorView();
    viewActivity = new TouchDisableView(this.activity);
    View mContent   = viewDecor.getChildAt(0);
    viewDecor.removeViewAt(0);
    viewActivity.setContent(mContent);
    ...
    addView(viewActivity);
}

注意方法中这一部分代码,这是目前一种常见的 View 注入方式, SlidingMenu 和 SwipeBack 等库都使用类似机制以达到获得 Activity 中根视图控制权的目的。

  • public void attachToActivity(Activity activity)

调用了上面的 initValue 方法,并通过执行

viewDecor.addView(this, 0);

将自己添加到 viewDecor 的子节点上。

此方法执行后, ResideMenu 成为了 Activity 中 DecorView 的唯一一个直接子节点,所有 TouchEvent 都由 ResideMenudispatchTouchEvent 最先处理,同时由 TouchDisableView 作为 ContentView 的容器,通过 TouchDisableViewonInterceptTouchEvent 返回值来控制是否屏蔽 ContentView 上的事件.例如,当 Menu 打开后, TouchDisableViewonInterceptTouchEvent 将会固定返回 true ,此时 TouchDisableView 上发生的所有 TouchEvent 都会被拦截,而不会分发给 ContentView 处理。

图:attachToActivity 执行前

view-tree-pre-init

attachToActivity 执行后

view-tree-after-init

  • public void openMenu(int direction)

    通过代码执行打开 menu 的动画。

  • public void closeMenu()

    通过代码执行打开关闭的动画。

  • private void showScrollViewMenu(ScrollView scrollViewMenu)

  • private void hideScrollViewMenu(ScrollView scrollViewMenu)

    展示/隐藏包含侧栏菜单的 scrollViewMenu.
    注意这里的显示和隐藏是通过 addViewremoveView 实现的.未被添加到视图 Tree 的 View 不会参与 measure , layout , draw 等相关流程,menu 未打开时,没有任何额外开销.这算是一项针对视图和 OverDraw 的优化吧。

  • private void setScaleDirectionByRawX(float currentRawX)

    根据当前 TouchEvent 的 X 轴位置与上一次 TouchEvent 的 X 轴位置判断当前滑动的方向。

  • private float getTargetScale(float currentRawX)

    获得当前缩放系数。

  • public boolean dispatchTouchEvent(MotionEvent ev)

    逻辑和流程太复杂,用文字不方便表述,看流程图吧。


  • private void setShadowAdjustScaleXByOrientation()

根据横竖屏设置 Shadow 缩放系数的调整值 shadowAdjustScaleXshadowAdjustScaleY .

在打开 Menu 的过程中,阴影越来越明显.其原因在于,阴影的 scale 系数比 content 的系数要小,两者之间的差值即是 shadowAdjustScaleXshadowAdjustScaleY
例如,menu 完全打开时,content 宽缩小到 50%(mScaleValue),而阴影宽只缩小为原来的 56%(mScaleValue+shadowAdjustScaleX),所以在打开的过程中,content 缩小的更快,shadow 缩小的更慢,相 比较而言,露出的 shadow 面积越来越大。

  • public void setDirectionDisable(int direction)
  • public void setSwipeDirectionDisable(int direction)
  • private boolean isInDisableDirection(int direction)

设置 disable direction.

 switch (ev.getAction()){
    case MotionEvent.ACTION_DOWN:
		...
    case MotionEvent.ACTION_MOVE:
            if (isInIgnoredView || isInDisableDirection(scaleDirection))
            break;
	...
}

参考 dispatchTouchEvent 中部分代码,设置了 disable direction 后,在对应的方向上滑动时,不会触发打开 menu 的效果,

  • public void addIgnoredView(View v)

  • public void removeIgnoredView(View v)

  • public void clearIgnoredViewList()

  • private boolean isInIgnoredView(MotionEvent ev)
    switch (ev.getAction()){
    case MotionEvent.ACTION_DOWN:
    ...
    isInIgnoredView = isInIgnoredView(ev) && !isOpened();
    ...
    case MotionEvent.ACTION_MOVE:
    if (isInIgnoredView || isInDisableDirection(scaleDirection))
    break;
    ...
    }

参考 dispatchTouchEvent 中部分代码,设置了 IgnoredView 后,在 IgnoredView 上开始的滑动事件,不会触发打开 menu 的效果。

4.1.2 ResideMenuItem

包装了侧栏菜单的一行,由一个 ImageView 和一个 TextView 组成,提供一些基本的对 Text 和 Icon 的设置方法。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
			  android:orientation="horizontal"
			  android:layout_width="match_parent"
			  android:layout_height="wrap_content"
			  android:gravity="center_vertical"
			  android:paddingTop="15dp"
			  android:paddingBottom="15dp">
	<ImageView
			android:layout_width="30dp"
			android:layout_height="30dp"
			android:scaleType="centerCrop"
			android:id="@+id/iv_icon"/>
	<TextView
			android:layout_width="match_parent"
			android:layout_height="wrap_content"
			android:textColor="@android:color/white"
			android:textSize="18sp"
			android:layout_marginLeft="10dp"
			android:id="@+id/tv_title"/>
</LinearLayout>

如果你对 ResideMenu 侧栏菜单的 Item 样式感到不满意,可以通过修改该 xml 及 ResideMenu 代码来实现。

如果你需要一个完全定制的侧栏菜单,并且不满足于 Icon+Text 的表现形式,那么你需要修改 ResideMenu 中的 layoutLeftMenulayoutRightMenu 的相关逻辑,添加方法使其支持加载指定自定义 View.

4.1.3 TouchDisableView

该类本身的功能非常单纯,在本项目中起一个容器的作用,通过重载 onInterceptTouchEvent 方法并返回指定值来控制是否拦截内部子 ViewTouch 事件。

以 ResideMenu 中的 AnimatorListener 回调为例:

@Override
public void onAnimationEnd(Animator animation) {
    // reset the view;
    if(isOpened()){
        viewActivity.setTouchDisable(true);
        viewActivity.setOnClickListener(viewActivityOnClickListener);
    }else{
        viewActivity.setTouchDisable(false);
        viewActivity.setOnClickListener(null);
        hideScrollViewMenu(scrollViewLeftMenu);
        hideScrollViewMenu(scrollViewRightMenu);
        if (menuListener != null)
            menuListener.closeMenu();
    }
}

当动画结束时,若 Menu 菜单出于打开状态,那么 mContent 也就是主界面此时应当出于缩小状态,不再响应任何触摸/点击事件,此时设置 viewActivity.setTouchDisable(true) 来拦截所有 TouchDisableView 上的点击事件。

反之.若动画结束后,menu 处于关闭状态,那么主界面处于展示状态,应当正常响应触摸/点击事件,此时设置 viewActivity.setTouchDisable(false) ,使事件能够按正常流程进行分发。

请结合事件分发流程图一起理解这部分。

5. 杂谈

在分析 ResideMenu 的过程中,我也尝试自己写了一个 ResideMenu 的效果扩展来印证分析过程中的一些结论:

Folder-ResideMenu

感兴趣的可以参考 Folder-ResideMenu

分析轮子,然后造轮子。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
27 人气
更多

推荐作者

诺曦

文章 0 评论 0

要走干脆点

文章 0 评论 0

把回忆走一遍

文章 0 评论 0

陌上青苔

文章 0 评论 0

Arthur

文章 0 评论 0

哄哄

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文