ResideMenu 源码解析
1. 功能介绍
特性:
通过手势滑动缩放主界面展现的侧边栏,类似 QQ 5.0+ 版本侧滑菜单的出现方式,支持左右双侧边栏。
动效文字比较难以描述,请以演示效果 Gif 为例:
优势:
- 性能优异,几乎没有额外的重绘和性能损耗。
- 良好的结构设计,易于扩展和改写。
- 与
DrawerLayout
或SlidingMenu
相比,完全是另一种风格,第一次见到时给人眼前一亮的感觉。 - 事件分发做了很好的处理,可以方便的与其它控件集成。
2. 总体设计
2.1 View 层次结构分析
View Tree:
2.2 动画原理简单介绍:
从视觉效果上来看,可能会有人以为 Menu 展开过程是个平移+缩小的效果,但是实际上这里只使用了一个 Scale
动画,并没有使用任何平移动画。
注意,缩放的中心点在屏幕外。
3. 事件分发流程图
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
都由 ResideMenu
的 dispatchTouchEvent
最先处理,同时由 TouchDisableView
作为 ContentView
的容器,通过 TouchDisableView
的 onInterceptTouchEvent
返回值来控制是否屏蔽 ContentView
上的事件.例如,当 Menu 打开后, TouchDisableView
的 onInterceptTouchEvent
将会固定返回 true
,此时 TouchDisableView
上发生的所有 TouchEvent
都会被拦截,而不会分发给 ContentView
处理。
图:attachToActivity 执行前
attachToActivity 执行后
public void openMenu(int direction)
通过代码执行打开 menu 的动画。
public void closeMenu()
通过代码执行打开关闭的动画。
private void showScrollViewMenu(ScrollView scrollViewMenu)
private void hideScrollViewMenu(ScrollView scrollViewMenu)
展示/隐藏包含侧栏菜单的 scrollViewMenu.
注意这里的显示和隐藏是通过addView
或removeView
实现的.未被添加到视图 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 缩放系数的调整值 shadowAdjustScaleX
和 shadowAdjustScaleY
.
在打开 Menu 的过程中,阴影越来越明显.其原因在于,阴影的 scale 系数比 content 的系数要小,两者之间的差值即是 shadowAdjustScaleX
和 shadowAdjustScaleY
例如,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
中的 layoutLeftMenu
和 layoutRightMenu
的相关逻辑,添加方法使其支持加载指定自定义 View.
4.1.3 TouchDisableView
该类本身的功能非常单纯,在本项目中起一个容器的作用,通过重载 onInterceptTouchEvent
方法并返回指定值来控制是否拦截内部子 View
的 Touch
事件。
以 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
分析轮子,然后造轮子。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论