zoukankan      html  css  js  c++  java
  • Android自己定义ViewGroup打造各种风格的SlidingMenu

    看鸿洋大大的QQ5.0側滑菜单的视频课程,对于側滑的时的动画效果的实现有了新的认识,似乎打通了任督二脉。眼下能够实现随意效果的側滑菜单了。感谢鸿洋大大!!

    鸿洋大大用的是HorizontalScrollView来实现的側滑菜单功能,HorizontalScrollView的优点是为我们攻克了滑动功能。处理了滑动冲突问题。让我们使用起来很方便。可是滑动和冲突处理都是android中的难点,是我们应该掌握的知识点,掌握了这些,我们能够不依赖于系统的API。随心所欲打造我们想要的效果。因此这篇文章我将直接自己定义ViewGroup来实现側滑菜单功能

    首先我们先来看一看效果图,第一个效果图是一个最普通的側滑菜单,我们一会儿会先做出这样的側滑菜单,然后再在此基础上实现另外两个效果

    第一种
    这里写图片描写叙述

    另外一种
    这里写图片描写叙述

    第三种
    这里写图片描写叙述

    实现第一种側滑菜单,继承自ViewGroup

    继承自ViewGroup须要我们自己来測量,布局,实现滑动的效果,处理滑动冲突,这些都是一些新手无从下手的知识点。希望看了这篇文章后能够对大家有一个帮助

    自己定义ViewGroup的一般思路是重写onMeasure方法,在onMeasure方法中调用measureChild来測量子View。然后调用setMeasuredDimension来測量自己的大小。然后重写onLayout方法,在onLayout中调用子View的layout方法来确定子View的位置,以下我们先来做好这两件工作

    这里写图片描写叙述

    初始时候我们的Content应该是显示在屏幕中的。而Menu应该是显示在屏幕外的。当Menu打开时。应该是这样的样子的
    这里写图片描写叙述
    mMenuRightPadding是Menu距屏幕右側的一个距离,由于我们Menu打开后,Content还是会留一部分,而不是全然隐藏的

    public class MySlidingMenu extends ViewGroup {
    public MySlidingMenu(Context context) {
            this(context, null, 0);
        }
    
        public MySlidingMenu(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public MySlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            DisplayMetrics metrics = new DisplayMetrics();
            WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            wm.getDefaultDisplay().getMetrics(metrics);
             //获取屏幕的宽和高
            mScreenWidth = metrics.widthPixels;
            mScreenHeight = metrics.heightPixels;   
             //设置Menu距离屏幕右側的距离。convertToDp是将代码中的100转换成100dp
            mMenuRightPadding = convertToDp(context,100);     
        }
    
     @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //拿到Menu。Menu是第0个孩子
            mMenu = (ViewGroup) getChildAt(0);
            //拿到Content,Content是第1个孩子
            mContent = (ViewGroup) getChildAt(1);
            //设置Menu的宽为屏幕的宽度减去Menu距离屏幕右側的距离
            mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mMenuRightPadding;
            //设置Content的宽为屏幕的宽度
            mContentWidth = mContent.getLayoutParams().width = mScreenWidth;
            //測量Menu
            measureChild(mMenu,widthMeasureSpec,heightMeasureSpec);
            //測量Content
            measureChild(mContent, widthMeasureSpec, heightMeasureSpec);
            //測量自己,自己的宽度为Menu宽度加上Content宽度,高度为屏幕高度
            setMeasuredDimension(mMenuWidth + mContentWidth, mScreenHeight);
        }
    
    @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            //摆放Menu的位置,依据上面图能够确定上下左右的坐标
            mMenu.layout(-mMenuWidth, 0, 0, mScreenHeight);
            //摆放Content的位置
            mContent.layout(0, 0, mScreenWidth, mScreenHeight);
        }
    
    
    /**
         * 将传进来的数转化为dp
         */
        private int convertToDp(Context context , int num){
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,num,context.getResources().getDisplayMetrics());
        }
    }

    眼下我们的側滑菜单中的两个子View的位置应该是这个样子
    这里写图片描写叙述
    接下来我们编写xml布局文件

    left_menu.xml 左側菜单的布局文件,是一个ListView

    <?

    xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/menu_listview" android:layout_width="wrap_content" android:divider="@null" android:dividerHeight="0dp" android:scrollbars="none" android:layout_height="wrap_content"> </ListView> </RelativeLayout>

    当中ListView的Item布局为left_menu_item.xml

    <?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:gravity="center_vertical"
        android:layout_height="match_parent">
        <ImageView
            android:id="@+id/menu_imageview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/menu_1"
            android:padding="20dp"
            />
        <TextView
            android:id="@+id/menu_textview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="菜单1"
            android:textColor="#000000"
            android:textSize="20sp"
            />
    </LinearLayout>

    我们再来编写内容区域的布局文件 content.xml 当中有一个header。header中有一个ImageView。这个ImageView是menu的开关。我们点击他的时候能够自己主动开关menu,然后header以下也是一个listview

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="65dp"
            android:background="#000000"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            >
            <ImageView
                android:id="@+id/menu_toggle"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:src="@drawable/toggle"
                android:paddingLeft="10dp"
                />
        </LinearLayout>
            <ListView
                android:id="@+id/content_listview"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:dividerHeight="0dp"
                android:divider="@null"
                android:scrollbars="none"
                />
    </LinearLayout>

    content的item的布局文件为 content_item.xml

    <?

    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:gravity="center_vertical" android:background="#ffffff" android:layout_height="match_parent"> <ImageView android:id="@+id/content_imageview" android:layout_width="80dp" android:layout_height="80dp" android:src="@drawable/content_1" android:layout_margin="20dp" /> <TextView android:id="@+id/content_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Content - 1" android:textColor="#000000" android:textSize="20sp"/> </LinearLayout>

    在activity_main.xml中。我们将menu和content加入到我们的slidingMenu中

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#aaaaaa"
       >
    <com.example.user.slidingmenu.MySlidingMenu
        android:id="@+id/slidingmenu"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        >
            <include
                android:id="@+id/menu"
                layout="@layout/left_menu"
                />
            <include
                android:id="@+id/content"
                layout="@layout/content"
                />
    </com.example.user.slidingmenu.MySlidingMenu>
    
    </RelativeLayout>
    

    如今应该是这样的效果
    这里写图片描写叙述
    左側菜单是隐藏在屏幕左側外部的,可是如今还不能滑动,假设想要实现滑动功能。我们能够使用View的scrollTo和scrollBy方法,这两个方法的差别是scrollTo是直接将view移动到指定的位置,scrollBy是相对于当前的位置移动一个偏移量,所以我们应该重写onTouchEvent方法,用来计算出当前手指的一个偏移量,然后使用scrollBy方法一点一点的移动,就形成了一个能够尾随手指移动的view的动画效果了

    在写代码之前,我们先扫清一下障碍,我们先来弄清楚这些坐标是怎么回事

    这里写图片描写叙述
    这里写图片描写叙述
    这里写图片描写叙述

    好了,把这些坐标弄清楚后。我们就简单多了,以下直接看onTouchEvent方法

    @Override
        public boolean onTouchEvent(MotionEvent event) {
            int action = event.getAction();
            switch (action){
                case MotionEvent.ACTION_DOWN:
                    mLastX = (int) event.getX();
                    mLastY = (int) event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int currentX = (int) event.getX();
                    int currentY = (int) event.getY();
                    //拿到x方向的偏移量
                    int dx = currentX - mLastX;
                    if (dx < 0){//向左滑动
                        //边界控制。假设Menu已经全然显示。再滑动的话
                        //Menu左側就会出现白边了,进行边界控制
                        if (getScrollX() + Math.abs(dx) >= 0) {
                            //直接移动到(0,0)位置,不会出现白边
                            scrollTo(0, 0);
    
                        } else {//Menu没有全然显示呢
                            //事实上这里dx还是-dx。大家不用刻意去记
                            //大家能够先使用dx,然后运行一下,发现
                            //移动的方向是相反的。那么果断这里加个负号就能够了
                            scrollBy(-dx, 0);
    
                        }
    
                    }else{//向右滑动
                        //边界控制,假设Content已经全然显示,再滑动的话
                        //Content右側就会出现白边了,进行边界控制
                        if (getScrollX() - dx <= -mMenuWidth) {
                            //直接移动到(-mMenuWidth,0)位置,不会出现白边
                            scrollTo(-mMenuWidth, 0);
    
                        } else {//Content没有全然显示呢
                            //依据手指移动
                            scrollBy(-dx, 0);
    
                        }
    
                    }
                    mLastX = currentX;
                    mLastY = currentY;
    
                    break;
    
    
            }
            return true;
        }

    如今我们的SlidingMenu依旧是不能够水平滑动的。可是listview能够竖直滑动,原因是我们的SlidingMenu默认是不拦截事件的,那么事件会传递给他的子View去运行。也就是说传递给了Content的ListView去运行了。所以listview是能够滑动的。为了简单,我们先重写onInterceptTouchEvent方法,我们返回true,让SlidingMenu拦截事件,我们的SlidingMenu就能够滑动了。可是ListView是不能滑动的,等下我们会进行滑动冲突的处理。如今先实现SlidingMenu的功能

    @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return true;
        }

    好了,如今我们能够自由的滑动我们的SlidingMenu了,而且进行了很好的边界控制,如今我们再加入个功能。就是当Menu打开大于二分之中的一个时,松开手指,Menu自己主动打开。

    当Menu打开小于二分之中的一个时,松开手指。Menu自己主动关闭。自己主动滑动的功能我们要借助Scroller来实现

    我们在构造方法中初始化一个Scroller

    public MySlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            ...
            mScroller = new Scroller(context);
            ...
        }

    然后重写computeScroll方法,这种方法是保证Scroller自己主动滑动的必须方法,这是一个模板方法。到哪里都这么些就好了

     @Override
        public void computeScroll() {
            if (mScroller.computeScrollOffset()){
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                invalidate();
            }
        }

    接着我们在onTouchEvent的ACTION_UP中进行推断,推断当前menu打开了多少

      case MotionEvent.ACTION_UP:
                    if (getScrollX() < -mMenuWidth / 2){//打开Menu
                        //调用startScroll方法,第一个參数是起始X坐标,第二个參数
                        //是起始Y坐标。第三个參数是X方向偏移量,第四个參数是Y方向偏移量
                        mScroller.startScroll(getScrollX(), 0, -mMenuWidth - getScrollX(), 0, 300);
                        //设置一个已经打开的标识,当实现点击开关自己主动打开关闭功能时会用到
                        isOpen = true;
                        //一定不要忘了调用这种方法重绘。否则没有动画效果
                        invalidate();
                    }else{//关闭Menu
                        //同上
                        mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 300);
                        isOpen = false;
                        invalidate();
                    }
    
                    break;

    关于startScroll中的startX和startY好推断,那么dx和dy怎么计算呢?事实上也很easy。比方我们startX坐标为30,我们想移动到-100,那么startX+dx = -100 –> dx = -100 - startX –> dx = -130

    好了如今我们就能够实现松开手指后自己主动滑动的动画效果了
    如今我们还须要点击content中左上角的一个三角。假设当前menu没有打开,则自己主动打开。假设已经打开,则自己主动关闭的功能,自己主动滑动的效果我们要借助Scroller.startScroll方法

    /**
         * 点击开关。开闭Menu,假设当前menu已经打开。则关闭,假设当前menu已经关闭。则打开
         */
        public void toggleMenu(){
            if (isOpen){
                closeMenu();
            }else{
                openMenu();
            }
        }
    
        /**
         * 关闭menu
         */
        private void closeMenu() {
            //也是使用startScroll方法。dx和dy的计算方法一样
            mScroller.startScroll(getScrollX(),0,-getScrollX(),0,500);
            invalidate();
            isOpen = false;
        }
    
        /**
         * 打开menu
         */
        private void openMenu() {
            mScroller.startScroll(getScrollX(),0,-mMenuWidth-getScrollX(),0,500);
            invalidate();
            isOpen = true;
        }

    然后我们能够在MainActivity中拿到我们content左上角三角形的imageview。然后给他设置一个点击事件,调用我们的toggleMenu方法

    mMenuToggle.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mSlidingMenu.toggleMenu();
                }
            });

    处理滑动冲突

    由于我们的menu和content是listview。listview是支持竖直滑动的。而我们的slidingMenu是支持水平滑动的,因此会出现滑动的冲突。刚才我们直接在onInterceptTouchEvent中返回了true,因此SlidingMenu就会拦截全部的事件,而ListView接收不到不论什么的事件。因此ListView不能滑动了,我们要解决这个滑动冲突很easy,仅仅须要推断当前是水平滑动还是竖直滑动,假设是水平滑动的话则让SlidingMenu拦截事件。假设是竖直滑动的话就不拦截事件。把事件交给子View的ListView去运行

    @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercept = false;
            int x = (int) ev.getX();
            int y = (int) ev.getY();
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    intercept = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    int deltaX = (int) ev.getX() - mLastXIntercept;
                    int deltaY = (int) ev.getY() - mLastYIntercept;
                    if (Math.abs(deltaX) > Math.abs(deltaY)){//横向滑动
                        intercept = true;
                    }else{//纵向滑动
                        intercept = false;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    intercept = false;
                    break;
            }
            mLastX = x;
            mLastY = y;
            mLastXIntercept = x;
            mLastYIntercept = y;
            return intercept;
        }

    好了,如今我们的滑动冲突就攻克了,我们既能够水平滑动SlidingMenu,又能够竖直滑动ListView,那么第一种SlidingMenu就已经实现了。我们再来看看另外两种怎么去实现

    实现另外一种QQ V6.2.3风格的SlidingMenu

    这里写图片描写叙述
    这样的SlidingMenu是和QQ v6.2.3 的側滑菜单风格一致的。我们发现Menu和Content的滑动速度是有一个速度差的,实际上我们能够通过改动Menu的偏移量来达到这样的效果
    这里写图片描写叙述
    此时Menu的偏移量为mMenuWidth的2/3。当我们慢慢打开Menu的同一时候。改动Menu的偏移量。终于改动为0
    这里写图片描写叙述
    这样就达到了一种速度差的效果,我们仅仅须要在onTouchEvent的ACTION_MOVE和computeScroll中加入一行例如以下代码就能够

    mMenu.setTranslationX(2*(mMenuWidth+getScrollX())/3);

    我们分析一下,在最開始,mMenuWidth+getScrollX=mMenuWidth,再乘以2/3,得到的就是mMenuWidth的2/3 , 当我们滑动至Menu全然打开时。mMenuWidth+getScrollX=0 , 这就达到了我们的效果

    为什么要在computeScroll中也加入这一行代码呢。由于当我们滑动过程中,假设我们手指离开屏幕,ACTION_MOVE肯定就不运行了,可是当我们手指离开屏幕后,会有一段自己主动打开或者关闭的动画,那么这段动画应该继续去设置Menu的偏移量。因此我们在computeScroll中也要加入这一行代码。

    好了,效果我们已经实现了。仅仅须要去设置Menu的偏移量就能够了。是不是很easy

    实现第三种QQ V5.0风格的SlidingMenu

    这里写图片描写叙述
    这个效果中Menu有一个偏移的效果,透明度的变化以及放大的效果。

    Content中有一个缩小的效果。


    首先我们要有一个变量,用来记录当前menu已经打开了多少百分比。


    这里写图片描写叙述
    这里写图片描写叙述

    这里我们要注意,getScrollX得到的数值正好是负值,所以我们计算的时候要将getScrollX的值取绝对值再去计算,我们在onTouchEvent的MOVE中要计算这个值,同一时候在computeScroll方法中也要计算这个值。由于当我们手指抬起时,可能会运行一段自己主动打开或者关闭的动画,那么我们在MOVE中的计算肯定停止了,可是在运行动画的过程中,是Scroller在起作用,那么computeScroll就会运行直到动画结束。因此我们要在computeScroll中相同进行计算

    scale = Math.abs((float)getScrollX()) / (float) mMenuWidth;

    scale的值是[0,1]的,因此我们就能够依据这个值来对menu的偏移量进行设置。
    我们能够通过设置View的setScaleX和setScaleY来对View进行放大缩小,当然这个缩放比例要依据我们的scale值来改变,首先我们的Menu有一个放大的效果。我们就指定为Menu从0.7放大到1.0,那么我们就能够这样写

    mMenu.setScaleX(0.7f + 0.3f*scale);
            mMenu.setScaleY(0.7f + 0.3f*scale);

    透明度是从0到1的,所以我们直接用scale的值就能够了

            mMenu.setAlpha(scale);
    

    我还给Menu设置了一个偏移量。这个偏移量大家能够自己计算,我是这样计算的

    mMenu.setTranslationX(mMenuWidth + getScrollX() - (mMenuWidth/2)*(1.0f-scale));

    设置完Menu后,我们再来设置Content。Content的大小是从1.0缩小到0.7,因此我们这样写

    mContent.setScaleX(1 - 0.3f*scale);
            mContent.setPivotX(0);
            mContent.setScaleY(1.0f - 0.3f * scale);

    当中mContent.setPivotX(0)是让Content的缩放中心店的X轴坐标为0点

    我们能够将这个变化的过程抽取为一个方法

    private void slidingMode3(){
            mMenu.setTranslationX(mMenuWidth + getScrollX() - (mMenuWidth/2)*(1.0f-scale));
            mMenu.setScaleX(0.7f + 0.3f*scale);
            mMenu.setScaleY(0.7f + 0.3f*scale);
            mMenu.setAlpha(scale);
    
            mContent.setScaleX(1 - 0.3f*scale);
            mContent.setPivotX(0);
            mContent.setScaleY(1.0f - 0.3f * scale);
        }

    将这种方法加入到onTouchEvent的ACTION_MOVE和computeScroll中就能够了。

    我们看到全部的滑动风格都是在基于第一种基础上,改动Menu或者Content的translationX或者scaleX scaleY的值来决定的,因此我们能够打造各种各样的SlidingMenu来。

    完整代码

    完整代码大家能够到我的GitHub中下载

  • 相关阅读:
    Sherlock and Squares
    [leetcode] Super Ugly Number
    [leetcode] Ugly Number II
    [leetcode] Ugly Number
    [leetcode] Burst Balloons
    菜根谭#268
    菜根谭#267
    菜根谭#266
    菜根谭#265
    菜根谭#264
  • 原文地址:https://www.cnblogs.com/cxchanpin/p/7224154.html
Copyright © 2011-2022 走看看