服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - Android自定义ViewGroup实现淘宝商品详情页

Android自定义ViewGroup实现淘宝商品详情页

2022-08-13 11:19Danny_姜 Android

这篇文章主要为大家详细介绍了Android自定义ViewGroup实现淘宝商品详情页,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

最近公司在新版本上有一个需要,要在首页添加一个滑动效果,具体就是仿照X宝的商品详情页,拉到页面底部时有一个粘滞效果,如下图X东的商品详情页,如果用户继续向上拉的话就进入商品图文描述界面:

Android自定义ViewGroup实现淘宝商品详情页

刚开始是想拿来主义,直接从网上找个现成的demo来用, 但是网上无一例外的答案都特别统一: 几乎全部是ScrollView中再套两个ScrollView,或者是一个LinearLayout中套两个ScrollView。 通过指定父view和子view的focus来切换滑动的处理界面---即通过view的requestDisallowInterceptTouchEvent方法来决定是哪一个ScrollView来处理滑动事件。

使用以上方法虽然可以解一时之渴, 但是存在几点缺陷:

1  扩展性不强 : 如果后续产品要求不止是两页滑动呢,是三页滑动呢, 难道要嵌3个ScrollView并通过N个判断来实现吗

2  兼容性不强 : 如果需要在某一个子页中需要处理左右滑动事件或者双指操作事件呢, 此方法就无法实现了

3 个人原因 : 个人喜欢自己掌握主动性,事件的处理自己来控制更靠谱一些(PS:就如同一份感情一样,需要细心去经营)

总和以上原因, 自己实现了一个ViewGroup,实现文章开头提到的效果, 废话不多说  直接上源码,以下只是部分主要源码,并对每一个方法都做了注释,可以参照注释理解。   文章最后对这个ViewGroup加了一点实现的细节以及如何使用此VIewGroup, 以及demo地址

?
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
package com.mcoy.snapscrollview;
 
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;
 
/**
 * @author jiangxinxing---mcoy in English
 *
 * 了解此ViewGroup之前, 有两点一定要做到心中有数
 * 一个是对Scroller的使用, 另一个是对onInterceptTouchEvent和onTouchEvent要做到很熟悉
 * 以下几个网站可以做参考用
 * http://blog.csdn.net/bigconvience/article/details/26697645
 * http://blog.csdn.net/androiddevelop/article/details/8373782
 * http://blog.csdn.net/xujainxing/article/details/8985063
 */
public class McoySnapPageLayout extends ViewGroup {
 
  。。。。
 
 
 public interface McoySnapPage {
 /**
 * 返回page根节点
 *
 * @return
 */
 View getRootView();
 
 /**
 * 是否滑动到最顶端
 * 第二页必须自己实现此方法,来判断是否已经滑动到第二页的顶部
 * 并决定是否要继续滑动到第一页
 */
 boolean isAtTop();
 
 /**
 * 是否滑动到最底部
 * 第一页必须自己实现此方法,来判断是否已经滑动到第二页的底部
 * 并决定是否要继续滑动到第二页
 */
 boolean isAtBottom();
 }
 
 public interface PageSnapedListener {
 
 /**
 * @mcoy
 * 当从某一页滑动到另一页完成时的回调函数
 */
 void onSnapedCompleted(int derection);
 }
 
  。。。。。。
 
 /**
 * 设置上下页面
 * @param pageTop
 * @param pageBottom
 */
 public void setSnapPages(McoySnapPage pageTop, McoySnapPage pageBottom) {
 mPageTop = pageTop;
 mPageBottom = pageBottom;
 addPagesAndRefresh();
 }
 
 private void addPagesAndRefresh() {
 // 设置页面id
 mPageTop.getRootView().setId(0);
 mPageBottom.getRootView().setId(1);
 addView(mPageTop.getRootView());
 addView(mPageBottom.getRootView());
 postInvalidate();
 }
 
 /**
 * @mcoy add
 * computeScroll方法会调用postInvalidate()方法, 而postInvalidate()方法中系统
 * 又会调用computeScroll方法, 因此会一直在循环互相调用, 循环的终结点是在computeScrollOffset()
 * 当computeScrollOffset这个方法返回false时,说明已经结束滚动。
 *
 * 重要:真正的实现此view的滚动是调用scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
 */
 @Override
 public void computeScroll() {
 //先判断mScroller滚动是否完成
 if (mScroller.computeScrollOffset()) {
 if (mScroller.getCurrY() == (mScroller.getFinalY())) {
 if (mNextDataIndex > mDataIndex) {
  mFlipDrection = FLIP_DIRECTION_DOWN;
  makePageToNext(mNextDataIndex);
 } else if (mNextDataIndex < mDataIndex) {
  mFlipDrection = FLIP_DIRECTION_UP;
  makePageToPrev(mNextDataIndex);
 }else{
  mFlipDrection = FLIP_DIRECTION_CUR;
 }
 if(mPageSnapedListener != null){
  mPageSnapedListener.onSnapedCompleted(mFlipDrection);
 }
 }
 //这里调用View的scrollTo()完成实际的滚动
 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
 //必须调用该方法,否则不一定能看到滚动效果
 postInvalidate();
 }
 }
 
