(一).前言:
这两天QQ进行了重大更新(6.X)尤其在UI风格上面由之前的蓝色换成了白色居多了,侧滑效果也发生了一些变化,那我们今天来模仿实现一个QQ6.X版本的侧滑界面效果。今天我们还是采用神器ViewDragHelper来实现,之前我们以前基于ViewDragHelper的使用和打造QQ5.X效果了,基本使用方法可以点击下面的连接:
- 神器ViewDragHelper完全解析,妈妈再也不担心我自定义ViewGroup滑动View操作啦~
- 神器ViewDragHelper完全解析之详解实现QQ5.X侧滑酷炫效果
如果对于ViewDragHelper不是特别了解的朋友可以查看上面的文章学习一下。
本次实例具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。
https://github.com/jiangqqlmj/DragHelper4QQ
FastDev4Android框架项目地址:https://github.com/jiangqqlmj/FastDev4Android
(二).ViewDragHelper的基本使用
前面我们学习ViewDragHelper的基本使用方法,同时也知道了里边的若干个方法的用途,下面我们还是把基本的使用步骤温习一下。要使用ViewDragHelper实现子View拖拽移动的步骤如下:
- 创建ViewDragHelper实例(传入Callback)
- 重写事件拦截处理方法onInterceptTouch和onTouchEvent
- 实现Callback,实现其中的相关方法tryCaptureView以及水平或者垂直方向移动的距离方法
更加具体分析大家可以看前一篇博客,或者我们今天这边会通过具体实例讲解一下。
(三).QQ5.X侧滑效果实现分析:
在正式版本QQ中的侧滑效果如下:
观察上面我们可以理解为两个View,一个是底部的相当于左侧功能View,另外一个是上层主功能内容View,我们在上面进行拖拽上层View或者左右滑动的时候,上层和下层的View相应进行滑动以及View大小变化,同时加入相关的动画。当然我们点击上层的View可以进行打开或者关闭侧滑菜单。
(四).侧滑效果自定义组件实现
1.首先我们这边集成自FrameLayout创建一个自定义View DragLayout。内部的定义的一些变量如下(主要包括一些配置类,手势,ViewDragHelper实例,屏幕宽高,拖拽的子视图View等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
//是否带有阴影效果
private boolean isShowShadow =
true
;
//手势处理类
private GestureDetectorCompat gestureDetector;
//视图拖拽移动帮助类
private ViewDragHelper dragHelper;
//滑动监听器
private DragListener dragListener;
//水平拖拽的距离
private int range;
//宽度
private int width;
//高度
private int height;
//main视图距离在ViewGroup距离左边的距离
private int mainLeft;
private Context context;
private ImageView iv_shadow;
//左侧布局
private RelativeLayout vg_left;
//右侧(主界面布局)
private CustomRelativeLayout vg_main;
然后在内部还定义了一个回调接口主要处理拖拽过程中的一些页面打开,关闭以及滑动中的事件回调:
/**
* 滑动相关回调接口
*/
public interface DragListener {
//界面打开
public void onOpen();
//界面关闭
public void onClose();
//界面滑动过程中
public void onDrag(float percent);
}
|
2.开始创建ViewDragHelper实例,依然在自定义View DragLayout初始化的时候创建,使用ViewDragHelper的静态方法:
1
2
3
4
5
|
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super
(context, attrs, defStyle);
gestureDetector =
new
GestureDetectorCompat(context,
new
YScrollDetector());
dragHelper = ViewDragHelper.create(
this
, dragHelperCallback);
}
|
其中create()方法创建的时候传入了一个dragHelperCallBack回调类,将会在第四点中讲到。
3.接着需要重写ViewGroup中事件方法,拦截触摸事件给ViewDragHelper内部进行处理,这样达到拖拽移动子View视图的目的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/**
* 拦截触摸事件
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return
dragHelper.shouldInterceptTouchEvent(ev) && gestureDetector.onTouchEvent(ev);
}
/**
* 将拦截的到事件给ViewDragHelper进行处理
* @param e
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent e) {
try
{
dragHelper.processTouchEvent(e);
}
catch
(Exception ex) {
ex.printStackTrace();
}
return
false
;
}
|
这边我们在onInterceptTouchEvent拦截让事件从父控件往子View中转移,然后在onTouchEvent方法中拦截让ViewDragHelper进行消费处理。
4.开始自定义创建ViewDragHelper.Callback的实例dragHelperCallback分别实现一个抽象方法tryCaptureView以及重写以下若干个方法来实现侧滑功能,下面一个个来看一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
/**
* 拦截所有的子View
* @param child Child the user is attempting to capture
* @param pointerId ID of the pointer attempting the capture
* @return
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return
true
;
}
该进行拦截ViewGroup(本例中为:DragLayout)中所有的子View,直接返回
true
,表示所有的子View都可以进行拖拽移动。
/**
* 水平方向移动
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if
(mainLeft + dx < 0) {
return
0;
}
else
if
(mainLeft + dx > range) {
return
range;
}
else
{
return
left;
}
}
|
实现该方法表示水平方向滑动,同时方法中会进行判断边界值,例如当上面的main view已经向左移动边界之外了,直接返回0,表示向左最左边只能x=0;然后向右移动会判断向右最变得距离range,至于range的初始化后边会讲到。除了这两种情况之外,就是直接返回left即可。
1
2
3
4
5
6
7
8
9
|
/**
* 设置水平方向滑动的最远距离
* @param child Child view to check 屏幕宽度
* @return
*/
@Override
public int getViewHorizontalDragRange(View child) {
return
width;
}
|
该方法有必要实现,因为该方法在Callback内部默认返回0,也就是说,如果的view的click事件为true,那么会出现整个子View没法拖拽移动的情况了。那么这边直接返回left view宽度了,表示水平方向滑动的最远距离了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/**
* 当拖拽的子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 == vg_main && mainLeft > range * 0.3) {
open();
}
else
if
(releasedChild == vg_left && mainLeft > range * 0.7) {
open();
}
else
{
close();
}
}
|
该方法在拖拽子View移动手指释放的时候被调用,这是会判断移动向左,向右的意图,进行打开或者关闭man view(上层视图)。下面是实现的最后一个方法:onViewPositionChanged
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
/**
* 子View被拖拽 移动的时候回调的方法
* @param changedView View whose position changed
* @param left New X coordinate of the left edge of the view
* @param top New Y coordinate of the top edge of the view
* @param dx Change in X position from the last call
* @param dy Change in Y position from the last call
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
if
(changedView == vg_main) {
mainLeft = left;
}
else
{
mainLeft = mainLeft + left;
}
if
(mainLeft < 0) {
mainLeft = 0;
}
else
if
(mainLeft > range) {
mainLeft = range;
}
if
(isShowShadow) {
iv_shadow.layout(mainLeft, 0, mainLeft + width, height);
}
if
(changedView == vg_left) {
vg_left.layout(0, 0, width, height);
vg_main.layout(mainLeft, 0, mainLeft + width, height);
}
dispatchDragEvent(mainLeft);
}
};
|
该方法是在我们进行拖拽移动子View的过程中进行回调,根据移动坐标位置,然后进行重新定义left view和main view。同时调用dispathDragEvent()方法进行拖拽事件相关处理分发同时根据状态来回调接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* 进行处理拖拽事件
* @param mainLeft
*/
private void dispatchDragEvent(int mainLeft) {
if
(dragListener ==
null
) {
return
;
}
float percent = mainLeft / (float) range;
//根据滑动的距离的比例,进行带有动画的缩小和放大View
animateView(percent);
//进行回调滑动的百分比
dragListener.onDrag(percent);
Status lastStatus = status;
if
(lastStatus != getStatus() && status == Status.Close) {
dragListener.onClose();
}
else
if
(lastStatus != getStatus() && status == Status.Open) {
dragListener.onOpen();
}
}
|
该方法中有一行代码float percent=mainLeft/(float)range;算到一个百分比后面会用到
5.至于子View布局的获取初始化以及宽高和水平滑动距离的大小设置方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/**
* 布局加载完成回调
* 做一些初始化的操作
*/
@Override
protected void onFinishInflate() {
super
.onFinishInflate();
if
(isShowShadow) {
iv_shadow =
new
ImageView(context);
iv_shadow.setImageResource(R.mipmap.shadow);
LayoutParams lp =
new
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
addView(iv_shadow, 1, lp);
}
//左侧界面
vg_left = (RelativeLayout) getChildAt(0);
//右侧(主)界面
vg_main = (CustomRelativeLayout) getChildAt(isShowShadow ? 2 : 1);
vg_main.setDragLayout(
this
);
vg_left.setClickable(
true
);
vg_main.setClickable(
true
);
}
以及控件大小发生变化回调的方法:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super
.onSizeChanged(w, h, oldw, oldh);
width = vg_left.getMeasuredWidth();
height = vg_left.getMeasuredHeight();
//可以水平拖拽滑动的距离 一共为屏幕宽度的80%
range = (int) (width * 0.8f);
}
|
在该方法中我们可以实时获取宽和高以及拖拽水平距离。
6.上面的所有核心代码都为使用ViewDragHelper实现子控件View拖拽移动的方法,但是根据我们这边侧滑效果还需要实现动画以及滑动过程中View的缩放效果,所以我们这边引入了一个动画开源库:
然后根据前面算出来的百分比来缩放View视图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* 根据滑动的距离的比例,进行平移动画
* @param percent
*/
private void animateView(float percent) {
float f1 = 1 - percent * 0.5f;
ViewHelper.setTranslationX(vg_left, -vg_left.getWidth() / 2.5f + vg_left.getWidth() / 2.5f * percent);
if
(isShowShadow) {
//阴影效果视图大小进行缩放
ViewHelper.setScaleX(iv_shadow, f1 * 1.2f * (1 - percent * 0.10f));
ViewHelper.setScaleY(iv_shadow, f1 * 1.85f * (1 - percent * 0.10f));
}
}
|
7.当然除了上面这些还缺少一个效果就是,当我们滑动过程中假如我们手指释放,按照常理来讲view就不会在进行移动了,那么这边我们需要一个加速度当我们释放之后,还能保持一定的速度,该怎么样实现呢?答案就是实现computeScroll()方法。
1
2
3
4
5
6
7
8
9
|
/**
* 有加速度,当我们停止滑动的时候,该不会立即停止动画效果
*/
@Override
public void computeScroll() {
if
(dragHelper.continueSettling(
true
)) {
ViewCompat.postInvalidateOnAnimation(
this
);
}
}
|
OK上面关于DragLayout的核心代码就差不多这么多了,下面是使用DragLayout类来实现侧滑效果啦!
(五).侧滑效果组件使用
1.首先使用的布局文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
<com.chinaztt.widget.DragLayout xmlns:android=
"http://schemas.android.com/apk/res/android"
android:id=
"@+id/dl"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:background=
"@android:color/transparent"
>
<!--下层 左边的布局-->
<include layout=
"@layout/left_view_layout"
/>
<!--上层 右边的主布局-->
<com.chinaztt.widget.CustomRelativeLayout
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:background=
"#FFFFFF"
>
<LinearLayout
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
android:orientation=
"vertical"
>
<RelativeLayout
android:id=
"@+id/rl_title"
android:layout_width=
"match_parent"
android:layout_height=
"49dp"
android:gravity=
"bottom"
android:background=
"@android:color/holo_orange_light"
>
<include layout=
"@layout/common_top_bar_layout"
/>
</RelativeLayout>
<!--中间内容后面放入Fragment-->
<FrameLayout
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
>
<fragment
android:id=
"@+id/main_info_fragment"
class=
"com.chinaztt.fragment.OneFragment"
android:layout_width=
"fill_parent"
android:layout_height=
"fill_parent"
/>
</FrameLayout>
</LinearLayout>
</com.chinaztt.widget.CustomRelativeLayout>
</com.chinaztt.widget.DragLayout>
|
该布局文件中父层View就是DragLayout,然后内部有两个RelativeLayout布局,分别充当下一层布局和上一层主布局。
2.下面我们来看一下下层菜单布局,这边我专门写了一个left_view_layout.xml文件,其中主要分为三块,第一块顶部为头像个人基本信息布局,中间为功能入口列表,底部是设置等功能,具体布局代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
<RelativeLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app=
"http://schemas.android.com/apk/res-auto"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:paddingTop=
"70dp"
android:background=
"@drawable/sidebar_bg"
>
<LinearLayout
android:id=
"@+id/ll1"
android:paddingLeft=
"30dp"
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:orientation=
"vertical"
>
<!--头像,昵称信息-->
<LinearLayout
android:layout_width=
"match_parent"
android:layout_height=
"70dp"
android:orientation=
"horizontal"
android:gravity=
"center_vertical"
>
<com.chinaztt.widget.RoundAngleImageView
android:id=
"@+id/iv_bottom"
android:layout_width=
"50dp"
android:layout_height=
"50dp"
android:scaleType=
"fitXY"
android:src=
"@drawable/icon_logo"
app:roundWidth=
"25dp"
app:roundHeight=
"25dp"
/>
<LinearLayout
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
android:gravity=
"center_vertical"
android:layout_gravity=
"center_vertical"
android:orientation=
"vertical"
>
<RelativeLayout
android:layout_width=
"fill_parent"
android:layout_height=
"wrap_content"
>
<TextView
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_centerVertical=
|