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

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

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

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

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

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

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

    [java] view plaincopy
    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,里面加入如下代码:

    [html] view plaincopy
    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作为主界面,里面没有加入任何新增的代码:

    [java] view plaincopy
    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的代码,也都是自动生成的内容:
    [html] view plaincopy
    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图片滚动,加入自动播放功能,使用自定义属性实现,霸气十足!

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

    源码下载,请点击这里


  • 相关阅读:
    陶瓷电容的结构、工艺、失效模式
    Vue.js最佳实践
    Vue 超快速学习
    CSS 小技巧
    HTML5 Canvas
    webkit下面的CSS设置滚动条
    Some untracked working tree files would be overwritten by checkout. Please move or remove them before you can checkout. View them
    JSCS: Please specify path to 'JSCS' package
    React中ref的使用方法
    React 60S倒计时
  • 原文地址:https://www.cnblogs.com/lanzhi/p/6470023.html
Copyright © 2011-2022 走看看