鹅厂系列一 : 仿QQ侧滑菜单

Posted z8z87878

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了鹅厂系列一 : 仿QQ侧滑菜单相关的知识,希望对你有一定的参考价值。

——不会的东西你不尝试的去做,你永远都不会做

好了,跟随潮流,还是先看下效果,不然可能都没人想看下去了(不会看到效果后不想看了吧O(∩_∩)O~)

效果后期

额,图片资源来自QQ_374.APK,里面四五千个图片,找这几个没把我累死,当然感谢QQ的资源,额,.先来看看初始布局,我不知道腾讯是怎么布局的,我自己为了做的像他们一点,我的布局暂时是像下面这样的,到了自定义控件的时候,还会进行重新测量和布局.

重写前

嗯,就是让左面板在主面板的下面,所以我们自定义的控件SlideLayout继承FrameLayout.一般自定义控件会涉及到三个方法,onMeasure 测量,onLayout布局,onDraw绘制,如果是继承ViewGroup的话我们一般需要重写布局方法,继承view的话要重写onDraw方法,当然你喜欢的话,都可以重写.我们这里继承的是FrameLayout,它继承的是ViewGroup,即它已经实现了布局方法,但是我说过了,我们要重新测量和布局,主要是对左面板slideView的测量和布局,我们要调整它的宽度和位置,所以我们先来重写onMeasure方法.onMeasure(int widthMeasureSpec, int heightMeasureSpec)看这两个参数,看名字就知道中式英语很像有没有,测量说明书,即我们能通过测量说明书来进行测量,其实这两个数是测量说明书上给定的规范值,所以叫测量规范,结合我们下面的代码进行讲解

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        mSlideView = getChildAt(0);
        mMainView = getChildAt(1);

        mWd_width = MeasureSpec.getSize(widthMeasureSpec);     //使用测量说明书得到我们想要知道的宽度和高度值,它是match_parent的,所以拿到的就是窗体的宽高度
        mWd_height = MeasureSpec.getSize(heightMeasureSpec);

        int sl_widthSpec = MeasureSpec.makeMeasureSpec((mWd_width /3) * 2,MeasureSpec.EXACTLY); //制作测量说明书规定值
        int sl_heightSpec = MeasureSpec.makeMeasureSpec(mWd_height,MeasureSpec.EXACTLY);
        mSlideView.measure(sl_widthSpec,sl_heightSpec);

        mMainView.measure(widthMeasureSpec,heightMeasureSpec);   //主面板和自己一样,都是填充整个窗体,所以测量说明书的规定值一样

        setMeasuredDimension(mWd_width, mWd_height);            //测量自己用这个方法.
    }

制作测量规范值的函数,第一个参数的意思是,你想要分配多少像素吧,没有给解释,这个不重要….重要的是第二个参数,第二个参数有三个值可以填,分别是UNSPECIFIED,EXACTLY,AT_MOST,我们看它们单词的意思就好理解了,第一个说不确定,就叫它自己看着办;第二个意思是精确的,即我们生活中确定以及肯定,它会禁它最大的努力去按第一个参数的值测量,你的值填的离谱,它也满足不了.第三个就和第二个挺像了,尽量,就是比精确的肯定性差一点.所以我们如果自己确定一个测量规范的话,我确定我要侧滑面板占屏幕的三分之二,所以我们这里用EXACTLY.测量好了,接着就是我们的布局了

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mSl_width = mSlideView.getWidth();
        mSlideView.layout(mSlideView.getLeft() -  mSl_width /3 ,mSlideView.getTop(),mSlideView.getRight(),mSlideView.getBottom());
    }

布局的时候,说明测量已经完成了,所以我们是可以拿到我们左面板的宽度的,然后执行layout(l,t,r,b)看参数就明白了,这个不讲了,我把它的left设为-自己看的三分之一,是自己为了做的像一点的想法.来看看我们重写测量布局后的效果
布局后

