引子
自定义ViewGroup,用于实现复杂的控件特效。凡是见到的非常花哨牛逼的效果,大多可以分解为若干个 小的效果,然后通过自定义ViewGroup进行组合。但是,在组合的过程中,明明两个牛逼控件各自运行好好的,组合起来就浑身毛病,比较多见的就是滑动冲突。
今天,提供一个可横向滑动的ViewGroup,内部可以放置多个子View,而且子View可以带竖向滑动效果。
本文只提供一个基础控件,重在提供一个写控件的思路,也让我自己日后温故知新。
注意:以下控件并没有考虑ViewGroup的padding和margin,所以,如果放到真实场景下,必然要做修改。
效果图
(每一个子view都是listView,纵向的滑动效果我没有录,相信大家都能看明白)
关键类或方法:
1)重写自定义layout的onMeasure,onLayout,让某一个子view占满layout,其他的都在屏幕之外
2)View基类本身自带的scrollBy方法,配合自定义layout的onTouchEvent截取的触摸事件,实现滑动
3)重写自定义layout的onInterceptTouchEvent方法,解决滑动冲突
4)Scroller类,实现layout的平滑回滚,用于当你滑到layout边界之外时回滚到界内,或者你想滚到某一个子view
5)VelocityTracker类,实现滑动速率的监听,当滑动速率超过临界值时,就算没有滑到下一个子view的临界点,也要用Scroller来平滑滚动到下一个子view
5)最后提一下,上面几个都是基于android框架的内容,但是仅仅有他们还不够,最后需要我们用自己的计算方式,结合1,2,3,4,5的原理,实现我们自己想要的效果。
我观察过网上很多人写的博客,发现每个人实现这个效果的计算方式各不相同。android框架的原理也许我们都能理解,但是能够写出来的控件质量有高有低,就看个人的数学修为了。
不得不说,数学思维逻辑还是很有用的。
源代码(拷贝到项目内可以直接使用)
HorizontalScrollViewEx.java 这个是自定义控件的源码
1 package tt.zhou;
2
3 import android.content.Context;
4 import android.util.AttributeSet;
5 import android.util.Log;
6 import android.view.MotionEvent;
7 import android.view.VelocityTracker;
8 import android.view.ViewConfiguration;
9 import android.view.ViewGroup;
10 import android.widget.Scroller;
11
12 /**
13 * 可以横向滚动的viewGroup,兼容纵向滚动的子view
14 */
15 public class HorizontalScrollViewEx extends ViewGroup {
16
17 //第一步,定义一个追踪器引用
18 private VelocityTracker mVelocityTracker;//滑动速度追踪器
19
20
21 public HorizontalScrollViewEx(Context context) {
22 this(context, null);
23 }
24
25 public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
26 this(context, attrs, 0);
27 }
28
29 public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
30 super(context, attrs, defStyleAttr);
31 init(context);
32 }
33
34 private void init(Context context) {
35 mScroller = new Scroller(context);
36 //初始化追踪器
37 mVelocityTracker = VelocityTracker.obtain();//获得追踪器对象,这里用obtain,按照谷歌的尿性,应该是考虑了对象重用
38 }
39
40 int childCount;
41
42 /**
43 * 确定每一个子view的宽高
44 * <p>
45 * 如果是逐个去测量子view的话,必须在测量之后,调用setMeasuredDimension来设置宽高
46 * <p>
47 * 这里测量出来的宽高,会在onLayout中用来作为参考
48 *
49 * @param widthMeasureSpec
50 * @param heightMeasureSpec
51 */
52 @Override
53 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//spec 测量模式,
54
55 int width = MeasureSpec.getSize(widthMeasureSpec);
56 int height = MeasureSpec.getSize(heightMeasureSpec);
57 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
58 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
59
60 childCount = getChildCount();
61 measureChildren(widthMeasureSpec, heightMeasureSpec);//逐个测量所有的子view
62
63 if (childCount == 0) {//如果子view数量为0,
64 setMeasuredDimension(0, 0);//那么整个viewGroup宽高也就是0
65 } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {//如果viewGroup的宽高都是matchParent
66 width = childCount * getChildAt(0).getMeasuredWidth();// 那么,本viewGroup的宽,就是index为0的子view的测量宽度 乘以 子view的个数
67 height = getChildAt(0).getMeasuredHeight();//高,就是子view的高
68 setMeasuredDimension(width, height);//用子view的宽高,来设定
69 } else if (widthMode == MeasureSpec.AT_MOST) {
70 width = childCount * getChildAt(0).getMeasuredWidth();
71 setMeasuredDimension(width, height);
72 } else {
73 height = getChildAt(0).getMeasuredHeight();
74 setMeasuredDimension(width, height);
75 Log.d("setMeasuredDimension", "" + width);
76 }
77 }
78
79 /**
80 * 这个方法用于,处理布局所有的子view,让他们按照代码写的规则去排布
81 *
82 * @param changed
83 * @param l left,当前viewGroup的左边线距离父组件左边线的距离
84 * @param t top,当前viewGroup的上边线距离父组件上边线的距离
85 * @param r right,当前viewGroup的左边线距离父组件右边线的距离
86 * @param b bottom,当前viewGroup的上边线距离父组件下边线的距离
87 */
88 @Override
89 protected void onLayout(boolean changed, int l, int t, int r, int b) {
90 Log.d("onLayout", ":" + l + "-" + t + "-" + r + "-" + b);
91 int count = getChildCount();
92 int offsetX = 0;
93 for (int i = 0; i < count; i++) {
94 int w = getChildAt(i).getMeasuredWidth();
95 int h = getChildAt(i).getMeasuredHeight();
96 Log.d("onLayout", "w:" + w + " - h:" + h);
97
98 getChildAt(i).layout(offsetX + l, t, offsetX + l + w, b);//保证每次都最多只完整显示一个子view,因为在onMeasure中,已经将子view的宽度设置为了 本viewGroup的宽度
99 offsetX += w;//每次的偏移量都递增
100 }
101 }
102
103
104 private float lastInterceptX, lastInterceptY;
105
106 /**
107 * 事件的拦截,
108 *
109 * @param event
110 * @return
111 */
112 @Override
113 public boolean onInterceptTouchEvent(MotionEvent event) {
114 boolean ifIntercept = false;
115 switch (event.getAction()) {
116 case MotionEvent.ACTION_DOWN:
117 lastInterceptX = event.getRawX();
118 lastInterceptY = event.getRawY();
119 break;
120 case MotionEvent.ACTION_MOVE:
121 //检查是横向移动的距离大,还是纵向
122 float xDistance = Math.abs(lastInterceptX - event.getRawX());
123 float yDistance = Math.abs(lastInterceptY - event.getRawY());
124 if (xDistance > yDistance) {
125 ifIntercept = true;
126 } else {
127 ifIntercept = false;
128 }
129 break;
130 case MotionEvent.ACTION_UP:
131 break;
132 case MotionEvent.ACTION_CANCEL:
133 break;
134 }
135 return ifIntercept;
136 }
137
138 private float downX;
139 private float distanceX;
140 private boolean isFirstTouch = true;
141 private int childIndex = -1;
142
143 @Override
144 public boolean onTouchEvent(MotionEvent event) {
145 int scrollX = getScrollX();//控件的左边界,与屏幕原点的X轴坐标
146 int scrollXMax = (getChildCount() - 1) * getChildAt(1).getMeasuredWidth();
147 final int childWidth = getChildAt(0).getWidth();
148 mVelocityTracker.addMovement(event);//在onTouchEvent这里,截取event对象
149 ViewConfiguration configuration = ViewConfiguration.get(getContext());
150 switch (event.getAction()) {
151 case MotionEvent.ACTION_DOWN:
152 break;
153 case MotionEvent.ACTION_MOVE:
154 //先让你滑动起来
155 float moveX = event.getRawX();
156 if (isFirstTouch) {//一次事件序列,只会赋值一次?
157 downX = moveX;
158 isFirstTouch = false;
159 }
160 Log.d("distanceX", "" + downX + "|" + moveX + "|" + distanceX);
161 distanceX = downX - moveX;
162
163 //判定是否可以滑动
164 //这里有一个隐患,由于不知道Move事件,会以什么频率来分发,所以,这里多少都会出现一点误差
165 if (getChildCount() >= 2) {//子控件在2个或者2个以上时,才有下面的效果
166 //如果命令是向左滑动,distanceX>0 ,那么判断命令是否可以执行
167 //如果命令是向右滑动,distanceX<0 ,那么判断命令是否可以执行
168 Log.d("scrollX", "scrollX:" + scrollX);
169 if (distanceX <= 0) {
170 if (scrollX >= 0)
171 scrollBy((int) distanceX, 0);//滑动
172 } else {
173 if (scrollX <= scrollXMax)
174 scrollBy((int) distanceX, 0);//滑动
175 }
176 }//如果只有一个,则不允许滑动,防止bug
177 break;
178 case MotionEvent.ACTION_UP:// 当手指松开的时候,要显示某一个完整的子view
179 mVelocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());//计算,最近的event到up之间的速率
180 float xVelocity = mVelocityTracker.getXVelocity();//当前横向的移动速率
181 float edgeXVelocity = configuration.getScaledMinimumFlingVelocity();//临界点
182 childIndex = (scrollX + childWidth / 2) / childWidth;//整除的方式,来确定X轴应该所在的单元,将每一个item的竖向中间线定为滑动的临界线
183 if (Math.abs(xVelocity) > edgeXVelocity) {//如果当前横向的速率大于零界点,
184 childIndex = xVelocity > 0 ? (childIndex - 1) : (childIndex + 1);//xVelocity正数,表示从左往右滑,所以child应该是要显示前面一个
185 }
186 // childIndex = Math.min(getChildCount() - 1, Math.max(childIndex, 0));//不可以超出左右边界,这种写法可能很难一眼看懂,那就替换成下面的写法
187 if (childIndex < 0)//计算出的childIndex可能是负数。那就赋值为0
188 childIndex = 0;
189 else if (childIndex >= getChildCount()) {//也有可能超出childIndex的最大值,那就赋值为最大值-1
190 childIndex = getChildCount() - 1;
191 }
192 smoothScrollBy(childIndex * childWidth - scrollX, 0);// 回滚的距离
193 mVelocityTracker.clear();
194 isFirstTouch = true;
195 break;
196 case MotionEvent.ACTION_CANCEL:
197 break;
198 }
199 downX = event.getRawX();
200 return super.onTouchEvent(event);
201 }
202
203 //实现平滑地回滚
204
205 /**
206 * 最叼的还是这个方法,平滑地回滚,从当前位置滚到目标位置
207 * @param dx
208 * @param dy
209 */
210 void smoothScrollBy(int dx, int dy) {
211 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500);//从当前滑动的位置,平滑地过度到目标位置
212 invalidate();
213 }
214
215 @Override
216 public void computeScroll() {
217 if (mScroller.computeScrollOffset()) {
218 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
219 invalidate();
220 }
221 }
222
223 private Scroller mScroller;//这个scroller是为了平滑滑动
224 }
activity_main.xml 这个是引用自定义控件的布局文件(记得改控件的包名)
1 <?xml version="1.0" encoding="utf-8"?>
2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent"
6 tools:context=".MainActivity">
7
8
9 <tt.zhou.HorizontalScrollViewEx
10 android:layout_width="match_parent"
11 android:layout_height="match_parent">
12
13 <ListView
14 android:id="@+id/lv_1"
15 android:layout_width="match_parent"
16 android:layout_height="match_parent"
17 android:background="@android:color/holo_blue_dark"></ListView>
18
19 <ListView
20 android:id="@+id/lv_2"
21 android:layout_width="match_parent"
22 android:layout_height="match_parent"
23 android:background="@android:color/holo_green_light"></ListView>
24
25 <ListView
26 android:id="@+id/lv_3"
27 android:layout_width="match_parent"
28 android:layout_height="match_parent"
29 android:background="@android:color/darker_gray"></ListView>
30
31 <ListView
32 android:id="@+id/lv_4"
33 android:layout_width="match_parent"
34 android:layout_height="match_parent"
35 android:background="@android:color/holo_blue_dark"></ListView>
36
37 <ListView
38 android:id="@+id/lv_5"
39 android:layout_width="match_parent"
40 android:layout_height="match_parent"
41 android:background="@android:color/holo_green_light"></ListView>
42 </tt.zhou.HorizontalScrollViewEx>
43
44
45 </LinearLayout>
MainActivity.java
1 package tt.zhou;
2
3 import android.app.Activity;
4 import android.os.Bundle;
5 import android.widget.ArrayAdapter;
6 import android.widget.ListView;
7
8 import java.util.ArrayList;
9 import java.util.List;
10
11 public class MainActivity extends Activity {
12
13 ListView lv_1, lv_2, lv_3, lv_4, lv_5;
14
15 @Override
16 protected void onCreate(Bundle savedInstanceState) {
17 super.onCreate(savedInstanceState);
18 setContentView(R.layout.activity_main);
19 initData();
20 init();
21 }
22
23 private void init() {
24 lv_1 = findViewById(R.id.lv_1);
25 lv_2 = findViewById(R.id.lv_2);
26 lv_3 = findViewById(R.id.lv_3);
27 lv_4 = findViewById(R.id.lv_4);
28 lv_5 = findViewById(R.id.lv_5);
29
30 ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data1);
31 lv_1.setAdapter(adapter1);
32 ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data2);
33 lv_2.setAdapter(adapter2);
34 ArrayAdapter<String> adapter3 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data3);
35 lv_3.setAdapter(adapter3);
36 ArrayAdapter<String> adapter4 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data4);
37 lv_4.setAdapter(adapter4);
38 ArrayAdapter<String> adapter5 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data5);
39 lv_5.setAdapter(adapter5);
40 }
41
42 private List<String> data1, data2, data3, data4, data5;
43
44 private void initData() {
45 data1 = new ArrayList<>();
46 for (int i = 0; i < 100; i++) {
47 data1.add("d1-" + i);
48 }
49 data2 = new ArrayList<>();
50 for (int i = 0; i < 100; i++) {
51 data2.add("d2-" + i);
52 }
53 data3 = new ArrayList<>();
54 for (int i = 0; i < 100; i++) {
55 data3.add("d3-" + i);
56 }
57 data4 = new ArrayList<>();
58 for (int i = 0; i < 100; i++) {
59 data4.add("d4-" + i);
60 }
61 data5 = new ArrayList<>();
62 for (int i = 0; i < 100; i++) {
63 data5.add("d5-" + i);
64 }
65 }
66 }
结语
上面的例子是,横向的layout,兼容竖向滑动的子view。
那么,按照这个原理,实现一个竖向的laytou,兼容横向滑动的子view,理解了上面提到的5个原理的同志们应该很容易写出来啦。
就酱紫咯。๑乛◡乛๑