 private void makePageToNext(int dataIndex) {
 mDataIndex = dataIndex;
  mCurrentScreen = getCurrentScreen();
 }
 
 private void makePageToPrev(int dataIndex) {
 mDataIndex = dataIndex;
  mCurrentScreen = getCurrentScreen();
 }
 
 public int getCurrentScreen() {
 for (int i = 0; i < getChildCount(); i++) {
 if (getChildAt(i).getId() == mDataIndex) {
 return i;
 }
 }
 return mCurrentScreen;
 }
 
 public View getCurrentView() {
 for (int i = 0; i < getChildCount(); i++) {
 if (getChildAt(i).getId() == mDataIndex) {
 return getChildAt(i);
 }
 }
 return null;
 }
 
 /*
 * (non-Javadoc)
 *
 * @see
 * android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
 * 重写了父类的onInterceptTouchEvent(),主要功能是在onTouchEvent()方法之前处理
 * touch事件。包括:down、up、move事件。
 * 当onInterceptTouchEvent()返回true时进入onTouchEvent()。
 */
 @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
 final int action = ev.getAction();
 if ((action == MotionEvent.ACTION_MOVE)
 && (mTouchState != TOUCH_STATE_REST)) {
 return true;
 }
 final float x = ev.getX();
 final float y = ev.getY();
 
