首先題外話,今天早上起床的時(shí)候,手滑一下把我的手機(jī)甩了出去,結(jié)果陪伴我兩年半的摩托羅拉里程碑一代就這么安息了,于是我今天決定怒更一記,紀(jì)念我死去的愛機(jī)。
如果你是網(wǎng)購達(dá)人,你的手機(jī)上一定少不了淘寶客戶端。關(guān)注特效的人一定都會(huì)發(fā)現(xiàn),淘寶不管是網(wǎng)站還是手機(jī)客戶端,主頁上都會(huì)有一個(gè)圖片滾動(dòng)播放器,上面展示一些它推薦的商品。這個(gè)幾乎可以用淘寶來冠名的功能,看起來還是挺炫的,我們今天就來實(shí)現(xiàn)一下。
實(shí)現(xiàn)原理其實(shí)還是之前那篇文章Android仿人人客戶端滑動(dòng)菜單的側(cè)滑菜單效果,史上最簡單的側(cè)滑實(shí)現(xiàn) ,算是以那個(gè)原理為基礎(chǔ)的另外一個(gè)變種。正所謂一通百通,真正掌握一種方法之后,就可以使用這個(gè)方法變換出各種不通的效果。
今天仍然還是實(shí)現(xiàn)一個(gè)自定義控件,然后我們在任意Activity的布局文件中引用一下,即可實(shí)現(xiàn)圖片滾動(dòng)器的效果。
在Eclipse中新建一個(gè)Android項(xiàng)目,項(xiàng)目名就叫做SlidingViewSwitcher。
新建一個(gè)類,名叫SlidingSwitcherView,這個(gè)類是繼承自RelativeLayout的,并且實(shí)現(xiàn)了OnTouchListener接口,具體代碼如下:
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
|
public class SlidingSwitcherView extends RelativeLayout implements OnTouchListener { /** * 讓菜單滾動(dòng),手指滑動(dòng)需要達(dá)到的速度。 */ public static final int SNAP_VELOCITY = 200 ; /** * SlidingSwitcherView的寬度。 */ private int switcherViewWidth; /** * 當(dāng)前顯示的元素的下標(biāo)。 */ private int currentItemIndex; /** * 菜單中包含的元素總數(shù)。 */ private int itemsCount; /** * 各個(gè)元素的偏移邊界值。 */ private int [] borders; /** * 最多可以滑動(dòng)到的左邊緣。值由菜單中包含的元素總數(shù)來定,marginLeft到達(dá)此值之后,不能再減少。 * */ private int leftEdge = 0 ; /** * 最多可以滑動(dòng)到的右邊緣。值恒為0,marginLeft到達(dá)此值之后,不能再增加。 */ private int rightEdge = 0 ; /** * 記錄手指按下時(shí)的橫坐標(biāo)。 */ private float xDown; /** * 記錄手指移動(dòng)時(shí)的橫坐標(biāo)。 */ private float xMove; /** * 記錄手機(jī)抬起時(shí)的橫坐標(biāo)。 */ private float xUp; /** * 菜單布局。 */ private LinearLayout itemsLayout; /** * 標(biāo)簽布局。 */ private LinearLayout dotsLayout; /** * 菜單中的第一個(gè)元素。 */ private View firstItem; /** * 菜單中第一個(gè)元素的布局,用于改變leftMargin的值,來決定當(dāng)前顯示的哪一個(gè)元素。 */ private MarginLayoutParams firstItemParams; /** * 用于計(jì)算手指滑動(dòng)的速度。 */ private VelocityTracker mVelocityTracker; /** * 重寫SlidingSwitcherView的構(gòu)造函數(shù),用于允許在XML中引用當(dāng)前的自定義布局。 * * @param context * @param attrs */ public SlidingSwitcherView(Context context, AttributeSet attrs) { super (context, attrs); } /** * 滾動(dòng)到下一個(gè)元素。 */ public void scrollToNext() { new ScrollTask().execute(- 20 ); } /** * 滾動(dòng)到上一個(gè)元素。 */ public void scrollToPrevious() { new ScrollTask().execute( 20 ); } /** * 在onLayout中重新設(shè)定菜單元素和標(biāo)簽元素的參數(shù)。 */ @Override protected void onLayout( boolean changed, int l, int t, int r, int b) { super .onLayout(changed, l, t, r, b); if (changed) { initializeItems(); initializeDots(); } } /** * 初始化菜單元素,為每一個(gè)子元素增加監(jiān)聽事件,并且改變所有子元素的寬度,讓它們等于父元素的寬度。 */ private void initializeItems() { switcherViewWidth = getWidth(); itemsLayout = (LinearLayout) getChildAt( 0 ); itemsCount = itemsLayout.getChildCount(); borders = new int [itemsCount]; for ( int i = 0 ; i < itemsCount; i++) { borders[i] = -i * switcherViewWidth; View item = itemsLayout.getChildAt(i); MarginLayoutParams params = (MarginLayoutParams) item.getLayoutParams(); params.width = switcherViewWidth; item.setLayoutParams(params); item.setOnTouchListener( this ); } leftEdge = borders[itemsCount - 1 ]; firstItem = itemsLayout.getChildAt( 0 ); firstItemParams = (MarginLayoutParams) firstItem.getLayoutParams(); } /** * 初始化標(biāo)簽元素。 */ private void initializeDots() { dotsLayout = (LinearLayout) getChildAt( 1 ); refreshDotsLayout(); } @Override public boolean onTouch(View v, MotionEvent event) { createVelocityTracker(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 手指按下時(shí),記錄按下時(shí)的橫坐標(biāo) xDown = event.getRawX(); break ; case MotionEvent.ACTION_MOVE: // 手指移動(dòng)時(shí),對比按下時(shí)的橫坐標(biāo),計(jì)算出移動(dòng)的距離,來調(diào)整左側(cè)布局的leftMargin值,從而顯示和隱藏左側(cè)布局 xMove = event.getRawX(); int distanceX = ( int ) (xMove - xDown) - (currentItemIndex * switcherViewWidth); firstItemParams.leftMargin = distanceX; if (beAbleToScroll()) { firstItem.setLayoutParams(firstItemParams); } break ; case MotionEvent.ACTION_UP: // 手指抬起時(shí),進(jìn)行判斷當(dāng)前手勢的意圖,從而決定是滾動(dòng)到左側(cè)布局,還是滾動(dòng)到右側(cè)布局 xUp = event.getRawX(); if (beAbleToScroll()) { if (wantScrollToPrevious()) { if (shouldScrollToPrevious()) { currentItemIndex--; scrollToPrevious(); refreshDotsLayout(); } else { scrollToNext(); } } else if (wantScrollToNext()) { if (shouldScrollToNext()) { currentItemIndex++; scrollToNext(); refreshDotsLayout(); } else { scrollToPrevious(); } } } recycleVelocityTracker(); break ; } return false ; } /** * 當(dāng)前是否能夠滾動(dòng),滾動(dòng)到第一個(gè)或最后一個(gè)元素時(shí)將不能再滾動(dòng)。 * * @return 當(dāng)前l(fā)eftMargin的值在leftEdge和rightEdge之間返回true,否則返回false。 */ private boolean beAbleToScroll() { return firstItemParams.leftMargin < rightEdge && firstItemParams.leftMargin > leftEdge; } /** * 判斷當(dāng)前手勢的意圖是不是想滾動(dòng)到上一個(gè)菜單元素。如果手指移動(dòng)的距離是正數(shù),則認(rèn)為當(dāng)前手勢是想要滾動(dòng)到上一個(gè)菜單元素。 * * @return 當(dāng)前手勢想滾動(dòng)到上一個(gè)菜單元素返回true,否則返回false。 */ private boolean wantScrollToPrevious() { return xUp - xDown > 0 ; } /** * 判斷當(dāng)前手勢的意圖是不是想滾動(dòng)到下一個(gè)菜單元素。如果手指移動(dòng)的距離是負(fù)數(shù),則認(rèn)為當(dāng)前手勢是想要滾動(dòng)到下一個(gè)菜單元素。 * * @return 當(dāng)前手勢想滾動(dòng)到下一個(gè)菜單元素返回true,否則返回false。 */ private boolean wantScrollToNext() { return xUp - xDown < 0 ; } /** * 判斷是否應(yīng)該滾動(dòng)到下一個(gè)菜單元素。如果手指移動(dòng)距離大于屏幕的1/2,或者手指移動(dòng)速度大于SNAP_VELOCITY, * 就認(rèn)為應(yīng)該滾動(dòng)到下一個(gè)菜單元素。 * * @return 如果應(yīng)該滾動(dòng)到下一個(gè)菜單元素返回true,否則返回false。 */ private boolean shouldScrollToNext() { return xDown - xUp > switcherViewWidth / 2 || getScrollVelocity() > SNAP_VELOCITY; } /** * 判斷是否應(yīng)該滾動(dòng)到上一個(gè)菜單元素。如果手指移動(dòng)距離大于屏幕的1/2,或者手指移動(dòng)速度大于SNAP_VELOCITY, * 就認(rèn)為應(yīng)該滾動(dòng)到上一個(gè)菜單元素。 * * @return 如果應(yīng)該滾動(dòng)到上一個(gè)菜單元素返回true,否則返回false。 */ private boolean shouldScrollToPrevious() { return xUp - xDown > switcherViewWidth / 2 || getScrollVelocity() > SNAP_VELOCITY; } /** * 刷新標(biāo)簽元素布局,每次currentItemIndex值改變的時(shí)候都應(yīng)該進(jìn)行刷新。 */ private void refreshDotsLayout() { dotsLayout.removeAllViews(); for ( int i = 0 ; i < itemsCount; i++) { LinearLayout.LayoutParams linearParams = new LinearLayout.LayoutParams( 0 , LayoutParams.FILL_PARENT); linearParams.weight = 1 ; RelativeLayout relativeLayout = new RelativeLayout(getContext()); ImageView image = new ImageView(getContext()); if (i == currentItemIndex) { image.setBackgroundResource(R.drawable.dot_selected); } else { image.setBackgroundResource(R.drawable.dot_unselected); } RelativeLayout.LayoutParams relativeParams = new RelativeLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); relativeParams.addRule(RelativeLayout.CENTER_IN_PARENT); relativeLayout.addView(image, relativeParams); dotsLayout.addView(relativeLayout, linearParams); } } /** * 創(chuàng)建VelocityTracker對象,并將觸摸事件加入到VelocityTracker當(dāng)中。 * * @param event * 右側(cè)布局監(jiān)聽控件的滑動(dòng)事件 */ private void createVelocityTracker(MotionEvent event) { if (mVelocityTracker == null ) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); } /** * 獲取手指在右側(cè)布局的監(jiān)聽View上的滑動(dòng)速度。 * * @return 滑動(dòng)速度,以每秒鐘移動(dòng)了多少像素值為單位。 */ private int getScrollVelocity() { mVelocityTracker.computeCurrentVelocity( 1000 ); int velocity = ( int ) mVelocityTracker.getXVelocity(); return Math.abs(velocity); } /** * 回收VelocityTracker對象。 */ private void recycleVelocityTracker() { mVelocityTracker.recycle(); mVelocityTracker = null ; } /** * 檢測菜單滾動(dòng)時(shí),是否有穿越border,border的值都存儲(chǔ)在{@link #borders}中。 * * @param leftMargin * 第一個(gè)元素的左偏移值 * @param speed * 滾動(dòng)的速度,正數(shù)說明向右滾動(dòng),負(fù)數(shù)說明向左滾動(dòng)。 * @return 穿越任何一個(gè)border了返回true,否則返回false。 */ private boolean isCrossBorder( int leftMargin, int speed) { for ( int border : borders) { if (speed > 0 ) { if (leftMargin >= border && leftMargin - speed < border) { return true ; } } else { if (leftMargin <= border && leftMargin - speed > border) { return true ; } } } return false ; } /** * 找到離當(dāng)前的leftMargin最近的一個(gè)border值。 * * @param leftMargin * 第一個(gè)元素的左偏移值 * @return 離當(dāng)前的leftMargin最近的一個(gè)border值。 */ private int findClosestBorder( int leftMargin) { int absLeftMargin = Math.abs(leftMargin); int closestBorder = borders[ 0 ]; int closestMargin = Math.abs(Math.abs(closestBorder) - absLeftMargin); for ( int border : borders) { int margin = Math.abs(Math.abs(border) - absLeftMargin); if (margin < closestMargin) { closestBorder = border; closestMargin = margin; } } return closestBorder; } class ScrollTask extends AsyncTask<Integer, Integer, Integer> { @Override protected Integer doInBackground(Integer... speed) { int leftMargin = firstItemParams.leftMargin; // 根據(jù)傳入的速度來滾動(dòng)界面,當(dāng)滾動(dòng)穿越border時(shí),跳出循環(huán)。 while ( true ) { leftMargin = leftMargin + speed[ 0 ]; if (isCrossBorder(leftMargin, speed[ 0 ])) { leftMargin = findClosestBorder(leftMargin); break ; } publishProgress(leftMargin); // 為了要有滾動(dòng)效果產(chǎn)生,每次循環(huán)使線程睡眠10毫秒,這樣肉眼才能夠看到滾動(dòng)動(dòng)畫。 sleep( 10 ); } return leftMargin; } @Override protected void onProgressUpdate(Integer... leftMargin) { firstItemParams.leftMargin = leftMargin[ 0 ]; firstItem.setLayoutParams(firstItemParams); } @Override protected void onPostExecute(Integer leftMargin) { firstItemParams.leftMargin = leftMargin; firstItem.setLayoutParams(firstItemParams); } } /** * 使當(dāng)前線程睡眠指定的毫秒數(shù)。 * * @param millis * 指定當(dāng)前線程睡眠多久,以毫秒為單位 */ private void sleep( long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } } |
細(xì)心的朋友可以看出來,我還是重用了很多之前的代碼,這里有幾個(gè)重要點(diǎn)我說一下。在onLayout方法里,重定義了各個(gè)包含圖片的控件的大小,然后為每個(gè)包含圖片的控件都注冊了一個(gè)touch事件監(jiān)聽器。這樣當(dāng)我們滑動(dòng)任何一樣圖片控件的時(shí)候,都會(huì)觸發(fā)onTouch事件,然后通過改變第一個(gè)圖片控件的leftMargin,去實(shí)現(xiàn)動(dòng)畫效果。之后在onLayout里又動(dòng)態(tài)加入了頁簽View,有幾個(gè)圖片控件就會(huì)加入幾個(gè)頁簽,然后根據(jù)currentItemIndex來決定高亮顯示哪一個(gè)頁簽。其它也沒什么要特別說明的了,更深的理解大家去看代碼和注釋吧。
然后看一下布局文件中如何使用我們自定義的這個(gè)控件,創(chuàng)建或打開activity_main.xml,里面加入如下代碼:
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
|
< LinearLayout xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:tools = "http://schemas.android.com/tools" android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:orientation = "horizontal" tools:context = ".MainActivity" > < com.example.viewswitcher.SlidingSwitcherView android:id = "@+id/slidingLayout" android:layout_width = "fill_parent" android:layout_height = "100dip" > < LinearLayout android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:orientation = "horizontal" > < Button android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:background = "@drawable/image1" /> < Button android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:background = "@drawable/image2" /> < Button android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:background = "@drawable/image3" /> < Button android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:background = "@drawable/image4" /> </ LinearLayout > < LinearLayout android:layout_width = "60dip" android:layout_height = "20dip" android:layout_alignParentBottom = "true" android:layout_alignParentRight = "true" android:layout_margin = "15dip" android:orientation = "horizontal" > </ LinearLayout > </ com.example.viewswitcher.SlidingSwitcherView > </ LinearLayout > |
我們可以看到,com.example.viewswitcher.SlidingSwitcherView的根目錄下放置了兩個(gè)LinearLayout。第一個(gè)LinearLayout中要放入需要滾動(dòng)顯示的圖片,這里我們加入了四個(gè)Button,每個(gè)Button都設(shè)置了一張背景圖片。第二個(gè)LinearLayout中不需要加入任何東西,只要控制好大小和位置,標(biāo)簽會(huì)在運(yùn)行的時(shí)候自動(dòng)加入到這個(gè)layout中。
然后創(chuàng)建或打開MainActivity作為主界面,里面沒有加入任何新增的代碼:
1
2
3
4
5
6
7
|
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } |
最后是給出AndroidManifest.xml的代碼,也都是自動(dòng)生成的內(nèi)容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<? xml version = "1.0" encoding = "utf-8" ?> < manifest xmlns:android = "http://schemas.android.com/apk/res/android" package = "com.example.viewswitcher" android:versionCode = "1" android:versionName = "1.0" > < uses-sdk android:minSdkVersion = "8" android:targetSdkVersion = "8" /> < application android:allowBackup = "true" android:icon = "@drawable/ic_launcher" android:label = "@string/app_name" android:theme = "@android:style/Theme.NoTitleBar" > < activity android:name = "com.example.viewswitcher.MainActivity" android:label = "@string/app_name" > < intent-filter > < action android:name = "android.intent.action.MAIN" /> < category android:name = "android.intent.category.LAUNCHER" /> </ intent-filter > </ activity > </ application > </ manifest > |
好了,現(xiàn)在我們來看下運(yùn)行效果吧,由于手機(jī)壞了,只能在模擬器上運(yùn)行了。
首先是程序打開的時(shí)候,界面顯示如下:
然后手指在圖片上滑動(dòng),我們可以看到圖片滾動(dòng)的效果:
不停的翻頁,頁簽也會(huì)跟著一起改變,下圖中我們可以看到高亮顯示的點(diǎn)是變換的:
恩,對比一下淘寶客戶端的效果,我覺得我們模仿的還是挺好的。咦,好像少了點(diǎn)什么。。。。。。原來圖片并不會(huì)自動(dòng)播放。。。。。
沒關(guān)系,我在后面的一篇文章中補(bǔ)充了自動(dòng)播放這個(gè)功能,而且不僅僅是自動(dòng)播放功能喔,請參考 Android使用自定義屬性實(shí)現(xiàn)圖片自動(dòng)播放滾動(dòng)的功能。
今天的文章就到這里了,有問題的朋友請?jiān)谙旅媪粞浴?/p>
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對服務(wù)器之家的支持。
原文鏈接:https://blog.csdn.net/guolin_blog/article/details/8769904