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

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

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

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

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

新建一个类,名叫SlidingSwitcherView,这个类是继承自RelativeLayout的,并且实现了OnTouchListener接口,具体代码如下:

[java] view
plain
copy

  1. public class SlidingSwitcherView extends RelativeLayout implements OnTouchListener {
  2. /**
  3. * 让菜单滚动,手指滑动需要达到的速度。
  4. */
  5. public static final int SNAP_VELOCITY = 200;
  6. /**
  7. * SlidingSwitcherView的宽度。
  8. */
  9. private int switcherViewWidth;
  10. /**
  11. * 当前显示的元素的下标。
  12. */
  13. private int currentItemIndex;
  14. /**
  15. * 菜单中包含的元素总数。
  16. */
  17. private int itemsCount;
  18. /**
  19. * 各个元素的偏移边界值。
  20. */
  21. private int[] borders;
  22. /**
  23. * 最多可以滑动到的左边缘。值由菜单中包含的元素总数来定,marginLeft到达此值之后,不能再减少。
  24. *
  25. */
  26. private int leftEdge = 0;
  27. /**
  28. * 最多可以滑动到的右边缘。值恒为0,marginLeft到达此值之后,不能再增加。
  29. */
  30. private int rightEdge = 0;
  31. /**
  32. * 记录手指按下时的横坐标。
  33. */
  34. private float xDown;
  35. /**
  36. * 记录手指移动时的横坐标。
  37. */
  38. private float xMove;
  39. /**
  40. * 记录手机抬起时的横坐标。
  41. */
  42. private float xUp;
  43. /**
  44. * 菜单布局。
  45. */
  46. private LinearLayout itemsLayout;
  47. /**
  48. * 标签布局。
  49. */
  50. private LinearLayout dotsLayout;
  51. /**
  52. * 菜单中的第一个元素。
  53. */
  54. private View firstItem;
  55. /**
  56. * 菜单中第一个元素的布局,用于改变leftMargin的值,来决定当前显示的哪一个元素。
  57. */
  58. private MarginLayoutParams firstItemParams;
  59. /**
  60. * 用于计算手指滑动的速度。
  61. */
  62. private VelocityTracker mVelocityTracker;
  63. /**
  64. * 重写SlidingSwitcherView的构造函数,用于允许在XML中引用当前的自定义布局。
  65. *
  66. * @param context
  67. * @param attrs
  68. */
  69. public SlidingSwitcherView(Context context, AttributeSet attrs) {
  70. super(context, attrs);
  71. }
  72. /**
  73. * 滚动到下一个元素。
  74. */
  75. public void scrollToNext() {
  76. new ScrollTask().execute(-20);
  77. }
  78. /**
  79. * 滚动到上一个元素。
  80. */
  81. public void scrollToPrevious() {
  82. new ScrollTask().execute(20);
  83. }
  84. /**
  85. * 在onLayout中重新设定菜单元素和标签元素的参数。
  86. */
  87. @Override
  88. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  89. super.onLayout(changed, l, t, r, b);
  90. if (changed) {
  91. initializeItems();
  92. initializeDots();
  93. }
  94. }
  95. /**
  96. * 初始化菜单元素,为每一个子元素增加监听事件,并且改变所有子元素的宽度,让它们等于父元素的宽度。
  97. */
  98. private void initializeItems() {
  99. switcherViewWidth = getWidth();
  100. itemsLayout = (LinearLayout) getChildAt(0);
  101. itemsCount = itemsLayout.getChildCount();
  102. borders = new int[itemsCount];
  103. for (int i = 0; i < itemsCount; i++) {
  104. borders[i] = -i * switcherViewWidth;
  105. View item = itemsLayout.getChildAt(i);
  106. MarginLayoutParams params = (MarginLayoutParams) item.getLayoutParams();
  107. params.width = switcherViewWidth;
  108. item.setLayoutParams(params);
  109. item.setOnTouchListener(this);
  110. }
  111. leftEdge = borders[itemsCount - 1];
  112. firstItem = itemsLayout.getChildAt(0);
  113. firstItemParams = (MarginLayoutParams) firstItem.getLayoutParams();
  114. }
  115. /**
  116. * 初始化标签元素。
  117. */
  118. private void initializeDots() {
  119. dotsLayout = (LinearLayout) getChildAt(1);
  120. refreshDotsLayout();
  121. }
  122. @Override
  123. public boolean onTouch(View v, MotionEvent event) {
  124. createVelocityTracker(event);
  125. switch (event.getAction()) {
  126. case MotionEvent.ACTION_DOWN:
  127. // 手指按下时,记录按下时的横坐标
  128. xDown = event.getRawX();
  129. break;
  130. case MotionEvent.ACTION_MOVE:
  131. // 手指移动时,对比按下时的横坐标,计算出移动的距离,来调整左侧布局的leftMargin值,从而显示和隐藏左侧布局
  132. xMove = event.getRawX();
  133. int distanceX = (int) (xMove - xDown) - (currentItemIndex * switcherViewWidth);
  134. firstItemParams.leftMargin = distanceX;
  135. if (beAbleToScroll()) {
  136. firstItem.setLayoutParams(firstItemParams);
  137. }
  138. break;
  139. case MotionEvent.ACTION_UP:
  140. // 手指抬起时,进行判断当前手势的意图,从而决定是滚动到左侧布局,还是滚动到右侧布局
  141. xUp = event.getRawX();
  142. if (beAbleToScroll()) {
  143. if (wantScrollToPrevious()) {
  144. if (shouldScrollToPrevious()) {
  145. currentItemIndex--;
  146. scrollToPrevious();
  147. refreshDotsLayout();
  148. } else {
  149. scrollToNext();
  150. }
  151. } else if (wantScrollToNext()) {
  152. if (shouldScrollToNext()) {
  153. currentItemIndex++;
  154. scrollToNext();
  155. refreshDotsLayout();
  156. } else {
  157. scrollToPrevious();
  158. }
  159. }
  160. }
  161. recycleVelocityTracker();
  162. break;
  163. }
  164. return false;
  165. }
  166. /**
  167. * 当前是否能够滚动,滚动到第一个或最后一个元素时将不能再滚动。
  168. *
  169. * @return 当前leftMargin的值在leftEdge和rightEdge之间返回true,否则返回false。
  170. */
  171. private boolean beAbleToScroll() {
  172. return firstItemParams.leftMargin < rightEdge && firstItemParams.leftMargin > leftEdge;
  173. }
  174. /**
  175. * 判断当前手势的意图是不是想滚动到上一个菜单元素。如果手指移动的距离是正数,则认为当前手势是想要滚动到上一个菜单元素。
  176. *
  177. * @return 当前手势想滚动到上一个菜单元素返回true,否则返回false。
  178. */
  179. private boolean wantScrollToPrevious() {
  180. return xUp - xDown > 0;
  181. }
  182. /**
  183. * 判断当前手势的意图是不是想滚动到下一个菜单元素。如果手指移动的距离是负数,则认为当前手势是想要滚动到下一个菜单元素。
  184. *
  185. * @return 当前手势想滚动到下一个菜单元素返回true,否则返回false。
  186. */
  187. private boolean wantScrollToNext() {
  188. return xUp - xDown < 0;
  189. }
  190. /**
  191. * 判断是否应该滚动到下一个菜单元素。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY,
  192. * 就认为应该滚动到下一个菜单元素。
  193. *
  194. * @return 如果应该滚动到下一个菜单元素返回true,否则返回false。
  195. */
  196. private boolean shouldScrollToNext() {
  197. return xDown - xUp > switcherViewWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;
  198. }
  199. /**
  200. * 判断是否应该滚动到上一个菜单元素。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY,
  201. * 就认为应该滚动到上一个菜单元素。
  202. *
  203. * @return 如果应该滚动到上一个菜单元素返回true,否则返回false。
  204. */
  205. private boolean shouldScrollToPrevious() {
  206. return xUp - xDown > switcherViewWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;
  207. }
  208. /**
  209. * 刷新标签元素布局,每次currentItemIndex值改变的时候都应该进行刷新。
  210. */
  211. private void refreshDotsLayout() {
  212. dotsLayout.removeAllViews();
  213. for (int i = 0; i < itemsCount; i++) {
  214. LinearLayout.LayoutParams linearParams = new LinearLayout.LayoutParams(0,
  215. LayoutParams.FILL_PARENT);
  216. linearParams.weight = 1;
  217. RelativeLayout relativeLayout = new RelativeLayout(getContext());
  218. ImageView image = new ImageView(getContext());
  219. if (i == currentItemIndex) {
  220. image.setBackgroundResource(R.drawable.dot_selected);
  221. } else {
  222. image.setBackgroundResource(R.drawable.dot_unselected);
  223. }
  224. RelativeLayout.LayoutParams relativeParams = new RelativeLayout.LayoutParams(
  225. LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
  226. relativeParams.addRule(RelativeLayout.CENTER_IN_PARENT);
  227. relativeLayout.addView(image, relativeParams);
  228. dotsLayout.addView(relativeLayout, linearParams);
  229. }
  230. }
  231. /**
  232. * 创建VelocityTracker对象,并将触摸事件加入到VelocityTracker当中。
  233. *
  234. * @param event
  235. *            右侧布局监听控件的滑动事件
  236. */
  237. private void createVelocityTracker(MotionEvent event) {
  238. if (mVelocityTracker == null) {
  239. mVelocityTracker = VelocityTracker.obtain();
  240. }
  241. mVelocityTracker.addMovement(event);
  242. }
  243. /**
  244. * 获取手指在右侧布局的监听View上的滑动速度。
  245. *
  246. * @return 滑动速度,以每秒钟移动了多少像素值为单位。
  247. */
  248. private int getScrollVelocity() {
  249. mVelocityTracker.computeCurrentVelocity(1000);
  250. int velocity = (int) mVelocityTracker.getXVelocity();
  251. return Math.abs(velocity);
  252. }
  253. /**
  254. * 回收VelocityTracker对象。
  255. */
  256. private void recycleVelocityTracker() {
  257. mVelocityTracker.recycle();
  258. mVelocityTracker = null;
  259. }
  260. /**
  261. * 检测菜单滚动时,是否有穿越border,border的值都存储在{@link #borders}中。
  262. *
  263. * @param leftMargin
  264. *            第一个元素的左偏移值
  265. * @param speed
  266. *            滚动的速度,正数说明向右滚动,负数说明向左滚动。
  267. * @return 穿越任何一个border了返回true,否则返回false。
  268. */
  269. private boolean isCrossBorder(int leftMargin, int speed) {
  270. for (int border : borders) {
  271. if (speed > 0) {
  272. if (leftMargin >= border && leftMargin - speed < border) {
  273. return true;
  274. }
  275. } else {
  276. if (leftMargin <= border && leftMargin - speed > border) {
  277. return true;
  278. }
  279. }
  280. }
  281. return false;
  282. }
  283. /**
  284. * 找到离当前的leftMargin最近的一个border值。
  285. *
  286. * @param leftMargin
  287. *            第一个元素的左偏移值
  288. * @return 离当前的leftMargin最近的一个border值。
  289. */
  290. private int findClosestBorder(int leftMargin) {
  291. int absLeftMargin = Math.abs(leftMargin);
  292. int closestBorder = borders[0];
  293. int closestMargin = Math.abs(Math.abs(closestBorder) - absLeftMargin);
  294. for (int border : borders) {
  295. int margin = Math.abs(Math.abs(border) - absLeftMargin);
  296. if (margin < closestMargin) {
  297. closestBorder = border;
  298. closestMargin = margin;
  299. }
  300. }
  301. return closestBorder;
  302. }
  303. class ScrollTask extends AsyncTask<Integer, Integer, Integer> {
  304. @Override
  305. protected Integer doInBackground(Integer... speed) {
  306. int leftMargin = firstItemParams.leftMargin;
  307. // 根据传入的速度来滚动界面,当滚动穿越border时,跳出循环。
  308. while (true) {
  309. leftMargin = leftMargin + speed[0];
  310. if (isCrossBorder(leftMargin, speed[0])) {
  311. leftMargin = findClosestBorder(leftMargin);
  312. break;
  313. }
  314. publishProgress(leftMargin);
  315. // 为了要有滚动效果产生,每次循环使线程睡眠10毫秒,这样肉眼才能够看到滚动动画。
  316. sleep(10);
  317. }
  318. return leftMargin;
  319. }
  320. @Override
  321. protected void onProgressUpdate(Integer... leftMargin) {
  322. firstItemParams.leftMargin = leftMargin[0];
  323. firstItem.setLayoutParams(firstItemParams);
  324. }
  325. @Override
  326. protected void onPostExecute(Integer leftMargin) {
  327. firstItemParams.leftMargin = leftMargin;
  328. firstItem.setLayoutParams(firstItemParams);
  329. }
  330. }
  331. /**
  332. * 使当前线程睡眠指定的毫秒数。
  333. *
  334. * @param millis
  335. *            指定当前线程睡眠多久,以毫秒为单位
  336. */
  337. private void sleep(long millis) {
  338. try {
  339. Thread.sleep(millis);
  340. } catch (InterruptedException e) {
  341. e.printStackTrace();
  342. }
  343. }
  344. }

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

