Android · 2015年3月4日 0

Android自定义控件实战——仿淘宝商品浏览界面

转载出处http://blog.csdn.net/zhongkejingwang/article/details/38656929

用手机淘宝浏览商品详情时,商品图片是放在后面的,在第一个ScrollView滚动到最底下时会有提示,继续拖动才能浏览图片。仿照这个效果写一个出来并不难,只要定义一个Layout管理两个ScrollView就行了,当第一个ScrollView滑到底部时,再次向上滑动进入第二个ScrollView。效果如下:

需要注意的地方是:

1、如果是手动滑到底部需要再次按下才能继续往下滑,自动滚动到底部则不需要

2、在由上一个ScrollView滑动到下一个ScrollView的过程中多只手指相继拖动也不会导致布局的剧变,也就是多个pointer的滑动不会导致move距离的剧变。

这个Layout的实现思路是:

在布局中放置两个ScrollView,并为其设置OnTouchListener,时刻判断ScrollView的滚动距离,一旦第一个ScrollView滚动到底部,则标识改为可向上拖动,此时开始记录滑动距离mMoveLen,根据mMoveLen重新layout两个ScrollView;同理,监听第二个ScrollView是否滚动到顶部,以往下拖动。

OK,明白了原理之后可以看代码了:

  1. package com.jingchen.tbviewer;
  2. import java.util.Timer;
  3. import java.util.TimerTask;
  4. import android.content.Context;
  5. import android.os.Handler;
  6. import android.os.Message;
  7. import android.util.AttributeSet;
  8. import android.view.MotionEvent;
  9. import android.view.VelocityTracker;
  10. import android.view.View;
  11. import android.widget.RelativeLayout;
  12. import android.widget.ScrollView;
  13. /**
  14.  * 包含两个ScrollView的容器
  15.  * 
  16.  * @author chenjing
  17.  * 
  18.  */
  19. public class ScrollViewContainer extends RelativeLayout {
  20.     /**
  21.      * 自动上滑
  22.      */
  23.     public static final int AUTO_UP = 0;
  24.     /**
  25.      * 自动下滑
  26.      */
  27.     public static final int AUTO_DOWN = 1;
  28.     /**
  29.      * 动画完成
  30.      */
  31.     public static final int DONE = 2;
  32.     /**
  33.      * 动画速度
  34.      */
  35.     public static final float SPEED = 6.5f;
  36.     private boolean isMeasured = false;
  37.     /**
  38.      * 用于计算手滑动的速度
  39.      */
  40.     private VelocityTracker vt;
  41.     private int mViewHeight;
  42.     private int mViewWidth;
  43.     private View topView;
  44.     private View bottomView;
  45.     private boolean canPullDown;
  46.     private boolean canPullUp;
  47.     private int state = DONE;
  48.     /**
  49.      * 记录当前展示的是哪个view,0是topView,1是bottomView
  50.      */
  51.     private int mCurrentViewIndex = 0;
  52.     /**
  53.      * 手滑动距离,这个是控制布局的主要变量
  54.      */
  55.     private float mMoveLen;
  56.     private MyTimer mTimer;
  57.     private float mLastY;
  58.     /**
  59.      * 用于控制是否变动布局的另一个条件,mEvents==0时布局可以拖拽了,mEvents==-1时可以舍弃将要到来的第一个move事件,
  60.      * 这点是去除多点拖动剧变的关键
  61.      */
  62.     private int mEvents;
  63.     private Handler handler = new Handler() {
  64.         @Override
  65.         public void handleMessage(Message msg) {
  66.             if (mMoveLen != 0) {
  67.                 if (state == AUTO_UP) {
  68.                     mMoveLen -= SPEED;
  69.                     if (mMoveLen <= -mViewHeight) {
  70.                         mMoveLen = -mViewHeight;
  71.                         state = DONE;
  72.                         mCurrentViewIndex = 1;
  73.                     }
  74.                 } else if (state == AUTO_DOWN) {
  75.                     mMoveLen += SPEED;
  76.                     if (mMoveLen >= 0) {
  77.                         mMoveLen = 0;
  78.                         state = DONE;
  79.                         mCurrentViewIndex = 0;
  80.                     }
  81.                 } else {
  82.                     mTimer.cancel();
  83.                 }
  84.             }
  85.             requestLayout();
  86.         }
  87.     };
  88.     public ScrollViewContainer(Context context) {
  89.         super(context);
  90.         init();
  91.     }
  92.     public ScrollViewContainer(Context context, AttributeSet attrs) {
  93.         super(context, attrs);
  94.         init();
  95.     }
  96.     public ScrollViewContainer(Context context, AttributeSet attrs, int defStyle) {
  97.         super(context, attrs, defStyle);
  98.         init();
  99.     }
  100.     private void init() {
  101.         mTimer = new MyTimer(handler);
  102.     }
  103.     @Override
  104.     public boolean dispatchTouchEvent(MotionEvent ev) {
  105.         switch (ev.getActionMasked()) {
  106.         case MotionEvent.ACTION_DOWN:
  107.             if (vt == null)
  108.                 vt = VelocityTracker.obtain();
  109.             else
  110.                 vt.clear();
  111.             mLastY = ev.getY();
  112.             vt.addMovement(ev);
  113.             mEvents = 0;
  114.             break;
  115.         case MotionEvent.ACTION_POINTER_DOWN:
  116.         case MotionEvent.ACTION_POINTER_UP:
  117.             // 多一只手指按下或抬起时舍弃将要到来的第一个事件move,防止多点拖拽的bug
  118.             mEvents = –1;
  119.             break;
  120.         case MotionEvent.ACTION_MOVE:
  121.             vt.addMovement(ev);
  122.             if (canPullUp && mCurrentViewIndex == 0 && mEvents == 0) {
  123.                 mMoveLen += (ev.getY() – mLastY);
  124.                 // 防止上下越界
  125.                 if (mMoveLen > 0) {
  126.                     mMoveLen = 0;
  127.                     mCurrentViewIndex = 0;
  128.                 } else if (mMoveLen < -mViewHeight) {
  129.                     mMoveLen = -mViewHeight;
  130.                     mCurrentViewIndex = 1;
  131.                 }
  132.                 if (mMoveLen < –8) {
  133.                     // 防止事件冲突
  134.                     ev.setAction(MotionEvent.ACTION_CANCEL);
  135.                 }
  136.             } else if (canPullDown && mCurrentViewIndex == 1 && mEvents == 0) {
  137.                 mMoveLen += (ev.getY() – mLastY);
  138.                 // 防止上下越界
  139.                 if (mMoveLen < -mViewHeight) {
  140.                     mMoveLen = -mViewHeight;
  141.                     mCurrentViewIndex = 1;
  142.                 } else if (mMoveLen > 0) {
  143.                     mMoveLen = 0;
  144.                     mCurrentViewIndex = 0;
  145.                 }
  146.                 if (mMoveLen > 8 – mViewHeight) {
  147.                     // 防止事件冲突
  148.                     ev.setAction(MotionEvent.ACTION_CANCEL);
  149.                 }
  150.             } else
  151.                 mEvents++;
  152.             mLastY = ev.getY();
  153.             requestLayout();
  154.             break;
  155.         case MotionEvent.ACTION_UP:
  156.             mLastY = ev.getY();
  157.             vt.addMovement(ev);
  158.             vt.computeCurrentVelocity(700);
  159.             // 获取Y方向的速度
  160.             float mYV = vt.getYVelocity();
  161.             if (mMoveLen == 0 || mMoveLen == -mViewHeight)
  162.                 break;
  163.             if (Math.abs(mYV) < 500) {
  164.                 // 速度小于一定值的时候当作静止释放,这时候两个View往哪移动取决于滑动的距离
  165.                 if (mMoveLen <= -mViewHeight / 2) {
  166.                     state = AUTO_UP;
  167.                 } else if (mMoveLen > -mViewHeight / 2) {
  168.                     state = AUTO_DOWN;
  169.                 }
  170.             } else {
  171.                 // 抬起手指时速度方向决定两个View往哪移动
  172.                 if (mYV < 0)
  173.                     state = AUTO_UP;
  174.                 else
  175.                     state = AUTO_DOWN;
  176.             }
  177.             mTimer.schedule(2);
  178.             try {
  179.                 vt.recycle();
  180.             } catch (Exception e) {
  181.                 e.printStackTrace();
  182.             }
  183.             break;
  184.         }
  185.         super.dispatchTouchEvent(ev);
  186.         return true;
  187.     }
  188.     @Override
  189.     protected void onLayout(boolean changed, int l, int t, int r, int b) {
  190.         topView.layout(0, (int) mMoveLen, mViewWidth,
  191.                 topView.getMeasuredHeight() + (int) mMoveLen);
  192.         bottomView.layout(0, topView.getMeasuredHeight() + (int) mMoveLen,
  193.                 mViewWidth, topView.getMeasuredHeight() + (int) mMoveLen
  194.                         + bottomView.getMeasuredHeight());
  195.     }
  196.     @Override
  197.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  198.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  199.         if (!isMeasured) {
  200.             isMeasured = true;
  201.             mViewHeight = getMeasuredHeight();
  202.             mViewWidth = getMeasuredWidth();
  203.             topView = getChildAt(0);
  204.             bottomView = getChildAt(1);
  205.             bottomView.setOnTouchListener(bottomViewTouchListener);
  206.             topView.setOnTouchListener(topViewTouchListener);
  207.         }
  208.     }
  209.     private OnTouchListener topViewTouchListener = new OnTouchListener() {
  210.         @Override
  211.         public boolean onTouch(View v, MotionEvent event) {
  212.             ScrollView sv = (ScrollView) v;
  213.             if (sv.getScrollY() == (sv.getChildAt(0).getMeasuredHeight() – sv
  214.                     .getMeasuredHeight()) && mCurrentViewIndex == 0)
  215.                 canPullUp = true;
  216.             else
  217.                 canPullUp = false;
  218.             return false;
  219.         }
  220.     };
  221.     private OnTouchListener bottomViewTouchListener = new OnTouchListener() {
  222.         @Override
  223.         public boolean onTouch(View v, MotionEvent event) {
  224.             ScrollView sv = (ScrollView) v;
  225.             if (sv.getScrollY() == 0 && mCurrentViewIndex == 1)
  226.                 canPullDown = true;
  227.             else
  228.                 canPullDown = false;
  229.             return false;
  230.         }
  231.     };
  232.     class MyTimer {
  233.         private Handler handler;
  234.         private Timer timer;
  235.         private MyTask mTask;
  236.         public MyTimer(Handler handler) {
  237.             this.handler = handler;
  238.             timer = new Timer();
  239.         }
  240.         public void schedule(long period) {
  241.             if (mTask != null) {
  242.                 mTask.cancel();
  243.                 mTask = null;
  244.             }
  245.             mTask = new MyTask(handler);
  246.             timer.schedule(mTask, 0, period);
  247.         }
  248.         public void cancel() {
  249.             if (mTask != null) {
  250.                 mTask.cancel();
  251.                 mTask = null;
  252.             }
  253.         }
  254.         class MyTask extends TimerTask {
  255.             private Handler handler;
  256.             public MyTask(Handler handler) {
  257.                 this.handler = handler;
  258.             }
  259.             @Override
  260.             public void run() {
  261.                 handler.obtainMessage().sendToTarget();
  262.             }
  263.         }
  264.     }
  265. }

