为了账号安全,请及时绑定邮箱和手机立即绑定

ResideMenu 源码解析

标签:
Android

1. 功能介绍

特性(Features):

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

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

优势:
  • 性能优异,几乎没有额外的重绘和性能损耗。

  • 良好的结构设计,易于扩展和改写。

  • DrawerLayoutSlidingMenu相比,完全是另一种风格,第一次见到时给人眼前一亮的感觉。

  • 事件分发做了很好的处理,可以方便的与其它控件集成。

2. 总体设计

2.1 View层次结构分析

View Tree:

原理简单介绍:

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

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

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执行后 

  • 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中的layoutLeftMenu 和layoutRightMenu的相关逻辑,添加方法使其支持加载指定自定义View.

4.1.3 TouchDisableView

该类本身的功能非常单纯,在本项目中起一个容器的作用,通过重载onInterceptTouchEvent方法并返回指定值来控制是否拦截内部子ViewTouch事件。 如果读者不了解onInterceptTouchEvent的运作机制,可以参考 View 事件传递

以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

分析轮子,然后造轮子.

原文链接:http://www.apkbus.com/blog-705730-60211.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消