本文首发于CSDN博客,转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/8769904
如果你是网购达人,你的手机上一定少不了淘宝客户端。关注特效的人一定都会发现,淘宝不管是网站还是手机客户端,主页上都会有一个图片滚动播放器,上面展示一些它推荐的商品。这个几乎可以用淘宝来冠名的功能,看起来还是挺炫的,我们今天就来实现一下。
实现原理其实还是之前那篇文章Android滑动菜单特效实现,仿人人客户端侧滑效果,史上最简单的侧滑实现 ,算是以那个原理为基础的另外一个变种。正所谓一通百通,真正掌握一种方法之后,就可以使用这个方法变换出各种不通的效果。
今天仍然还是实现一个自定义控件,然后我们在任意Activity的布局文件中引用一下,即可实现图片滚动器的效果。
在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 itemsLayout = (LinearLayout) getChildAt(0); 121 itemsCount = itemsLayout.getChildCount(); 122 borders = 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 item.setOnTouchListener(this); 130 } 131 leftEdge = borders[itemsCount - 1]; 132 firstItem = itemsLayout.getChildAt(0); 133 firstItemParams = (MarginLayoutParams) firstItem.getLayoutParams(); 134 } 135 136 /** 137 * 初始化标签元素。 138 */ 139 private void initializeDots() { 140 dotsLayout = (LinearLayout) getChildAt(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 LinearLayout.LayoutParams linearParams = new LinearLayout.LayoutParams(0, 243 LayoutParams.FILL_PARENT); 244 linearParams.weight = 1; 245 RelativeLayout relativeLayout = new RelativeLayout(getContext()); 246 ImageView image = 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 RelativeLayout.LayoutParams relativeParams = 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 mVelocityTracker.computeCurrentVelocity(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 mVelocityTracker = 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 leftMargin = leftMargin + speed[0]; 345 if (isCrossBorder(leftMargin, speed[0])) { 346 leftMargin = findClosestBorder(leftMargin); 347 break; 348 } 349 publishProgress(leftMargin); 350 // 为了要有滚动效果产生,每次循环使线程睡眠10毫秒,这样肉眼才能够看到滚动动画。 351 sleep(10); 352 } 353 return leftMargin; 354 } 355 356 @Override 357 protected void onProgressUpdate(Integer... leftMargin) { 358 firstItemParams.leftMargin = leftMargin[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>
好了,现在我们来看下运行效果吧,由于手机坏了,只能在模拟器上运行了。
首先是程序打开的时候,界面显示如下:
然后手指在图片上滑动,我们可以看到图片滚动的效果:
不停的翻页,页签也会跟着一起改变,下图中我们可以看到高亮显示的点是变换的:
恩,对比一下淘宝客户端的效果,我觉得我们模仿的还是挺好的。咦,好像少了点什么。。。。。。原来图片并不会自动播放。。。。。
没关系,我在后面的一篇文章中补充了自动播放这个功能,而且不仅仅是自动播放功能喔,请参考 Android图片滚动,加入自动播放功能,使用自定义属性实现,霸气十足!
今天的文章就到这里了,有问题的朋友请在下面留言。