demo地址:https://github.com/cmlbeliever/BounceLayout
最近任务比较少,闲来时间就来研究了android事件传播机制。根据总结分析的结果,打造出万能弹性layout,支持内嵌可滚动view!
先看图片(笔记本分辨率不兼容,将就看看)

这里写图片描述

核心内容分析

  1. 当手指移动时,判断移动方向,如果水平或垂直方向移动超过10个像素,则表示为移动事件,需要拦截!
  2. 判断手机按下时所在的view是否可以滚动
  3. 根据手指移动方向,与手指按下时view是否可以移动判断是否拦截事件
  4. 根据viewgroup事件拦截顺序onInterceptTouchEvent->onTouchEvent进行对应的逻辑处理

可滚动view坐标保存
1、由于内嵌可滚动view会导致事件冲突,所以在在移动时需要判断事件是否由内嵌的viewgroup消费。
2、在view布局处理好后,将可滚动的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
35
36
37
38
39
40
41
42
43
44
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);

if (changed) {
// 保存可滚动对象
positionMap.clear();
saveChildRect(this);
}

}

/**
* 找出所有可滚动的view,并存储位置
*
* @param view
*/
private void saveChildRect(ViewGroup view) {


if (view instanceof ScrollView || view instanceof HorizontalScrollView || view instanceof ScrollingView || view instanceof AbsListView) {

int[] location = new int[2];
// view.getLocationOnScreen(location);
view.getLocationInWindow(location);

RectF rectF = new RectF();
rectF.left = location[0];
rectF.top = location[1];
rectF.right = rectF.left + view.getMeasuredWidth();
rectF.bottom = rectF.top + view.getMeasuredHeight();

Log.d(TAG, "saveChildRect===>(" + rectF);

positionMap.put(rectF, view);
} else {
int childCount = view.getChildCount();
for (int i = 0; i < childCount; i++) {
if (view.getChildAt(i) instanceof ViewGroup) {
this.saveChildRect((ViewGroup) view.getChildAt(i));
}
}
}
}

3、当用户按下时,根据按下坐标查找是否在可滚动viewgroup内

1
2
3
4
5
6
7
8
9
10
private View findViewByPosition(float x, float y) {

for (RectF rectF : positionMap.keySet()) {
if (rectF.left <= x && x <= rectF.right && rectF.top <= y && y <= rectF.bottom) {
return positionMap.get(rectF);
}
}

return null;
}

注意调用时需要传入相对屏幕的坐标而不是相对于viewgroup的坐标 touchView = findViewByPosition(ev.getRawX(), ev.getRawY());

4、当用户在垂直方向移动,则判断touchView是否可以在垂直方向移动,如果可以,则事件交给touchView处理,否则进行layout的移动

1
2
3
4
5
6
7
//垂直方向移动
if (direction == DIRECTION_Y && canChildScrollVertically((int) -dy)) {
MotionEvent event = MotionEvent.obtain(ev);
event.setAction(MotionEvent.ACTION_CANCEL);
onTouchEvent(event);
return super.onInterceptTouchEvent(ev);
}

5、当用户在水平方向移动,则判断touchView是否可以在水平方向移动,如果可以,则事件交给touchView处理,否则进行layout的移动

1
2
3
4
5
6
7
//水平方向移动处理
if (direction == DIRECTION_X && canChildScrollHorizontally((int) -dx)) {
MotionEvent event = MotionEvent.obtain(ev);
event.setAction(MotionEvent.ACTION_CANCEL);
onTouchEvent(event);
return super.onInterceptTouchEvent(ev);
}

这样弹性layout的核心内容就分析完毕了,demo地址:
https://github.com/cmlbeliever/BounceLayout
主要代码:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
package com.cml.newframe.scrollablebox;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Build;
import android.support.v4.view.ScrollingView;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.AbsListView;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.ScrollView;

import java.util.HashMap;
import java.util.Map;

/**
* Created by cmlBeliever on 2016/3/25.
* <p>弹性layout</p>
*/
public class BounceLayout extends LinearLayout {

private static final String TAG = BounceLayout.class.getSimpleName();
private static final int DIRECTION_X = 1;
private static final int DIRECTION_Y = 2;
private static final int DIRECTION_NONE = 3;

/**
* 可移动比率 默认为0.5
*/
private float transRatio = 0.5f;

/**
* 手指按下的坐标
*/
private PointF initPoint = new PointF();

private Animator translateAnim;
private int direction = DIRECTION_NONE;

//手指按下时所在的view
private View touchView;
private Map<RectF, ViewGroup> positionMap = new HashMap<>();

public BounceLayout(Context context) {
super(context);
this.init();
}

public BounceLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.init();
}

public BounceLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.init();
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public BounceLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.init();
}

