在Eclipse中新建一个Android项目,项目名就叫做SlidingViewSwitcher。
新建一个类,名叫SlidingSwitcherView,这个类是继承自RelativeLayout的,并且实现了OnTouchListener接口,具体代码如下:
1. public class SlidingSwitcherView extends RelativeLayout implements OnTouchListener {
2.
3. /**
4. * 让菜单滚动,手指滑动需要达到的速度。
5. */
6. public static final int SNAP_VELOCITY = 200;
7.
8. /**
9. * SlidingSwitcherView的宽度。
10. */
11. private int switcherViewWidth;
12.
13. /**
14. * 当前显示的元素的下标。
15. */
16. private int currentItemIndex;
17.
18. /**
19. * 菜单中包含的元素总数。
20. */
21. private int itemsCount;
22.
23. /**
24. * 各个元素的偏移边界值。
25. */
26. private int[] borders;
27.
28. /**
29. * 最多可以滑动到的左边缘。值由菜单中包含的元素总数来定,marginLeft到达此值之后,不能再减少。
30. *
31. */
32. private int leftEdge = 0;
33.
34. /**
35. * 最多可以滑动到的右边缘。值恒为0,marginLeft到达此值之后,不能再增加。
36. */
37. private int rightEdge = 0;
38.
39. /**
40. * 记录手指按下时的横坐标。
41. */
42. private float xDown;
43.
44. /**
45. * 记录手指移动时的横坐标。
46. */
47. private float xMove;
48.
49. /**
50. * 记录手机抬起时的横坐标。
51. */
52. private float xUp;
53.
54. /**
55. * 菜单布局。
56. */
57. private LinearLayout itemsLayout;
58.
59. /**
60. * 标签布局。
61. */
62. private LinearLayout dotsLayout;
63.
64. /**
65. * 菜单中的第一个元素。
66. */
67. private View firstItem;
68.
69. /**
70. * 菜单中第一个元素的布局,用于改变leftMargin的值,来决定当前显示的哪一个元素。
71. */
72. private MarginLayoutParams firstItemParams;
73.
74. /**
75. * 用于计算手指滑动的速度。
76. */
77. private VelocityTracker mVelocityTracker;
78.
79. /**
80. * 重写SlidingSwitcherView的构造函数,用于允许在XML中引用当前的自定义布局。
81. *
82. * @param context
83. * @param attrs
84. */
85. public SlidingSwitcherView(Context context, AttributeSet attrs) {
86. super(context, attrs);
87. }
88.
89. /**
90. * 滚动到下一个元素。
91. */
92. public void scrollToNext() {
93. new ScrollTask().execute(-20);
94. }
95.
96. /**
97. * 滚动到上一个元素。
98. */
99. public void scrollToPrevious() {
100. new ScrollTask().execute(20);
101. }
102.
103. /**
104. * 在onLayout中重新设定菜单元素和标签元素的参数。
105. */
106. @Override
107. protected void onLayout(boolean changed, int l, int t, int r, int b) {
108. super.onLayout(changed, l, t, r, b);
109. if (changed) {
110. initializeItems();
111. initializeDots();
112. }
113. }
114.
115. /**
116. * 初始化菜单元素,为每一个子元素增加监听事件,并且改变所有子元素的宽度,让它们等于父元素的宽度。
117. */
118. private void initializeItems() {
119. switcherViewWidth = getWidth();
120. 0);
121. itemsCount = itemsLayout.getChildCount();
122. new int[itemsCount];
123. for (int i = 0; i < itemsCount; i++) {
124. borders[i] = -i * switcherViewWidth;
125. View item = itemsLayout.getChildAt(i);
126. MarginLayoutParams params = (MarginLayoutParams) item.getLayoutParams();
127. params.width = switcherViewWidth;
128. item.setLayoutParams(params);
129. this);
130. }
131. 1];
132. 0);
133. firstItemParams = (MarginLayoutParams) firstItem.getLayoutParams();
134. }
135.
136. /**
137. * 初始化标签元素。
138. */
139. private void initializeDots() {
140. 1);
141. refreshDotsLayout();
142. }
143.
144. @Override
145. public boolean onTouch(View v, MotionEvent event) {
146. createVelocityTracker(event);
147. switch (event.getAction()) {
148. case MotionEvent.ACTION_DOWN:
149. // 手指按下时,记录按下时的横坐标
150. xDown = event.getRawX();
151. break;
152. case MotionEvent.ACTION_MOVE:
153. // 手指移动时,对比按下时的横坐标,计算出移动的距离,来调整左侧布局的leftMargin值,从而显示和隐藏左侧布局
154. xMove = event.getRawX();
155. int distanceX = (int) (xMove - xDown) - (currentItemIndex * switcherViewWidth);
156. firstItemParams.leftMargin = distanceX;
157. if (beAbleToScroll()) {
158. firstItem.setLayoutParams(firstItemParams);
159. }
160. break;
161. case MotionEvent.ACTION_UP:
162. // 手指抬起时,进行判断当前手势的意图,从而决定是滚动到左侧布局,还是滚动到右侧布局
163. xUp = event.getRawX();
164. if (beAbleToScroll()) {
165. if (wantScrollToPrevious()) {
166. if (shouldScrollToPrevious()) {
167. currentItemIndex--;
168. scrollToPrevious();
169. refreshDotsLayout();
170. else {
171. scrollToNext();
172. }
173. else if (wantScrollToNext()) {
174. if (shouldScrollToNext()) {
175. currentItemIndex++;
176. scrollToNext();
177. refreshDotsLayout();
178. else {
179. scrollToPrevious();
180. }
181. }
182. }
183. recycleVelocityTracker();
184. break;
185. }
186. return false;
187. }
188.
189. /**
190. * 当前是否能够滚动,滚动到第一个或最后一个元素时将不能再滚动。
191. *
192. * @return 当前leftMargin的值在leftEdge和rightEdge之间返回true,否则返回false。
193. */
194. private boolean beAbleToScroll() {
195. return firstItemParams.leftMargin < rightEdge && firstItemParams.leftMargin > leftEdge;
196. }
197.
198. /**
199. * 判断当前手势的意图是不是想滚动到上一个菜单元素。如果手指移动的距离是正数,则认为当前手势是想要滚动到上一个菜单元素。
200. *
201. * @return 当前手势想滚动到上一个菜单元素返回true,否则返回false。
202. */
203. private boolean wantScrollToPrevious() {
204. return xUp - xDown > 0;
205. }
206.
207. /**
208. * 判断当前手势的意图是不是想滚动到下一个菜单元素。如果手指移动的距离是负数,则认为当前手势是想要滚动到下一个菜单元素。
209. *
210. * @return 当前手势想滚动到下一个菜单元素返回true,否则返回false。
211. */
212. private boolean wantScrollToNext() {
213. return xUp - xDown < 0;
214. }
215.
216. /**
217. * 判断是否应该滚动到下一个菜单元素。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY,
218. * 就认为应该滚动到下一个菜单元素。
219. *
220. * @return 如果应该滚动到下一个菜单元素返回true,否则返回false。
221. */
222. private boolean shouldScrollToNext() {
223. return xDown - xUp > switcherViewWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;
224. }
225.
226. /**
227. * 判断是否应该滚动到上一个菜单元素。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY,
228. * 就认为应该滚动到上一个菜单元素。
229. *
230. * @return 如果应该滚动到上一个菜单元素返回true,否则返回false。
231. */
232. private boolean shouldScrollToPrevious() {
233. return xUp - xDown > switcherViewWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;
234. }
235.
236. /**
237. * 刷新标签元素布局,每次currentItemIndex值改变的时候都应该进行刷新。
238. */
239. private void refreshDotsLayout() {
240. dotsLayout.removeAllViews();
241. for (int i = 0; i < itemsCount; i++) {
242. new LinearLayout.LayoutParams(0,
243. LayoutParams.FILL_PARENT);
244. 1;
245. new RelativeLayout(getContext());
246. new ImageView(getContext());
247. if (i == currentItemIndex) {
248. image.setBackgroundResource(R.drawable.dot_selected);
249. else {
250. image.setBackgroundResource(R.drawable.dot_unselected);
251. }
252. new RelativeLayout.LayoutParams(
253. LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
254. relativeParams.addRule(RelativeLayout.CENTER_IN_PARENT);
255. relativeLayout.addView(image, relativeParams);
256. dotsLayout.addView(relativeLayout, linearParams);
257. }
258. }
259.
260. /**
261. * 创建VelocityTracker对象,并将触摸事件加入到VelocityTracker当中。
262. *
263. * @param event
264. * 右侧布局监听控件的滑动事件
265. */
266. private void createVelocityTracker(MotionEvent event) {
267. if (mVelocityTracker == null) {
268. mVelocityTracker = VelocityTracker.obtain();
269. }
270. mVelocityTracker.addMovement(event);
271. }
272.
273. /**
274. * 获取手指在右侧布局的监听View上的滑动速度。
275. *
276. * @return 滑动速度,以每秒钟移动了多少像素值为单位。
277. */
278. private int getScrollVelocity() {
279. 1000);
280. int velocity = (int) mVelocityTracker.getXVelocity();
281. return Math.abs(velocity);
282. }
283.
284. /**
285. * 回收VelocityTracker对象。
286. */
287. private void recycleVelocityTracker() {
288. mVelocityTracker.recycle();
289. null;
290. }
291.
292. /**
293. * 检测菜单滚动时,是否有穿越border,border的值都存储在{@link #borders}中。
294. *
295. * @param leftMargin
296. * 第一个元素的左偏移值
297. * @param speed
298. * 滚动的速度,正数说明向右滚动,负数说明向左滚动。
299. * @return 穿越任何一个border了返回true,否则返回false。
300. */
301. private boolean isCrossBorder(int leftMargin, int speed) {
302. for (int border : borders) {
303. if (speed > 0) {
304. if (leftMargin >= border && leftMargin - speed < border) {
305. return true;
306. }
307. else {
308. if (leftMargin <= border && leftMargin - speed > border) {
309. return true;
310. }
311. }
312. }
313. return false;
314. }
315.
316. /**
317. * 找到离当前的leftMargin最近的一个border值。
318. *
319. * @param leftMargin
320. * 第一个元素的左偏移值
321. * @return 离当前的leftMargin最近的一个border值。
322. */
323. private int findClosestBorder(int leftMargin) {
324. int absLeftMargin = Math.abs(leftMargin);
325. int closestBorder = borders[0];
326. int closestMargin = Math.abs(Math.abs(closestBorder) - absLeftMargin);
327. for (int border : borders) {
328. int margin = Math.abs(Math.abs(border) - absLeftMargin);
329. if (margin < closestMargin) {
330. closestBorder = border;
331. closestMargin = margin;
332. }
333. }
334. return closestBorder;
335. }
336.
337. class ScrollTask extends AsyncTask<Integer, Integer, Integer> {
338.
339. @Override
340. protected Integer doInBackground(Integer... speed) {
341. int leftMargin = firstItemParams.leftMargin;
342. // 根据传入的速度来滚动界面,当滚动穿越border时,跳出循环。
343. while (true) {
344. 0];
345. if (isCrossBorder(leftMargin, speed[0])) {
346. leftMargin = findClosestBorder(leftMargin);
347. break;
348. }
349. publishProgress(leftMargin);
350. // 为了要有滚动效果产生,每次循环使线程睡眠10毫秒,这样肉眼才能够看到滚动动画。
351. 10);
352. }
353. return leftMargin;
354. }
355.
356. @Override
357. protected void onProgressUpdate(Integer... leftMargin) {
358. 0];
359. firstItem.setLayoutParams(firstItemParams);
360. }
361.
362. @Override
363. protected void onPostExecute(Integer leftMargin) {
364. firstItemParams.leftMargin = leftMargin;
365. firstItem.setLayoutParams(firstItemParams);
366. }
367. }
368.
369. /**
370. * 使当前线程睡眠指定的毫秒数。
371. *
372. * @param millis
373. * 指定当前线程睡眠多久,以毫秒为单位
374. */
375. private void sleep(long millis) {
376. try {
377. Thread.sleep(millis);
378. catch (InterruptedException e) {
379. e.printStackTrace();
380. }
381. }
382. }
细心的朋友可以看出来,我还是重用了很多之前的代码,这里有几个重要点我说一下。在onLayout方法里,重定义了各个包含图片的控件的大小,然后为每个包含图片的控件都注册了一个touch事件监听器。这样当我们滑动任何一样图片控件的时候,都会触发onTouch事件,然后通过改变第一个图片控件的leftMargin,去实现动画效果。之后在onLayout里又动态加入了页签View,有几个图片控件就会加入几个页签,然后根据currentItemIndex来决定高亮显示哪一个页签。其它也没什么要特别说明的了,更深的理解大家去看代码和注释吧。
然后看一下布局文件中如何使用我们自定义的这个控件,创建或打开activity_main.xml,里面加入如下代码:
1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2. xmlns:tools="http://schemas.android.com/tools"
3. android:layout_width="fill_parent"
4. android:layout_height="fill_parent"
5. android:orientation="horizontal"
6. tools:context=".MainActivity" >
7.
8. <com.example.viewswitcher.SlidingSwitcherView
9. android:id="@+id/slidingLayout"
10. android:layout_width="fill_parent"
11. android:layout_height="100dip" >
12.
13. <LinearLayout
14. android:layout_width="fill_parent"
15. android:layout_height="fill_parent"
16. android:orientation="horizontal" >
17.
18. <Button
19. android:layout_width="fill_parent"
20. android:layout_height="fill_parent"
21. android:background="@drawable/image1" />
22.
23. <Button
24. android:layout_width="fill_parent"
25. android:layout_height="fill_parent"
26. android:background="@drawable/image2" />
27.
28. <Button
29. android:layout_width="fill_parent"
30. android:layout_height="fill_parent"
31. android:background="@drawable/image3" />
32.
33. <Button
34. android:layout_width="fill_parent"
35. android:layout_height="fill_parent"
36. android:background="@drawable/image4" />
37. </LinearLayout>
38.
39. <LinearLayout
40. android:layout_width="60dip"
41. android:layout_height="20dip"
42. android:layout_alignParentBottom="true"
43. android:layout_alignParentRight="true"
44. android:layout_margin="15dip"
45. android:orientation="horizontal" >
46. </LinearLayout>
47. </com.example.viewswitcher.SlidingSwitcherView>
48.
49. </LinearLayout>
我们可以看到,com.example.viewswitcher.SlidingSwitcherView的根目录下放置了两个LinearLayout。第一个LinearLayout中要放入需要滚动显示的图片,这里我们加入了四个Button,每个Button都设置了一张背景图片。第二个LinearLayout中不需要加入任何东西,只要控制好大小和位置,标签会在运行的时候自动加入到这个layout中。
然后创建或打开MainActivity作为主界面,里面没有加入任何新增的代码:
1. public class MainActivity extends Activity {
2.
3. @Override
4. protected void onCreate(Bundle savedInstanceState) {
5. super.onCreate(savedInstanceState);
6. setContentView(R.layout.activity_main);
7. }
8.
9. }
最后是给出AndroidManifest.xml的代码,也都是自动生成的内容:
1. <?xml version="1.0" encoding="utf-8"?>
2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3. package="com.example.viewswitcher"
4. android:versionCode="1"
5. android:versionName="1.0" >
6.
7. <uses-sdk
8. android:minSdkVersion="8"
9. android:targetSdkVersion="8" />
10.
11. <application
12. android:allowBackup="true"
13. android:icon="@drawable/ic_launcher"
14. android:label="@string/app_name"
15. android:theme="@android:style/Theme.NoTitleBar" >
16. <activity
17. android:name="com.example.viewswitcher.MainActivity"
18. android:label="@string/app_name" >
19. <intent-filter>
20. <action android:name="android.intent.action.MAIN" />
21.
22. <category android:name="android.intent.category.LAUNCHER" />
23. </intent-filter>
24. </activity>
25. </application>
26.
27. </manifest>
好了,现在我们来看下运行效果吧,由于手机坏了,只能在模拟器上运行了。
首先是程序打开的时候,界面显示如下:
然后手指在图片上滑动,我们可以看到图片滚动的效果:
不停的翻页,页签也会跟着一起改变,下图中我们可以看到高亮显示的点是变换的:
恩,对比一下淘宝客户端的效果,我觉得我们模仿的还是挺好的。咦,好像少了点什么。。。。。。原来图片并不会自动播放。。。。。
既然是要加入自动播放的功能,那么就有一个非常重要的问题需要考虑。如果当前已经滚动到了最后一张图片,应该怎么办?由于我们目前的实现方案是,所有的图片都按照布局文件里面定义的顺序横向排列,然后通过偏移第一个图片的leftMargin,来决定显示哪一张图片。因此当图片滚动在最后一张时,我们可以让程序迅速地回滚到第一张图片,然后从头开始滚动。这种效果和淘宝客户端是有一定差异的(淘宝并没有回滚机制,而是很自然地由最后一张图片滚动到第一张图片),我也研究过淘宝图片滚动器的实现方法,并不难实现。但是由于我们是基于上次的代码进行开发的,方案上无法实现和淘宝客户端一样的效果,因此这里也就不追求和它完全一致了,各有风格也挺好的。
好了,现在开始实现功能,首先是打开SlidingSwitcherView,在里面加入一个新的AsyncTask,专门用于回滚到第一张图片:
1. [java] view plaincopyprint?
2. class ScrollToFirstItemTask extends AsyncTask {
3. @Override
4. protected Integer doInBackground(Integer... speed) {
5. int leftMargin = firstItemParams.leftMargin;
6. while (true) {
7. leftMargin = leftMargin + speed[0];
8. // 当leftMargin大于0时,说明已经滚动到了第一个元素,跳出循环
9. if (leftMargin > 0) {
10. leftMargin = 0;
11. break;
12. }
13. publishProgress(leftMargin);
14. sleep(20);
15. }
16. return leftMargin;
17. }
18. @Override
19. protected void onProgressUpdate(Integer... leftMargin) {
20. firstItemParams.leftMargin = leftMargin[0];
21. firstItem.setLayoutParams(firstItemParams);
22. }
23. @Override
24. protected void onPostExecute(Integer leftMargin) {
25. firstItemParams.leftMargin = leftMargin;
26. firstItem.setLayoutParams(firstItemParams);
27. }
28. }
29. 然后在SlidingSwitcherView里面加入一个新的方法:
30. [java] view plaincopyprint?
31. /**
32. * 滚动到第一个元素。
33. */
34. public void scrollToFirstItem() {
35. new ScrollToFirstItemTask().execute(20 * itemsCount);
36. }
复制代码
这个方法非常简单,就是启动了我们新增的ScrollToFirstItemTask,滚动速度设定为20 * itemsCount,这样当我们需要滚动的图片数量越多,回滚速度就会越快。定义好这个方法后,只要在任意地方调用scrollToFirstItem这个方法,就可以立刻从当前图片回滚到第一张图片了。
OK,然后我们要定义一个方法用于启动自动播放功能。仍然是在SlidingSwitcherView中新增如下代码:
1. [java] view plaincopyprint?
2. /**
3. * 用于在定时器当中操作UI界面。
4. */
5. private Handler handler = new Handler();
6. /**
7. * 开启图片自动播放功能,当滚动到最后一张图片的时候,会自动回滚到第一张图片。
8. */
9. public void startAutoPlay() {
10. new Timer().scheduleAtFixedRate(new TimerTask() {
11. @Override
12. public void run() {
13. if (currentItemIndex == itemsCount - 1) {
14. currentItemIndex = 0;
15. handler.post(new Runnable() {
16. @Override
17. public void run() {
18. scrollToFirstItem();
19. refreshDotsLayout();
20. }
21. });
22. } else {
23. currentItemIndex++;
24. handler.post(new Runnable() {
25. @Override
26. public void run() {
27. scrollToNext();
28. refreshDotsLayout();
29. }
30. });
31. }
32. }
33. }, 3000, 3000);
34. }
复制代码
我们可以看到,这个方法里启用了一个定时器,每隔三秒就会执行一次。然后在定时器的执行逻辑里面进行判断当前图片是否是最后一张,如果不是最后一张就滚动到下一张图片,如果是最后一张就回滚到第一张图片。其中需要注意,定时器中的代码是在子线程中运行的,而滚动图片操作和更新页签操作都是UI操作,因此需要放到Handler中去执行
。
之后只要在Activity创建的时候去调用SlidingSwitcherView的startAutoPlay方法,自动播放功能就实现了!
结束了?Naive!! 如果就这么结束了,怎么对得起大家的期待,如此简单的功能还要用一篇文章来讲简直是弱爆了。
接下来才是今天的重点,我们要使用自定义属性来启用自动播放功能,这样才能让你更加接近高手,才能让你更加玩转Android。
那我们继续,在res/values目录下新建一个attrs.xml文件,里面加入代码:
复制代码
其中,auto_play是我们将要使用的属性名,格式是布尔型。SlidingSwitcherView这个值可以随意,主要在代码中需要引用相应的id。
然后重写SlidingSwitcherView的构造函数,在里面加入从布局文件中获取自定义属性的代码:
1. [java] view plaincopyprint?
2. public SlidingSwitcherView(Context context, AttributeSet attrs) {
3. super(context, attrs);
4. TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingSwitcherView);
5. boolean isAutoPlay = a.getBoolean(R.styleable.SlidingSwitcherView_auto_play, false);
6. if (isAutoPlay) {
7. startAutoPlay();
8. }
9. a.recycle();
10. }
复制代码
可以看到,我们在构造函数中去获取auto_play的值,如果为true,就调用startAutoPlay方法,从而启用了自动播放的功能。
接下来就是见证奇迹的时刻!让我们打开activity_main.xml,在里面加入两行关键性代码。在最外层的LinearLayout加入xmlns:myattr="http://schemas.android.com/apk/res/com.example.viewswitcher"。在我们自定义的com.example.viewswitcher.SlidingSwitcherView加入myattr:auto_play="true"。
完整XML代码如下:
1. [html] view plaincopyprint?
2. xmlns:tools="http://schemas.android.com/tools"
3. xmlns:myattr="http://schemas.android.com/apk/res/com.example.viewswitcher"
4. android:layout_width="fill_parent"
5. android:layout_height="fill_parent"
6. android:orientation="vertical"
7. tools:context=".MainActivity" >
8. android:id="@+id/slidingLayout"
9. myattr:auto_play="true"
10. android:layout_width="fill_parent"
11. android:layout_height="100dip" >
12. android:layout_width="fill_parent"
13. android:layout_height="fill_parent"
14. android:orientation="horizontal" >
15. android:layout_width="fill_parent"
16. android:layout_height="fill_parent"
17. android:background="@drawable/image1" />
18. android:layout_width="fill_parent"
19. android:layout_height="fill_parent"
20. android:background="@drawable/image2" />
21. android:layout_width="fill_parent"
22. android:layout_height="fill_parent"
23. android:background="@drawable/image3" />
24. android:layout_width="fill_parent"
25. android:layout_height="fill_parent"
26. android:background="@drawable/image4" />
27. android:layout_width="60dip"
28. android:layout_height="20dip"
29. android:layout_alignParentBottom="true"
30. android:layout_alignParentRight="true"
31. android:layout_margin="15dip"
32. android:orientation="horizontal" >
复制代码
也就是说,我们只需要通过设定myattr:auto_play是等于true还是false,就可以决定是否启用自动播放功能,非常简单方便。