然后看一下布局文件中如何使用我们自定义的这个控件,创建或打开activity_main.xml,里面加入如下代码:

[html] view
plain
copy

  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. <com.example.viewswitcher.SlidingSwitcherView
  8. android:id="@+id/slidingLayout"
  9. android:layout_width="fill_parent"
  10. android:layout_height="100dip" >
  11. <LinearLayout
  12. android:layout_width="fill_parent"
  13. android:layout_height="fill_parent"
  14. android:orientation="horizontal" >
  15. <Button
  16. android:layout_width="fill_parent"
  17. android:layout_height="fill_parent"
  18. android:background="@drawable/image1" />
  19. <Button
  20. android:layout_width="fill_parent"
  21. android:layout_height="fill_parent"
  22. android:background="@drawable/image2" />
  23. <Button
  24. android:layout_width="fill_parent"
  25. android:layout_height="fill_parent"
  26. android:background="@drawable/image3" />
  27. <Button
  28. android:layout_width="fill_parent"
  29. android:layout_height="fill_parent"
  30. android:background="@drawable/image4" />
  31. </LinearLayout>
  32. <LinearLayout
  33. android:layout_width="60dip"
  34. android:layout_height="20dip"
  35. android:layout_alignParentBottom="true"
  36. android:layout_alignParentRight="true"
  37. android:layout_margin="15dip"
  38. android:orientation="horizontal" >
  39. </LinearLayout>
  40. </com.example.viewswitcher.SlidingSwitcherView>
  41. </LinearLayout>

