Android实现图片滚动和页签控件功能的实现代码

时间:2022-12-08 10:57:10

首先题外话,今天早上起床的时候,手滑一下把我的手机甩了出去,结果陪伴我两年半的摩托罗拉里程碑一代就这么安息了,于是我今天决定怒更一记,纪念我死去的爱机。

如果你是网购达人,你的手机上一定少不了淘宝客户端。关注特效的人一定都会发现,淘宝不管是网站还是手机客户端,主页上都会有一个图片滚动播放器,上面展示一些它推荐的商品。这个几乎可以用淘宝来冠名的功能,看起来还是挺炫的,我们今天就来实现一下。

实现原理其实还是之前那篇文章Android仿人人客户端滑动菜单的侧滑菜单效果,史上最简单的侧滑实现  ,算是以那个原理为基础的另外一个变种。正所谓一通百通,真正掌握一种方法之后,就可以使用这个方法变换出各种不通的效果。

今天仍然还是实现一个自定义控件,然后我们在任意Activity的布局文件中引用一下,即可实现图片滚动器的效果。

在Eclipse中新建一个Android项目,项目名就叫做SlidingViewSwitcher。

新建一个类,名叫SlidingSwitcherView,这个类是继承自RelativeLayout的,并且实现了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 {
 /**
 * 让菜单滚动,手指滑动需要达到的速度。
 */
 public static final int SNAP_VELOCITY = 200;
 /**
 * SlidingSwitcherView的宽度。
 */
 private int switcherViewWidth;
 /**
 * 当前显示的元素的下标。
 */
 private int currentItemIndex;
 /**
 * 菜单中包含的元素总数。
 */
 private int itemsCount;
 /**
 * 各个元素的偏移边界值。
 */
 private int[] borders;
 /**
 * 最多可以滑动到的左边缘。值由菜单中包含的元素总数来定,marginLeft到达此值之后,不能再减少。
 *
 */
 private int leftEdge = 0;
 /**
 * 最多可以滑动到的右边缘。值恒为0,marginLeft到达此值之后,不能再增加。
 */
 private int rightEdge = 0;
 /**
 * 记录手指按下时的横坐标。
 */
 private float xDown;
 /**
 * 记录手指移动时的横坐标。
 */
 private float xMove;
 /**
 * 记录手机抬起时的横坐标。
 */
 private float xUp;
 /**
 * 菜单布局。
 */
 private LinearLayout itemsLayout;
 /**
 * 标签布局。
 */
 private LinearLayout dotsLayout;
 /**
 * 菜单中的第一个元素。
 */
 private View firstItem;
 /**
 * 菜单中第一个元素的布局,用于改变leftMargin的值,来决定当前显示的哪一个元素。
 */
 private MarginLayoutParams firstItemParams;
 /**
 * 用于计算手指滑动的速度。
 */
 private VelocityTracker mVelocityTracker;
 /**
 * 重写SlidingSwitcherView的构造函数,用于允许在XML中引用当前的自定义布局。
 *
 * @param context
 * @param attrs
 */
 public SlidingSwitcherView(Context context, AttributeSet attrs) {
 super(context, attrs);
 }
 /**
 * 滚动到下一个元素。
 */
 public void scrollToNext() {
 new ScrollTask().execute(-20);
 }
 /**
 * 滚动到上一个元素。
 */
 public void scrollToPrevious() {
 new ScrollTask().execute(20);
 }
 /**
 * 在onLayout中重新设定菜单元素和标签元素的参数。
 */
 @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();
 }
 }
 /**
 * 初始化菜单元素,为每一个子元素增加监听事件,并且改变所有子元素的宽度,让它们等于父元素的宽度。
 */
 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();
 }
 /**
 * 初始化标签元素。
 */
 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:
  // 手指按下时,记录按下时的横坐标
  xDown = event.getRawX();
  break;
 case MotionEvent.ACTION_MOVE:
  // 手指移动时,对比按下时的横坐标,计算出移动的距离,来调整左侧布局的leftMargin值,从而显示和隐藏左侧布局
  xMove = event.getRawX();
  int distanceX = (int) (xMove - xDown) - (currentItemIndex * switcherViewWidth);
  firstItemParams.leftMargin = distanceX;
  if (beAbleToScroll()) {
  firstItem.setLayoutParams(firstItemParams);
  }
  break;
 case MotionEvent.ACTION_UP:
  // 手指抬起时,进行判断当前手势的意图,从而决定是滚动到左侧布局,还是滚动到右侧布局
  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;
 }
 /**
 * 当前是否能够滚动,滚动到第一个或最后一个元素时将不能再滚动。
 *
 * @return 当前leftMargin的值在leftEdge和rightEdge之间返回true,否则返回false。
 */
 private boolean beAbleToScroll() {
 return firstItemParams.leftMargin < rightEdge && firstItemParams.leftMargin > leftEdge;
 }
 /**
 * 判断当前手势的意图是不是想滚动到上一个菜单元素。如果手指移动的距离是正数,则认为当前手势是想要滚动到上一个菜单元素。
 *
 * @return 当前手势想滚动到上一个菜单元素返回true,否则返回false。
 */
 private boolean wantScrollToPrevious() {
 return xUp - xDown > 0;
 }
 /**
 * 判断当前手势的意图是不是想滚动到下一个菜单元素。如果手指移动的距离是负数,则认为当前手势是想要滚动到下一个菜单元素。
 *
 * @return 当前手势想滚动到下一个菜单元素返回true,否则返回false。
 */
 private boolean wantScrollToNext() {
 return xUp - xDown < 0;
 }
 /**
 * 判断是否应该滚动到下一个菜单元素。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY,
 * 就认为应该滚动到下一个菜单元素。
 *
 * @return 如果应该滚动到下一个菜单元素返回true,否则返回false。
 */
 private boolean shouldScrollToNext() {
 return xDown - xUp > switcherViewWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;
 }
 /**
 * 判断是否应该滚动到上一个菜单元素。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY,
 * 就认为应该滚动到上一个菜单元素。
 *
 * @return 如果应该滚动到上一个菜单元素返回true,否则返回false。
 */
 private boolean shouldScrollToPrevious() {
 return xUp - xDown > switcherViewWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;
 }
 /**
 * 刷新标签元素布局,每次currentItemIndex值改变的时候都应该进行刷新。
 */
 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);
 }
 }
 /**
 * 创建VelocityTracker对象,并将触摸事件加入到VelocityTracker当中。
 *
 * @param event
 *  右侧布局监听控件的滑动事件
 */
 private void createVelocityTracker(MotionEvent event) {
 if (mVelocityTracker == null) {
  mVelocityTracker = VelocityTracker.obtain();
 }
 mVelocityTracker.addMovement(event);
 }
 /**
 * 获取手指在右侧布局的监听View上的滑动速度。
 *
 * @return 滑动速度,以每秒钟移动了多少像素值为单位。
 */
 private int getScrollVelocity() {
 mVelocityTracker.computeCurrentVelocity(1000);
 int velocity = (int) mVelocityTracker.getXVelocity();
 return Math.abs(velocity);
 }
 /**
 * 回收VelocityTracker对象。
 */
 private void recycleVelocityTracker() {
 mVelocityTracker.recycle();
 mVelocityTracker = null;
 }
 /**
 * 检测菜单滚动时,是否有穿越border,border的值都存储在{@link #borders}中。
 *
 * @param leftMargin
 *  第一个元素的左偏移值
 * @param speed
 *  滚动的速度,正数说明向右滚动,负数说明向左滚动。
 * @return 穿越任何一个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;
 }
 /**
 * 找到离当前的leftMargin最近的一个border值。
 *
 * @param leftMargin
 *  第一个元素的左偏移值
 * @return 离当前的leftMargin最近的一个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;
  // 根据传入的速度来滚动界面,当滚动穿越border时,跳出循环。
  while (true) {
  leftMargin = leftMargin + speed[0];
  if (isCrossBorder(leftMargin, speed[0])) {
   leftMargin = findClosestBorder(leftMargin);
   break;
  }
  publishProgress(leftMargin);
  // 为了要有滚动效果产生,每次循环使线程睡眠10毫秒,这样肉眼才能够看到滚动动画。
  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);
 }
 }
 /**
 * 使当前线程睡眠指定的毫秒数。
 *
 * @param millis
 *  指定当前线程睡眠多久,以毫秒为单位
 */
 private void sleep(long millis) {
 try {
  Thread.sleep(millis);
 } catch (InterruptedException e) {
  e.printStackTrace();
 }
 }
}