好了,这时候开始我们的拖动了.视图拖动我们用这个类ViewDragHelper,视图拖动帮助者,首选看看怎么得到它的对象static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb)它是个静态方法,所以我们不是new出来的,第一个参数说拖动VIEW的父容器是谁,因为我们拖动的是主面板和左面板,所以他们的父容器是自己,即填this,第二个参数是灵敏度,它还有一个重载函数,只有两个参数,这个参数没写默认是1.0f,所以我们一般也填1.0f.第三个参数是ViewDragHelper 的内部类Callback ,我们需要写个类继承它.

class MyDragCallBack extends ViewDragHelper.Callback {

        @Override  //表示要捕获那个孩子,即处理哪个孩子的拖到事件,返回false不处理
        public boolean tryCaptureView(View child, int pointerId) {
            return true;
        }

这个是抽象方法,我们必须实现,因为两个孩子都要拖动,所以直接返回true,要像横向拖动,我们还得重写它的一个方法

   @Override //
        public int clampViewPositionHorizontal(View child, int left, int dx) {  //left相对于屏幕左侧的偏移值,是拖动的建议值

            if (child == mMainView){            //主面板拖动范围
                if (left < 0){
                    left = 0;
                }else if (left > (mSl_width)){
                    left = mSl_width;
                }

            }else if (child == mSlideView){      //左面板拖动范围
                if (left > 0){
                    left = 0;
                }else if (left < -mSl_width){
                    left = mSl_width;
                }

            }
            return left;
        }

它默认是返回0的,即拖动不了的,同理它还有纵向的,这里我们不需要,所以不重新,到这里还是拖不动的,因为,我们的ViewDragHelper拚什么拖动呢,我们还要把touch事件传递给他,它才能决定该不该拖动

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {


        return mViewDragHelper.shouldInterceptTouchEvent(event);   //交给它去判断该不该拦截
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        try {

            mViewDragHelper.processTouchEvent(event);    //有可能会出错,所以try一下
        }catch (Exception e){
            e.printStackTrace();
        }
        return true;
    }

好了,这里就可以拖动了,但是,只是一个view拖动了,另一个并不跟着动啊,所以我们还要另外重写一个函数,onViewPositionChanged看名字就知道了吧,拖动的view改变的时候调用,所以我们在这里动态的layout就好了

@Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);

            if (changedView == mMainView){

                mSlideView.layout( left/3 - mSl_width/3,mSlideView.getTop(),left/3 - mSl_width/3+mSl_width,mSlideView.getBottom());

            }else if (changedView == mSlideView){

                mMainView.layout(mSl_width + left,mMainView.getTop(),mSl_width + left + mWd_width,mMainView.getBottom());
            }




            invalidate();
        }

还有,当松开手的时候,我们应该看看左面板滑出来多少了,然后根据值判断是不是该关闭,即我们重写onViewReleased这个方法

@Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {

            if (mMainView.getLeft() < mSl_width/2){

                closeMenu();
            }else {

                openMenu();

            }
        }

 @Override
    public void computeScroll() {  //不停计算,不停调用
        if (mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);  //不停刷新直到停止滑动
        }
    }


    public void closeMenu() {

        mViewDragHelper.smoothSlideViewTo(mMainView,0,mMainView.getTop());
        ViewCompat.postInvalidateOnAnimation(this);  //兼容,刷新界面.
        mMenuIsOpen = false;

    }


    public void openMenu() {

        mViewDragHelper.smoothSlideViewTo(mMainView,mSl_width,mMainView.getTop());
        ViewCompat.postInvalidateOnAnimation(this);
        mSlideView.layout(0,mSlideView.getTop(),mSl_width,mSlideView.getBottom());
        mMenuIsOpen = true;
    }

