美文网首页高级UIAndroid自定义View
ViewDragHelper实战,实现QQ侧滑菜单

ViewDragHelper实战,实现QQ侧滑菜单

作者: h2coder | 来源:发表于2020-03-04 11:26 被阅读0次

    前言

    前面使用ViewDragHelper实现过滑动解锁浮动按钮,可以说ViewDragHelper真是一个神器,实现拽托、移动,都只需要很少的代码就可以实现,手势相关代码都在ViewDragHelper内部帮我们封装好了,本篇再次使用ViewDragHelper来实现QQ的侧滑菜单。

    说到这个侧边栏,一开始学习安卓的时候,看的是一脸懵逼,一直都想实现它,但因当时水平不过,不足以有能力去实现,如今做安卓也有些年头了,终于也用ViewDragHelper实现了这个功能。想想还是有些兴奋呢~

    当然本篇的代码,只能实现基本的左滑菜单,并且可能会有一些手势冲突没有处理,但是主线依旧是左侧滑,其他处理都是锦上添花~如果实际开发中要用,最好还是使用已经造好的轮子。(轮子虽好,但始终不是自己的,有空自己动手,才能理解到精髓喔)

    成品展示

    ViewDragHelper实战,实现QQ侧滑菜单.gif

    分析

    布局分析

    侧滑菜单,一共分为3个部分:

    • 页面整体,包含菜单部分内容部分

    • 菜单部分,和内容布局同一个层级,被SlidingMenu控件包裹。

    • 内容部分,同上,和菜单部分同一个层级,被SlidingMenu控件包裹。不难发现我们的效果中,打开菜单时,内容区域会有一个黑色遮罩,根据滑动的距离从透明到半透明,所以内容部分还有一个遮罩。

    xml布局

    • 总布局
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout 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"
        tools:context=".MainActivity">
    
        <com.zh.android.slidingmenu.sample.SlidingMenu
            android:id="@+id/sliding_menu"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <include layout="@layout/layout_menu" />
    
            <include layout="@layout/layout_content" />
        </com.zh.android.slidingmenu.sample.SlidingMenu>
    </FrameLayout>
    
    • 菜单布局
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/menu_list"
        android:layout_width="280dp"
        android:layout_height="match_parent" />
    
    • 内容布局
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout 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">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/tool_bar"
                android:layout_width="match_parent"
                android:layout_height="?actionBarSize"
                android:background="@color/colorPrimary"
                app:title="@string/app_name"
                app:titleTextColor="@android:color/white" />
    
            <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/content_list"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" />
        </LinearLayout>
    
        <View
            android:id="@+id/content_bg"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:alpha="0"
            android:background="#8C000000"
            tools:alpha="1" />
    </FrameLayout>
    

    行为分析

    • 打开菜单

      1. 菜单从左向右拉出,菜单跟随内容一起向右移动。
      2. 内容从左侧拉到完全出现,不能继续拉,内容也不能继续拉。
    • 关闭菜单

      1. 内容从右向左拉回,菜单跟随内容一起向左移动。
      2. 内容从右向左移动,完全移出屏幕时,内容不能继续向左拉。
    • 其他细节

      1. 拽托菜单和内容都可以拉动整体菜单的移动。
      2. 遮罩根据拽托的位置,形成一个比值,让遮罩的透明度改变。
        • 菜单打开时,比值为1,菜单关闭时,比值为0。
        • 从关闭到打开,比值从0开始,逐渐趋向1。

    逐步实现

    • 创建侧滑菜单布局 SlidingMenu

      1. 复写onFinishInflate(),获取子控件和布局子控件,必须只有菜单和内容这2个子控件。
      2. 复写onLayout(),布局菜单和内容。
        • 菜单布局,left值为负的菜单宽度,right值为父布局的left,所以一开始菜单都默认隐藏在屏幕的左侧,是看不到的。
        • 内容区域,所有值都和父布局一样即可,就是铺满父布局。
    public class SlidingMenu extends FrameLayout {
        /**
         * 菜单View
         */
        private View vMenuView;
        /**
         * 内容View
         */
        private View vContentView;
    
        public SlidingMenu(@NonNull Context context) {
            this(context, null);
        }
    
        public SlidingMenu(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SlidingMenu(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context, attrs, defStyleAttr);
        }
        
        private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            //初始化...
        }
        
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            //获取所有子View,确保只有菜单和内容这2个控件
            int childCount = getChildCount();
            if (childCount != 2) {
                throw new IllegalStateException("侧滑菜单内只能有2个子View,分别是菜单和内容");
            }
            vMenuView = getChildAt(0);
            vContentView = getChildAt(1);
        }
        
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            //布局菜单和内容,菜单在最左边,普通状态是看不到的
            vMenuView.layout(-vMenuView.getMeasuredWidth(), top, left, bottom);
            //内容View铺满整个父控件
            vContentView.layout(left, top, right, bottom);
        }
    }
    
    • 新建菜单状态回调接口,提供以下回调方法:

      • onMenuOpen(),当菜单开启时回调。
      • onSliding(float fraction),正在滑动时回调,回传fraction值,为滑动比值。
      • onMenuClose(),当菜单关闭时回调。
    /**
     * 菜单状态改变监听
     */
    private OnMenuStateChangeListener mMenuStateChangeListener;
    
    /**
     * 菜单状态改变监听
     */
    public interface OnMenuStateChangeListener {
        /**
         * 当菜单开启时回调
         */
        void onMenuOpen();
    
        /**
         * 正在滑动时回调
         *
         * @param fraction 滑动百分比值
         */
        void onSliding(float fraction);
    
        /**
         * 当菜单关闭时回调
         */
        void onMenuClose();
    }
    
    /**
     * 设置状态改变监听
     *
     * @param menuStateChangeListener 监听器
     */
    public void setOnMenuStateChangeListener(OnMenuStateChangeListener menuStateChangeListener) {
        mMenuStateChangeListener = menuStateChangeListener;
    }
    
    • 创建ViewDragHelper,将事件委托给ViewDragHelper处理
      • 复写onInterceptTouchEvent(),事件委托给ViewDragHelper。
      • 复写onTouchEvent(),事件委托给ViewDragHelper。
      • 复写computeScroll(),因为ViewDragHelper的滚动是依靠Scroller的,所以需要将Scroller的相关处理交给ViewDragHelper。
    /**
     * 拽托帮助类
     */
    private ViewDragHelper mViewDragHelper;
    
    /**
     * 初始化
     */
    private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
            //...先省略,下面会说
        }
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //将onInterceptTouchEvent委托给ViewDragHelper
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将onTouchEvent委托给ViewDragHelper
        mViewDragHelper.processTouchEvent(event);
        return true;
    }
    
    @Override
    public void computeScroll() {
        super.computeScroll();
        //判断是否移动到头了,未到头则继续
        if (mViewDragHelper != null) {
            if (mViewDragHelper.continueSettling(true)) {
                invalidate();
            }
        }
    }
    
    • 复写ViewDragHelper,处理横向侧滑

      1. 复写tryCaptureView(),确定菜单和内容可以拽托
      2. 复写getViewHorizontalDragRange(),返回拽托范围,返回非0值即可,某些情况需要该值来确定是否可以拽托。
      3. 复写clampViewPositionHorizontal(),处理横向拽托,由于菜单和内容都可以拽托,拽托这2部分都会回调,但方法传入的child为菜单或内容。所以需要判断拽托的控件来处理。
        • 拽托的是菜单,处理如下:

          • 左边界left不能超过菜单宽度,因坐标系,屏幕左侧为负值,所以不能小于负的菜单宽度。
          • 左边界left不能超过0,因为只能完全显示菜单后,就不能继续向右拽托了。
          • 其他情况为允许值,直接返回传入的left值即可。
        • 拽托的是内容,处理如下:

          • 左边界left不能超过0,因为内容不能拽托出屏幕左侧。
          • 左边界left不能超过菜单宽度,因为菜单完全显示后,内容就不能继续向右拽托了。
          • 其他情况为允许值,直接返回传入的left值即可。
    //...省略其他代码
    
    /**
     * 初始化
     */
    private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                //菜单和内容都可以拽托
                return child == vMenuView || child == vContentView;
            }
    
            @Override
            public int getViewHorizontalDragRange(@NonNull View child) {
                //拽托范围,返回非0值即可,某些情况需要该值来确定是否可以拽托
                return vContentView.getWidth();
            }
    
            @Override
            public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
                //处理横向拽托,获取菜单宽度
                int menuWidth = vMenuView.getWidth();
                //因为拽托菜单和内容传入的对象不同,我们需要处理2种情况
                if (child == vMenuView) {
                    //拽托的是菜单
                    if (left < -menuWidth) {
                        //左边距离,最多只能完全隐藏于屏幕最左侧
                        return -menuWidth;
                    } else if (left > 0) {
                        //左边距离,最多能完全出现在屏幕
                        return 0;
                    } else {
                        return left;
                    }
                } else if (child == vContentView) {
                    //拽托的是内容区域,不能移动超出最左边的屏幕
                    if (left < 0) {
                        return 0;
                    } else if (left > menuWidth) {
                        //最多不能超过菜单的宽度
                        return menuWidth;
                    } else {
                        return left;
                    }
                }
                return 0;
            }
        });
    }
    
    //...省略其他代码
    
    • 处理拽托时,联动处理

    如果只复写clampViewPositionHorizontal(),只能拽托菜单或者内容的单独移动,它们并不是联动的,我们需要拽托菜单时,内容随着一起移动。相应的,拽托内容时,菜单也会随着一起移动。

    1. 复写onViewPositionChanged(),当拽托菜单或内容时,回调移动位置等相关信息。
    2. 同样需要判断child对象是菜单还是内容()。
      • 拽托对象为菜单时,手动调用内容View的layout()方法,让内容View移动。
      • 拽托对象为内容时,手动调用菜单View的layout()方法,让内容View移动。
    3. 处理开、关状态处理以及回调监听。
    4. 定义菜单打开、关闭方法
      • openMenu(),打开菜单。
      • closeMenu(),关闭菜单。
    //...省略其他代码
    
    /**
     * 初始化
     */
    private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
                @Override
                public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
                    super.onViewPositionChanged(changedView, left, top, dx, dy);
                    //处理联动,拽托菜单布局,让内容布局跟着动
                    if (changedView == vMenuView) {
                        int newLeft = vContentView.getLeft() + dx;
                        int right = newLeft + vContentView.getWidth();
                        vContentView.layout(newLeft, top, right, getBottom());
                    } else if (changedView == vContentView) {
                        //拽托内容布局,让菜单布局跟着动
                        int newLeft = vMenuView.getLeft() + dx;
                        vMenuView.layout(newLeft, top, left, getBottom());
                    }
                    //处理滑动中的回调,计算滑动比值
                    if (mMenuStateChangeListener != null) {
                        float fraction = (vContentView.getLeft() * 1f) / vMenuView.getWidth();
                        mMenuStateChangeListener.onSliding(fraction);
                    }
                    //处理开、关状态,由于该方法会不断被回调,所以需要加上状态值,保证只回调一次给监听器
                    if ((vMenuView.getLeft() == -vMenuView.getWidth()) && isOpenMenu) {
                        //关
                        isOpenMenu = false;
                        if (mMenuStateChangeListener != null) {
                            mMenuStateChangeListener.onMenuClose();
                        }
                    } else if (vMenuView.getLeft() == 0 && !isOpenMenu) {
                        //开
                        isOpenMenu = true;
                        if (mMenuStateChangeListener != null) {
                            mMenuStateChangeListener.onMenuOpen();
                        }
                    }
                }
        });
        
            /**
         * 打开菜单
         */
        public void openMenu() {
            mViewDragHelper.smoothSlideViewTo(vMenuView, 0, vMenuView.getTop());
            ViewCompat.postInvalidateOnAnimation(SlidingMenu.this);
        }
    
        /**
         * 关闭菜单
         */
        public void closeMenu() {
            mViewDragHelper.smoothSlideViewTo(vMenuView, -vMenuView.getWidth(), vMenuView.getTop());
            ViewCompat.postInvalidateOnAnimation(SlidingMenu.this);
        }
    }
    
    //...省略其他代码
    
    • 处理松手回弹和fling操作

      • 首先,先处理fling操作,当向左快速惯性滑动时,xvel值小于0,关闭菜单。如果是向右,则xvel值大于300则打开菜单。
      • 如果不是fling操作,则处理为松手回台,判断菜单打开时的left时,如果小于菜单宽度的一半,则为关闭,否则为打开。
    //...省略其他代码
    
    /**
     * 初始化
     */
    private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
                @Override
                public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
                    super.onViewReleased(releasedChild, xvel, yvel);
                    //fling操作
                    if (xvel < 0) {
                        //向左
                        closeMenu();
                        return;
                    } else if (xvel > 300) {
                        //向右
                        openMenu();
                        return;
                    }
                    //松手回弹
                    float halfMenuWidth = vMenuView.getWidth() / 2f;
                    //如果菜单打开的范围小于菜单的一半,则当为关
                    if (vMenuView.getLeft() < -halfMenuWidth) {
                        //关
                        closeMenu();
                    } else {
                        //开
                        openMenu();
                    }
                }
        });
    }
    
    //...省略其他代码
    
    • 遮罩

    上面说到内容区域有一个遮罩,当菜单从关闭到打开的过程中,是从亮到黑,其实就是一个半透明的遮罩,它的透明度从0到1的过程。
    我们可以以内容区域的左边距(就是left值)为起点,和菜单View的宽度做一个比值,将比值交给回调,外部收到回调时,再进行遮罩透明度设置。

    @Override
    public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
        super.onViewPositionChanged(changedView, left, top, dx, dy);
        ...
        //处理滑动中的回调,计算滑动比值
        if (mMenuStateChangeListener != null) {
            float fraction = (vContentView.getLeft() * 1f) / vMenuView.getWidth();
            mMenuStateChangeListener.onSliding(fraction);
        }
        ...
    }
    

    设置回调,处理遮罩透明度

    public class MainActivity extends AppCompatActivity {
        /**
         * 侧滑菜单
         */
        private SlidingMenu vSlidingMenu;
    
        /**
         * 透明度估值器
         */
        private FloatEvaluator mAlphaEvaluator;
        
            @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            findView();
            bindView();
        }
    
        private void findView() {
            ...省略其他控件
            
            vSlidingMenu = findViewById(R.id.sliding_menu);
            vContentBg = findViewById(R.id.content_bg);
            
            ...省略其他控件
        }
        
        private void bindView() {
            //创建估值器
            mAlphaEvaluator = new FloatEvaluator();
            //------------ 重点:设置侧滑菜单的状态切换监听 ------------
            vSlidingMenu.setOnMenuStateChangeListener(new SlidingMenu.OnMenuStateChangeListener() {
                @Override
                public void onMenuOpen() {
                    Log.d(TAG, "菜单打开");
                    //让黑色遮罩,禁用触摸
                    vContentBg.setClickable(true);
                }
    
                @Override
                public void onSliding(float fraction) {
                    Log.d(TAG, "菜单拽托中,百分比:" + fraction);
                    //设定最小、最大透明度值
                    float startValue = 0;
                    float endValue = 0.55f;
                    //估值当前的透明度值,并设置
                    Float value = mAlphaEvaluator.evaluate(fraction, startValue, endValue);
                    vContentBg.setAlpha(value);
                }
    
                @Override
                public void onMenuClose() {
                    Log.d(TAG, "菜单关闭");
                    //让黑色遮罩,恢复触摸
                    vContentBg.setClickable(false);
                }
            });
            //------------ 重点:设置侧滑菜单的状态切换监听 ------------
        }
    }
    

    完整代码

    public class SlidingMenu extends FrameLayout {
        /**
         * 菜单View
         */
        private View vMenuView;
        /**
         * 内容View
         */
        private View vContentView;
        /**
         * 拽托帮助类
         */
        private ViewDragHelper mViewDragHelper;
        /**
         * 菜单状态改变监听
         */
        private OnMenuStateChangeListener mMenuStateChangeListener;
        /**
         * 菜单是否开启
         */
        private boolean isOpenMenu;
    
        public SlidingMenu(@NonNull Context context) {
            this(context, null);
        }
    
        public SlidingMenu(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SlidingMenu(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context, attrs, defStyleAttr);
        }
    
        /**
         * 初始化
         */
        private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
                @Override
                public boolean tryCaptureView(@NonNull View child, int pointerId) {
                    //菜单和内容都可以拽托
                    return child == vMenuView || child == vContentView;
                }
    
                @Override
                public int getViewHorizontalDragRange(@NonNull View child) {
                    //拽托范围,返回非0值即可,某些情况需要该值来确定是否可以拽托
                    return vContentView.getWidth();
                }
    
                @Override
                public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
                    //处理横向拽托,获取菜单宽度
                    int menuWidth = vMenuView.getWidth();
                    //因为拽托菜单和内容传入的对象不同,我们需要处理2种情况
                    if (child == vMenuView) {
                        //拽托的是菜单
                        if (left < -menuWidth) {
                            //左边距离,最多只能完全隐藏于屏幕最左侧
                            return -menuWidth;
                        } else if (left > 0) {
                            //左边距离,最多能完全出现在屏幕
                            return 0;
                        } else {
                            return left;
                        }
                    } else if (child == vContentView) {
                        //拽托的是内容区域,不能移动超出最左边的屏幕
                        if (left < 0) {
                            return 0;
                        } else if (left > menuWidth) {
                            //最多不能超过菜单的宽度
                            return menuWidth;
                        } else {
                            return left;
                        }
                    }
                    return 0;
                }
    
                @Override
                public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
                    super.onViewPositionChanged(changedView, left, top, dx, dy);
                    //处理联动,拽托菜单布局,让内容布局跟着动
                    if (changedView == vMenuView) {
                        int newLeft = vContentView.getLeft() + dx;
                        int right = newLeft + vContentView.getWidth();
                        vContentView.layout(newLeft, top, right, getBottom());
                    } else if (changedView == vContentView) {
                        //拽托内容布局,让菜单布局跟着动
                        int newLeft = vMenuView.getLeft() + dx;
                        vMenuView.layout(newLeft, top, left, getBottom());
                    }
                    //处理滑动中的回调,计算滑动比值
                    if (mMenuStateChangeListener != null) {
                        float fraction = (vContentView.getLeft() * 1f) / vMenuView.getWidth();
                        mMenuStateChangeListener.onSliding(fraction);
                    }
                    //处理开、关状态,由于该方法会不断被回调,所以需要加上状态值,保证只回调一次给监听器
                    if ((vMenuView.getLeft() == -vMenuView.getWidth()) && isOpenMenu) {
                        //关
                        isOpenMenu = false;
                        if (mMenuStateChangeListener != null) {
                            mMenuStateChangeListener.onMenuClose();
                        }
                    } else if (vMenuView.getLeft() == 0 && !isOpenMenu) {
                        //开
                        isOpenMenu = true;
                        if (mMenuStateChangeListener != null) {
                            mMenuStateChangeListener.onMenuOpen();
                        }
                    }
                }
    
                @Override
                public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
                    super.onViewReleased(releasedChild, xvel, yvel);
                    //fling操作
                    if (xvel < 0) {
                        //向左
                        closeMenu();
                        return;
                    } else if (xvel > 300) {
                        //向右
                        openMenu();
                        return;
                    }
                    //松手回弹
                    float halfMenuWidth = vMenuView.getWidth() / 2f;
                    //如果菜单打开的范围小于菜单的一半,则当为关
                    if (vMenuView.getLeft() < -halfMenuWidth) {
                        //关
                        closeMenu();
                    } else {
                        //开
                        openMenu();
                    }
                }
            });
        }
    
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            //获取所有子View,确保只有菜单和内容这2个控件
            int childCount = getChildCount();
            if (childCount != 2) {
                throw new IllegalStateException("侧滑菜单内只能有2个子View,分别是菜单和内容");
            }
            vMenuView = getChildAt(0);
            vContentView = getChildAt(1);
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            //布局菜单和内容,菜单在最左边,普通状态是看不到的
            vMenuView.layout(-vMenuView.getMeasuredWidth(), top, left, bottom);
            //内容View铺满整个父控件
            vContentView.layout(left, top, right, bottom);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            //将onInterceptTouchEvent委托给ViewDragHelper
            return mViewDragHelper.shouldInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            //将onTouchEvent委托给ViewDragHelper
            mViewDragHelper.processTouchEvent(event);
            return true;
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            //判断是否移动到头了,未到头则继续
            if (mViewDragHelper != null) {
                if (mViewDragHelper.continueSettling(true)) {
                    invalidate();
                }
            }
        }
    
        /**
         * 打开菜单
         */
        public void openMenu() {
            mViewDragHelper.smoothSlideViewTo(vMenuView, 0, vMenuView.getTop());
            ViewCompat.postInvalidateOnAnimation(SlidingMenu.this);
        }
    
        /**
         * 关闭菜单
         */
        public void closeMenu() {
            mViewDragHelper.smoothSlideViewTo(vMenuView, -vMenuView.getWidth(), vMenuView.getTop());
            ViewCompat.postInvalidateOnAnimation(SlidingMenu.this);
        }
    
        /**
         * 菜单状态改变监听
         */
        public interface OnMenuStateChangeListener {
            /**
             * 当菜单开启时回调
             */
            void onMenuOpen();
    
            /**
             * 正在滑动时回调
             *
             * @param fraction 滑动比值
             */
            void onSliding(float fraction);
    
            /**
             * 当菜单关闭时回调
             */
            void onMenuClose();
        }
    
        /**
         * 设置状态改变监听
         *
         * @param menuStateChangeListener 监听器
         */
        public void setOnMenuStateChangeListener(OnMenuStateChangeListener menuStateChangeListener) {
            mMenuStateChangeListener = menuStateChangeListener;
        }
    }
    

    总结

    项目代码,我提交到了github上,有需要的同学可以自行clone:Github地址

    使用ViewDragHelper这个神器,无论是做拽托、移动都很方便,本篇的难点其实在于坐标计算,尤其在onViewPositionChanged()方法中拽托联动菜单和内容这2个部分,计算2个控件的4个点会比较费脑子外,其他计算倒还好。

    相关文章

      网友评论

        本文标题:ViewDragHelper实战,实现QQ侧滑菜单

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