细心的朋友可以看出来,我还是重用了很多之前的代码,这里有几个重要点我说一下。在onLayout方法里,重定义了各个包含图片的控件的大小,然后为每个包含图片的控件都注册了一个touch事件监听器。这样当我们滑动任何一样图片控件的时候,都会触发onTouch事件,然后通过改变第一个图片控件的leftMargin,去实现动画效果。之后在onLayout里又动态加入了页签View,有几个图片控件就会加入几个页签,然后根据currentItemIndex来决定高亮显示哪一个页签。其它也没什么要特别说明的了,更深的理解大家去看代码和注释吧。

然后看一下布局文件中如何使用我们自定义的这个控件,创建或打开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的根目录下放置了两个LinearLayout。第一个LinearLayout中要放入需要滚动显示的图片,这里我们加入了四个Button,每个Button都设置了一张背景图片。第二个LinearLayout中不需要加入任何东西,只要控制好大小和位置,标签会在运行的时候自动加入到这个layout中。

然后创建或打开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的代码,也都是自动生成的内容:

?
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>

好了,现在我们来看下运行效果吧,由于手机坏了,只能在模拟器上运行了。

首先是程序打开的时候,界面显示如下:

Android实现图片滚动和页签控件功能的实现代码

然后手指在图片上滑动,我们可以看到图片滚动的效果:

 Android实现图片滚动和页签控件功能的实现代码

不停的翻页,页签也会跟着一起改变,下图中我们可以看到高亮显示的点是变换的:

 Android实现图片滚动和页签控件功能的实现代码

恩,对比一下淘宝客户端的效果,我觉得我们模仿的还是挺好的。咦,好像少了点什么。。。。。。原来图片并不会自动播放。。。。。

没关系,我在后面的一篇文章中补充了自动播放这个功能,而且不仅仅是自动播放功能喔,请参考 Android使用自定义属性实现图片自动播放滚动的功能。

今天的文章就到这里了,有问题的朋友请在下面留言。

源码下载,请点击这里

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:https://blog.csdn.net/guolin_blog/article/details/8769904