在这里介绍下smoothSlideViewTo(View child, int finalLeft, int finalTop)看参数就明白了是吧,但是实现它的效果我们要ViewCompat.postInvalidateOnAnimation(this)刷新界面,这是兼容国产各大rom的,还要重写computeScroll()这个方法,continueSettling(true)返回true表示还没滑玩,所以继续刷新界面.好了到这里我们就做的差不多了,来看看效果吧
这里写图片描述

好吧,为什么会生成倒的gif图片我也不知道,因为总是大了,弄了好几次,把握不到度啊,治疗治疗颈椎病吧.看到这应该发现问题了吧,当在ViewGroup和listView上左右滑时划不动.看到这也许有人说了,去ononInterceptTouchEvent那设置滑动判断然后返回true拦截事件,再去Listview 的onTouchEvent判断返回false表示我不消费这个事件,我想说我都做了,而且log显示进入了我的判断拦截事件.还是不行,我都开始怀疑人生.怀疑我对touch事件的认知,就去网上看别人怎么说,说的都是我认知的,都不能解决这个问题.真的而且36度的天啊,很热,很沮丧的感觉.反正弄了将近一天,真的各种自己重写view拦截…全部没用…所以没原理没方法真的基本做不出来..突然重写了ViewDragHelper.callback的一个方法就好了,是的,就是这个方法,我真的无法解释,我都无语了.因为我想我事件都交给ViewDragHelper处理了,点ViewDragHelper.shouldInterceptTouchEvent(event)进去看源码,看不懂…看到一个变量名有drag什么,所以试一试的心态重写了这个方法的.真的不知道这是撒原理,平常我们不重写这个不是也能拖动么,所以很少重写,以后我每次都重写了

 @Override
        public int getViewHorizontalDragRange(View child) {
            // 返回拖拽的范围
            return mSl_width/3 * 2;
        }

重写后效果就能拖了,各控件的touch事件也正常,到这基本完了,最后来弄我们的拖动监听吧

public interface OnSlideListener{
        /**
         * @param view    //触摸的是主界面还是菜单界面
         * @param left  //滑动了多长,负数向左滑,正数向右
         * @param persent //滑动相对于总长度的百分比
         */
        void onSlide(View view,int left,float persent);
    }

    private OnSlideListener mOnSlideListener;
    public void setOnSlideListener(OnSlideListener listener){
        mOnSlideListener = listener;
    }

在 onViewPositionChanged中添加


            if (mOnSlideListener != null){
                mOnSlideListener.onSlide(changedView,left,left * 1.0f / mSl_width);
            }

嗯,这里还是贴下完整代码吧,等下资源审核通过后我把项目文件下载路径放在最下面,感兴趣的朋友可以去下载看看

import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

/**
 * Created by Root on 2016/6/20.
 */
public class SlideLayout extends FrameLayout{


    private View mSlideView;
    private View mMainView;
    private ViewDragHelper mViewDragHelper;
    private int mWd_width;
    private int mWd_height;
    private int mSl_width;
    private boolean mMenuIsOpen;


    public SlideLayout(Context context) {
        this(context,null);
    }

    public SlideLayout(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mViewDragHelper = ViewDragHelper.create(this,1.0f,new MyDragCallBack());


    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        mSlideView = getChildAt(0);
        mMainView = getChildAt(1);

        mWd_width = MeasureSpec.getSize(widthMeasureSpec);     //使用测量说明书得到我们想要知道的宽度和高度值
        mWd_height = MeasureSpec.getSize(heightMeasureSpec);

        int sl_widthSpec = MeasureSpec.makeMeasureSpec((mWd_width /3) * 2,MeasureSpec.EXACTLY); //制作测量说明书规定值
        int sl_heightSpec = MeasureSpec.makeMeasureSpec(mWd_height,MeasureSpec.EXACTLY);
        mSlideView.measure(sl_widthSpec,sl_heightSpec);

        mMainView.measure(widthMeasureSpec,heightMeasureSpec);   //主面板和自己一样,都是填充整个窗体,所以测量说明书的规定值一样

        setMeasuredDimension(mWd_width, mWd_height);            //测量自己用这个方法.
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mSl_width = mSlideView.getWidth();
        mSlideView.layout(mSlideView.getLeft() -  mSl_width /3 ,mSlideView.getTop(),mSlideView.getRight(),mSlideView.getBottom());
    }



    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {


        return mViewDragHelper.shouldInterceptTouchEvent(event);   //交给它去判断该不该拦截
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        try {

            mViewDragHelper.processTouchEvent(event);    //有可能会出错,所以try一下
        }catch (Exception e){
            e.printStackTrace();
        }
        return true;
    }