我们可以看到,com.example.viewswitcher.SlidingSwitcherView的根目录下放置了两个LinearLayout。第一个LinearLayout中要放入需要滚动显示的图片,这里我们加入了四个Button,每个Button都设置了一张背景图片。第二个LinearLayout中不需要加入任何东西,只要控制好大小和位置,标签会在运行的时候自动加入到这个layout中。

然后创建或打开MainActivity作为主界面,里面没有加入任何新增的代码:

[java] view
plain
copy

  1. public class MainActivity extends Activity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. }
  7. }

最后是给出AndroidManifest.xml的代码,也都是自动生成的内容:

[html] view
plain
copy

  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. <uses-sdk
  7. android:minSdkVersion="8"
  8. android:targetSdkVersion="8" />
  9. <application
  10. android:allowBackup="true"
  11. android:icon="@drawable/ic_launcher"
  12. android:label="@string/app_name"
  13. android:theme="@android:style/Theme.NoTitleBar" >
  14. <activity
  15. android:name="com.example.viewswitcher.MainActivity"
  16. android:label="@string/app_name" >
  17. <intent-filter>
  18. <action android:name="android.intent.action.MAIN" />
  19. <category android:name="android.intent.category.LAUNCHER" />
  20. </intent-filter>
  21. </activity>
  22. </application>
  23. </manifest>

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

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

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

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

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

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

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

