Welcome to Ray's Blog

Stay Hungry Stay Foolish - Steve Jobs

0%

Android 仿JD阻尼效果加载商品信息和商品详情


概述

效果需求:页面分俩部分:pageUp 和 pageDown,俩部分均 extends ScrollView 拥有滑动效果。当界面分别处于上下页面时,需要判断其内部的内容是否可以滚动,并执行相应的滚动策略。如果滑动到上下界面交界处,则根据滑动的距离是否超过设定滑动距离判断是滑动进入另一页,还是继续留在本页。如果在上下界面交界处滑动距离没有超过设定的滑动距离,那么会有阻尼回弹的效果恢复到 pageUp 的底部或者 pageDown 的顶部。

原理

  1. DragLayout:根据滑动效果刷新 View;
  2. DragScrollViewDragWebView:根据子 View 是否处于顶部或者底部来设置父类 ViewGroup 是否消费滑动事件;

源码

DragLayout extends ViewGroup

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
package com.chenley.an.yutownhelper.views;

import android.annotation.SuppressLint;
import android.content.Context;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

/**
* 实现京东淘宝详情的阻尼效果,实现上下两个frameLayout拖动切换
*/
@SuppressLint("NewApi") public class DragLayout extends ViewGroup {
private final ViewDragHelper mDragHelper; //系统滑动帮助类
private GestureDetectorCompat gestureDetector; //手势检测类

/* 上下两个frameLayout,在Activity中注入fragment */
private View frameView1, frameView2;
private int viewHeight;//高度
private static final int VEL_THRESHOLD = 100; // 滑动速度的阈值,超过这个绝对值认为是上下
private static final int DISTANCE_THRESHOLD = 100; // 单位是像素,当上下滑动速度不够时,通过这个阈值来判定是应该粘到顶部还是底部
private int downTop1; // 手指按下的时候,frameView1的getTop值
private ShowNextPageDrag nextPageListener; // 手指松开是否加载下一页的notifier

public DragLayout(Context context) {
this(context, null);
}

public DragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mDragHelper = ViewDragHelper.create(this, 10f, new DragHelperCallback());
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_BOTTOM);
gestureDetector = new GestureDetectorCompat(context, new YScrollDetector());
}

/**
* View视图加载完成
*/
@Override protected void onFinishInflate() {
super.onFinishInflate();
frameView1 = getChildAt(0); //为上下View赋值
frameView2 = getChildAt(1);//为上下View赋值
}

/**
* Y方向滑动距离检测类
*/
class YScrollDetector extends SimpleOnGestureListener {

@Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) {
// 垂直滑动时dy>dx,才被认定是上下拖动
return Math.abs(dy) > Math.abs(dx);
}
}

/**
* 计算滚动
*/
@Override public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}

/**
* 拖动回调
*/
private class DragHelperCallback extends ViewDragHelper.Callback {

@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
int childIndex = 1;
if (changedView == frameView2) {
childIndex = 2;
}
// 一个view位置改变,另一个view的位置要跟进
onViewPosChanged(childIndex, top);
}

@Override public boolean tryCaptureView(View child, int pointerId) {
// 两个子View都需要跟踪,返回true
return true;
}

@Override public int getViewVerticalDragRange(View child) {
// 这个用来控制拖拽过程中松手后,自动滑行的速度,暂时给一个随意的数值
return 1;
}

@Override public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 滑动松开后,需要向上或者乡下粘到特定的位置
animTopOrBottom(releasedChild, yvel);
}

@Override public int clampViewPositionVertical(View child, int top, int dy) {
int finalTop = top;
if (child == frameView1) {
// 拖动的时第一个view
if (top > 0) {
// 不让第一个view往下拖,因为顶部会白板
finalTop = 0;
}
} else if (child == frameView2) {
// 拖动的时第二个view
if (top < 0) {
finalTop = 0;
}
}

// finalTop代表的是理论上应该拖动到的位置。此处计算拖动的距离除以一个参数(3),是让滑动的速度变慢。数值越大,滑动的越慢
return child.getTop() + (finalTop - child.getTop()) / 3;
}
}

/**
* 当View位置发生变化时,刷新View显示
*/
private void onViewPosChanged(int viewIndex, int posTop) {
if (viewIndex == 1) {
int offsetTopBottom = viewHeight + frameView1.getTop() - frameView2.getTop();
frameView2.offsetTopAndBottom(offsetTopBottom);
} else if (viewIndex == 2) {
int offsetTopBottom = frameView2.getTop() - viewHeight - frameView1.getTop();
frameView1.offsetTopAndBottom(offsetTopBottom);
}
// 有的时候会默认白板,这个很恶心。后面有时间再优化
invalidate();
}

/**
* 滑动松开后,需要向上或者乡下粘到特定的位置
*/
private void animTopOrBottom(View releasedChild, float yvel) {
int finalTop = 0;
if (releasedChild == frameView1) {
if (yvel < -VEL_THRESHOLD || (downTop1 == 0 && frameView1.getTop() < -DISTANCE_THRESHOLD)) {
finalTop = -viewHeight;
if (null != nextPageListener) {
nextPageListener.onDragNext();
}
}
} else {
if (yvel > VEL_THRESHOLD || (downTop1 == -viewHeight
&& releasedChild.getTop() > DISTANCE_THRESHOLD)) {
finalTop = viewHeight;
}
}
//缓慢滑动到指定位置
if (mDragHelper.smoothSlideViewTo(releasedChild, 0, finalTop)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}

/**
* 是否拦截触摸事件
*/
@Override public boolean onInterceptTouchEvent(MotionEvent ev) {

if (frameView1.getBottom() > 0 && frameView1.getTop() < 0) {
return false;//返回false不拦截事件
}

boolean yScroll = gestureDetector.onTouchEvent(ev); //返回true,表示消费事件
boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev); //是否消费事件
int action = ev.getActionMasked();

if (action == MotionEvent.ACTION_DOWN) {
mDragHelper.processTouchEvent(ev);
downTop1 = frameView1.getTop();
}

return shouldIntercept && yScroll;//是否拦截事件
}