    class MyDragCallBack extends ViewDragHelper.Callback {

        @Override  //表示要捕获那个孩子,即处理哪个孩子的拖到事件,返回false不处理
        public boolean tryCaptureView(View child, int pointerId) {
            return true;
        }

        @Override //
        public int clampViewPositionHorizontal(View child, int left, int dx) {  //left相对于屏幕左侧的偏移值,是拖动的建议值

            if (child == mMainView){            //主面板拖动范围
                if (left < 0){
                    left = 0;
                }else if (left > (mSl_width)){
                    left = mSl_width;
                }

            }else if (child == mSlideView){      //左面板拖动范围
                if (left > 0){
                    left = 0;
                }else if (left < -mSl_width){
                    left = mSl_width;
                }

            }
            return left;
        }

        @Override
        public int getViewHorizontalDragRange(View child) {
            // 返回拖拽的范围
            return mSl_width/3 * 2;
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);

            if (changedView == mMainView){

                mSlideView.layout( left/3 - mSl_width/3,mSlideView.getTop(),left/3 - mSl_width/3+mSl_width,mSlideView.getBottom());

            }else if (changedView == mSlideView){

                mMainView.layout(mSl_width + left,mMainView.getTop(),mSl_width + left + mWd_width,mMainView.getBottom());
            }

            if (mOnSlideListener != null){
                mOnSlideListener.onSlide(changedView,left,left * 1.0f / mSl_width);
            }


            invalidate();
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {

            if (mMainView.getLeft() < mSl_width/2){

                closeMenu();
            }else {

                openMenu();

            }
        }

    }

    @Override
    public void computeScroll() {  //不停计算,不停调用
        if (mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);  //不停刷新直到停止滑动
        }
    }


    public void closeMenu() {

        mViewDragHelper.smoothSlideViewTo(mMainView,0,mMainView.getTop());
        ViewCompat.postInvalidateOnAnimation(this);  //兼容,刷新界面.
        mMenuIsOpen = false;

    }


    public void openMenu() {

        mViewDragHelper.smoothSlideViewTo(mMainView,mSl_width,mMainView.getTop());
        ViewCompat.postInvalidateOnAnimation(this);
        mSlideView.layout(0,mSlideView.getTop(),mSl_width,mSlideView.getBottom());
        mMenuIsOpen = true;
    }


    public boolean isMenuIsOpen(){
        return mMenuIsOpen;
    }


    public interface OnSlideListener{
        /**
         * @param view    //触摸的是主界面还是菜单界面
         * @param left  //滑动了多长,负数向左滑,正数向右
         * @param persent //滑动相对于总长度的百分比
         */
        void onSlide(View view,int left,float persent);
    }

    private OnSlideListener mOnSlideListener;
    public void setOnSlideListener(OnSlideListener listener){
        mOnSlideListener = listener;
    }


}

完整代码下载路径http://download.csdn.net/detail/z8z87878/9556794

以上是关于鹅厂系列一 : 仿QQ侧滑菜单的主要内容,如果未能解决你的问题,请参考以下文章

鹅厂系列四 : 仿QQ下拉刷新

Android使用DrawerLayout仿qq6.6版本侧滑效果

Android使用DrawerLayout仿qq6.6版本侧滑效果

仿QQ6.0主页面侧滑效果

OC仿QQ侧滑

Android高仿网易新闻客户端之侧滑菜单