源码下载,请点击这里

Android实现图片滚动控件,含页签功能,让你的应用像淘宝一样炫起来的更多相关文章

  1. Android高级图片滚动控件,编写3D版的图片轮播器

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/17482089 大家好,好久不见了,最近由于工作特别繁忙,已经有一个多月的时间没写博 ...

  2. Android自定义“图片+文字”控件四种实现方法之 二--------个人最推荐的一种

    http://blog.csdn.net/yanzi1225627/article/details/8633872 第二种方法也要新建一个图片+文字的xml布局文件,然后写一个类继承自LinearLa ...

  3. android设置图片自适应控件大小

    在XML文件的ImageView属性中加上:android:scaleType="fitXY"

  4. android水平循环滚动控件

    CycleScrollView.java package com.example.test; import android.content.Context; import android.graphi ...

  5. Android开发技巧——定制仿微信图片裁剪控件

    拍照--裁剪,或者是选择图片--裁剪,是我们设置头像或上传图片时经常需要的一组操作.上篇讲了Camera的使用,这篇讲一下我对图片裁剪的实现. 背景 下面的需求都来自产品. 裁剪图片要像微信那样,拖动 ...

  6. [android] 手机卫士自定义滚动控件

    TextView控件设置单行显示 android:singleLine=”true” 设置TextView开始的位置显示省略号,android:ellipsize=”start” 设置滚动属性,and ...

  7. Android微信九宫格图片展示控件

    版权声明:本文为xing_star原创文章,转载请注明出处! 本文同步自http://javaexception.com/archives/214 Android微信九宫格图片展示控件 半年前,公司产 ...

  8. HorizontalScrollView水平滚动控件

    HorizontalScrollView水平滚动控件 一.简介 用法ScrollView大致相同 二.方法 1)HorizontalScrollView水平滚动控件使用方法 1.在layout布局文件 ...

  9. ScrollView垂直滚动控件

    ScrollView垂直滚动控件 一.简介 二.方法 1)ScrollView垂直滚动控件使用方法 1.在layout布局文件的最外层建立一个ScrollView控件 2.在ScrollView控件中 ...

