logo头像
Snippet 博客主题

Android仿京东、天猫商品详情页

本文于589天之前发表,文中内容可能已经过时。

前言

前面在介绍控件TabLayout控件和CoordinatorLayout使用的时候说了下实现京东、天猫详情页面的效果,今天要说的是优化版,是我们线上实现的效果,首先看一下效果:
这里写图片描述
这里写图片描述

项目结构分析

首先我们来分析一下要实现上面的效果,我们需要怎么做。顶部是一个可以滑动切换Tab,可以用ViewPager+Fragment实现,也可以使用系统的TabLayout控件实现;而下面的 View是一个可以滑动拖动效果的View,可以采用网上一个叫做DragLayout的控件,我这里是自己实现了一个,主要是通过对View的事件分发的一些处理;然后滑动到下面就是一个图文详情的View(Fragment),本页面包含两个界面:详情页面和参数页面;最后是评价的View(Fragment)。经过上面的分析,我们的界面至少需要4个Fragement,首先来看一下项目结构:
这里写图片描述

代码讲解

代码比较多,这里只讲解几个核心的方法类。首先我们来看一下我们自己是的这个具有阻尼效果的View,我们知道要实现的效果,我们需要对View的事件做一个全面的实现。这里首先说一下View的事件分发的流程:onInterceptTouchEvent()–>dispatchTouchEvent()–>onTouchEvent();
首先我们需要对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
ensureTarget();
if (null == mTarget) {
return false;
}
if (!isEnabled()) {
return false;
}
final int aciton = MotionEventCompat.getActionMasked(ev);
boolean shouldIntercept = false;
switch (aciton) {
case MotionEvent.ACTION_DOWN: {
mInitMotionX = ev.getX();
mInitMotionY = ev.getY();
shouldIntercept = false;
break;
}
case MotionEvent.ACTION_MOVE: {
final float x = ev.getX();
final float y = ev.getY();
final float xDiff = x - mInitMotionX;
final float yDiff = y - mInitMotionY;
if (canChildScrollVertically((int) yDiff)) {
shouldIntercept = false;
} else {
final float xDiffabs = Math.abs(xDiff);
final float yDiffabs = Math.abs(yDiff);
if (yDiffabs > mTouchSlop && yDiffabs >= xDiffabs
&& !(mStatus == Status.CLOSE && yDiff > 0
|| mStatus == Status.OPEN && yDiff < 0)) {
shouldIntercept = true;
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
shouldIntercept = false;
break;
}
}
return shouldIntercept;

最后转发给onTouchEvent

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
ensureTarget();
if (null == mTarget) {
return false;
}
if (!isEnabled()) {
return false;
}
boolean wantTouch = true;
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
if (mTarget instanceof View) {
wantTouch = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
final float y = ev.getY();
final float yDiff = y - mInitMotionY;
if (canChildScrollVertically(((int) yDiff))) {
wantTouch = false;
} else {
processTouchEvent(yDiff);
wantTouch = true;
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
finishTouchEvent();
wantTouch = false;
break;
}
}
return wantTouch;

滑动事件完了之后我们需要调用request方法对View做一个重绘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final int left = l;
final int right = r;
int top;
int bottom;
final int offset = (int) mSlideOffset;
View child;
for (int i = 0; i < getChildCount(); i++) {
child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
if (child == mBehindView) {
top = b + offset;
bottom = top + b - t;
} else {
top = t + offset;
bottom = b + offset;
}
child.layout(left, top, right, bottom);
}

上下滑动也是涉及到两个界面:mFrontView和mBehindView,然后通过判断滑动事件来显示哪一个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
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
package com.xzh.gooddetail.view;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import com.xzh.gooddetail.R;
public class SlideDetailsLayout extends ViewGroup {
public interface OnSlideDetailsListener {
void onStatusChanged(Status status);
}
public enum Status {
CLOSE,
OPEN;
public static Status valueOf(int stats) {
if (0 == stats) {
return CLOSE;
} else if (1 == stats) {
return OPEN;
} else {
return CLOSE;
}
}
}
private static final float DEFAULT_PERCENT = 0.2f;
private static final int DEFAULT_DURATION = 300;
private View mFrontView;
private View mBehindView;
private float mTouchSlop;
private float mInitMotionY;
private float mInitMotionX;
private View mTarget;
private float mSlideOffset;
private Status mStatus = Status.CLOSE;
private boolean isFirstShowBehindView = true;
private float mPercent = DEFAULT_PERCENT;
private long mDuration = DEFAULT_DURATION;
private int mDefaultPanel = 0;
private OnSlideDetailsListener mOnSlideDetailsListener;
public SlideDetailsLayout(Context context) {
this(context, null);
}
public SlideDetailsLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideDetailsLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlideDetailsLayout, defStyleAttr, 0);
mPercent = a.getFloat(R.styleable.SlideDetailsLayout_percent, DEFAULT_PERCENT);
mDuration = a.getInt(R.styleable.SlideDetailsLayout_duration, DEFAULT_DURATION);
mDefaultPanel = a.getInt(R.styleable.SlideDetailsLayout_default_panel, 0);
a.recycle();
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
public void setOnSlideDetailsListener(OnSlideDetailsListener listener) {
this.mOnSlideDetailsListener = listener;
}
public void smoothOpen(boolean smooth) {
if (mStatus != Status.OPEN) {
mStatus = Status.OPEN;
final float height = -getMeasuredHeight();
animatorSwitch(0, height, true, smooth ? mDuration : 0);
}
}
public void smoothClose(boolean smooth) {
if (mStatus != Status.CLOSE) {
mStatus = Status.CLOSE;
final float height = -getMeasuredHeight();
animatorSwitch(height, 0, true, smooth ? mDuration : 0);
}
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected void onFinishInflate() {
final int childCount = getChildCount();
if (1 >= childCount) {
throw new RuntimeException("SlideDetailsLayout only accept childs more than 1!!");
}
mFrontView = getChildAt(0);
mBehindView = getChildAt(1);
if (mDefaultPanel == 1) {
post(new Runnable() {
@Override
public void run() {
smoothOpen(false);
}
});
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int pWidth = MeasureSpec.getSize(widthMeasureSpec);
final int pHeight = MeasureSpec.getSize(heightMeasureSpec);
final int childWidthMeasureSpec =
MeasureSpec.makeMeasureSpec(pWidth, MeasureSpec.EXACTLY);
final int childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(pHeight, MeasureSpec.EXACTLY);
View child;
for (int i = 0; i < getChildCount(); i++) {
child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec);
}
setMeasuredDimension(pWidth, pHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int left = l;
final int right = r;
int top;
int bottom;
final int offset = (int) mSlideOffset;
View child;
for (int i = 0; i < getChildCount(); i++) {
child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
if (child == mBehindView) {
top = b + offset;
bottom = top + b - t;
} else {
top = t + offset;
bottom = b + offset;
}
child.layout(left, top, right, bottom);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
if (null == mTarget) {
return false;
}
if (!isEnabled()) {
return false;
}
final int aciton = MotionEventCompat.getActionMasked(ev);
boolean shouldIntercept = false;
switch (aciton) {
case MotionEvent.ACTION_DOWN: {
mInitMotionX = ev.getX();
mInitMotionY = ev.getY();
shouldIntercept = false;
break;
}
case MotionEvent.ACTION_MOVE: {
final float x = ev.getX();
final float y = ev.getY();
final float xDiff = x - mInitMotionX;
final float yDiff = y - mInitMotionY;
if (canChildScrollVertically((int) yDiff)) {
shouldIntercept = false;
} else {
final float xDiffabs = Math.abs(xDiff);
final float yDiffabs = Math.abs(yDiff);
if (yDiffabs > mTouchSlop && yDiffabs >= xDiffabs
&& !(mStatus == Status.CLOSE && yDiff > 0
|| mStatus == Status.OPEN && yDiff < 0)) {
shouldIntercept = true;
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
shouldIntercept = false;
break;
}
}
return shouldIntercept;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
ensureTarget();
if (null == mTarget) {
return false;
}
if (!isEnabled()) {
return false;
}
boolean wantTouch = true;
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
if (mTarget instanceof View) {
wantTouch = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
final float y = ev.getY();
final float yDiff = y - mInitMotionY;
if (canChildScrollVertically(((int) yDiff))) {
wantTouch = false;
} else {
processTouchEvent(yDiff);
wantTouch = true;
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
finishTouchEvent();
wantTouch = false;
break;
}
}
return wantTouch;
}
private void processTouchEvent(final float offset) {
if (Math.abs(offset) < mTouchSlop) {
return;
}
final float oldOffset = mSlideOffset;
if (mStatus == Status.CLOSE) {
// reset if pull down
if (offset >= 0) {
mSlideOffset = 0;
} else {
mSlideOffset = offset;
}
if (mSlideOffset == oldOffset) {
return;
}
} else if (mStatus == Status.OPEN) {
final float pHeight = -getMeasuredHeight();
if (offset <= 0) {
mSlideOffset = pHeight;
} else {
final float newOffset = pHeight + offset;
mSlideOffset = newOffset;
}
if (mSlideOffset == oldOffset) {
return;
}
}
requestLayout();
}
private void finishTouchEvent() {
final int pHeight = getMeasuredHeight();
final int percent = (int) (pHeight * mPercent);
final float offset = mSlideOffset;
boolean changed = false;
if (Status.CLOSE == mStatus) {
if (offset <= -percent) {
mSlideOffset = -pHeight;
mStatus = Status.OPEN;
changed = true;
} else {
mSlideOffset = 0;
}
} else if (Status.OPEN == mStatus) {
if ((offset + pHeight) >= percent) {
mSlideOffset = 0;
mStatus = Status.CLOSE;
changed = true;
} else {
mSlideOffset = -pHeight;
}
}
animatorSwitch(offset, mSlideOffset, changed);
}
private void animatorSwitch(final float start, final float end) {
animatorSwitch(start, end, true, mDuration);
}
private void animatorSwitch(final float start, final float end, final long duration) {
animatorSwitch(start, end, true, duration);
}
private void animatorSwitch(final float start, final float end, final boolean changed) {
animatorSwitch(start, end, changed, mDuration);
}
private void animatorSwitch(final float start,
final float end,
final boolean changed,
final long duration) {
ValueAnimator animator = ValueAnimator.ofFloat(start, end);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mSlideOffset = (float) animation.getAnimatedValue();
requestLayout();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (changed) {
if (mStatus == Status.OPEN) {
checkAndFirstOpenPanel();
}
if (null != mOnSlideDetailsListener) {
mOnSlideDetailsListener.onStatusChanged(mStatus);
}
}
}
});
animator.setDuration(duration);
animator.start();
}
private void checkAndFirstOpenPanel() {
if (isFirstShowBehindView) {
isFirstShowBehindView = false;
mBehindView.setVisibility(VISIBLE);
}
}
private void ensureTarget() {
if (mStatus == Status.CLOSE) {
mTarget = mFrontView;
} else {
mTarget = mBehindView;
}
}
protected boolean canChildScrollVertically(int direction) {
if (mTarget instanceof AbsListView) {
return canListViewSroll((AbsListView) mTarget);
} else if (mTarget instanceof FrameLayout ||
mTarget instanceof RelativeLayout ||
mTarget instanceof LinearLayout) {
View child;
for (int i = 0; i < ((ViewGroup) mTarget).getChildCount(); i++) {
child = ((ViewGroup) mTarget).getChildAt(i);
if (child instanceof AbsListView) {
return canListViewSroll((AbsListView) child);
}
}
}
if (android.os.Build.VERSION.SDK_INT < 14) {
return ViewCompat.canScrollVertically(mTarget, -direction) || mTarget.getScrollY() > 0;
} else {
return ViewCompat.canScrollVertically(mTarget, -direction);
}
}
protected boolean canListViewSroll(AbsListView absListView) {
if (mStatus == Status.OPEN) {
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() <
absListView.getPaddingTop());
} else {
final int count = absListView.getChildCount();
return count > 0
&& (absListView.getLastVisiblePosition() < count - 1
|| absListView.getChildAt(count - 1)
.getBottom() > absListView.getMeasuredHeight());
}
}
@Override
protected Parcelable onSaveInstanceState() {
SavedState ss = new SavedState(super.onSaveInstanceState());
ss.offset = mSlideOffset;
ss.status = mStatus.ordinal();
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mSlideOffset = ss.offset;
mStatus = Status.valueOf(ss.status);
if (mStatus == Status.OPEN) {
mBehindView.setVisibility(VISIBLE);
}
requestLayout();
}
static class SavedState extends BaseSavedState {
private float offset;
private int status;
public SavedState(Parcel source) {
super(source);
offset = source.readFloat();
status = source.readInt();
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeFloat(offset);
out.writeInt(status);
}
public static final Creator<SavedState> CREATOR =
new Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}

接下来就是一些Fragment等的页面填充,也没啥好讲的,代码又很多可以优化的地方,在优化的地方,笔者也列出了优化的方案,大家可以根据自己的实际情况做页面级的优化。
附:Android仿京东、天猫商品详情页源码

支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者

上一篇