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


    本文首发于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图片滚动,加入自动播放功能,使用自定义属性实现,霸气十足!

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

    源码下载,请点击这里

  • 相关阅读:
    ASP.NET Web API 控制器执行过程(一)
    ASP.NET Web API 控制器创建过程(二)
    ASP.NET Web API 控制器创建过程(一)
    ASP.NET Web API WebHost宿主环境中管道、路由
    ASP.NET Web API Selfhost宿主环境中管道、路由
    ASP.NET Web API 管道模型
    ASP.NET Web API 路由对象介绍
    ASP.NET Web API 开篇示例介绍
    ASP.NET MVC 视图(五)
    ASP.NET MVC 视图(四)
  • 原文地址:https://www.cnblogs.com/guolin/p/4039943.html
Copyright © 2020-2023  润新知