随机推荐

  1. SpringBoot错误信息总结(不定时更新)

    1." java.lang.IllegalStateException: @Bean method ShiroConfig.cacheManager called as a bean ref ...

  2. [HTML] Change an HTML5 input's placeholder color with CSS

    We will look at what CSS selectors to use to change an HTML5 inputs placeholder color. This can diff ...

  3. andriod first app-computer

    andriod first app-computer 个人信息:就读于燕大本科软件project专业 眼下大三; 本人博客:google搜索"cqs_2012"就可以; 个人爱好: ...

  4. [array] leetCode-26. Remove Duplicates from Sorted Array - Easy

    26. Remove Duplicates from Sorted Array - Easy descrition Given a sorted array, remove the duplicate ...

  5. nginx 代理服务器

    目前现状:只有1个机器能上网(web),其他机器不能方法:能上网的做一个代理web服务器中转,其他机器连接它即可。采用nginxNginx配置如下:server{        resolver 8. ...

  6. STL MAP取消排序

    class MyLess{public: bool operator()(const CString str1,const CString str2)const { return TRUE; }}; ...

  7. CImage将图片转为指定像素大小

    CFileDialog fDlg(true, "jpg", "",   OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,   &q ...

  8. 大数据(十四) - Storm

    storm是一个分布式实时计算引擎 storm/Jstorm的安装.配置.启动差点儿一模一样 storm是twitter开源的 storm的特点 storm支持热部署,即时上限或下线app 能够在st ...

  9. JS生成一个种子随机数(伪随机数)

    原文链接:https://geniuspeng.github.io/2016/09/12/js-random/ 最近有一个需求,需要生成一个随机数,但是又不能完全随机,就是说需要一个种子seed,se ...

  10. IfSpeed 带宽计算

    http://www.360doc.com/content/11/0304/22/2614615_98214710.shtml http://www.cisco.com/support/zh/477/ ...