@Override public boolean onTouchEvent(MotionEvent e) {
mDragHelper.processTouchEvent(e); // 该行代码可能会抛异常,正式发布时请将这行代码加上try catch
return true;
}

/**
* 计算布局
*/
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (null == frameView1 || null == frameView2) return;
if (frameView1.getTop() == 0) {
frameView1.layout(l, 0, r, b - t);
frameView2.layout(l, 0, r, b - t);

viewHeight = frameView1.getMeasuredHeight();
frameView2.offsetTopAndBottom(viewHeight);
} else {
frameView1.layout(l, frameView1.getTop(), r, frameView1.getBottom());
frameView2.layout(l, frameView2.getTop(), r, frameView2.getBottom());
}
}

/**
* 测量
*/
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);

int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
}

/**
* 获取测量策略
* @param size
* @param measureSpec
* @param childMeasuredState
* @return
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}

/**
* 设置下一页监听
*/
public void setNextPageListener(ShowNextPageDrag nextPageListener) {
this.nextPageListener = nextPageListener;
}

/**
* 显示下一页接口
*/
public interface ShowNextPageDrag {
void onDragNext();
}
}

DragScrollView extends ScrollView 自定义 ScrollView

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

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ScrollView;

public class DragScrollView extends ScrollView {
private static final String TAG = "DragScrollView@@";

private boolean allowDragBottom = true; //时候允许滑动到底部
private float downY = 0; //开始触摸点的Y位置
private boolean needConsumeTouch = true; //时候消费触摸事件
private int maxScroll = -1; // 最大滑动距离

public DragScrollView(Context arg0) {
this(arg0, null);
}

public DragScrollView(Context arg0, AttributeSet arg1) {
this(arg0, arg1, 0);
}

public DragScrollView(Context arg0, AttributeSet arg1, int arg2) {
super(arg0, arg1, arg2);
}

@Override protected void onScrollChanged(int l, int t, int oldl, int oldt) {
if (maxScroll < 0) {
maxScroll = computeVerticalScrollRange(); //滚动的时候计算View的整体高度,并且只计算一次
}
Log.i(TAG, "onScrollChanged: maxScroll=" + maxScroll);
super.onScrollChanged(l, t, oldl, oldt);
}

@Override public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
downY = ev.getRawY();
needConsumeTouch = true;

//是都可以滑动到View的底部判断条件:1、View的高度不为0;2、view的滚动距离+显示的高度大于等于view的最大高度-2
if (maxScroll > 0 && getScrollY() + getMeasuredHeight() >= maxScroll - 2) {
allowDragBottom = true;
} else {
allowDragBottom = false;
}
Log.i(TAG, "MotionEvent.ACTION_DOWN: maxScroll=" + maxScroll);
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
if (!needConsumeTouch) {
getParent().requestDisallowInterceptTouchEvent(false);
return false;
} else if (allowDragBottom) {
if (downY - ev.getRawY() > 2) {
needConsumeTouch = false;
/**
* @param disallowIntercept True if the child does not want the parent to
* intercept touch events.
*/
getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
}
Log.i(TAG, "MotionEvent.ACTION_MOVE: maxScroll=" + maxScroll);
}
getParent().requestDisallowInterceptTouchEvent(needConsumeTouch);
Log.i(TAG, "dispatchTouchEvent: maxScroll=" + maxScroll);
return super.dispatchTouchEvent(ev);
}
}

DragWebView extends WebView 自定义 WebView

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

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.webkit.WebView;

public class DragWebView extends WebView {

private boolean allowDragTop = true; // 如果是true,则允许拖动至底部的下一页
private float downY = 0; //ACTION_DWON的Y位置
private boolean needConsumeTouch = true;//是否消费触摸事件

public DragWebView(Context arg0) {
this(arg0, null);
}

public DragWebView(Context arg0, AttributeSet arg1) {
this(arg0, arg1, 0);
}

public DragWebView(Context arg0, AttributeSet arg1, int arg2) {
super(arg0, arg1, arg2);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
downY = ev.getRawY();//获取初始触摸Y位置
needConsumeTouch = true;
allowDragTop = isAtTop();//判断View是否处在屏幕顶部
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
if (!needConsumeTouch) {
getParent().requestDisallowInterceptTouchEvent(false);
return false;
} else if (allowDragTop) {
if (ev.getRawY() - downY > 2) {
needConsumeTouch = false;
getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
}
}
getParent().requestDisallowInterceptTouchEvent(needConsumeTouch);
return super.dispatchTouchEvent(ev);
}

/**
* 根据滚动的距离判断View顶部时候在屏幕的顶部
* @return
*/
private boolean isAtTop() {
return getScrollY() == 0;
}
}

DEMO 地址

Github:YutownHelper