Android FrameLayout+ViewDragHelper实现QQ7.1.0侧滑菜单

大家好!这篇介绍FrameLayout+ViewDragHelper实现QQ7.1.0侧滑菜单,在QQ侧滑菜单上我而外添加了主页面左侧阴影效果(希望出现立体效果),实际上功能很简单,如果我有没有写好的地方,欢迎大家多多意见哈,如果你有任何想法都可以提出来探讨。
其他先不说,我们先上最后的效果图,我效果图录制的不好,后面我争取录制更好的,大家不要介意哈:

GIF.gif

1、背景及其基础知识

a、FrameLayout(帧布局)

帧布局很简单布局,属于Android六大布局之一,它里面所有的一级控件都是叠加放在左上角的,没有相关位置属性。其他有关帧布局的详情看大神们写的Android布局详解之一:FrameLayout,在这里我就不多说了哈,我们主要用到的是自定义帧布局,也就是继承FrameLayout布局。

b、ViewDragHelper

官方在v4的支持包中,提供了ViewDragHelper这样一个类帮助我们方便的编写自定义ViewGroup,非常强大和好用的类,可以扩展很多特效出来,下拉、侧滑等等,官方注解:

/**
 * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
 * of useful operations and state tracking for allowing a user to drag and reposition
 * views within their parent ViewGroup.
 */

用法
第一步,声明

mDragger = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());

第一个参数当前ViewGroup,第二个参数主要是控制灵敏度,主要设置触动速度,第三个参数最重要,许多操作都在里面,继承ViewDragHelper.Callback抽象类,自己实现里面需要实现的方法
第二步,拦截onInterceptTouchEvent交给ViewDragHelper处理,onTouchEvent也需要传给它

 /**
     * 拦截触摸事件
     * @param ev
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mDragger.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragger.processTouchEvent(event);
        return true;
    }

第三步,实现ViewDragHelper.Callback抽象类里相关的方法,简单介绍下其中重要的一些方法的意思:

//进行捕获拦截,那些View可以进行drag操作
//传递当前触摸的子View实例,如果当前的子View需要进行拖拽移动返回true
tryCaptureView(View child, int pointerId)
//控制水平方向移动的范围
clampViewPositionHorizontal(View child, int left, int dx)
//控制垂直方向移动的范围
clampViewPositionVertical(View child, int top, int dy)
//返回可拖动的子视图的垂直移动范围的大小。 这个方法应该返回0,因为视图不能垂直移动。
getViewVerticalDragRange(View child)
//返回可拖动的子视图的水平移动范围的大小。 这个方法应该返回0,因为视图不能水平移动。
getViewHorizontalDragRange(View child)
//当捕获视图的位置发生变化时调用。
onViewPositionChanged(View changedView, int left, int top, int dx, int dy)
//调用子视图不再被积极地拖拽。如果相关的话,还提供了抛掷速度
onViewReleased(View releasedChild, float xvel, float yvel)
//当父视图中的一个订阅边被触摸时调用 ,由用户而不是子视图当前捕获。
//注意,必须开启边界使用的模式:dragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
onEdgeTouched(int edgeFlags, int pointerId)
//当人为的从边界拖动(此时并没有拖动子view)时,可以选择关联某一个子view,实现只要从边界拖动,不管是否触碰到子view,都能控制子view一起拖动的效果
//注意同上个方法,必须要开启才可以使用
onEdgeDragStarted(int edgeFlags, int pointerId)

2、编写的步骤和思路(我代码里有非常详细的注释,相信基本都能看懂)

a、基础类创建

创建自定义VDHLayout继承FrameLayout,实现里面主要方法:

private Context context;
//定义一个ViewDragHelper
public ViewDragHelper mDragger;

public VDHLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        mDragger = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
    }

b、创建布局(***-这个是表示必须需要的布局)



    
    
        
        
        
        

    
    
    
    
    
        
        

        
        
        
        

    


c、申明要使用的变量和初始化数据

//主页面
    private RelativeLayout mainView;
    //左侧菜单
    private RelativeLayout leftView;
    //主页面左边阴影
    private LinearLayout left_shade;
    //左侧菜单宽高
    private int width, height;
    //主页面宽度,屏幕宽度
    private int mainWidth;
    //水平拖拽的距离
    private int range;
    //自定义监听事件
    private VDHLayoutListener vdhLayoutListener;
    //页面状态 默认为关闭
    private Status status = Status.CLOSE;
    //主页面close时,逐渐变暗
    private ImageView main_top_bg;
    //滑动进度
    private float percent;

/**
     * 调用进行left和main 视图进行初始位置布局
     * @param changed
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        leftView.layout(-(width / 2), 0, width / 2, height);
        mainView.layout(0, 0, mainWidth, height);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = leftView.getMeasuredWidth();
        height = leftView.getMeasuredHeight();
        mainWidth = getWidth();
//        width = (int)(mainWidth*0.5);
    }

    /**
     * 当View中所有的子控件均被映射成xml后触发
     * 

* 布局加载完成回调 * 做一些初始化的操作 */ @Override protected void onFinishInflate() { super.onFinishInflate(); leftView = (RelativeLayout) getChildAt(0); left_shade = (LinearLayout) getChildAt(1); mainView = (RelativeLayout) getChildAt(2); main_top_bg = (ImageView) mainView.findViewById(R.id.main_top_bg); main_top_bg.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { close(); } }); main_top_bg.setAlpha(0f); main_top_bg.setVisibility(GONE); }

