自定义控件实战高级实用篇 ,仿淘宝商品浏览界面,图书阅读器平滑翻页,滚动选择器

本文详细介绍了如何在Android中实现自定义控件,包括仿淘宝商品浏览界面的滚动效果和图书阅读器的平滑翻页功能。通过在布局中使用两个ScrollView并监听滑动事件,实现在用户滑动到底部时切换显示内容。同时,文章还展示了如何创建一个滚动选择器PickerView,提供了平滑滚动的选择体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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


需要注意的地方是:

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

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

这个Layout的实现思路是:

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

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

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

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

    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的布局:

[html]  view plain copy
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值