private void init() {
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = ev.getX() - initPoint.x;
float dy = ev.getY() - initPoint.y;

if (direction == DIRECTION_NONE) {
//x方向移动
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 10) {
direction = DIRECTION_X;
} else if (Math.abs(dy) > 10) {
direction = DIRECTION_Y;
}
} else {
//x方向移动
if (direction == DIRECTION_X) {
if (Math.abs(dx) <= getMeasuredWidth() * transRatio) {
ViewCompat.setTranslationX(getChildAt(0), dx);
}
} else if (direction == DIRECTION_Y) {
if (Math.abs(dy) <= getMeasuredHeight() * transRatio) {
ViewCompat.setTranslationY(getChildAt(0), dy);
}
}
return true;
}

break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:

dx = ViewCompat.getTranslationX(getChildAt(0));
dy = ViewCompat.getTranslationY(getChildAt(0));

if (direction == DIRECTION_X) {
startTransAnim(DIRECTION_X, dx, 0);
} else if (direction == DIRECTION_Y) {
startTransAnim(DIRECTION_Y, dy, 0);
}

if (direction != DIRECTION_NONE) {
direction = DIRECTION_NONE;
return true;

}

break;
}


return super.onTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
initPoint.x = ev.getX();
initPoint.y = ev.getY();
touchView = findViewByPosition(ev.getRawX(), ev.getRawY());
Log.d(TAG, "onInterceptTouchEvent===>touchView:" + touchView);
break;
case MotionEvent.ACTION_MOVE:
float dx = ev.getX() - initPoint.x;
float dy = ev.getY() - initPoint.y;

Log.d(TAG, "onInterceptTouchEvent===>ACTION_MOVE touchView:" + touchView);

if (direction == DIRECTION_NONE) {
//x方向移动
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 10) {
direction = DIRECTION_X;
} else if (Math.abs(dy) > 10) {
direction = DIRECTION_Y;
}
}

//垂直方向移动
if (direction == DIRECTION_Y && canChildScrollVertically((int) -dy)) {
MotionEvent event = MotionEvent.obtain(ev);
event.setAction(MotionEvent.ACTION_CANCEL);
onTouchEvent(event);
return super.onInterceptTouchEvent(ev);
}

if (direction == DIRECTION_X && canChildScrollHorizontally((int) -dx)) {
MotionEvent event = MotionEvent.obtain(ev);
event.setAction(MotionEvent.ACTION_CANCEL);
onTouchEvent(event);
return super.onInterceptTouchEvent(ev);
}

break;
}


return direction == DIRECTION_NONE ? super.onInterceptTouchEvent(ev) : true;
}


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);

if (changed) {
// 保存可滚动对象
positionMap.clear();
saveChildRect(this);
}

}

private View findViewByPosition(float x, float y) {

for (RectF rectF : positionMap.keySet()) {
if (rectF.left <= x && x <= rectF.right && rectF.top <= y && y <= rectF.bottom) {
return positionMap.get(rectF);
}
}

return null;
}

/**
* 找出所有可滚动的view,并存储位置
*
* @param view
*/
private void saveChildRect(ViewGroup view) {


if (view instanceof ScrollView || view instanceof HorizontalScrollView || view instanceof ScrollingView || view instanceof AbsListView) {

int[] location = new int[2];
// view.getLocationOnScreen(location);
view.getLocationInWindow(location);

RectF rectF = new RectF();
rectF.left = location[0];
rectF.top = location[1];
rectF.right = rectF.left + view.getMeasuredWidth();
rectF.bottom = rectF.top + view.getMeasuredHeight();

Log.d(TAG, "saveChildRect===>(" + rectF);

positionMap.put(rectF, view);
} else {
int childCount = view.getChildCount();
for (int i = 0; i < childCount; i++) {
if (view.getChildAt(i) instanceof ViewGroup) {
this.saveChildRect((ViewGroup) view.getChildAt(i));
}
}
}
}


private void startTransAnim(int direction, float start, float end) {
if (translateAnim != null && translateAnim.isRunning()) {
translateAnim.end();
}
String proName = direction == DIRECTION_X ? "translationX" : "translationY";
translateAnim = ObjectAnimator.ofFloat(getChildAt(0), proName, start, end);
translateAnim.setInterpolator(new AccelerateDecelerateInterpolator());
translateAnim.start();
}


/**
* @param direction 负数:向上滑动 else 向下滑动
* @return
*/
private boolean canChildScrollVertically(int direction) {
return touchView == null ? false : ViewCompat.canScrollVertically(touchView, direction);
}


/**
* @param direction 负数:向左滑动 else 向右滑动
* @return
*/
private boolean canChildScrollHorizontally(int direction) {
return touchView == null ? false : ViewCompat.canScrollHorizontally(touchView, direction);
}

public float getTransRatio() {
return transRatio;
}

/**
* 设置偏移部分百分比
*
* @param transRatio 0-1 ,1 100%距离
*/
public void setTransRatio(float transRatio) {
this.transRatio = transRatio;
}
}