 switch (action) {
 case MotionEvent.ACTION_MOVE:
 // 记录y与mLastMotionY差值的绝对值。
   // yDiff大于gapBetweenTopAndBottom时就认为界面拖动了足够大的距离,屏幕就可以移动了。
 final int yDiff = (int)(y - mLastMotionY);
 boolean yMoved = Math.abs(yDiff) > gapBetweenTopAndBottom;
 if (yMoved) {
 if(MCOY_DEBUG) {
  Log.e(TAG, "yDiff is " + yDiff);
  Log.e(TAG, "mPageTop.isFlipToBottom() is " + mPageTop.isAtBottom());
  Log.e(TAG, "mCurrentScreen is " + mCurrentScreen);
  Log.e(TAG, "mPageBottom.isFlipToTop() is " + mPageBottom.isAtTop());
 }
 if(yDiff < 0 && mPageTop.isAtBottom() && mCurrentScreen == 0
  || yDiff > 0 && mPageBottom.isAtTop() && mCurrentScreen == 1){
  Log.e("mcoy", "121212121212121212121212");
  mTouchState = TOUCH_STATE_SCROLLING;
 }
 }
 break;
 case MotionEvent.ACTION_DOWN:
 // Remember location of down touch
 mLastMotionY = y;
 Log.e("mcoy", "mScroller.isFinished() is " + mScroller.isFinished());
 mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
  : TOUCH_STATE_SCROLLING;
 break;
 case MotionEvent.ACTION_CANCEL:
 case MotionEvent.ACTION_UP:
 // Release the drag
 mTouchState = TOUCH_STATE_REST;
 break;
 }
 boolean intercept = mTouchState != TOUCH_STATE_REST;
 Log.e("mcoy", "McoySnapPageLayout---onInterceptTouchEvent return " + intercept);
 return intercept;
 }
 
 /*
 * (non-Javadoc)
 *
 * @see android.view.View#onTouchEvent(android.view.MotionEvent)
 * 主要功能是处理onInterceptTouchEvent()返回值为true时传递过来的touch事件
 */
 @Override
 public boolean onTouchEvent(MotionEvent ev) {
 Log.e("mcoy", "onTouchEvent--" + System.currentTimeMillis());
  if (mVelocityTracker == null) {
   mVelocityTracker = VelocityTracker.obtain();
  }
  mVelocityTracker.addMovement(ev);
  
 final int action = ev.getAction();
 final float x = ev.getX();
 final float y = ev.getY();
 switch (action) {
 case MotionEvent.ACTION_DOWN:
 if (!mScroller.isFinished()) {
 mScroller.abortAnimation();
 }
 break;
 case MotionEvent.ACTION_MOVE:
  if(mTouchState != TOUCH_STATE_SCROLLING){
     // 记录y与mLastMotionY差值的绝对值。
     // yDiff大于gapBetweenTopAndBottom时就认为界面拖动了足够大的距离,屏幕就可以移动了。
    final int yDiff = (int) Math.abs(y - mLastMotionY);
    boolean yMoved = yDiff > gapBetweenTopAndBottom;
    if (yMoved) {
     mTouchState = TOUCH_STATE_SCROLLING;
    }
   }
   // 手指拖动屏幕的处理
   if ((mTouchState == TOUCH_STATE_SCROLLING)) {
    // Scroll to follow the motion event
    final int deltaY = (int) (mLastMotionY - y);
    mLastMotionY = y;
    final int scrollY = getScrollY();
    if(mCurrentScreen == 0){//显示第一页,只能上拉时使用
     if(mPageTop != null && mPageTop.isAtBottom()){
     scrollBy(0, Math.max(-1 * scrollY, deltaY));
     }
    }else{
     if(mPageBottom != null && mPageBottom.isAtTop()){
     scrollBy(0, deltaY);
     }
    }
   }
 break;
 case MotionEvent.ACTION_CANCEL:
 case MotionEvent.ACTION_UP:
 // 弹起手指后,切换屏幕的处理
 if (mTouchState == TOUCH_STATE_SCROLLING) {
  final VelocityTracker velocityTracker = mVelocityTracker;
    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
    int velocityY = (int) velocityTracker.getYVelocity();
    if (Math.abs(velocityY) > SNAP_VELOCITY) {
     if( velocityY > 0 && mCurrentScreen == 1 && mPageBottom.isAtTop()){
      snapToScreen(mDataIndex-1);
     }else if(velocityY < 0 && mCurrentScreen == 0){
      snapToScreen(mDataIndex+1);
     }else{
      snapToScreen(mDataIndex);
     }
    } else {
     snapToDestination();
    }
    if (mVelocityTracker != null) {
     mVelocityTracker.recycle();
     mVelocityTracker = null;
    }
 }else{
 }
 mTouchState = TOUCH_STATE_REST;
 break;
 
 default:
 break;
 }
 return true;
 }
 
 private void clearOnTouchEvents(){
 mTouchState = TOUCH_STATE_REST;
 if (mVelocityTracker != null) {
    mVelocityTracker.recycle();
    mVelocityTracker = null;
   }
 }
 
 private void snapToDestination() {
 // 计算应该去哪个屏
 final int flipHeight = getHeight() / 8;
  
  int whichScreen = -1;
  final int topEdge = getCurrentView().getTop();
 
  if(topEdge < getScrollY() && (getScrollY()-topEdge) >= flipHeight && mCurrentScreen == 0){
   //向下滑动
   whichScreen = mDataIndex + 1;
  }else if(topEdge > getScrollY() && (topEdge - getScrollY()) >= flipHeight && mCurrentScreen == 1){
   //向上滑动
   whichScreen = mDataIndex - 1;
  }else{
   whichScreen = mDataIndex;
  }
  Log.e(TAG, "snapToDestination mDataIndex = " + mDataIndex);
  Log.e(TAG, "snapToDestination whichScreen = " + whichScreen);
  snapToScreen(whichScreen);
 }
 
 private void snapToScreen(int dataIndex) {
  if (!mScroller.isFinished())
   return;
  
  final int direction = dataIndex - mDataIndex;
  mNextDataIndex = dataIndex;
  boolean changingScreens = dataIndex != mDataIndex;
  View focusedChild = getFocusedChild();
  if (focusedChild != null && changingScreens) {
   focusedChild.clearFocus();
  }
  //在这里判断是否已到目标位置~
  int newY = 0;
 switch (direction) {
 case 1: //需要滑动到第二页
 Log.e(TAG, "the direction is 1");
 newY = getCurrentView().getBottom(); // 最终停留的位置
 break;
 case -1: //需要滑动到第一页
 Log.e(TAG, "the direction is -1");
 Log.e(TAG, "getCurrentView().getTop() is "
  + getCurrentView().getTop() + " getHeight() is "
  + getHeight());
 newY = getCurrentView().getTop() - getHeight(); // 最终停留的位置
 break;
 case 0: //滑动距离不够, 因此不造成换页,回到滑动之前的位置
 Log.e(TAG, "the direction is 0");
 newY = getCurrentView().getTop(); //第一页的top是0, 第二页的top应该是第一页的高度
 break;
 default:
 break;
 }
  final int cy = getScrollY(); // 启动的位置
  Log.e(TAG, "the newY is " + newY + " cy is " + cy);
  final int delta = newY - cy; // 滑动的距离,正值是往左滑<—,负值是往右滑—>
  mScroller.startScroll(0, cy, 0, delta, Math.abs(delta));
  invalidate();
 }
 
 
 
}

McoySnapPage是定义在VIewGroup的一个接口, 比如说我们需要类似某东商品详情那样,有上下两页的效果。 那我就需要自己定义两个类实现这个接口,并实现接口的方法。getRootView需要返回当前页需要显示的布局内容;isAtTop需要返回当前页是否已经在顶端; isAtBottom需要返回当前页是否已经在底部

onInterceptTouchEventonTouchEvent决定当前的滑动状态, 并决定是有当前VIewGroup拦截touch事件还是由子view去消费touch事件

Demo地址

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://blog.csdn.net/zxm317122667/article/details/47018357?utm_source=blogxgwz6

延伸 · 阅读

精彩推荐