手写酷狗侧滑菜单效果

作者: 拂晓是个小人物 | 来源:发表于2019-01-22 17:56 被阅读108次

    上一节分析了事件传递的源码 Android事件分发机制源码详解-最新 API,那么趁热打铁来个应用的小示例。我这里尝试写一个酷狗的侧滑菜单。

    本文的大体内容如下:

    1. 分析
    2. 具体实现
    3. 处理滑动缩放效果
    4. 滑动临界值的处理
    5. 处理快速滑动
    6. 智能处理开关

    先看一下酷狗自己的效果,然后对比下我们实现的效果,已经很接近了。


    效果动图,可以查看下方录制的视频

    酷狗自己的侧滑效果视频地址
    我们自己写的侧滑效果视频地址
    界面是不卡顿的,我为了演示退出按钮在拖动的过程中的变化,故意滑动的慢了些。

    分析

    先分析下酷狗菜单的滑动体验:

    1. 打开软件,默认菜单是关闭的
    2. 在内容的空白区域或屏幕左侧向右滑动,菜单打开
    3. 点击主页导航三道杠,菜单快速打开
    4. 菜单打开的过程中,有几个效果
      • 右侧内容区域一个缩放动画
      • 左侧菜单一个缩放动画和一个透明度渐变动画
    5. 菜单关闭状态:往右滑动距离小于 halfMenuWidth,松手关闭菜单;如果距离大于 halfMenuWidth,则松手打开菜单
    6. 菜单打开状态:往左滑动距离小于 halfMenuWidth,松手打开菜单;如果距离大于 halfMenuWidth,则松手关闭菜单
    7. 快速左右滑动屏幕,关闭或者打开菜单
    8. 菜单处于打开状态,点击菜单区域,打开菜单的内容
    9. 菜单处于打开状态,点击右侧内容区域,关闭菜单,内容区域点击无响应

    具体实现

    因为可以左右来回滑动,我这里选择继承自HorizontalScrollView通过自定义扩展来实现,起名为「DrawerMenu」抽屉菜单。

    继承自 HorizontalScrollView,实现一下构造,这里我用到了一个自定义属性dmRightMargin,表示菜单打开时的右边距,不同项目值可能都不太一样。一个菜单布局和一个内容布局很简单不贴了。

    <?xml version="1.0" encoding="utf-8"?>
    <com.lm.kugoumenu.DrawerMenu xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/skin_menu_bg"
        android:fadingEdge="none"
        android:overScrollMode="never"
        app:dmRightMargin="45dp"
        tools:context=".KuGouActivity">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">
            <!-- 菜单-->
            <include layout="@layout/kugou_menu" />
    
            <!-- 内容 -->
            <include layout="@layout/kugou_content" />
        </LinearLayout>
    
    </com.lm.kugoumenu.DrawerMenu>
    

    运行到手机上可以先看下效果,果不其然的布局显示乱了,因为我们什么都没做呢。

    我们指定下菜单和内容的大小就可以了。

    // 解析完,拿到菜单和内容,指定大小
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    
        ViewGroup container = (ViewGroup) getChildAt(0);
        int childCount = container.getChildCount();
        if (childCount != 2) {
            throw new RuntimeException("LinearLayout must be contains two childviews.");
        }
    
        mMenuView = container.getChildAt(0);
        LinearLayout.LayoutParams menuParams = (LinearLayout.LayoutParams) mMenuView.getLayoutParams();
        menuParams.width = getScreenWidth() - mDrawerRightMargin;
        mMenuView.setLayoutParams(menuParams);
    
        mContentView = container.getChildAt(1);
        LinearLayout.LayoutParams contentParams = (LinearLayout.LayoutParams) mContentView.getLayoutParams();
        contentParams.width = getScreenWidth();
        mContentView.setLayoutParams(contentParams);
    }
    

    指定完我们再看下效果,已经可以滑动了,只不过没什么效果而已,同时打开应用默认菜单打开的,可以通过 scrollTo() 滚动一个菜单的宽度即可。

    scrollTo(mMenuWidth,0); 加到 onFinishInflate() 方法里边再次尝试发现没什么用,这是因为我们的调用时机不对,onFinishInflate()方法是在我们的 XML 布局文件解析完后调用的,这个时候布局虽然解析完了,但是并没有摆放呢,所以把滑动的代码放到 onLayout 即可。

    处理滑动缩放效果

    在滑动的过程中需要处理菜单和内容的动画效果,我们监听滑动过程,执行缩放和透明度动画;

    View 里边有一个 onScrollChanged() 方法, 只要 View 的内容有滑动,就会回调到这个方法中来。这样我们就不用自己处理滑动的监听了

    
    float scaleRatio = 1.0f * l / mMenuWidth; // 1.0 -> 0
    float contentScale = 0.7f + 0.3f * scaleRatio; // 1.0 -> 0
    // ViewCompat 已经过时了
    // ViewCompat.setScaleX(mContentView, contentScale);
    
    // 内容 缩放动画
    mContentView.setScaleX(contentScale);
    mContentView.setScaleY(contentScale);
    
    // 菜单 透明度动画和缩放动画
    float menuScale = 0.7f + 0.3f * (1 - scaleRatio); // 0.7 -> 1
    float menuAlpha = 0.6f + 0.4f * (1 - scaleRatio); // 0.6 -> 1
    mMenuView.setScaleX(menuScale);
    mMenuView.setScaleY(menuScale);
    mMenuView.setAlpha(menuAlpha);
    
    

    运行起来,发现可以正常打开了,但是我们的内容页不见了,这是因为内容页缩放后不在我们视野里边了,下图展示了菜单的几种状态(灰色框代表手机屏幕,粉色框为菜单,深色框为内容页),我们想要的就是中间这幅图展示的,菜单完全打开内容部分缩小看的见;然而现在的状态其实是右图,是由于默认的缩放中心点是 View 的中心位置,缩放后其实已经跑到我们的屏幕外边去了,我们修改 contentView 左侧边的中点位置为缩放的中心位置即可。

    image

    滑动临界值的处理

    当我们滑动手机屏幕,距离超过一定值时松手,我们来处理下使它能够友好的关闭或者打开,这就用到了上一篇讲到的事件分发的知识了

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // 注意:默认界面一打开,我们实际上是往右滑动了一个 Menu 的宽度的;
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            int scrollX = getScrollX();
            if (scrollX > mMenuWidth / 2) {
                closeMenu();
            } else {
                openMenu();
            }
            return true; // ViewGroup处理,消费掉事件
        }
        return super.onTouchEvent(ev);
    }
    

    处理快速滑动

    左右快速滑动页面可以打开和关闭菜单的处理,思路是这样的:我们监测到屏幕快速滑动,配合滑动的方向来决定菜单的打开与否。

    这里我借助于一个手势解析处理类 「GestureDetector」,来帮助我们监测快速滑动。GestureDetector 用起来很简单,直接 new 就好了,需要一个上下文和手势的监听回调 GestureDetector(Context, OnGestureListener)

    // GestureDetector.SimpleOnGestureListener 的一个回调方法,惯性滑动(飞滑)时,回调到这里,前提是想要响应,需要在 ViewGroup.onTouchEvent(MotionEvent)中让 GestureDetector 来处理事件
    
    // 右滑  x 为正,左滑为负,上滑  y 为负,下滑为正
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        if (mMenuOpen) {
            if (velocityX < 0 && Math.abs(velocityX) > Math.abs(velocityY)) { // 关闭
                closeMenu();
                return true;
            }
        } else {
            if (velocityX > 0 && Math.abs(velocityX) > Math.abs(velocityY)) { // 打开
                openMenu();
                return true;
            }
        }
        return super.onFling(e1, e2, velocityX, velocityY);
    }
    

    智能处理开关

    效果再描述一下:
    现象一:菜单处于打开状态,点击菜单区域,打开菜单的内容
    现象二:菜单处于打开状态,点击右侧内容区域,关闭菜单,内容区域点击无响应

    这个就需要我们根据不同的点击位置,来处理事件拦截与否;菜单打开时点击内容区域,就把事件拦截掉,点击其它位置交给系统处理。

    这里给出两种方案可以参考:
    方案一: 根据位置拦截事件
    方案二:点击对应区域,不拦截不消费,但是写自己的消费事件逻辑

    // 方案一
    // 如果使用这种方式处理的话,放开 onTouchEvent() 中的 「拦截代码块」
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 菜单打开,点击菜单右侧部分 要关闭菜单; 点击菜单区域,则菜单列表响应点击
        mTouchContentCloseMenu = false;
        if (mMenuOpen && ev.getRawX() > mMenuWidth) {
            mTouchContentCloseMenu = true;
            Log.e(TAG, "onInterceptTouchEvent: <<<<<<<<<<<<" );
            // 一旦拦截,事件就直接走到自己的 onTouchEvent() 方法里了
            return true;
        }
    
        return super.onInterceptTouchEvent(ev);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // 注意点:默认界面一打开,我们实际上是往右滑动了一个 Menu 的宽度的;
    
        // 拦截代码块:配合拦截事件代码一块使用 (如果在事件分发中写逻辑,这个地方可以屏蔽掉)
        if (mTouchContentCloseMenu && ev.getAction() == MotionEvent.ACTION_UP) {
            Log.e(TAG, "onTouchEvent: --> closeMenu()");
            mTouchContentCloseMenu = false;
            closeMenu();
            return true;
        }
    
        if (mGestureDetector.onTouchEvent(ev)) {
            return true;
        }
    
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            int scrollX = getScrollX();
            //Log.e(TAG, "onTouchEvent: >>> scrollX=" + scrollX + " mMenuWidth / 2=" + (mMenuWidth / 2) + " x=" + ev.getX() + " rawx=" + ev.getRawX());
            if (scrollX > mMenuWidth / 2) {
                closeMenu();
            } else {
                openMenu();
            }
            return true;
        }
        return super.onTouchEvent(ev);
    }
    
    
    
    // 方案二
    // 如果使用这种方式处理的话,屏蔽掉 onTouchEvent() 中的「拦截代码块」
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mMenuOpen && ev.getAction() == MotionEvent.ACTION_DOWN) {
            if (ev.getRawX() > mMenuWidth) {
    
                closeMenu();
    
                // 返回 false 不分发 不处理事件,连 ViewGroup 自己的 onTouchEvent() 方法都不会走;
                // 事件会直接交给顶级父 View,最终事件会传递到 Activity 的 onTouchEvent()
    
                // 返回 true,表示事件分发成功(事实上并没有分发给子 View),并且被消费(事实需要我们自己去处理消费事件的逻辑,即 closeMenu() )
                // 后续事件(MOVE UP)会直接走到自己的 onTouchEvent()方法中
                // 如果我们在这里自己写了事件消费的逻辑(调用了closeMenu()),意味着没有后续事件的,因为菜单已关闭,可以屏蔽掉 closeMenu(),自己打日志体会吧
                return false;
            }
        }
        return super.dispatchTouchEvent(ev);
    }
    

    最后,还有一个小瑕疵,就是在滑动打开菜单的过程中,「退出」按钮在酷狗上的做法是,一开始的位置在内容页的下面,伴随着继续往右滑动,会完全显示出来,是一个很好的细节体验,这里就不再贴代码了,留个小悬念自己思考下「源码里边有处理」。

    好了,到目前为止,我们自己写的仿酷狗侧滑菜单已经实现的七七八八了,不知道最后这个「智能处理」有没有讲清楚,嗯,反正我是明白了,如果对事件分发还不太熟悉的,可以翻看上一篇Android事件分发机制源码详解-最新 API ,如果有疑问的话,写一遍运行起来打个日志瞅一瞅。

    可以用微信扫一扫获取 demo 源码和图片资源哦~

    微信扫码快速直达

    相关文章

      网友评论

        本文标题:手写酷狗侧滑菜单效果

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