d、限制侧边布局滑动范围(通过ViewDragHelper.Callback限制)

首先进行哪些View可以移动的捕获

       /**
         * 进行捕获拦截,那些View可以进行drag操作
         * 传递当前触摸的子View实例,如果当前的子View需要进行拖拽移动返回true
         * @param child
         * @param pointerId
         * @return 直接返回true,拦截所有的VIEW
         */
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return true;
        }

限制View只能水平移动

       /**
         * 决定拖拽的View在垂直方向上面移动到的位置
         * @param child
         * @param top
         * @param dy
         * @return
         */
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            top = 0;
            return top;
        }

限制View水平上移动的范围,水平上移动的范围不能超过侧边页面的宽度width

       /**
         * 决定拖拽的View在水平方向上面移动到的位置
         * @param child
         * @param left
         * @param dx
         * @return
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            if (child == mainView) {//主页面按住滑动
            }
            if (child == leftView) {//侧边页面按住滑动
                left = mainView.getLeft() + dx;
            }
            if (left < 0) {
                left = 0;
            } else if (left > width) {
                left = width;
            }
            return left;
        }

e、侧边和主页面滑动控制(通过ViewDragHelper.Callback控制),这是最重要的

我们可以从QQ上操作看到,侧边页面是两边往中间收缩,速度只有主页面的一般
代码如下:

       /**
         * 当前被触摸的View位置变化时回调
         * changedView为位置变化的View,left/top变化时新的x左/y顶坐标,dx/dy为从旧到新的偏移量
         * @param changedView
         * @param left
         * @param top
         * @param dx
         * @param dy
         */
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            //设置主页面位置
            leftView.layout(left / 2 - width / 2, 0, width / 2 + left / 2, height);
            //设置侧边页面位置
            mainView.layout(left, 0, mainWidth + left, height);
            //设置主页面左边阴影位子
            left_shade.layout(left - left_shade.getMeasuredWidth(), 0, left, height);
            //记录移动位置
            range = left;
            //滑动时主页面上明暗变化
            percent = range / (float) width;
            main_top_bg.setAlpha(percent / 2f);
            if (percent == 0) {
                main_top_bg.setVisibility(View.GONE);
            } else {
                main_top_bg.setVisibility(View.VISIBLE);
            }
            //滑动进度返回
            if (vdhLayoutListener != null) {
                vdhLayoutListener.onDrag(percent);
            }
        }