注释写的很清楚了,有几个关键点需要讲一下:

1、由于这里为两个ScrollView设置了OnTouchListener,所以在其他地方不能再设置了,否则就白搭了。

2、两个ScrollView的layout参数统一由mMoveLen决定。

3、变量mEvents有两个作用:一是防止手动滑到底部或顶部时继续滑动而改变布局,必须再次按下才能继续滑动;二是在新的pointer down或up时把mEvents设置成-1可以舍弃将要到来的第一个move事件,防止mMoveLen出现剧变。为什么会出现剧变呢?因为假设一开始只有一只手指在滑动,记录的坐标值是这个pointer的事件坐标点,这时候另一只手指按下了导致事件又多了一个pointer,这时候到来的move事件的坐标可能就变成了新的pointer的坐标,这时计算与上一次坐标的差值就会出现剧变,变化的距离就是两个pointer间的距离。所以要把这个move事件舍弃掉,让mLastY值记录这个pointer的坐标再开始计算mMoveLen。pointer up的时候也一样。

理解了这几点,看起来就没什么难度了,代码量也很小。

MainActivity的布局:

  1. <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  2.     android:layout_width=“match_parent”
  3.     android:layout_height=“match_parent” >
  4.     <com.jingchen.tbviewer.ScrollViewContainer
  5.         android:layout_width=“match_parent”
  6.         android:layout_height=“match_parent” >
  7.         <ScrollView
  8.             android:layout_width=“match_parent”
  9.             android:layout_height=“match_parent” >
  10.             <RelativeLayout
  11.                 android:layout_width=“wrap_content”
  12.                 android:layout_height=“wrap_content” >
  13.                 <LinearLayout
  14.                     android:id=“@+id/imagesLayout”
  15.                     android:layout_width=“match_parent”
  16.                     android:layout_height=“wrap_content”
  17.                     android:gravity=“center_horizontal”
  18.                     android:orientation=“vertical” >
  19.                     <ImageView
  20.                         android:layout_width=“wrap_content”
  21.                         android:layout_height=“wrap_content”
  22.                         android:background=“@drawable/h” />
  23.                     <ImageView
  24.                         android:layout_width=“wrap_content”
  25.                         android:layout_height=“wrap_content”
  26.                         android:background=“@drawable/i” />
  27.                     <ImageView
  28.                         android:layout_width=“wrap_content”
  29.                         android:layout_height=“wrap_content”
  30.                         android:background=“@drawable/j” />
  31.                     <ImageView
  32.                         android:layout_width=“wrap_content”
  33.                         android:layout_height=“wrap_content”
  34.                         android:background=“@drawable/k” />
  35.                     <ImageView
  36.                         android:layout_width=“wrap_content”
  37.                         android:layout_height=“wrap_content”
  38.                         android:background=“@drawable/l” />
  39.                     <ImageView
  40.                         android:layout_width=“wrap_content”
  41.                         android:layout_height=“wrap_content”
  42.                         android:background=“@drawable/m” />
  43.                 </LinearLayout>
  44.                 <TextView
  45.                     android:layout_width=“match_parent”
  46.                     android:layout_height=“60dp”
  47.                     android:layout_below=“@id/imagesLayout”
  48.                     android:background=“#eeeeee”
  49.                     android:gravity=“center”
  50.                     android:text=“继续拖动,查看更多美女”
  51.                     android:textSize=“20sp” />
  52.             </RelativeLayout>
  53.         </ScrollView>
  54.         <ScrollView
  55.             android:layout_width=“match_parent”
  56.             android:layout_height=“match_parent”
  57.             android:background=“#000000” >
  58.             <LinearLayout
  59.                 android:layout_width=“match_parent”
  60.                 android:layout_height=“match_parent”
  61.                 android:gravity=“center_horizontal”
  62.                 android:orientation=“vertical” >
  63.                 <ImageView
  64.                     android:layout_width=“wrap_content”
  65.                     android:layout_height=“wrap_content”
  66.                     android:background=“@drawable/a” />
  67.                 <ImageView
  68.                     android:layout_width=“wrap_content”
  69.                     android:layout_height=“wrap_content”
  70.                     android:background=“@drawable/b” />
  71.                 <ImageView
  72.                     android:layout_width=“wrap_content”
  73.                     android:layout_height=“wrap_content”
  74.                     android:background=“@drawable/c” />
  75.                 <ImageView
  76.                     android:layout_width=“wrap_content”
  77.                     android:layout_height=“wrap_content”
  78.                     android:background=“@drawable/d” />
  79.                 <ImageView
  80.                     android:layout_width=“wrap_content”
  81.                     android:layout_height=“wrap_content”
  82.                     android:background=“@drawable/e” />
  83.                 <ImageView
  84.                     android:layout_width=“wrap_content”
  85.                     android:layout_height=“wrap_content”
  86.                     android:background=“@drawable/f” />
  87.                 <ImageView
  88.                     android:layout_width=“wrap_content”
  89.                     android:layout_height=“wrap_content”
  90.                     android:background=“@drawable/g” />
  91.             </LinearLayout>
  92.         </ScrollView>
  93.     </com.jingchen.tbviewer.ScrollViewContainer>
  94. </RelativeLayout>

在ScrollView中放了几张图片而已。

MainActivity的代码:

  1. package com.jingchen.tbviewer;
  2. import android.app.Activity;
  3. import android.os.Bundle;
  4. import android.view.Menu;
  5. public class MainActivity extends Activity
  6. {
  7.     @Override
  8.     protected void onCreate(Bundle savedInstanceState)
  9.     {
  10.         super.onCreate(savedInstanceState);
  11.         setContentView(R.layout.activity_main);
  12.     }
  13.     @Override
  14.     public boolean onCreateOptionsMenu(Menu menu)
  15.     {
  16.         getMenuInflater().inflate(R.menu.main, menu);
  17.         return true;
  18.     }
  19. }

啥也没有……

好了,到此结束~

源码下载

Share this: