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