f、当手指松开,页面自动判断位置,进行关闭或者打开侧滑

       /**
         * 当拖拽的子View,手势释放的时候回调的方法, 然后根据左滑或者右滑的距离进行判断打开或者关闭
         * @param releasedChild
         * @param xvel
         * @param yvel
         */
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            if (xvel > 0) {
                open();
            } else if (xvel < 0) {
                close();
            } else if (releasedChild == mainView && range > width / 2) {
                open();
            } else if (releasedChild == leftView && range < width / 2) {
                close();
            } else if (releasedChild == mainView) {
                close();
            } else {
                open();
            }
        }

   /**
     * 关闭侧边菜单
     */
    public void close() {
        //继续滑动
        if (mDragger.smoothSlideViewTo(mainView, 0, 0)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
        if (vdhLayoutListener != null) {
            vdhLayoutListener.close();
        }
    }

   /**
     * 打开侧边菜单
     */
    public void open() {
        if (mDragger.smoothSlideViewTo(mainView, width, 0)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
        if (vdhLayoutListener != null) {
            vdhLayoutListener.open();
        }
    }

3、主要代码的源码

package com.xiaoqiang.qqmenu.view;

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;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;

import com.xiaoqiang.qqmenu.R;

/** 
 * description: 防QQ 7.1.0侧滑菜单
 * autour: xiaoqiang
 * mail:[email protected]
 * qq:773860458
 * date: 2017/6/16 14:28
*/
public class VDHLayout extends FrameLayout {

    private Context context;
    //定义一个ViewDragHelper
    public ViewDragHelper mDragger;
    //主页面
    private RelativeLayout mainView;
    //左侧菜单
    private RelativeLayout leftView;
    //主页面左边阴影
    private LinearLayout left_shade;
    //左侧菜单宽高
    private int width, height;
    //主页面宽度,屏幕宽度
    private int mainWidth;
    //水平拖拽的距离
    private int range;
    //自定义监听事件
    private VDHLayoutListener vdhLayoutListener;
    //页面状态 默认为关闭
    private Status status = Status.CLOSE;
    //主页面close时,逐渐变暗
    private ImageView main_top_bg;
    //滑动进度
    private float percent;

    public VDHLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        mDragger = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
    }

    class DragHelperCallback extends ViewDragHelper.Callback {
        /**
         * 进行捕获拦截,那些View可以进行drag操作
         * 传递当前触摸的子View实例,如果当前的子View需要进行拖拽移动返回true
         * @param child
         * @param pointerId
         * @return 直接返回true,拦截所有的VIEW
         */
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return true;
        }

        /**
         * 决定拖拽的View在水平方向上面移动到的位置
         * @param child
         * @param left
         * @param dx
         * @return
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            if (child == mainView) {//主页面按住滑动
            }
            if (child == leftView) {//侧边页面按住滑动
                left = mainView.getLeft() + dx;
            }
            if (left < 0) {
                left = 0;
            } else if (left > width) {
                left = width;
            }
            return left;
        }

        /**
         * 决定拖拽的View在垂直方向上面移动到的位置
         * @param child
         * @param top
         * @param dy
         * @return
         */
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            top = 0;
            return top;
        }


        @Override
        public int getViewHorizontalDragRange(View child) {
            return 1;
        }

        /**
         * 当前被触摸的View位置变化时回调
         * changedView为位置变化的View,left/top变化时新的x左/y顶坐标,dx/dy为从旧到新的偏移量
         * @param changedView
         * @param left
         * @param top
         * @param dx
         * @param dy
         */
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            //设置主页面位置
            leftView.layout(left / 2 - width / 2, 0, width / 2 + left / 2, height);
            //设置侧边页面位置
            mainView.layout(left, 0, mainWidth + left, height);
            //设置主页面左边阴影位子
            left_shade.layout(left - left_shade.getMeasuredWidth(), 0, left, height);
            //记录移动位置
            range = left;
            //滑动时主页面上明暗变化
            percent = range / (float) width;
            main_top_bg.setAlpha(percent / 2f);
            if (percent == 0) {
                main_top_bg.setVisibility(View.GONE);
            } else {
                main_top_bg.setVisibility(View.VISIBLE);
            }
            //滑动进度返回
            if (vdhLayoutListener != null) {
                vdhLayoutListener.onDrag(percent);
            }
        }

        /**
         * 当拖拽的子View,手势释放的时候回调的方法, 然后根据左滑或者右滑的距离进行判断打开或者关闭
         * @param releasedChild
         * @param xvel
         * @param yvel
         */
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            if (xvel > 0) {
                open();
            } else if (xvel < 0) {
                close();
            } else if (releasedChild == mainView && range > width / 2) {
                open();
            } else if (releasedChild == leftView && range < width / 2) {
                close();
            } else if (releasedChild == mainView) {
                close();
            } else {
                open();
            }
        }
    }

    /**
     * 拦截触摸事件
     * @param ev
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mDragger.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragger.processTouchEvent(event);
        return true;
    }

    /**
     * 调用进行left和main 视图进行初始位置布局
     * @param changed
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        leftView.layout(-(width / 2), 0, width / 2, height);
        mainView.layout(0, 0, mainWidth, height);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = leftView.getMeasuredWidth();
        height = leftView.getMeasuredHeight();
        mainWidth = getWidth();
//        width = (int)(mainWidth*0.5);
    }

    /**
     * 当View中所有的子控件均被映射成xml后触发
     * 

* 布局加载完成回调 * 做一些初始化的操作 */ @Override protected void onFinishInflate() { super.onFinishInflate(); leftView = (RelativeLayout) getChildAt(0); left_shade = (LinearLayout) getChildAt(1); mainView = (RelativeLayout) getChildAt(2); main_top_bg = (ImageView) mainView.findViewById(R.id.main_top_bg); main_top_bg.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { close(); } }); main_top_bg.setAlpha(0f); main_top_bg.setVisibility(GONE); } /** * 有加速度,当我们停止滑动的时候,该不会立即停止动画效果 */ @Override public void computeScroll() { if (mDragger.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } } /** * 页面状态(滑动,打开,关闭) */ public enum Status { DRAG, OPEN, CLOSE } /** * 页面状态设置 * * @return */ public Status getStatus() { if (range == 0) { status = Status.CLOSE; } else if (range == width) { status = Status.OPEN; } else { status = Status.DRAG; } return status; } /** * 关闭侧边菜单 */ public void close() { //继续滑动 if (mDragger.smoothSlideViewTo(mainView, 0, 0)) { ViewCompat.postInvalidateOnAnimation(this); } if (vdhLayoutListener != null) { vdhLayoutListener.close(); } } /** * 打开侧边菜单 */ public void open() { if (mDragger.smoothSlideViewTo(mainView, width, 0)) { ViewCompat.postInvalidateOnAnimation(this); } if (vdhLayoutListener != null) { vdhLayoutListener.open(); } } public interface VDHLayoutListener { //打开侧边页面 void open(); //关闭侧边页面 void close(); //打开关闭侧边页面进度返回,0——关闭,1——打开 void onDrag(float percent); } public void setVdhLayoutListener(VDHLayoutListener vdhLayoutListener) { this.vdhLayoutListener = vdhLayoutListener; } }

4、工程代码下载

源码下载

谢谢大家的观看,谢谢大家的支持,谢谢大家的喜欢,谢谢大家的关注,每篇文章,我都会尽我最大的力量编写,谢谢!分享一些好的技术,Kotlin技术最近也逐渐火起来了,后续我也会增加Kotlin分享,也许以后Android都用Kotlin进行开发。
下一期将发表个小的效果,用Toast实现一个顶部自定义通知,欢迎大家关注,提意见,谢谢!

你可能感兴趣的:(Android FrameLayout+ViewDragHelper实现QQ7.1.0侧滑菜单)