**博客地址 **:  [http://blog.csdn.net/shulianghan/article/details/41520569](http://blog.csdn.net/shulianghan/article/details/41520569)





  代码下载 :





  — **GitHub **:  [https://github.com/han1202012/WheelViewDemo.git](https://github.com/han1202012/WheelViewDemo.git)





  — **CSDN **:  [http://download.csdn.net/detail/han1202012/8208997](http://download.csdn.net/detail/han1202012/8208997) ;



#### 博客总结 :



  博文内容 : 本文完整地分析了 WheelView 所有的源码, 包括其 适配器类型 , 两种回调接口 ( 选中条目改变回调 , 和 开始结束滚动回调 ), 以及详细的分析了 WheelView 主题源码, 其中 组件宽高测量 , 手势监听器添加 , 以及 精准的绘图方法 是主要目的, 花了将近1周时间, 感觉很值, 在这里分享给大家;










  自定义组件宽高获取策略 : MeasureSpec 最大模式 取 默认值 和 给定值中较小的那个 , 未定义模式取默认值 , 精准模式取 给定值 ;





  自定义组件维护各种回调监听器策略 : 维护集合, 将监听器置于集合中, 回调接口时遍历集合元素, 回调每个元素的接口方法;





  自定义组件手势监听器添加方法 : 创建手势监听器, 将手势监听器传入手势探测器, 在 onTouchEvent() 方法中回调手势监听器的 onTouchEvent()方法;



## 一. WheelView 简介

## 1. WheelView 效果



  在 Android 中实现类似与 IOS 的 WheelView 控件 : 如图





  ![](http://img2.tuicool.com/bMfU7b.png)



## 2. WheelView 使用流程

### (1) 基本流程简介








  a. 创建 WheelView 组件 : 使用 构造方法 或者 从布局文件获取 WheelView 组件;





  b. 设置显示条目数 : 调用 WheelView 组件对象的 setVisibleItems 方法 设置;





  c. 设置是否循环 : 设置 WheelView 是否循环, 调用 setCyclic() 方法设置;





  d. 设置适配器 : 调用 WheelView 组件的 setAdapter() 方法设置;





  e. 设置条目改变监听器 : 调用 WheelView 组件对象的 addChangingListener() 方法设置;





  f. 设置滚动监听器 : 调用 WheelView 组件对象的 addScrollingListener() 方法设置;



### (2) 代码实例



  a. 创建 WheelView 对象 :



```

//创建 WheelView 组件 final WheelView wheelLeft = new WheelView(context);

    
    

      **b. 设置 WheelView 显示条目数**:
    

    
    ```
<span class="comment">//设置 WheelView 组件最多显示 5 个元素</span>
        wheelLeft.setVisibleItems(<span class="number">5</span>);
  **c. 设置 WheelView 是否滚动循环**:



```

//设置 WheelView 元素是否循环滚动 wheelLeft.setCyclic(false);

    
    

      **d. 设置 WheelView 适配器**:
    

    
    ```
//设置 WheelView 适配器
        wheelLeft.setAdapter(new ArrayWheelAdapter<span class="tag">&lt;<span class="title">String</span>&gt;</span>(left));
  **e. 设置条目改变监听器**:



```

//为左侧的 WheelView 设置条目改变监听器 wheelLeft.addChangingListener(new OnWheelChangedListener() { @Override public void onChanged(WheelView wheel, int oldValue, int newValue) { //设置右侧的 WheelView 的适配器 wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[newValue])); wheelRight.setCurrentItem(right[newValue].length / 2); } });

    
    

      **f. 设置滚动监听器**:
    

    
    ```
wheelLeft.addScrollingListener(<span class="keyword">new</span> OnWheelScrollListener() {
      
      <span class="annotation">@Override</span>
      <span class="keyword">public</span> <span class="keyword">void</span> onScrollingStarted(WheelView wheel) {
        <span class="comment">// TODO Auto-generated method stub</span>
        
      }
      
      <span class="annotation">@Override</span>
      <span class="keyword">public</span> <span class="keyword">void</span> onScrollingFinished(WheelView wheel) {
        <span class="comment">// TODO Auto-generated method stub</span>
        
      }
    });
## 二. WheelView  适配器 监听器 相关接口分析

## 1. 适配器 分析



  这里定义了一个适配器接口, 以及两个适配器类, 一个用于任意类型的数据集适配, 一个用于数字适配;





  适配器操作 : 在 WheelView.java 中通过 setAdapter(WheelAdapter adapter) 和 getAdapter() 方法设置 获取 适配器;





  &#8212; 适配器常用操作 : 在 WheelView 中定义了 getItem(), getItemsCount(), getMaxmiumLength() 方法获取 适配器的相关信息;



```

/**

  • 获取该 WheelView 的适配器
  • @return
  • 	返回适配器
    

*/ public WheelAdapter getAdapter() { return adapter; }

<span class="javadoc">/**
  • 设置适配器
  • @param adapter
  •   要设置的适配器
    

*/ public void setAdapter(WheelAdapter adapter) { this.adapter = adapter; invalidateLayouts(); invalidate(); }

    
    ### (1) 适配器接口 ( interface WheelAdapter )
    
    

      适配器接口 :  **WheelAdapter** ;
    

    
    

      &#8212; 接口作用 : 该接口是所有适配器的接口, 适配器类都需要实现该接口;
    

    
    

      接口抽象方法介绍 :
    

    
    

      &#8212;  getItemsCount() : 获取适配器数据集合中元素个数;
    

    
    ```
<span class="javadoc">/**
  * 获取条目的个数
  * 
  * <span class="javadoctag">@return</span> 
  * 		WheelView 的条目个数
  */</span>
    <span class="keyword">public</span> <span class="keyword">int</span> getItemsCount();
  &#8212;  getItem(int index) : 获取适配器集合的中指定索引元素;



```

/**

  • 根据索引位置获取 WheelView 的条目
  • @param index
  •   条目的索引
    
  • @return
  • 	WheelView 上显示的条目的值
    

*/ public String getItem(int index);

    
    

      &#8212;  getMaximumLength() : 获取 WheelView 在界面上的显示宽度;
    

    
    ```
<span class="javadoc">/**
  * 获取条目的最大长度. 用来定义 WheelView 的宽度. 如果返回 -1, 就会使用默认宽度
  * 
  * <span class="javadoctag">@return</span> 
  * 		条目的最大宽度 或者 -1
  */</span>
    <span class="keyword">public</span> <span class="keyword">int</span> getMaximumLength();
### (2) 数组适配器 ( class ArrayWheelAdapter<T> implements WheelAdapter )



  适配器作用 : 该适配器可以传入 任何数据类型的数组 , 可以是 字符串数组, 也可以是任何对象的数组, 传入的数组作为适配器的数据源;





  成员变量分析 :





  &#8212; 数据源 :



```

/** 适配器的数据源 */ private T items[];

    
    

      &#8212; WheelView 最大宽度 :
    

    
    ```
<span class="javadoc">/** WheelView 的宽度 */</span>
    <span class="keyword">private</span> <span class="keyword">int</span> length;
  构造方法分析 :





  &#8212;  ArrayWheelAdapter(T items[], int length) : 传入 T 类型 对象数组, 以及 WheelView 的宽度;



```

/**

  • 构造方法
  • @param items
  •   适配器数据源 集合 T 类型的数组
    
  • @param length
  •   适配器数据源 集合 T 数组长度
    

*/ public ArrayWheelAdapter(T items[], int length) { this.items = items; this.length = length; }

    
    

      &#8212;  ArrayWheelAdapter(T items[]) : 传入 T 类型对象数组, 宽度使用默认的宽度;
    

    
    ```
<span class="javadoc">/**
  * 构造方法
  * 
  * <span class="javadoctag">@param</span> items
  *		  适配器数据源集合 T 类型数组
  */</span>
    <span class="keyword">public</span> ArrayWheelAdapter(T items[]) {
     <span class="keyword">this</span>(items, DEFAULT_LENGTH);
    }
  实现的父类方法分析 :





  &#8212;  getItem(int index) : 根据索引获取数组中对应位置的对象的字符串类型;



```

@Override public String getItem(int index) { //如果这个索引值合法, 就返回 item 数组对应的元素的字符串形式 if (index >= && index < items.length) { return items[index].toString(); } return null; }

    
    

      &#8212;  getItemsCount() : 获取数据集广大小, 直接返回数组大小;
    

    
    ```
<span class="annotation">@Override</span>
    <span class="keyword">public</span> <span class="keyword">int</span> getItemsCount() {
    	<span class="comment">//返回 item 数组的长度</span>
        <span class="keyword">return</span> items.length;
    }
  &#8212;  getMaximumLength() : 获取 WheelView 的最大宽度;



```

@Override public int getMaximumLength() { //返回 item 元素的宽度 return length; }

    
    ### (3) 数字适配器 ( class NumericWheelAdapter implements WheelAdapter )
    
    

      NumericWheelAdapter 适配器作用 : 数字作为 WheelView 适配器的显示值;
    

    
    

      成员变量分析 :
    

    
    

      &#8212; 最小值 : WheelView 数值显示的最小值;
    

    
    ```
<span class="javadoc">/** 设置的最小值 */</span>
    <span class="keyword">private</span> <span class="keyword">int</span> minValue;
  &#8212; 最大值 : WheelView 数值显示的最大值;



```

/** 设置的最大值 */ private int maxValue;

    
    

      &#8212; **格式化字符串**: 用于字符串的格式化;
    

    
    ```
<span class="javadoc">/** 格式化字符串, 用于格式化 货币, 科学计数, 十六进制 等格式 */</span>
    <span class="keyword">private</span> String format;
  构造方法分析 :





  &#8212;  NumericWheelAdapter() : 默认的构造方法, 使用默认的最大最小值;



```

/** * 默认的构造方法, 使用默认的最大最小值 */ public NumericWheelAdapter() { this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE); }

    
    

      &#8212;  NumericWheelAdapter(int minValue, int maxValue) : 传入一个最大最小值;
    

    
    ```
<span class="javadoc">/**
  * 构造方法
  * 
  * <span class="javadoctag">@param</span> minValue
  *		  最小值
  * <span class="javadoctag">@param</span> maxValue
  *		  最大值
  */</span>
    <span class="keyword">public</span> NumericWheelAdapter(<span class="keyword">int</span> minValue, <span class="keyword">int</span> maxValue) {
     <span class="keyword">this</span>(minValue, maxValue, <span class="keyword">null</span>);
    }
  &#8212;  NumericWheelAdapter(int minValue, int maxValue, String format) : 传入最大最小值, 以及数字格式化方式;



```

/**

  • 构造方法
  • @param minValue
  •   最小值
    
  • @param maxValue
  •   最大值
    
  • @param format
  •   格式化字符串
    

*/ public NumericWheelAdapter(int minValue, int maxValue, String format) { this.minValue = minValue; this.maxValue = maxValue; this.format = format; }

    
    

      实现的父类方法 :
    

    
    

      &#8212; 获取条目 : 如果需要格式化, 先进行格式化;
    

    
    ```
<span class="variable">@Override</span>
  public String getItem(<span class="keyword">int</span> <span class="keyword">index</span>) {
    String result = <span class="string">""</span>;
    <span class="keyword">if</span> (<span class="keyword">index</span> &gt;= <span class="number"></span> && <span class="keyword">index</span> &lt; getItemsCount()) {
      <span class="keyword">int</span> value = minValue + <span class="keyword">index</span>;
      <span class="regexp">//</span>如果 <span class="keyword">format</span> 不为 null, 那么格式化字符串, 如果为 null, 直接返回数字
      <span class="keyword">if</span>(<span class="keyword">format</span> != null){
        result = String.<span class="keyword">format</span>(<span class="keyword">format</span>, value);
      }<span class="keyword">else</span>{
        result = Integer.toString(value);
      }
      <span class="keyword">return</span> result;
    }
    <span class="keyword">return</span> null;
  }
  &#8212; **获取元素个数 **:



```

@Override public int getItemsCount() { //返回数字总个数 return maxValue - minValue + 1; }

    
    

      &#8212; **获取 WheelView 最大宽度 **:
    

    
    ```
<span class="annotation">@Override</span>
  <span class="keyword">public</span> <span class="keyword">int</span> getMaximumLength() {
    <span class="comment">//获取 最大值 和 最小值 中的 较大的数字</span>
    <span class="keyword">int</span> max = Math.max(Math.abs(maxValue), Math.abs(minValue));
    <span class="comment">//获取这个数字 的 字符串形式的 字符串长度</span>
    <span class="keyword">int</span> maxLen = Integer.toString(max).length();
    <span class="keyword">if</span> (minValue &lt; <span class="number"></span>) {
      maxLen++;
    }
    <span class="keyword">return</span> maxLen;
  }
## 2. 监听器相关接口

### (1) 条目改变监听器 ( interface OnWheelChangedListener )



  监听器作用 : 在 WheelView 条目改变的时候, 回调该监听器的接口方法, 执行条目改变对应的操作;





  接口方法介绍 :





  &#8212;  onChanged(WheelView wheel, int oldValue, int newValue) : 传入 WheelView 组件对象, 以及 旧的 和 新的 条目值索引;



```

/**

  • 当前条目改变时回调该方法
  • @param wheel
  •   条目改变的 WheelView 对象
    
  • @param oldValue
  •   WheelView 旧的条目值
    
  • @param newValue
  •   WheelView 新的条目值
    

*/ void onChanged(WheelView wheel, int oldValue, int newValue);

    
    ### (2) 滚动监听器 ( interface OnWheelScrollListener )
    
    

      滚动监听器作用 : 在 WheelView 滚动动作 开始 和 结束的时候回调对应的方法, 在对应方法中进行相应的操作;
    

    
    

      接口方法介绍 :
    

    
    

      &#8212; 开始滚动方法 : 在滚动开始的时候回调该方法;
    

    
    ```
<span class="javadoc">/**
  * 在 WheelView 滚动开始的时候回调该接口
  * 
  * <span class="javadoctag">@param</span> wheel
  *		  开始滚动的 WheelView 对象
  */</span>
    <span class="keyword">void</span> onScrollingStarted(WheelView wheel);
  &#8212; 停止滚动方法 : 在滚动结束的时候回调该方法;



```

/**

  • 在 WheelView 滚动结束的时候回调该接口
  • @param wheel
  •   结束滚动的 WheelView 对象
    

*/ void onScrollingFinished(WheelView wheel);

    
    ## 三. WheelView 解析
    
    ## 1. 触摸 点击 手势 动作操作控制组件 模块
    
    ### (1) 创建手势监听器
    
    

      手势监听器创建及对应方法 :
    

    
    

      &#8212;  onDown(MotionEvent e) : 在按下的时候回调该方法, e 参数是按下的事件;
    

    
    

      &#8212;  onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) : 滚动的时候回调该方法, e1 滚动第一次按下事件, e2 当前滚动的触摸事件, X 上一次滚动到这一次滚动 x 轴距离, Y 上一次滚动到这一次滚动 y 轴距离;
    

    
    

      &#8212;  onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) : 快速急冲滚动时回调的方法, e1 e2 与上面参数相同,  velocityX 是手势在 x 轴的速度,  velocityY 是手势在 y 轴的速度;
    

    
    

      &#8212; 代码示例 :
    

    
    ```
<span class="comment">/*
         * 手势监听器监听到 滚动操作后回调
         * 
         * 参数解析 : 
         * MotionEvent e1 : 触发滚动时第一次按下的事件
         * MotionEvent e2 : 触发当前滚动的移动事件
         * float distanceX : 自从上一次调用 该方法 到这一次 x 轴滚动的距离, 
         * 				注意不是 e1 到 e2 的距离, e1 到 e2 的距离是从开始滚动到现在的滚动距离
         * float distanceY : 自从上一次回调该方法到这一次 y 轴滚动的距离
         * 
         * 返回值 : 如果事件成功触发, 执行完了方法中的操作, 返回true, 否则返回 false 
         * (non-Javadoc)
         * @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float)
         */</span>
        <span class="keyword">public</span> <span class="keyword">boolean</span> onScroll(MotionEvent e1, MotionEvent e2, <span class="keyword">float</span> distanceX, <span class="keyword">float</span> distanceY) {
        	<span class="comment">//开始滚动, 并回调滚动监听器集合中监听器的 开始滚动方法</span>
            startScrolling();
            doScroll((<span class="keyword">int</span>) -distanceY);
            <span class="keyword">return</span> <span class="keyword">true</span>;
        }

        <span class="comment">/*
         * 当一个急冲手势发生后 回调该方法, 会计算出该手势在 x 轴 y 轴的速率
         * 
         * 参数解析 : 
         * -- MotionEvent e1 : 急冲动作的第一次触摸事件;
         * -- MotionEvent e2 : 急冲动作的移动发生的时候的触摸事件;
         * -- float velocityX : x 轴的速率
         * -- float velocityY : y 轴的速率
         * 
         * 返回值 : 如果执行完毕返回 true, 否则返回false, 这个就是自己定义的
         * 
         * (non-Javadoc)
         * @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float)
         */</span>
        <span class="keyword">public</span> <span class="keyword">boolean</span> onFling(MotionEvent e1, MotionEvent e2, <span class="keyword">float</span> velocityX, <span class="keyword">float</span> velocityY) {
        	<span class="comment">//计算上一次的 y 轴位置, 当前的条目高度 加上 剩余的 不够一行高度的那部分</span>
            lastScrollY = currentItem * getItemHeight() + scrollingOffset;
            <span class="comment">//如果可以循环最大值是无限大, 不能循环就是条目数的高度值</span>
            <span class="keyword">int</span> maxY = isCyclic ? <span class="number">0x7FFFFFFF</span> : adapter.getItemsCount() * getItemHeight();
            <span class="keyword">int</span> minY = isCyclic ? -maxY : <span class="number"></span>;
            <span class="comment">/*
             * Scroll 开始根据一个急冲手势滚动, 滚动的距离与初速度有关
             * 参数介绍 : 
             * -- int startX : 开始时的 X轴位置
             * -- int startY : 开始时的 y轴位置
             * -- int velocityX : 急冲手势的 x 轴的初速度, 单位 px/s
             * -- int velocityY : 急冲手势的 y 轴的初速度, 单位 px/s
             * -- int minX : x 轴滚动的最小值
             * -- int maxX : x 轴滚动的最大值
             * -- int minY : y 轴滚动的最小值
             * -- int maxY : y 轴滚动的最大值
             */</span>
            scroller.fling(<span class="number"></span>, lastScrollY, <span class="number"></span>, (<span class="keyword">int</span>) -velocityY / <span class="number">2</span>, <span class="number"></span>, <span class="number"></span>, minY, maxY);
            setNextMessage(MESSAGE_SCROLL);
            <span class="keyword">return</span> <span class="keyword">true</span>;
        }
    };
### (2) 创建手势探测器



  手势探测器创建 : 调用 其构造函数, 传入 上下文对象 和 手势监听器对象;





  &#8212; 禁止长按操作 : 调用 setIsLongpressEnabled(false) 方法, 禁止长按操作, 因为 长按操作会屏蔽滚动事件;



```

//创建一个手势处理 gestureDetector = new GestureDetector(context, gestureListener); /* * 是否允许长按操作, * 如果设置为 true 用户按下不松开, 会返回一个长按事件, * 如果设置为 false, 按下不松开滑动的话 会收到滚动事件. */ gestureDetector.setIsLongpressEnabled(false);

    
    ### (3) 将手势探测器 与 组件结合
    
    

      关联手势探测器 与 组件 : 在组件的 onTouchEvent(MotionEvent event) 方法中, 调用手势探测器的 gestureDetector.onTouchEvent(event) 方法即可;
    

    
    ```
<span class="comment">/*
  * 继承自 View 的触摸事件, 当出现触摸事件的时候, 就会回调该方法
  * (non-Javadoc)
  * @see android.view.View#onTouchEvent(android.view.MotionEvent)
  */</span>
    @Override
    <span class="keyword">public</span> boolean onTouchEvent(MotionEvent <span class="keyword">event</span>) {
    	<span class="comment">//获取适配器</span>
     WheelAdapter adapter = getAdapter();
     <span class="keyword">if</span> (adapter == <span class="keyword">null</span>) {
      <span class="keyword">return</span> <span class="keyword">true</span>;
     }

     <span class="comment">/*
      * gestureDetector.onTouchEvent(event) : 分析给定的动作, 如果可用, 调用 手势检测器的 onTouchEvent 方法
      * -- 参数解析 : ev , 触摸事件
      * -- 返回值 : 如果手势监听器成功执行了该方法, 返回true, 如果执行出现意外 返回 false;
      */</span>
     <span class="keyword">if</span> (!gestureDetector.onTouchEvent(<span class="keyword">event</span>) && <span class="keyword">event</span>.getAction() == MotionEvent.ACTION_UP) {
      justify();
     }
     <span class="keyword">return</span> <span class="keyword">true</span>;
    }
## 2. Scroller 简介

### (1) Scroller 简介



  Scroller 通用作用 : Scroller 组件并不是一个布局组件, 该组件是运行在后台的, 通过一些方法设定 Scroller 对象 的操作 或者 动画, 然后让 Scroller 运行在后台中 用于模拟滚动操作 , 在 适当的时机 获取该对象的坐标信息 , 这些信息是在后台运算出来的;





  Scroller 在本 View 中作用 : Android 的这个自定义的 WheelView 组件, 可以 平滑的滚动 , 当我们做一个 加速滑动时, 会根据速度计算出滑动的距离 , 这些数据都是在 Scroller 中计算出来的;



### (2) 设定 Scroller 对象的动作参数



  终止滚动 :





  &#8212; 终止滚动 跳转到目标位置 : 终止平缓的动画, 直接跳转到最终的 x y 轴的坐标位置;



```

public void abortAnimation()

    
    

      &#8212; **终止滚动 停止在当前位置 **: 强行结束 Scroll 的滚动;
    

    
    ```
<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">void</span> forceFinished(<span class="keyword">boolean</span> finished)
  设置滚动参数 :





  &#8212; 设置最终 x 轴坐标 :



```

public void setFinalX(int newX)

    
    

      &#8212; 设置最终 y 轴坐标 :
    

    
    ```
<span class="keyword">public</span> <span class="keyword">void</span> setFinalY(<span class="keyword">int</span> newY)
  &#8212; 设置滚动摩擦力 :



```

public final void setFriction(float friction)

    
    

      设置动作 :
    

    
    

      &#8212; 开始滚动 : 传入参数 开始 x 位置, 开始 y 位置, x 轴滚动距离, y 轴滚动距离;
    

    
    ```
<span class="keyword">public</span> <span class="keyword">void</span> startScroll(<span class="keyword">int</span> startX, <span class="keyword">int</span> startY, <span class="keyword">int</span> dx, <span class="keyword">int</span> dy)
  &#8212; **开始滚动 设定时间**: 最后一个参数是时间, 单位是 ms;



```

public void startScroll(int startX, int startY, int dx, int dy, int duration)

    
    

      &#8212; **急冲滚动**: 根据一个 急冲 手势进行滚动, 传入参数 : x轴开始位置, y轴开始位置, x 轴速度, y 轴速度, x 轴最小速度, x 轴最大速度, y 轴最小速度, y 轴最大速度;
    

    
    ```
<span class="keyword">public</span> <span class="keyword">void</span> fling(<span class="keyword">int</span> startX, <span class="keyword">int</span> startY, <span class="keyword">int</span> velocityX, <span class="keyword">int</span> velocityY,
            <span class="keyword">int</span> minX, <span class="keyword">int</span> maxX, <span class="keyword">int</span> minY, <span class="keyword">int</span> maxY)
  **延长滚动时间**: 延长滚动的时间, 让滚动滚的更远一些;



```

public void extendDuration(int extend)

    
    ### (3) 获取 Scroll 后台运行参数
    
    

      获取当前数据 :
    

    
    

      &#8212; 获取当前 x 轴坐标 :
    

    
    ```
<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">int</span> getCurrX()
  &#8212; 获取当前 y 轴坐标 :



```

public final int getCurrY()

    
    

      &#8212; 获取当前速度 :
    

    
    ```
<span class="keyword">public</span> <span class="keyword">float</span> getCurrVelocity()
  获取开始结束时的数据 :





  &#8212; 获取开始 x 轴坐标 :



```

public final int getStartX()

    
    

      &#8212; 获取开始 y 轴坐标 :
    

    
    ```
<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">int</span> getStartY()
  &#8212; 获取最终 x 轴坐标 : 该参数只在急冲滚动时有效;



```

public final int getFinalX()

    
    

      &#8212; 获取最终 y 轴坐标 : 该参数只在急冲滚动时有效;
    

    
    ```
<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">int</span> getFinalY()
  查看是否滚动完毕 :



```

public final boolean isFinished()

    
    

      获取从开始滚动到现在的时间 :
    

    
    ```
<span class="keyword">public</span> <span class="keyword">int</span> timePassed()
  获取新位置 : 调用该方法可以获取新位置, 如果返回 true 说明动画还没执行完毕;



```

public boolean computeScrollOffset()

    
    ### (4) Scroll 在 WheelView 中的运用
    
    

      Scroller 创建 :
    

    
    ```
<span class="comment">//使用默认的 时间 和 插入器 创建一个滚动器</span>
        scroller = <span class="keyword">new</span> Scroller(context);
  手势监听器 SimpleOnGestureListener 对象中的 onDown() 方法 : 如果滚动还在执行, 那么强行停止 Scroller 滚动;



```

//按下操作 public boolean onDown(MotionEvent e) { //如果滚动在执行 if (isScrollingPerformed) { //滚动强制停止, 按下的时候不能继续滚动 scroller.forceFinished(true); //清理信息 clearMessages(); return true; } return false; }

    
    

      当手势监听器 SimpleOnGestureListener 对象中有急冲动作时 onFling() 方法中 : 手势监听器监听到了 急冲动作, 那么 Scroller 也进行对应操作;
    

    
    ```
<span class="keyword">public</span> <span class="keyword">boolean</span> onFling(MotionEvent e1, MotionEvent e2, <span class="keyword">float</span> velocityX, <span class="keyword">float</span> velocityY) {
        	<span class="comment">//计算上一次的 y 轴位置, 当前的条目高度 加上 剩余的 不够一行高度的那部分</span>
            lastScrollY = currentItem * getItemHeight() + scrollingOffset;
            <span class="comment">//如果可以循环最大值是无限大, 不能循环就是条目数的高度值</span>
            <span class="keyword">int</span> maxY = isCyclic ? <span class="number">0x7FFFFFFF</span> : adapter.getItemsCount() * getItemHeight();
            <span class="keyword">int</span> minY = isCyclic ? -maxY : <span class="number"></span>;
            <span class="comment">/*
             * Scroll 开始根据一个急冲手势滚动, 滚动的距离与初速度有关
             * 参数介绍 : 
             * -- int startX : 开始时的 X轴位置
             * -- int startY : 开始时的 y轴位置
             * -- int velocityX : 急冲手势的 x 轴的初速度, 单位 px/s
             * -- int velocityY : 急冲手势的 y 轴的初速度, 单位 px/s
             * -- int minX : x 轴滚动的最小值
             * -- int maxX : x 轴滚动的最大值
             * -- int minY : y 轴滚动的最小值
             * -- int maxY : y 轴滚动的最大值
             */</span>
            scroller.fling(<span class="number"></span>, lastScrollY, <span class="number"></span>, (<span class="keyword">int</span>) -velocityY / <span class="number">2</span>, <span class="number"></span>, <span class="number"></span>, minY, maxY);
            setNextMessage(MESSAGE_SCROLL);
            <span class="keyword">return</span> <span class="keyword">true</span>;
        }
  动画控制 Handler 中 :





  &#8212; 滚动 : 获取当前 Scroller 的 y 轴位置, 与上一次的 y 轴位置对比, 如果 间距 delta 不为0, 就滚动;





  &#8212; 查看是否停止 : 如果现在距离 到 最终距离 小于最小滚动距离, 强制停止;





  &#8212; 执行 msg.what 指令 : 如果需要停止, 强制停止, 否则调整坐标;



```

/**

  • 动画控制器

  • animation handler

  • 可能会造成内存泄露 : 添加注解 HandlerLeak

  • Handler 类应该应该为static类型,否则有可能造成泄露。

  • 在程序消息队列中排队的消息保持了对目标Handler类的应用。

  • 如果Handler是个内部类,那 么它也会保持它所在的外部类的引用。

  • 为了避免泄露这个外部类,应该将Handler声明为static嵌套类,并且使用对外部类的弱应用。 */ @SuppressLint(“HandlerLeak”) private Handler animationHandler = new Handler() { public void handleMessage(Message msg) { //回调该方法获取当前位置, 如果返回true, 说明动画还没有执行完毕 scroller.computeScrollOffset(); //获取当前 y 位置 int currY = scroller.getCurrY(); //获取已经滚动了的位置, 使用上一次位置 减去 当前位置 int delta = lastScrollY - currY; lastScrollY = currY; if (delta != ) { //改变值不为 0 , 继续滚动 doScroll(delta); }

    /*

    • 如果滚动到了指定的位置, 滚动还没有停止
    • 这时需要强制停止 */ if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) { currY = scroller.getFinalY(); scroller.forceFinished(true); }

    /*

    • 如果滚动没有停止
    • 再向 Handler 发送一个停止 */ if (!scroller.isFinished()) { animationHandler.sendEmptyMessage(msg.what); } else if (msg.what == MESSAGE_SCROLL) { justify(); } else { finishScrolling(); } } };
    
    ## 3. StaticLayout  布局容器
    
    ### (1) StaticLayout 解析
    
    

      StaticLayout 解析 : 该组件用于显示文本, 一旦该文本被显示后, 就不能再编辑, 如果想要修改文本, 使用 DynamicLayout 布局即可;
    

    
    

      &#8212; 使用场景 : 一般情况下不会使用该组件, 当想要自定义组件 或者 想要使用 Canvas 绘制文本时 才使用该布局;
    

    
    

      常用方法解析 :
    

    
    

      &#8212; 获取底部 Padding : 获取底部 到最后一行文字的 间隔, 单位是 px;
    

    
    ```
<span class="keyword">public</span> <span class="keyword">int</span> getBottomPadding()
  &#8212; 获取顶部 Padding :



```

public int getTopPadding()

    
    

      &#8212; **获取省略个数**: 获取某一行需要省略的字符个数;
    

    
    ```
<span class="keyword">public</span> <span class="keyword">int</span> getEllipsisCount(<span class="keyword">int</span> line)
  &#8212; **获取省略开始位置**: 获取某一行要省略的字符串的第一个位置索引;



```

public int getEllipsisStart(int line)

    
    

      &#8212; **获取省略的宽度**: 获取某一行省略字符串的宽度, 单位 px;
    

    
    ```
<span class="keyword">public</span> <span class="keyword">int</span> getEllipsisStart(<span class="keyword">int</span> line)
  &#8212; **获取是否处理特殊符号**:



```

public boolean getLineContainsTab(int line)

    
    

      &#8212; **获取文字的行数**:
    

    
    ```
<span class="keyword">public</span> <span class="keyword">int</span> getLineCount()
  &#8212; **获取顶部位置**: 获取某一行顶部的位置;



```

public int getLineTop(int line)

    
    

      &#8212; **获取某一行底部位置**:
    

    
    ```
<span class="keyword">public</span> <span class="keyword">int</span> getLineDescent(<span class="keyword">int</span> line)
  &#8212; **获取行的方向**: 字符串从左至右 还是从右至左;



```

public final Directions getLineDirections(int line)

    
    

      &#8212; **获取某行第一个字符索引**: 获取的是 某一行 第一个字符 在整个字符串的索引;
    

    
    ```
<span class="keyword">public</span> <span class="keyword">int</span> getLineStart(<span class="keyword">int</span> line)
  &#8212; **获取该行段落方向**: 获取该行文字方向, 左至右 或者 右至左;



```

public int getParagraphDirection(int line)

    
    

      &#8212; **获取某个垂直位置显示的行数**:
    

    
    ```
<span class="keyword">public</span> <span class="keyword">int</span> getLineForVertical(<span class="keyword">int</span> vertical)
### (2) 布局显示



  布局创建 :





  &#8212; 三种布局 : WheelView 中涉及到了三种 StaticLayout 布局, 普通条目布局 itemLayout, 选中条目布局 valueLayout, 标签布局 labelLayout;





  &#8212; 创建时机 : 在 View 组件 每次 onMeasure() 和 onDraw() 方法中都要重新创建对应布局;





  &#8212; 创建布局源码 :



```

/**

  • 创建布局
  • @param widthItems
  •    	布局条目宽度
    
  • @param widthLabel
  •    	label 宽度
    

/ private void createLayouts(int widthItems, int widthLabel) { / * 创建普通条目布局 * 如果 普通条目布局 为 null 或者 普通条目布局的宽度 大于 传入的宽度, 这时需要重新创建布局 * 如果 普通条目布局存在, 并且其宽度小于传入的宽度, 此时需要将 */ if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {

  <span class="comment">/*
   * android.text.StaticLayout.StaticLayout(
   * CharSequence source, TextPaint paint, 
   * int width, Alignment align, 
   * float spacingmult, float spacingadd, boolean includepad)
   * 传入参数介绍 : 
   * CharSequence source : 需要分行显示的字符串
   * TextPaint paint : 绘制字符串的画笔
   * int width : 条目的宽度
   * Alignment align : Layout 的对齐方式, ALIGN_CENTER 居中对齐, ALIGN_NORMAL 左对齐, Alignment.ALIGN_OPPOSITE 右对齐
   * float spacingmult : 行间距, 1.5f 代表 1.5 倍字体高度
   * float spacingadd : 基础行距上增加多少 , 真实行间距 等于 spacingmult 和 spacingadd 的和
   * boolean includepad : 
   */</span>
  itemsLayout = <span class="keyword">new</span> StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,
      widthLabel &gt; <span class="number"></span> ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, <span class="number">1</span>,
      ADDITIONAL_ITEM_HEIGHT, <span class="keyword">false</span>);
} <span class="keyword">else</span> {
  <span class="comment">//调用 Layout 内置的方法 increaseWidthTo 将宽度提升到指定的宽度</span>
  itemsLayout.increaseWidthTo(widthItems);
}

<span class="comment">/*
 * 创建选中条目
 */</span>
<span class="keyword">if</span> (!isScrollingPerformed && (valueLayout == <span class="keyword">null</span> || valueLayout.getWidth() &gt; widthItems)) {
  String text = getAdapter() != <span class="keyword">null</span> ? getAdapter().getItem(currentItem) : <span class="keyword">null</span>;
  valueLayout = <span class="keyword">new</span> StaticLayout(text != <span class="keyword">null</span> ? text : <span class="string">""</span>, valuePaint, widthItems,
      widthLabel &gt; <span class="number"></span> ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, <span class="number">1</span>,
      ADDITIONAL_ITEM_HEIGHT, <span class="keyword">false</span>);
} <span class="keyword">else</span> <span class="keyword">if</span> (isScrollingPerformed) {
  valueLayout = <span class="keyword">null</span>;
} <span class="keyword">else</span> {
  valueLayout.increaseWidthTo(widthItems);
}

<span class="comment">/*
 * 创建标签条目
 */</span>
<span class="keyword">if</span> (widthLabel &gt; <span class="number"></span>) {
  <span class="keyword">if</span> (labelLayout == <span class="keyword">null</span> || labelLayout.getWidth() &gt; widthLabel) {
    labelLayout = <span class="keyword">new</span> StaticLayout(label, valuePaint, widthLabel, Layout.Alignment.ALIGN_NORMAL, <span class="number">1</span>,
        ADDITIONAL_ITEM_HEIGHT, <span class="keyword">false</span>);
  } <span class="keyword">else</span> {
    labelLayout.increaseWidthTo(widthLabel);
  }
}

}

    
    ## 4. 监听器管理
    
    

      监听器集合维护  :
    

    
    

      &#8212; 定义监听器集合 : 在 View 组件中 定义一个 List 集合, 集合中存放 监听器元素;
    

    
    ```
<span class="javadoc">/** 条目改变监听器集合  封装了条目改变方法, 当条目改变时回调 */</span>
    <span class="keyword">private</span> List&lt;OnWheelChangedListener&gt; changingListeners = <span class="keyword">new</span> LinkedList&lt;OnWheelChangedListener&gt;();
    <span class="javadoc">/** 条目滚动监听器集合, 该监听器封装了 开始滚动方法, 结束滚动方法 */</span>
    <span class="keyword">private</span> List&lt;OnWheelScrollListener&gt; scrollingListeners = <span class="keyword">new</span> LinkedList&lt;OnWheelScrollListener&gt;();
  &#8212; 提供对监听器集合的添加删除接口 : 提供 对集合 进行 添加 和 删除的接口;



```

/**

  • 添加 WheelView 选择的元素改变监听器
  • @param listener
  •   the listener
    

*/ public void addChangingListener(OnWheelChangedListener listener) { changingListeners.add(listener); }

<span class="javadoc">/**
  • 移除 WheelView 元素改变监听器
  • @param listener
  •   the listener
    

*/ public void removeChangingListener(OnWheelChangedListener listener) { changingListeners.remove(listener); }

    
    

      &#8212; **调用监听器接口**:
    

    
    ```
<span class="javadoc">/**
  * 回调元素改变监听器集合的元素改变监听器元素的元素改变方法
  * 
  * <span class="javadoctag">@param</span> oldValue
  *		  旧的 WheelView选中的值
  * <span class="javadoctag">@param</span> newValue
  *		  新的 WheelView选中的值
  */</span>
    <span class="keyword">protected</span> <span class="keyword">void</span> notifyChangingListeners(<span class="keyword">int</span> oldValue, <span class="keyword">int</span> newValue) {
     <span class="keyword">for</span> (OnWheelChangedListener listener : changingListeners) {
      listener.onChanged(<span class="keyword">this</span>, oldValue, newValue);
     }
    }
## 5. 自定义 View 对象的宽高

### (1) onMeasure 方法 MeasureSpec 模式解析



  常规处理方法 : 组件的宽高有三种情况, widthMeasureSpec 有三种模式 最大模式, 精准模式, 未定义模式;





  &#8212; 最大模式 : 在 组件的宽或高 warp_content 属性时, 会使用最大模式;





  &#8212; 精准模式 : 当给组件宽 或者高 定义一个值 或者 使用 match_parent 时, 会使用精准模式;





  **处理宽高的常规代码** :



```

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec);

<span class="comment">//获取宽度 和 高度的模式 和 大小</span>

int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec);

Log.i(TAG, “宽度 : widthMode : “ + getMode(widthMode) + ” , widthSize : “ + widthSize + "\n” + “高度 : heightMode : “ + getMode(heightMode) + ” , heightSize : “ + heightSize);

int width = ; int height = ; /*

  • 精准模式
  •    精准模式下 高度就是精确的高度
    

*/ if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; //未定义模式 和 最大模式 } else { //未定义模式下 获取布局需要的高度 height = 100;

  <span class="comment">//最大模式下 获取 布局高度 和 布局所需高度的最小值</span>
  <span class="keyword">if</span> (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
  }

}

if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else { width = 100; if (heightMode == MeasureSpec.AT_MOST) { width = Math.min(width, widthSize); } }

Log.i(TAG, “最终结果 : 宽度 : “ + width + ” , 高度 : “ + height);

setMeasuredDimension(width, height);

}

public String getMode(int mode) { String modeName = ”"; if(mode == MeasureSpec.EXACTLY){ modeName = “精准模式”; }else if(mode == MeasureSpec.AT_MOST){ modeName = “最大模式”; }else if(mode == MeasureSpec.UNSPECIFIED){ modeName = “未定义模式”; }

<span class="keyword">return</span> modeName;

}

    
    ### (2) 测试上述代码
    
    

      使用下面的自定义组件测试 :
    

    
    ```
<span class="keyword">package</span> cn.org.octopus.wheelview;

<span class="keyword">import</span> android.content.Context;
<span class="keyword">import</span> android.graphics.Canvas;
<span class="keyword">import</span> android.graphics.Color;
<span class="keyword">import</span> android.util.AttributeSet;
<span class="keyword">import</span> android.util.Log;
<span class="keyword">import</span> android.view.View;

<span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyView</span> <span class="keyword">extends</span> <span class="title">View</span> {</span>

  <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> String TAG = <span class="string">"octopus.my.view"</span>;
  
  <span class="keyword">public</span> MyView(Context context, AttributeSet attrs) {
    <span class="keyword">super</span>(context, attrs);
  }

  <span class="keyword">public</span> MyView(Context context) {
    <span class="keyword">super</span>(context);
  }

  <span class="keyword">public</span> MyView(Context context, AttributeSet attrs, <span class="keyword">int</span> defStyle) {
    <span class="keyword">super</span>(context, attrs, defStyle);
  }
  
  
  
  
  <span class="annotation">@Override</span>
  <span class="keyword">protected</span> <span class="keyword">void</span> onMeasure(<span class="keyword">int</span> widthMeasureSpec, <span class="keyword">int</span> heightMeasureSpec) {
    <span class="keyword">super</span>.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    <span class="comment">//获取宽度 和 高度的模式 和 大小</span>
  <span class="keyword">int</span> widthMode = MeasureSpec.getMode(widthMeasureSpec);
  <span class="keyword">int</span> heightMode = MeasureSpec.getMode(heightMeasureSpec);
  <span class="keyword">int</span> widthSize = MeasureSpec.getSize(widthMeasureSpec);
  <span class="keyword">int</span> heightSize = MeasureSpec.getSize(heightMeasureSpec);
  
  Log.i(TAG, <span class="string">"宽度 : widthMode : "</span> + getMode(widthMode) + <span class="string">" , widthSize : "</span> + widthSize + <span class="string">"\n"</span> 
      + <span class="string">"高度 : heightMode : "</span> + getMode(heightMode) + <span class="string">" , heightSize : "</span> + heightSize);
  
  <span class="keyword">int</span> width = <span class="number"></span>;
  <span class="keyword">int</span> height = <span class="number"></span>;
  <span class="comment">/*
   * 精准模式
   * 		精准模式下 高度就是精确的高度
   */</span>
  <span class="keyword">if</span> (heightMode == MeasureSpec.EXACTLY) {
      height = heightSize;
  <span class="comment">//未定义模式 和 最大模式</span>
  } <span class="keyword">else</span> {
    <span class="comment">//未定义模式下 获取布局需要的高度</span>
      height = <span class="number">100</span>;

      <span class="comment">//最大模式下 获取 布局高度 和 布局所需高度的最小值</span>
      <span class="keyword">if</span> (heightMode == MeasureSpec.AT_MOST) {
    height = Math.min(height, heightSize);
      }
  }
  
  <span class="keyword">if</span> (widthMode == MeasureSpec.EXACTLY) {
      width = widthSize;
  } <span class="keyword">else</span> {
      width = <span class="number">100</span>;
      <span class="keyword">if</span> (heightMode == MeasureSpec.AT_MOST) {
    width = Math.min(width, widthSize);
      }
  }

  Log.i(TAG, <span class="string">"最终结果 : 宽度 : "</span> + width + <span class="string">" , 高度 : "</span> + height);
  
  setMeasuredDimension(width, height);
    
  }
  
  
  <span class="keyword">public</span> String getMode(<span class="keyword">int</span> mode) {
    String modeName = <span class="string">""</span>;
    <span class="keyword">if</span>(mode == MeasureSpec.EXACTLY){
      modeName = <span class="string">"精准模式"</span>;
    }<span class="keyword">else</span> <span class="keyword">if</span>(mode == MeasureSpec.AT_MOST){
      modeName = <span class="string">"最大模式"</span>;
    }<span class="keyword">else</span> <span class="keyword">if</span>(mode == MeasureSpec.UNSPECIFIED){
      modeName = <span class="string">"未定义模式"</span>;
    }
      
    <span class="keyword">return</span> modeName;
  }
  
  <span class="annotation">@Override</span>
  <span class="keyword">protected</span> <span class="keyword">void</span> onDraw(Canvas canvas) {
    <span class="keyword">super</span>.onDraw(canvas);
    
    canvas.drawColor(Color.BLUE);
  }

}
  给定具体值情况 :





  &#8212; 组件信息 :



```

<cn.org.octopus.wheelview.MyView android:layout_width=“300dip” android:layout_height=“300dip”/>

    
    

      &#8212; **日志信息**:
    

    
    ```
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.304</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">450</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.304</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 高度 : heightMode : 最大模式 , heightSize : <span class="number">850</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.304</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 最终结果 : 宽度 : <span class="number">450</span> , 高度 : <span class="number">100</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.304</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">450</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.304</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 高度 : heightMode : 精准模式 , heightSize : <span class="number">450</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.304</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 最终结果 : 宽度 : <span class="number">450</span> , 高度 : <span class="number">450</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.335</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">450</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.335</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 高度 : heightMode : 最大模式 , heightSize : <span class="number">850</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.335</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 最终结果 : 宽度 : <span class="number">450</span> , 高度 : <span class="number">100</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.335</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">450</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.335</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 高度 : heightMode : 精准模式 , heightSize : <span class="number">450</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">40</span>:<span class="number">24.335</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2609</span>): 最终结果 : 宽度 : <span class="number">450</span> , 高度 : <span class="number">450</span>
  **warp_content 情况**:





  &#8212; 组件信息 :



```

<cn.org.octopus.wheelview.MyView android:layout_width=“wrap_content” android:layout_height=“wrap_content”/>

    
    

      &#8212; **日志信息**:
    

    
    ```
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.351</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 宽度 : widthMode : 最大模式 , widthSize : <span class="number">492</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.351</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 高度 : heightMode : 最大模式 , heightSize : <span class="number">850</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.351</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 最终结果 : 宽度 : <span class="number">100</span> , 高度 : <span class="number">100</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.351</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">100</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.351</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 高度 : heightMode : 最大模式 , heightSize : <span class="number">802</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.351</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 最终结果 : 宽度 : <span class="number">100</span> , 高度 : <span class="number">100</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.390</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 宽度 : widthMode : 最大模式 , widthSize : <span class="number">492</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.390</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 高度 : heightMode : 最大模式 , heightSize : <span class="number">850</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.390</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 最终结果 : 宽度 : <span class="number">100</span> , 高度 : <span class="number">100</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.390</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">100</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.390</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 高度 : heightMode : 最大模式 , heightSize : <span class="number">802</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">37</span>:<span class="number">47.390</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">1803</span>): 最终结果 : 宽度 : <span class="number">100</span> , 高度 : <span class="number">100</span>
  match_parent 情况 :





  &#8212; 组件信息 :



```

<cn.org.octopus.wheelview.MyView android:layout_width=“match_parent” android:layout_height=“match_parent”/>

    
    

      &#8212; 日志信息 :
    

    
    ```
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">296</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">492</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">296</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 高度 : heightMode : 精准模式 , heightSize : <span class="number">850</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">296</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 最终结果 : 宽度 : <span class="number">492</span> , 高度 : <span class="number">850</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">296</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">492</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">296</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 高度 : heightMode : 精准模式 , heightSize : <span class="number">802</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">296</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 最终结果 : 宽度 : <span class="number">492</span> , 高度 : <span class="number">802</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">328</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">492</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">328</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 高度 : heightMode : 精准模式 , heightSize : <span class="number">850</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">328</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 最终结果 : 宽度 : <span class="number">492</span> , 高度 : <span class="number">850</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">328</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 宽度 : widthMode : 精准模式 , widthSize : <span class="number">492</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">328</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 高度 : heightMode : 精准模式 , heightSize : <span class="number">802</span>
<span class="number">11</span>-<span class="number">30</span> <span class="number">01</span>:<span class="number">39</span>:08.<span class="number">328</span>: I/octopus.<span class="keyword">my</span>.view(<span class="number">2249</span>): 最终结果 : 宽度 : <span class="number">492</span> , 高度 : <span class="number">802</span>
  **博客地址  **:  [http://blog.csdn.net/shulianghan/article/details/41520569#t17](http://blog.csdn.net/shulianghan/article/details/41520569#t17)





  代码下载 :





  &#8212;  **GitHub **:  [https://github.com/han1202012/WheelViewDemo.git](https://github.com/han1202012/WheelViewDemo.git)





  &#8212;  **CSDN **:  [http://download.csdn.net/detail/han1202012/8208997](http://download.csdn.net/detail/han1202012/8208997) ;



## 四. 详细代码

## 1. WheelAdapter

```

package cn.org.octopus.wheelview.widget;

/**

/ public interface WheelAdapter { /* * 获取条目的个数 * * @return * WheelView 的条目个数 */ public int getItemsCount();

<span class="javadoc">/**
 * 根据索引位置获取 WheelView 的条目
 * 
 * <span class="javadoctag">@param</span> index
 *            条目的索引
 * <span class="javadoctag">@return</span> 
 * 		WheelView 上显示的条目的值
 */</span>
<span class="keyword">public</span> String getItem(<span class="keyword">int</span> index);

<span class="javadoc">/**
 * 获取条目的最大长度. 用来定义 WheelView 的宽度. 如果返回 -1, 就会使用默认宽度
 * 
 * <span class="javadoctag">@return</span> 
 * 		条目的最大宽度 或者 -1
 */</span>
<span class="keyword">public</span> <span class="keyword">int</span> getMaximumLength();

}

    
    ## 2. ArrayWheelAdapter
    
    ```
<span class="keyword">package</span> cn.org.octopus.wheelview.widget;

<span class="javadoc">/**
 * WheelView 的适配器类
 * 
 * <span class="javadoctag">@param</span> &lt;T&gt;
 *			元素类型
 */</span>
<span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ArrayWheelAdapter</span>&lt;<span class="title">T</span>&gt; <span class="keyword">implements</span> <span class="title">WheelAdapter</span> {</span>

  <span class="javadoc">/** 适配器的 元素集合(数据源) 默认长度为 -1 */</span>
  <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> DEFAULT_LENGTH = -<span class="number">1</span>;

  <span class="javadoc">/** 适配器的数据源 */</span>
  <span class="keyword">private</span> T items[];
  <span class="javadoc">/** WheelView 的宽度 */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> length;

  <span class="javadoc">/**
   * 构造方法
   * 
   * <span class="javadoctag">@param</span> items
   *			适配器数据源 集合 T 类型的数组
   * <span class="javadoctag">@param</span> length
   *			适配器数据源 集合 T 数组长度
   */</span>
  <span class="keyword">public</span> ArrayWheelAdapter(T items[], <span class="keyword">int</span> length) {
    <span class="keyword">this</span>.items = items;
    <span class="keyword">this</span>.length = length;
  }

  <span class="javadoc">/**
   * 构造方法
   * 
   * <span class="javadoctag">@param</span> items
   *			适配器数据源集合 T 类型数组
   */</span>
  <span class="keyword">public</span> ArrayWheelAdapter(T items[]) {
    <span class="keyword">this</span>(items, DEFAULT_LENGTH);
  }

  
  <span class="annotation">@Override</span>
  <span class="keyword">public</span> String getItem(<span class="keyword">int</span> index) {
    <span class="comment">//如果这个索引值合法, 就返回 item 数组对应的元素的字符串形式</span>
    <span class="keyword">if</span> (index &gt;= <span class="number"></span> && index &lt; items.length) {
      <span class="keyword">return</span> items[index].toString();
    }
    <span class="keyword">return</span> <span class="keyword">null</span>;
  }

  <span class="annotation">@Override</span>
  <span class="keyword">public</span> <span class="keyword">int</span> getItemsCount() {
    <span class="comment">//返回 item 数组的长度</span>
    <span class="keyword">return</span> items.length;
  }

  <span class="annotation">@Override</span>
  <span class="keyword">public</span> <span class="keyword">int</span> getMaximumLength() {
    <span class="comment">//返回 item 元素的宽度</span>
    <span class="keyword">return</span> length;
  }

}
## 3. NumericWheelAdapter

```

package cn.org.octopus.wheelview.widget;

/**

  • 显示数字的 WheelAdapter */ public class NumericWheelAdapter implements WheelAdapter {

/** 默认最小值 */ public static final int DEFAULT_MAX_VALUE = 9;

/** 默认最大值 */ private static final int DEFAULT_MIN_VALUE = ;

/** 设置的最小值 / private int minValue; /* 设置的最大值 */ private int maxValue;

/** 格式化字符串, 用于格式化 货币, 科学计数, 十六进制 等格式 */ private String format;

/**

  • 默认的构造方法, 使用默认的最大最小值 */ public NumericWheelAdapter() { this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE); }

/**

  • 构造方法
  • @param minValue
  •    	最小值
    
  • @param maxValue
  •    	最大值
    

*/ public NumericWheelAdapter(int minValue, int maxValue) { this(minValue, maxValue, null); }

/**

  • 构造方法
  • @param minValue
  •    	最小值
    
  • @param maxValue
  •    	最大值
    
  • @param format
  •    	格式化字符串
    

*/ public NumericWheelAdapter(int minValue, int maxValue, String format) { this.minValue = minValue; this.maxValue = maxValue; this.format = format; }

@Override public String getItem(int index) { String result = ”"; if (index >= && index < getItemsCount()) { int value = minValue + index; //如果 format 不为 null, 那么格式化字符串, 如果为 null, 直接返回数字 if(format != null){ result = String.format(format, value); }else{ result = Integer.toString(value); } return result; } return null; }

@Override public int getItemsCount() { //返回数字总个数 return maxValue - minValue + 1; }

@Override public int getMaximumLength() { //获取 最大值 和 最小值 中的 较大的数字 int max = Math.max(Math.abs(maxValue), Math.abs(minValue)); //获取这个数字 的 字符串形式的 字符串长度 int maxLen = Integer.toString(max).length(); if (minValue < ) { maxLen++; } return maxLen; } }

    
    ## 4. OnWheelChangedListener
    
    ```
<span class="keyword">package</span> cn.org.octopus.wheelview.widget;

<span class="javadoc">/**
 * 条目改变监听器
 */</span>
<span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">OnWheelChangedListener</span> {</span>
    <span class="javadoc">/**
     * 当前条目改变时回调该方法
     * 
     * <span class="javadoctag">@param</span> wheel
     *            条目改变的 WheelView 对象
     * <span class="javadoctag">@param</span> oldValue
     *            WheelView 旧的条目值
     * <span class="javadoctag">@param</span> newValue
     *            WheelView 新的条目值
     */</span>
    <span class="keyword">void</span> onChanged(WheelView wheel, <span class="keyword">int</span> oldValue, <span class="keyword">int</span> newValue);
}
## 5. OnWheelScrollListener

```

package cn.org.octopus.wheelview.widget;

/**

  • WheelView 滚动监听器 / public interface OnWheelScrollListener { /*

    • 在 WheelView 滚动开始的时候回调该接口
    • @param wheel
    •        开始滚动的 WheelView 对象
      

    */ void onScrollingStarted(WheelView wheel);

    /**

    • 在 WheelView 滚动结束的时候回调该接口
    • @param wheel
    •        结束滚动的 WheelView 对象
      

    */ void onScrollingFinished(WheelView wheel); }

    
    ## 6. WheelView
    
    ```
<span class="keyword">package</span> cn.org.octopus.wheelview.widget;

<span class="keyword">import</span> java.util.LinkedList;
<span class="keyword">import</span> java.util.List;

<span class="keyword">import</span> cn.org.octopus.wheelview.R;
<span class="keyword">import</span> android.annotation.SuppressLint;
<span class="keyword">import</span> android.content.Context;
<span class="keyword">import</span> android.graphics.Canvas;
<span class="keyword">import</span> android.graphics.Paint;
<span class="keyword">import</span> android.graphics.Rect;
<span class="keyword">import</span> android.graphics.drawable.Drawable;
<span class="keyword">import</span> android.graphics.drawable.GradientDrawable;
<span class="keyword">import</span> android.graphics.drawable.GradientDrawable.Orientation;
<span class="keyword">import</span> android.os.Handler;
<span class="keyword">import</span> android.os.Message;
<span class="keyword">import</span> android.text.Layout;
<span class="keyword">import</span> android.text.StaticLayout;
<span class="keyword">import</span> android.text.TextPaint;
<span class="keyword">import</span> android.util.AttributeSet;
<span class="keyword">import</span> android.view.GestureDetector;
<span class="keyword">import</span> android.view.GestureDetector.SimpleOnGestureListener;
<span class="keyword">import</span> android.view.MotionEvent;
<span class="keyword">import</span> android.view.View;
<span class="keyword">import</span> android.view.animation.Interpolator;
<span class="keyword">import</span> android.widget.Scroller;

<span class="javadoc">/**
 * WheelView 主对象
 */</span>
<span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">WheelView</span> <span class="keyword">extends</span> <span class="title">View</span> {</span>
  <span class="javadoc">/** 滚动花费时间 Scrolling duration */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> SCROLLING_DURATION = <span class="number">400</span>;

  <span class="javadoc">/** 最小的滚动值, 每次最少滚动一个单位 */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> MIN_DELTA_FOR_SCROLLING = <span class="number">1</span>;

  <span class="javadoc">/** 当前条目中的文字颜色 */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> VALUE_TEXT_COLOR = <span class="number">0xF0FF6347</span>;

  <span class="javadoc">/** 非当前条目的文字颜色 */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> ITEMS_TEXT_COLOR = <span class="number">0xFF000000</span>;

  <span class="javadoc">/** 顶部和底部的阴影颜色 */</span>
  <span class="comment">//private static final int[] SHADOWS_COLORS = new int[] { 0xFF5436EE, 0x0012CEAE, 0x0012CEAE };</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span>[] SHADOWS_COLORS = <span class="keyword">new</span> <span class="keyword">int</span>[] { <span class="number">0xFF111111</span>, <span class="number">0x00AAAAAA</span>, <span class="number">0x00AAAAAA</span> };

  <span class="javadoc">/** 额外的条目高度 Additional items height (is added to standard text item height) */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> ADDITIONAL_ITEM_HEIGHT = <span class="number">15</span>;

  <span class="javadoc">/** 字体大小 */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> TEXT_SIZE = <span class="number">24</span>;

  <span class="javadoc">/** 顶部 和 底部 条目的隐藏大小, 
   * 如果是正数 会隐藏一部份, 
   * 0 顶部 和 底部的字正好紧贴 边缘, 
   * 负数时 顶部和底部 与 字有一定间距 */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> ITEM_OFFSET = TEXT_SIZE / <span class="number">5</span>;

  <span class="javadoc">/** Additional width for items layout */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> ADDITIONAL_ITEMS_SPACE = <span class="number">10</span>;

  <span class="javadoc">/** Label offset */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> LABEL_OFFSET = <span class="number">8</span>;

  <span class="javadoc">/** Left and right padding value */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> PADDING = <span class="number">10</span>;

  <span class="javadoc">/** 默认的可显示的条目数 */</span>
  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> DEF_VISIBLE_ITEMS = <span class="number">5</span>;

  <span class="javadoc">/** WheelView 适配器 */</span>
  <span class="keyword">private</span> WheelAdapter adapter = <span class="keyword">null</span>;
  <span class="javadoc">/** 当前显示的条目索引 */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> currentItem = <span class="number"></span>;

  <span class="javadoc">/** 条目宽度 */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> itemsWidth = <span class="number"></span>;
  <span class="javadoc">/** 标签宽度 */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> labelWidth = <span class="number"></span>;

  <span class="javadoc">/** 可见的条目数 */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> visibleItems = DEF_VISIBLE_ITEMS;

  <span class="javadoc">/** 条目高度 */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> itemHeight = <span class="number"></span>;

  <span class="javadoc">/** 绘制普通条目画笔 */</span>
  <span class="keyword">private</span> TextPaint itemsPaint;
  <span class="javadoc">/** 绘制选中条目画笔 */</span>
  <span class="keyword">private</span> TextPaint valuePaint;

  <span class="javadoc">/** 普通条目布局
   * StaticLayout 布局用于控制 TextView 组件, 一般情况下不会直接使用该组件, 
   * 除非你自定义一个组件 或者 想要直接调用  Canvas.drawText() 方法
   *  */</span>
  <span class="keyword">private</span> StaticLayout itemsLayout;
  <span class="keyword">private</span> StaticLayout labelLayout;
  <span class="javadoc">/** 选中条目布局 */</span>
  <span class="keyword">private</span> StaticLayout valueLayout;

  <span class="javadoc">/** 标签 在选中条目的右边出现 */</span>
  <span class="keyword">private</span> String label;
  <span class="javadoc">/** 选中条目的背景图片 */</span>
  <span class="keyword">private</span> Drawable centerDrawable;

  <span class="javadoc">/** 顶部阴影图片 */</span>
  <span class="keyword">private</span> GradientDrawable topShadow;
  <span class="javadoc">/** 底部阴影图片 */</span>
  <span class="keyword">private</span> GradientDrawable bottomShadow;

  <span class="javadoc">/** 是否在滚动 */</span>
  <span class="keyword">private</span> <span class="keyword">boolean</span> isScrollingPerformed;
  <span class="javadoc">/** 滚动的位置 */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> scrollingOffset;

  <span class="javadoc">/** 手势检测器 */</span>
  <span class="keyword">private</span> GestureDetector gestureDetector;
  <span class="javadoc">/** 
   * Scroll 类封装了滚动动作. 
   * 开发者可以使用 Scroll 或者 Scroll 实现类 去收集产生一个滚动动画所需要的数据, 返回一个急冲滑动的手势.
   * 该对象可以追踪随着时间推移滚动的偏移量, 但是这些对象不会自动向 View 对象提供这些位置.
   * 如果想要使滚动动画看起来比较平滑, 开发者需要在适当的时机  获取 和 使用新的坐标; 
   *  */</span>
  <span class="keyword">private</span> Scroller scroller;
  <span class="javadoc">/** 之前所在的 y 轴位置 */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> lastScrollY;

  <span class="javadoc">/** 是否循环 */</span>
  <span class="keyword">boolean</span> isCyclic = <span class="keyword">false</span>;

  <span class="javadoc">/** 条目改变监听器集合  封装了条目改变方法, 当条目改变时回调 */</span>
  <span class="keyword">private</span> List&lt;OnWheelChangedListener&gt; changingListeners = <span class="keyword">new</span> LinkedList&lt;OnWheelChangedListener&gt;();
  <span class="javadoc">/** 条目滚动监听器集合, 该监听器封装了 开始滚动方法, 结束滚动方法 */</span>
  <span class="keyword">private</span> List&lt;OnWheelScrollListener&gt; scrollingListeners = <span class="keyword">new</span> LinkedList&lt;OnWheelScrollListener&gt;();

  <span class="javadoc">/**
   * 构造方法
   */</span>
  <span class="keyword">public</span> WheelView(Context context, AttributeSet attrs, <span class="keyword">int</span> defStyle) {
    <span class="keyword">super</span>(context, attrs, defStyle);
    initData(context);
  }

  <span class="javadoc">/**
   * 构造方法
   */</span>
  <span class="keyword">public</span> WheelView(Context context, AttributeSet attrs) {
    <span class="keyword">super</span>(context, attrs);
    initData(context);
  }

  <span class="javadoc">/**
   * 构造方法
   */</span>
  <span class="keyword">public</span> WheelView(Context context) {
    <span class="keyword">super</span>(context);
    initData(context);
  }

  <span class="javadoc">/**
   * 初始化数据
   * 
   * <span class="javadoctag">@param</span> context
   *			上下文对象
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> initData(Context context) {
    <span class="comment">//创建一个手势处理</span>
    gestureDetector = <span class="keyword">new</span> GestureDetector(context, gestureListener);
    <span class="comment">/*
     * 是否允许长按操作, 
     * 如果设置为 true 用户按下不松开, 会返回一个长按事件, 
     * 如果设置为 false, 按下不松开滑动的话 会收到滚动事件.
     */</span>
    gestureDetector.setIsLongpressEnabled(<span class="keyword">false</span>);
    
    <span class="comment">//使用默认的 时间 和 插入器 创建一个滚动器</span>
    scroller = <span class="keyword">new</span> Scroller(context);
  }

  <span class="javadoc">/**
   * 获取该 WheelView 的适配器
   * 
   * <span class="javadoctag">@return</span> 
   * 		返回适配器
   */</span>
  <span class="keyword">public</span> WheelAdapter getAdapter() {
    <span class="keyword">return</span> adapter;
  }

  <span class="javadoc">/**
   * 设置适配器
   * 
   * <span class="javadoctag">@param</span> adapter
   *			要设置的适配器
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> setAdapter(WheelAdapter adapter) {
    <span class="keyword">this</span>.adapter = adapter;
    invalidateLayouts();
    invalidate();
  }

  <span class="javadoc">/**
   * 设置 Scroll 的插入器
   * 
   * <span class="javadoctag">@param</span> interpolator
   *			the interpolator
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> setInterpolator(Interpolator interpolator) {
    <span class="comment">//强制停止滚动</span>
    scroller.forceFinished(<span class="keyword">true</span>);
    <span class="comment">//创建一个 Scroll 对象</span>
    scroller = <span class="keyword">new</span> Scroller(getContext(), interpolator);
  }

  <span class="javadoc">/**
   * 获取课件条目数
   * 
   * <span class="javadoctag">@return</span> the count of visible items
   */</span>
  <span class="keyword">public</span> <span class="keyword">int</span> getVisibleItems() {
    <span class="keyword">return</span> visibleItems;
  }

  <span class="javadoc">/**
   * 设置可见条目数
   * 
   * <span class="javadoctag">@param</span> count
   *			the new count
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> setVisibleItems(<span class="keyword">int</span> count) {
    visibleItems = count;
    invalidate();
  }

  <span class="javadoc">/**
   * 获取标签
   * 
   * <span class="javadoctag">@return</span> the label
   */</span>
  <span class="keyword">public</span> String getLabel() {
    <span class="keyword">return</span> label;
  }

  <span class="javadoc">/**
   * 设置标签
   * 
   * <span class="javadoctag">@param</span> newLabel
   *			the label to set
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> setLabel(String newLabel) {
    <span class="keyword">if</span> (label == <span class="keyword">null</span> || !label.equals(newLabel)) {
      label = newLabel;
      labelLayout = <span class="keyword">null</span>;
      invalidate();
    }
  }

  <span class="javadoc">/**
   * 添加 WheelView 选择的元素改变监听器
   * 
   * <span class="javadoctag">@param</span> listener
   *			the listener
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> addChangingListener(OnWheelChangedListener listener) {
    changingListeners.add(listener);
  }

  <span class="javadoc">/**
   * 移除 WheelView 元素改变监听器
   * 
   * <span class="javadoctag">@param</span> listener
   *			the listener
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> removeChangingListener(OnWheelChangedListener listener) {
    changingListeners.remove(listener);
  }

  <span class="javadoc">/**
   * 回调元素改变监听器集合的元素改变监听器元素的元素改变方法
   * 
   * <span class="javadoctag">@param</span> oldValue
   *			旧的 WheelView选中的值
   * <span class="javadoctag">@param</span> newValue
   *			新的 WheelView选中的值
   */</span>
  <span class="keyword">protected</span> <span class="keyword">void</span> notifyChangingListeners(<span class="keyword">int</span> oldValue, <span class="keyword">int</span> newValue) {
    <span class="keyword">for</span> (OnWheelChangedListener listener : changingListeners) {
      listener.onChanged(<span class="keyword">this</span>, oldValue, newValue);
    }
  }

  <span class="javadoc">/**
   * 添加 WheelView 滚动监听器
   * 
   * <span class="javadoctag">@param</span> listener
   *			the listener
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> addScrollingListener(OnWheelScrollListener listener) {
    scrollingListeners.add(listener);
  }

  <span class="javadoc">/**
   * 移除 WheelView 滚动监听器
   * 
   * <span class="javadoctag">@param</span> listener
   *			the listener
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> removeScrollingListener(OnWheelScrollListener listener) {
    scrollingListeners.remove(listener);
  }

  <span class="javadoc">/**
   * 通知监听器开始滚动
   */</span>
  <span class="keyword">protected</span> <span class="keyword">void</span> notifyScrollingListenersAboutStart() {
    <span class="keyword">for</span> (OnWheelScrollListener listener : scrollingListeners) {
      <span class="comment">//回调开始滚动方法</span>
      listener.onScrollingStarted(<span class="keyword">this</span>);
    }
  }

  <span class="javadoc">/**
   * 通知监听器结束滚动
   */</span>
  <span class="keyword">protected</span> <span class="keyword">void</span> notifyScrollingListenersAboutEnd() {
    <span class="keyword">for</span> (OnWheelScrollListener listener : scrollingListeners) {
      <span class="comment">//回调滚动结束方法</span>
      listener.onScrollingFinished(<span class="keyword">this</span>);
    }
  }

  <span class="javadoc">/**
   * 获取当前选中元素的索引
   * 
   * <span class="javadoctag">@return</span> 
   * 		当前元素索引
   */</span>
  <span class="keyword">public</span> <span class="keyword">int</span> getCurrentItem() {
    <span class="keyword">return</span> currentItem;
  }

  <span class="javadoc">/**
   * 设置当前元素的位置, 如果索引是错误的 不进行任何操作
   * -- 需要考虑该 WheelView 是否能循环
   * -- 根据是否需要滚动动画来确定是 ①滚动到目的位置 还是 ②晴空所有条目然后重绘
   * 
   * <span class="javadoctag">@param</span> index
   *			要设置的元素索引值
   * <span class="javadoctag">@param</span> animated
   *			动画标志位
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> setCurrentItem(<span class="keyword">int</span> index, <span class="keyword">boolean</span> animated) {
    <span class="comment">//如果没有适配器或者元素个数为0 直接返回</span>
    <span class="keyword">if</span> (adapter == <span class="keyword">null</span> || adapter.getItemsCount() == <span class="number"></span>) {
      <span class="keyword">return</span>; <span class="comment">// throw?</span>
    }
    <span class="comment">//目标索引小于 0 或者大于 元素索引最大值(个数 -1)</span>
    <span class="keyword">if</span> (index &lt; <span class="number"></span> || index &gt;= adapter.getItemsCount()) {
      <span class="comment">//入股WheelView 可循环, 修正索引值, 如果不可循环直接返回</span>
      <span class="keyword">if</span> (isCyclic) {
        <span class="keyword">while</span> (index &lt; <span class="number"></span>) {
          index += adapter.getItemsCount();
        }
        index %= adapter.getItemsCount();
      } <span class="keyword">else</span> {
        <span class="keyword">return</span>; <span class="comment">// throw?</span>
      }
    }
    
    <span class="comment">//如果当前的索引不是传入的 索引</span>
    <span class="keyword">if</span> (index != currentItem) {
      
      <span class="comment">/*
       * 如果需要动画, 就滚动到目标位置
       * 如果不需要动画, 重新设置布局
       */</span>
      <span class="keyword">if</span> (animated) {
        <span class="comment">/*
         * 开始滚动, 每个元素滚动间隔 400 ms, 滚动次数是 目标索引值 减去 当前索引值, 这是滚动的真实方法
         */</span>
        scroll(index - currentItem, SCROLLING_DURATION);
      } <span class="keyword">else</span> {
        <span class="comment">//所有布局设置为 null, 滚动位置设置为 0</span>
        invalidateLayouts();

        <span class="keyword">int</span> old = currentItem;
        currentItem = index;

        <span class="comment">//便利回调元素改变监听器集合中的监听器元素中的元素改变方法</span>
        notifyChangingListeners(old, currentItem);

        <span class="comment">//重绘</span>
        invalidate();
      }
    }
  }

  <span class="javadoc">/**
   * 设置当前选中的条目, 没有动画, 当索引出错不做任何操作
   * 
   * <span class="javadoctag">@param</span> index
   *			要设置的索引
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> setCurrentItem(<span class="keyword">int</span> index) {
    setCurrentItem(index, <span class="keyword">false</span>);
  }

  <span class="javadoc">/**
   * 获取 WheelView 是否可以循环
   * -- 如果可循环 : 第一个之前是最后一个, 最后一个之后是第一个;
   * -- 如果不可循环 : 到第一个就不能上翻, 最后一个不能下翻 
   * 
   * <span class="javadoctag">@return</span>
   */</span>
  <span class="keyword">public</span> <span class="keyword">boolean</span> isCyclic() {
    <span class="keyword">return</span> isCyclic;
  }

  <span class="javadoc">/**
   * 设置 WheelView 循环标志
   * 
   * <span class="javadoctag">@param</span> isCyclic
   *			the flag to set
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> setCyclic(<span class="keyword">boolean</span> isCyclic) {
    <span class="keyword">this</span>.isCyclic = isCyclic;

    invalidate();
    invalidateLayouts();
  }

  <span class="javadoc">/**
   * 使布局无效
   * 将 选中条目 和 普通条目设置为 null, 滚动位置设置为0
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> invalidateLayouts() {
    itemsLayout = <span class="keyword">null</span>;
    valueLayout = <span class="keyword">null</span>;
    scrollingOffset = <span class="number"></span>;
  }

  <span class="javadoc">/**
   * 初始化资源
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> initResourcesIfNecessary() {
    <span class="comment">/*
     * 设置绘制普通条目的画笔, 允许抗拒齿, 允许 fake-bold
     * 设置文字大小为 24
     */</span>
    <span class="keyword">if</span> (itemsPaint == <span class="keyword">null</span>) {
      itemsPaint = <span class="keyword">new</span> TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FAKE_BOLD_TEXT_FLAG);
      itemsPaint.setTextSize(TEXT_SIZE);
    }

    <span class="comment">/*
     * 设置绘制选中条目的画笔
     * 设置文字大小 24
     */</span>
    <span class="keyword">if</span> (valuePaint == <span class="keyword">null</span>) {
      valuePaint = <span class="keyword">new</span> TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FAKE_BOLD_TEXT_FLAG | Paint.DITHER_FLAG);
      valuePaint.setTextSize(TEXT_SIZE);
      valuePaint.setShadowLayer(<span class="number">0.1</span>f, <span class="number"></span>, <span class="number">0.1</span>f, <span class="number">0xFFC0C0C0</span>);
    }

    <span class="comment">//选中的条目背景</span>
    <span class="keyword">if</span> (centerDrawable == <span class="keyword">null</span>) {
      centerDrawable = getContext().getResources().getDrawable(R.drawable.wheel_val);
    }

    <span class="comment">//创建顶部阴影图片</span>
    <span class="keyword">if</span> (topShadow == <span class="keyword">null</span>) {
      <span class="comment">/*
       * 构造方法中传入颜色渐变方向
       * 阴影颜色
       */</span>
      topShadow = <span class="keyword">new</span> GradientDrawable(Orientation.TOP_BOTTOM, SHADOWS_COLORS);
    }

    <span class="comment">//创建底部阴影图片</span>
    <span class="keyword">if</span> (bottomShadow == <span class="keyword">null</span>) {
      bottomShadow = <span class="keyword">new</span> GradientDrawable(Orientation.BOTTOM_TOP, SHADOWS_COLORS);
    }

    <span class="comment">/*
     * 设置 View 组件的背景
     */</span>
    setBackgroundResource(R.drawable.wheel_bg);
  }

  <span class="javadoc">/**
   * 计算布局期望的高度
   * 
   * <span class="javadoctag">@param</span> layout
   *	  组件的布局的
   * <span class="javadoctag">@return</span> 
   * 		布局需要的高度
   */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> getDesiredHeight(Layout layout) {
    <span class="keyword">if</span> (layout == <span class="keyword">null</span>) {
      <span class="keyword">return</span> <span class="number"></span>;
    }

    <span class="comment">/*
     * 布局需要的高度是 条目个数 * 可见条目数 减去 顶部和底部隐藏的一部份 减去 额外的条目高度
     */</span>
    <span class="keyword">int</span> desired = getItemHeight() * visibleItems - ITEM_OFFSET * <span class="number">2</span> - ADDITIONAL_ITEM_HEIGHT;

    <span class="comment">// 将计算的布局高度 与 最小高度比较, 取最大值</span>
    desired = Math.max(desired, getSuggestedMinimumHeight());

    <span class="keyword">return</span> desired;
  }

  <span class="javadoc">/**
   * 根据条目获取字符串
   * 
   * <span class="javadoctag">@param</span> index
   *			条目索引
   * <span class="javadoctag">@return</span> 
   * 		条目显示的字符串
   */</span>
  <span class="keyword">private</span> String getTextItem(<span class="keyword">int</span> index) {
    <span class="keyword">if</span> (adapter == <span class="keyword">null</span> || adapter.getItemsCount() == <span class="number"></span>) {
      <span class="keyword">return</span> <span class="keyword">null</span>;
    }
    <span class="comment">//适配器显示的字符串个数</span>
    <span class="keyword">int</span> count = adapter.getItemsCount();
    
    <span class="comment">//考虑 index 小于 0 的情况</span>
    <span class="keyword">if</span> ((index &lt; <span class="number"></span> || index &gt;= count) && !isCyclic) {
      <span class="keyword">return</span> <span class="keyword">null</span>;
    } <span class="keyword">else</span> {
      <span class="keyword">while</span> (index &lt; <span class="number"></span>) {
        index = count + index;
      }
    }

    <span class="comment">//index 大于 0</span>
    index %= count;
    <span class="keyword">return</span> adapter.getItem(index);
  }

  <span class="javadoc">/**
   * 根据当前值创建 字符串
   * 
   * <span class="javadoctag">@param</span> useCurrentValue
   * 		是否在滚动
   * <span class="javadoctag">@return</span> the text
   * 		生成的字符串
   */</span>
  <span class="keyword">private</span> String buildText(<span class="keyword">boolean</span> useCurrentValue) {
    <span class="comment">//创建字符串容器</span>
    StringBuilder itemsText = <span class="keyword">new</span> StringBuilder();
    <span class="comment">//计算出显示的条目相对位置, 例如显示 5个, 第 3 个是正中见选中的布局</span>
    <span class="keyword">int</span> addItems = visibleItems / <span class="number">2</span> + <span class="number">1</span>;

    <span class="comment">/*
     * 遍历显示的条目
     * 获取当前显示条目 上下 各 addItems 个文本, 将该文本添加到显示文本中去
     * 如果不是最后一个 都加上回车
     */</span>
    <span class="keyword">for</span> (<span class="keyword">int</span> i = currentItem - addItems; i &lt;= currentItem + addItems; i++) {
      <span class="comment">//如果在滚动</span>
      <span class="keyword">if</span> (useCurrentValue || i != currentItem) {
        String text = getTextItem(i);
        <span class="keyword">if</span> (text != <span class="keyword">null</span>) {
          itemsText.append(text);
        }
      }
      <span class="keyword">if</span> (i &lt; currentItem + addItems) {
        itemsText.append(<span class="string">"\n"</span>);
      }
    }

    <span class="keyword">return</span> itemsText.toString();
  }

  <span class="javadoc">/**
   * 返回 条目的字符串
   * 
   * <span class="javadoctag">@return</span> 
   * 		条目最大宽度
   */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> getMaxTextLength() {
    WheelAdapter adapter = getAdapter();
    <span class="keyword">if</span> (adapter == <span class="keyword">null</span>) {
      <span class="keyword">return</span> <span class="number"></span>;
    }

    <span class="comment">//如果获取的最大条目宽度不为 -1, 可以直接返回该条目宽度</span>
    <span class="keyword">int</span> adapterLength = adapter.getMaximumLength();
    <span class="keyword">if</span> (adapterLength &gt; <span class="number"></span>) {
      <span class="keyword">return</span> adapterLength;
    }

    String maxText = <span class="keyword">null</span>;
    <span class="keyword">int</span> addItems = visibleItems / <span class="number">2</span>;
    <span class="comment">/*
     * 遍历当前显示的条目, 获取字符串长度最长的那个, 返回这个最长的字符串长度
     */</span>
    <span class="keyword">for</span> (<span class="keyword">int</span> i = Math.max(currentItem - addItems, <span class="number"></span>); i &lt; Math.min(currentItem + visibleItems,
        adapter.getItemsCount()); i++) {
      String text = adapter.getItem(i);
      <span class="keyword">if</span> (text != <span class="keyword">null</span> && (maxText == <span class="keyword">null</span> || maxText.length() &lt; text.length())) {
        maxText = text;
      }
    }

    <span class="keyword">return</span> maxText != <span class="keyword">null</span> ? maxText.length() : <span class="number"></span>;
  }

  <span class="javadoc">/**
   * 获取每个条目的高度
   * 
   * <span class="javadoctag">@return</span> 
   * 		条目的高度
   */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> getItemHeight() {
    <span class="comment">//如果条目高度不为 0, 直接返回</span>
    <span class="keyword">if</span> (itemHeight != <span class="number"></span>) {
      <span class="keyword">return</span> itemHeight;
    <span class="comment">//如果条目的高度为 0, 并且普通条目布局不为null, 条目个数大于 2 </span>
    } <span class="keyword">else</span> <span class="keyword">if</span> (itemsLayout != <span class="keyword">null</span> && itemsLayout.getLineCount() &gt; <span class="number">2</span>) {
      <span class="comment">/*
       * itemsLayout.getLineTop(2) : 获取顶部第二行上面的垂直(y轴)位置, 如果行数等于
       */</span>
      itemHeight = itemsLayout.getLineTop(<span class="number">2</span>) - itemsLayout.getLineTop(<span class="number">1</span>);
      <span class="keyword">return</span> itemHeight;
    }

    <span class="comment">//如果上面都不符合, 使用整体高度处以 显示条目数</span>
    <span class="keyword">return</span> getHeight() / visibleItems;
  }

  <span class="javadoc">/**
   * 计算宽度并创建文字布局
   * 
   * <span class="javadoctag">@param</span> widthSize
   *			输入的布局宽度
   * <span class="javadoctag">@param</span> mode
   *			布局模式
   * <span class="javadoctag">@return</span> 
   * 		计算的宽度
   */</span>
  <span class="keyword">private</span> <span class="keyword">int</span> calculateLayoutWidth(<span class="keyword">int</span> widthSize, <span class="keyword">int</span> mode) {
    initResourcesIfNecessary();

    <span class="keyword">int</span> width = widthSize;

    <span class="comment">//获取最长的条目显示字符串字符个数</span>
    <span class="keyword">int</span> maxLength = getMaxTextLength();
    
    <span class="keyword">if</span> (maxLength &gt; <span class="number"></span>) {
      <span class="comment">/*
       * 使用方法 FloatMath.ceil() 方法有以下警告
       * Use java.lang.Math#ceil instead of android.util.FloatMath#ceil() since it is faster as of API 8
       */</span>
      <span class="comment">//float textWidth = FloatMath.ceil(Layout.getDesiredWidth("0", itemsPaint));</span>
      <span class="comment">//向上取整  计算一个字符串宽度</span>
      <span class="keyword">float</span> textWidth = (<span class="keyword">float</span>) Math.ceil(Layout.getDesiredWidth(<span class="string">"0"</span>, itemsPaint));
      
      <span class="comment">//获取字符串总的宽度</span>
      itemsWidth = (<span class="keyword">int</span>) (maxLength * textWidth);
    } <span class="keyword">else</span> {
      itemsWidth = <span class="number"></span>;
    }
    
    <span class="comment">//总宽度加上一些间距</span>
    itemsWidth += ADDITIONAL_ITEMS_SPACE; <span class="comment">// make it some more</span>

    <span class="comment">//计算 label 的长度</span>
    labelWidth = <span class="number"></span>;
    <span class="keyword">if</span> (label != <span class="keyword">null</span> && label.length() &gt; <span class="number"></span>) {
      labelWidth = (<span class="keyword">int</span>) Math.ceil(Layout.getDesiredWidth(label, valuePaint));
      <span class="comment">//labelWidth = (int) FloatMath.ceil(Layout.getDesiredWidth(label, valuePaint));</span>
    }

    <span class="keyword">boolean</span> recalculate = <span class="keyword">false</span>;
    <span class="comment">//精准模式</span>
    <span class="keyword">if</span> (mode == MeasureSpec.EXACTLY) {
      <span class="comment">//精准模式下, 宽度就是给定的宽度</span>
      width = widthSize;
      recalculate = <span class="keyword">true</span>;
    } <span class="keyword">else</span> {
      <span class="comment">//未定义模式</span>
      width = itemsWidth + labelWidth + <span class="number">2</span> * PADDING;
      <span class="keyword">if</span> (labelWidth &gt; <span class="number"></span>) {
        width += LABEL_OFFSET;
      }

      <span class="comment">// 获取 ( 计算出来的宽度 与 最小宽度的 ) 最大值</span>
      width = Math.max(width, getSuggestedMinimumWidth());

      <span class="comment">//最大模式 如果 给定的宽度 小于 计算出来的宽度, 那么使用最小的宽度 ( 给定宽度 | 计算出来的宽度 )</span>
      <span class="keyword">if</span> (mode == MeasureSpec.AT_MOST && widthSize &lt; width) {
        width = widthSize;
        recalculate = <span class="keyword">true</span>;
      }
    }

    <span class="comment">/*
     * 重新计算宽度 , 如果宽度是给定的宽度, 不是我们计算出来的宽度, 需要重新进行计算
     * 重新计算的宽度是用于
     * 
     * 计算 itemsWidth , 这个与返回的 宽度无关, 与创建布局有关
     */</span>
    <span class="keyword">if</span> (recalculate) {
      <span class="keyword">int</span> pureWidth = width - LABEL_OFFSET - <span class="number">2</span> * PADDING;
      <span class="keyword">if</span> (pureWidth &lt;= <span class="number"></span>) {
        itemsWidth = labelWidth = <span class="number"></span>;
      }
      <span class="keyword">if</span> (labelWidth &gt; <span class="number"></span>) {
        <span class="keyword">double</span> newWidthItems = (<span class="keyword">double</span>) itemsWidth * pureWidth / (itemsWidth + labelWidth);
        itemsWidth = (<span class="keyword">int</span>) newWidthItems;
        labelWidth = pureWidth - itemsWidth;
      } <span class="keyword">else</span> {
        itemsWidth = pureWidth + LABEL_OFFSET; <span class="comment">// no label</span>
      }
    }

    <span class="keyword">if</span> (itemsWidth &gt; <span class="number"></span>) {
      <span class="comment">//创建布局</span>
      createLayouts(itemsWidth, labelWidth);
    }

    <span class="keyword">return</span> width;
  }

  <span class="javadoc">/**
   * 创建布局
   * 
   * <span class="javadoctag">@param</span> widthItems
   *			布局条目宽度
   * <span class="javadoctag">@param</span> widthLabel
   *			label 宽度
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> createLayouts(<span class="keyword">int</span> widthItems, <span class="keyword">int</span> widthLabel) {
    <span class="comment">/*
     * 创建普通条目布局
     * 如果 普通条目布局 为 null 或者 普通条目布局的宽度 大于 传入的宽度, 这时需要重新创建布局
     * 如果 普通条目布局存在, 并且其宽度小于传入的宽度, 此时需要将
     */</span>
    <span class="keyword">if</span> (itemsLayout == <span class="keyword">null</span> || itemsLayout.getWidth() &gt; widthItems) {
      
      <span class="comment">/*
       * android.text.StaticLayout.StaticLayout(
       * CharSequence source, TextPaint paint, 
       * int width, Alignment align, 
       * float spacingmult, float spacingadd, boolean includepad)
       * 传入参数介绍 : 
       * CharSequence source : 需要分行显示的字符串
       * TextPaint paint : 绘制字符串的画笔
       * int width : 条目的宽度
       * Alignment align : Layout 的对齐方式, ALIGN_CENTER 居中对齐, ALIGN_NORMAL 左对齐, Alignment.ALIGN_OPPOSITE 右对齐
       * float spacingmult : 行间距, 1.5f 代表 1.5 倍字体高度
       * float spacingadd : 基础行距上增加多少 , 真实行间距 等于 spacingmult 和 spacingadd 的和
       * boolean includepad : 
       */</span>
      itemsLayout = <span class="keyword">new</span> StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,
          widthLabel &gt; <span class="number"></span> ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, <span class="number">1</span>,
          ADDITIONAL_ITEM_HEIGHT, <span class="keyword">false</span>);
    } <span class="keyword">else</span> {
      <span class="comment">//调用 Layout 内置的方法 increaseWidthTo 将宽度提升到指定的宽度</span>
      itemsLayout.increaseWidthTo(widthItems);
    }

    <span class="comment">/*
     * 创建选中条目
     */</span>
    <span class="keyword">if</span> (!isScrollingPerformed && (valueLayout == <span class="keyword">null</span> || valueLayout.getWidth() &gt; widthItems)) {
      String text = getAdapter() != <span class="keyword">null</span> ? getAdapter().getItem(currentItem) : <span class="keyword">null</span>;
      valueLayout = <span class="keyword">new</span> StaticLayout(text != <span class="keyword">null</span> ? text : <span class="string">""</span>, valuePaint, widthItems,
          widthLabel &gt; <span class="number"></span> ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, <span class="number">1</span>,
          ADDITIONAL_ITEM_HEIGHT, <span class="keyword">false</span>);
    } <span class="keyword">else</span> <span class="keyword">if</span> (isScrollingPerformed) {
      valueLayout = <span class="keyword">null</span>;
    } <span class="keyword">else</span> {
      valueLayout.increaseWidthTo(widthItems);
    }

    <span class="comment">/*
     * 创建标签条目
     */</span>
    <span class="keyword">if</span> (widthLabel &gt; <span class="number"></span>) {
      <span class="keyword">if</span> (labelLayout == <span class="keyword">null</span> || labelLayout.getWidth() &gt; widthLabel) {
        labelLayout = <span class="keyword">new</span> StaticLayout(label, valuePaint, widthLabel, Layout.Alignment.ALIGN_NORMAL, <span class="number">1</span>,
            ADDITIONAL_ITEM_HEIGHT, <span class="keyword">false</span>);
      } <span class="keyword">else</span> {
        labelLayout.increaseWidthTo(widthLabel);
      }
    }
  }

  <span class="comment">/*
   * 测量组件大小
   * (non-Javadoc)
   * @see android.view.View#onMeasure(int, int)
   */</span>
  <span class="annotation">@Override</span>
  <span class="keyword">protected</span> <span class="keyword">void</span> onMeasure(<span class="keyword">int</span> widthMeasureSpec, <span class="keyword">int</span> heightMeasureSpec) {
    <span class="comment">//获取宽度 和 高度的模式 和 大小</span>
    <span class="keyword">int</span> widthMode = MeasureSpec.getMode(widthMeasureSpec);
    <span class="keyword">int</span> heightMode = MeasureSpec.getMode(heightMeasureSpec);
    <span class="keyword">int</span> widthSize = MeasureSpec.getSize(widthMeasureSpec);
    <span class="keyword">int</span> heightSize = MeasureSpec.getSize(heightMeasureSpec);

    <span class="comment">//宽度就是 计算的布局的宽度</span>
    <span class="keyword">int</span> width = calculateLayoutWidth(widthSize, widthMode);

    <span class="keyword">int</span> height;
    <span class="comment">/*
     * 精准模式
     * 		精准模式下 高度就是精确的高度
     */</span>
    <span class="keyword">if</span> (heightMode == MeasureSpec.EXACTLY) {
      height = heightSize;
    
    <span class="comment">//未定义模式 和 最大模式</span>
    } <span class="keyword">else</span> {
      <span class="comment">//未定义模式下 获取布局需要的高度</span>
      height = getDesiredHeight(itemsLayout);

      <span class="comment">//最大模式下 获取 布局高度 和 布局所需高度的最小值</span>
      <span class="keyword">if</span> (heightMode == MeasureSpec.AT_MOST) {
        height = Math.min(height, heightSize);
      }
    }

    <span class="comment">//设置组件的宽和高</span>
    setMeasuredDimension(width, height);
  }

  <span class="comment">/*
   * 绘制组件
   * (non-Javadoc)
   * @see android.view.View#onDraw(android.graphics.Canvas)
   */</span>
  <span class="annotation">@Override</span>
  <span class="keyword">protected</span> <span class="keyword">void</span> onDraw(Canvas canvas) {
    <span class="keyword">super</span>.onDraw(canvas);

    <span class="comment">//如果条目布局为 null, 就创建该布局</span>
    <span class="keyword">if</span> (itemsLayout == <span class="keyword">null</span>) {
      <span class="comment">/*
       * 如果 条目宽度为0, 说明该宽度没有计算, 先计算, 计算完之后会创建布局
       * 如果 条目宽度 大于 0, 说明已经计算过宽度了, 直接创建布局
       */</span>
      <span class="keyword">if</span> (itemsWidth == <span class="number"></span>) {
        calculateLayoutWidth(getWidth(), MeasureSpec.EXACTLY);
      } <span class="keyword">else</span> {
        <span class="comment">//创建普通条目布局, 选中条目布局, 标签条目布局</span>
        createLayouts(itemsWidth, labelWidth);
      }
    }

    <span class="comment">//如果条目宽度大于0</span>
    <span class="keyword">if</span> (itemsWidth &gt; <span class="number"></span>) {
      canvas.save();
      <span class="comment">// 使用平移方法忽略 填充的空间 和 顶部底部隐藏的一部份条目</span>
      canvas.translate(PADDING, -ITEM_OFFSET);
      <span class="comment">//绘制普通条目</span>
      drawItems(canvas);
      <span class="comment">//绘制选中条目</span>
      drawValue(canvas);
      canvas.restore();
    }

    <span class="comment">//在中心位置绘制</span>
    drawCenterRect(canvas);
    <span class="comment">//绘制阴影</span>
    drawShadows(canvas);
  }

  <span class="javadoc">/**
   * Draws shadows on top and bottom of control
   * 
   * <span class="javadoctag">@param</span> canvas
   *			the canvas for drawing
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> drawShadows(Canvas canvas) {
    topShadow.setBounds(<span class="number"></span>, <span class="number"></span>, getWidth(), getHeight() / visibleItems);
    topShadow.draw(canvas);

    bottomShadow.setBounds(<span class="number"></span>, getHeight() - getHeight() / visibleItems, getWidth(), getHeight());
    bottomShadow.draw(canvas);
  }

  <span class="javadoc">/**
   * 绘制选中条目
   * 
   * <span class="javadoctag">@param</span> canvas
   *			画布
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> drawValue(Canvas canvas) {
    valuePaint.setColor(VALUE_TEXT_COLOR);
    
    <span class="comment">//将当前 View 状态属性值 转为整型集合, 赋值给 普通条目布局的绘制属性</span>
    valuePaint.drawableState = getDrawableState();

    Rect bounds = <span class="keyword">new</span> Rect();
    <span class="comment">//获取选中条目布局的边界</span>
    itemsLayout.getLineBounds(visibleItems / <span class="number">2</span>, bounds);

    <span class="comment">// 绘制标签</span>
    <span class="keyword">if</span> (labelLayout != <span class="keyword">null</span>) {
      canvas.save();
      canvas.translate(itemsLayout.getWidth() + LABEL_OFFSET, bounds.top);
      labelLayout.draw(canvas);
      canvas.restore();
    }

    <span class="comment">// 绘制选中条目</span>
    <span class="keyword">if</span> (valueLayout != <span class="keyword">null</span>) {
      canvas.save();
      canvas.translate(<span class="number"></span>, bounds.top + scrollingOffset);
      valueLayout.draw(canvas);
      canvas.restore();
    }
  }

  <span class="javadoc">/**
   * 绘制普通条目
   * 
   * <span class="javadoctag">@param</span> canvas
   *			画布
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> drawItems(Canvas canvas) {
    canvas.save();

    <span class="comment">//获取 y 轴 定点高度</span>
    <span class="keyword">int</span> top = itemsLayout.getLineTop(<span class="number">1</span>);
    canvas.translate(<span class="number"></span>, -top + scrollingOffset);

    <span class="comment">//设置画笔颜色</span>
    itemsPaint.setColor(ITEMS_TEXT_COLOR);
    <span class="comment">//将当前 View 状态属性值 转为整型集合, 赋值给 普通条目布局的绘制属性</span>
    itemsPaint.drawableState = getDrawableState();
    <span class="comment">//将布局绘制到画布上</span>
    itemsLayout.draw(canvas);

    canvas.restore();
  }

  <span class="javadoc">/**
   * 绘制当前选中条目的背景图片
   * 
   * <span class="javadoctag">@param</span> canvas
   *			画布
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> drawCenterRect(Canvas canvas) {
    <span class="keyword">int</span> center = getHeight() / <span class="number">2</span>;
    <span class="keyword">int</span> offset = getItemHeight() / <span class="number">2</span>;
    centerDrawable.setBounds(<span class="number"></span>, center - offset, getWidth(), center + offset);
    centerDrawable.draw(canvas);
  }

  <span class="comment">/*
   * 继承自 View 的触摸事件, 当出现触摸事件的时候, 就会回调该方法
   * (non-Javadoc)
   * @see android.view.View#onTouchEvent(android.view.MotionEvent)
   */</span>
  <span class="annotation">@Override</span>
  <span class="keyword">public</span> <span class="keyword">boolean</span> onTouchEvent(MotionEvent event) {
    <span class="comment">//获取适配器</span>
    WheelAdapter adapter = getAdapter();
    <span class="keyword">if</span> (adapter == <span class="keyword">null</span>) {
      <span class="keyword">return</span> <span class="keyword">true</span>;
    }

    <span class="comment">/*
     * gestureDetector.onTouchEvent(event) : 分析给定的动作, 如果可用, 调用 手势检测器的 onTouchEvent 方法
     * -- 参数解析 : ev , 触摸事件
     * -- 返回值 : 如果手势监听器成功执行了该方法, 返回true, 如果执行出现意外 返回 false;
     */</span>
    <span class="keyword">if</span> (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {
      justify();
    }
    <span class="keyword">return</span> <span class="keyword">true</span>;
  }

  <span class="javadoc">/**
   * 滚动 WheelView
   * 
   * <span class="javadoctag">@param</span> delta
   *			滚动的值
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> doScroll(<span class="keyword">int</span> delta) {
    scrollingOffset += delta;
    
    <span class="comment">//计算滚动的条目数, 使用滚动的值 处于 单个条目高度, 注意计算整数值</span>
    <span class="keyword">int</span> count = scrollingOffset / getItemHeight();
    <span class="comment">/*
     * pos 是滚动后的目标元素索引
     * 计算当前位置, 当前条目数 减去 滚动的条目数
     * 注意 滚动条目数可正 可负
     */</span>
    <span class="keyword">int</span> pos = currentItem - count;
    <span class="comment">//如果是可循环的, 并且条目数大于0</span>
    <span class="keyword">if</span> (isCyclic && adapter.getItemsCount() &gt; <span class="number"></span>) {
      <span class="comment">//设置循环, 如果位置小于0, 那么该位置就显示最后一个元素</span>
      <span class="keyword">while</span> (pos &lt; <span class="number"></span>) {
        pos += adapter.getItemsCount();
      }
      <span class="comment">//如果位置正无限大, 模条目数 取余</span>
      pos %= adapter.getItemsCount();
      
    <span class="comment">// (前提 : 不可循环  条目数大于0, 可循环 条目数小于0, 条目数小于0, 不可循环) , 如果滚动在执行</span>
    } <span class="keyword">else</span> <span class="keyword">if</span> (isScrollingPerformed) {
      <span class="comment">//位置一旦小于0, 计算的位置就赋值为 0, 条目滚动数为0</span>
      <span class="keyword">if</span> (pos &lt; <span class="number"></span>) {
        count = currentItem;
        pos = <span class="number"></span>;
        
      <span class="comment">//位置大于条目数的时候, 当前位置等于(条目数 - 1), 条目滚动数等于 当前位置 减去 (条目数 - 1)</span>
      } <span class="keyword">else</span> <span class="keyword">if</span> (pos &gt;= adapter.getItemsCount()) {
        count = currentItem - adapter.getItemsCount() + <span class="number">1</span>;
        pos = adapter.getItemsCount() - <span class="number">1</span>;
      }
    
    } <span class="keyword">else</span> {
      <span class="comment">// fix position</span>
      pos = Math.max(pos, <span class="number"></span>);
      pos = Math.min(pos, adapter.getItemsCount() - <span class="number">1</span>);
    }

    <span class="comment">//滚动的高度</span>
    <span class="keyword">int</span> offset = scrollingOffset;
    
    <span class="comment">/*
     * 如果当前位置不是滚动后的目标位置, 就将当前位置设置为目标位置
     * 否则就重绘组件
     */</span>
    <span class="keyword">if</span> (pos != currentItem) {
      setCurrentItem(pos, <span class="keyword">false</span>);
    } <span class="keyword">else</span> {
      <span class="comment">//重绘组件</span>
      invalidate();
    }

    <span class="comment">// 将滚动后剩余的小数部分保存</span>
    scrollingOffset = offset - count * getItemHeight();
    <span class="keyword">if</span> (scrollingOffset &gt; getHeight()) {
      scrollingOffset = scrollingOffset % getHeight() + getHeight();
    }
  }

  <span class="javadoc">/**
   * 手势监听器
   */</span>
  <span class="keyword">private</span> SimpleOnGestureListener gestureListener = <span class="keyword">new</span> SimpleOnGestureListener() {
    
    <span class="comment">//按下操作</span>
    <span class="keyword">public</span> <span class="keyword">boolean</span> onDown(MotionEvent e) {
      <span class="comment">//如果滚动在执行</span>
      <span class="keyword">if</span> (isScrollingPerformed) {
        <span class="comment">//滚动强制停止, 按下的时候不能继续滚动</span>
        scroller.forceFinished(<span class="keyword">true</span>);
        <span class="comment">//清理信息</span>
        clearMessages();
        <span class="keyword">return</span> <span class="keyword">true</span>;
      }
      <span class="keyword">return</span> <span class="keyword">false</span>;
    }

    <span class="comment">/*
     * 手势监听器监听到 滚动操作后回调
     * 
     * 参数解析 : 
     * MotionEvent e1 : 触发滚动时第一次按下的事件
     * MotionEvent e2 : 触发当前滚动的移动事件
     * float distanceX : 自从上一次调用 该方法 到这一次 x 轴滚动的距离, 
     * 				注意不是 e1 到 e2 的距离, e1 到 e2 的距离是从开始滚动到现在的滚动距离
     * float distanceY : 自从上一次回调该方法到这一次 y 轴滚动的距离
     * 
     * 返回值 : 如果事件成功触发, 执行完了方法中的操作, 返回true, 否则返回 false 
     * (non-Javadoc)
     * @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float)
     */</span>
    <span class="keyword">public</span> <span class="keyword">boolean</span> onScroll(MotionEvent e1, MotionEvent e2, <span class="keyword">float</span> distanceX, <span class="keyword">float</span> distanceY) {
      <span class="comment">//开始滚动, 并回调滚动监听器集合中监听器的 开始滚动方法</span>
      startScrolling();
      doScroll((<span class="keyword">int</span>) -distanceY);
      <span class="keyword">return</span> <span class="keyword">true</span>;
    }

    <span class="comment">/*
     * 当一个急冲手势发生后 回调该方法, 会计算出该手势在 x 轴 y 轴的速率
     * 
     * 参数解析 : 
     * -- MotionEvent e1 : 急冲动作的第一次触摸事件;
     * -- MotionEvent e2 : 急冲动作的移动发生的时候的触摸事件;
     * -- float velocityX : x 轴的速率
     * -- float velocityY : y 轴的速率
     * 
     * 返回值 : 如果执行完毕返回 true, 否则返回false, 这个就是自己定义的
     * 
     * (non-Javadoc)
     * @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float)
     */</span>
    <span class="keyword">public</span> <span class="keyword">boolean</span> onFling(MotionEvent e1, MotionEvent e2, <span class="keyword">float</span> velocityX, <span class="keyword">float</span> velocityY) {
      <span class="comment">//计算上一次的 y 轴位置, 当前的条目高度 加上 剩余的 不够一行高度的那部分</span>
      lastScrollY = currentItem * getItemHeight() + scrollingOffset;
      <span class="comment">//如果可以循环最大值是无限大, 不能循环就是条目数的高度值</span>
      <span class="keyword">int</span> maxY = isCyclic ? <span class="number">0x7FFFFFFF</span> : adapter.getItemsCount() * getItemHeight();
      <span class="keyword">int</span> minY = isCyclic ? -maxY : <span class="number"></span>;
      <span class="comment">/*
       * Scroll 开始根据一个急冲手势滚动, 滚动的距离与初速度有关
       * 参数介绍 : 
       * -- int startX : 开始时的 X轴位置
       * -- int startY : 开始时的 y轴位置
       * -- int velocityX : 急冲手势的 x 轴的初速度, 单位 px/s
       * -- int velocityY : 急冲手势的 y 轴的初速度, 单位 px/s
       * -- int minX : x 轴滚动的最小值
       * -- int maxX : x 轴滚动的最大值
       * -- int minY : y 轴滚动的最小值
       * -- int maxY : y 轴滚动的最大值
       */</span>
      scroller.fling(<span class="number"></span>, lastScrollY, <span class="number"></span>, (<span class="keyword">int</span>) -velocityY / <span class="number">2</span>, <span class="number"></span>, <span class="number"></span>, minY, maxY);
      setNextMessage(MESSAGE_SCROLL);
      <span class="keyword">return</span> <span class="keyword">true</span>;
    }
  };

  <span class="comment">// Handler 中的  Message 信息</span>
  <span class="javadoc">/** 滚动信息 */</span>
  <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">int</span> MESSAGE_SCROLL = <span class="number"></span>;
  <span class="javadoc">/** 调整信息 */</span>
  <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">int</span> MESSAGE_JUSTIFY = <span class="number">1</span>;

  <span class="javadoc">/**
   * 清空之前的 Handler 队列, 发送下一个消息到 Handler 中
   * 
   * <span class="javadoctag">@param</span> message
   *			要发送的消息
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> setNextMessage(<span class="keyword">int</span> message) {
    <span class="comment">//清空 Handler 队列中的  what 消息</span>
    clearMessages();
    <span class="comment">//发送消息到 Handler 中</span>
    animationHandler.sendEmptyMessage(message);
  }

  <span class="javadoc">/**
   * 清空队列中的信息
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> clearMessages() {
    <span class="comment">//删除 Handler 执行队列中的滚动操作</span>
    animationHandler.removeMessages(MESSAGE_SCROLL);
    animationHandler.removeMessages(MESSAGE_JUSTIFY);
  }

  <span class="javadoc">/**
   * 动画控制器
   *  animation handler
   *  
   *  可能会造成内存泄露 : 添加注解 HandlerLeak
   *  Handler 类应该应该为static类型,否则有可能造成泄露。
   *  在程序消息队列中排队的消息保持了对目标Handler类的应用。
   *  如果Handler是个内部类,那 么它也会保持它所在的外部类的引用。
   *  为了避免泄露这个外部类,应该将Handler声明为static嵌套类,并且使用对外部类的弱应用。
   */</span>
  <span class="annotation">@SuppressLint</span>(<span class="string">"HandlerLeak"</span>)
  <span class="keyword">private</span> Handler animationHandler = <span class="keyword">new</span> Handler() {
    <span class="keyword">public</span> <span class="keyword">void</span> handleMessage(Message msg) {
      <span class="comment">//回调该方法获取当前位置, 如果返回true, 说明动画还没有执行完毕</span>
      scroller.computeScrollOffset();
      <span class="comment">//获取当前 y 位置</span>
      <span class="keyword">int</span> currY = scroller.getCurrY();
      <span class="comment">//获取已经滚动了的位置, 使用上一次位置 减去 当前位置</span>
      <span class="keyword">int</span> delta = lastScrollY - currY;
      lastScrollY = currY;
      <span class="keyword">if</span> (delta != <span class="number"></span>) {
        <span class="comment">//改变值不为 0 , 继续滚动</span>
        doScroll(delta);
      }

      <span class="comment">/*
       * 如果滚动到了指定的位置, 滚动还没有停止
       * 这时需要强制停止
       */</span>
      <span class="keyword">if</span> (Math.abs(currY - scroller.getFinalY()) &lt; MIN_DELTA_FOR_SCROLLING) {
        currY = scroller.getFinalY();
        scroller.forceFinished(<span class="keyword">true</span>);
      }
      
      <span class="comment">/*
       * 如果滚动没有停止
       * 再向 Handler 发送一个停止
       */</span>
      <span class="keyword">if</span> (!scroller.isFinished()) {
        animationHandler.sendEmptyMessage(msg.what);
      } <span class="keyword">else</span> <span class="keyword">if</span> (msg.what == MESSAGE_SCROLL) {
        justify();
      } <span class="keyword">else</span> {
        finishScrolling();
      }
    }
  };

  <span class="javadoc">/**
   * 调整 WheelView
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> justify() {
    <span class="keyword">if</span> (adapter == <span class="keyword">null</span>) {
      <span class="keyword">return</span>;
    }
    <span class="comment">//上一次的 y 轴的位置为 0</span>
    lastScrollY = <span class="number"></span>;
    <span class="keyword">int</span> offset = scrollingOffset;
    <span class="keyword">int</span> itemHeight = getItemHeight();
    <span class="comment">/*
     * 当滚动补偿 大于 0, 说明还有没有滚动的部分,  needToIncrease 是 当前条目是否小于条目数
     * 如果 滚动补偿不大于 0,  needToIncrease 是当前条目是否大于 0
     */</span>
    <span class="keyword">boolean</span> needToIncrease = offset &gt; <span class="number"></span> ? currentItem &lt; adapter.getItemsCount() : currentItem &gt; <span class="number"></span>;
    <span class="keyword">if</span> ((isCyclic || needToIncrease) && Math.abs((<span class="keyword">float</span>) offset) &gt; (<span class="keyword">float</span>) itemHeight / <span class="number">2</span>) {
      <span class="keyword">if</span> (offset &lt; <span class="number"></span>)
        offset += itemHeight + MIN_DELTA_FOR_SCROLLING;
      <span class="keyword">else</span>
        offset -= itemHeight + MIN_DELTA_FOR_SCROLLING;
    }
    <span class="keyword">if</span> (Math.abs(offset) &gt; MIN_DELTA_FOR_SCROLLING) {
      scroller.startScroll(<span class="number"></span>, <span class="number"></span>, <span class="number"></span>, offset, SCROLLING_DURATION);
      setNextMessage(MESSAGE_JUSTIFY);
    } <span class="keyword">else</span> {
      finishScrolling();
    }
  }

  <span class="javadoc">/**
   * WheelView 开始滚动
   */</span>
  <span class="keyword">private</span> <span class="keyword">void</span> startScrolling() {
    <span class="comment">//如果没有滚动, 将滚动状态 isScrollingPerformed 设为 true</span>
    <span class="keyword">if</span> (!isScrollingPerformed) {
      isScrollingPerformed = <span class="keyword">true</span>;
      <span class="comment">//通知监听器开始滚动 回调所有的 滚动监听集合中 的 开始滚动方法</span>
      notifyScrollingListenersAboutStart();
    }
  }

  <span class="javadoc">/**
   * 结束滚动
   * 	设置滚动状态为 false, 回调滚动监听器的停止滚动方法
   */</span>
  <span class="keyword">void</span> finishScrolling() {
    <span class="keyword">if</span> (isScrollingPerformed) {
      notifyScrollingListenersAboutEnd();
      isScrollingPerformed = <span class="keyword">false</span>;
    }
    <span class="comment">//设置布局无效</span>
    invalidateLayouts();
    <span class="comment">//重绘布局</span>
    invalidate();
  }

  <span class="javadoc">/**
   * 滚动 WheelView
   * 
   * <span class="javadoctag">@param</span> itemsToSkip
   *			滚动的元素个数
   * <span class="javadoctag">@param</span> time
   *			每次滚动的间隔
   */</span>
  <span class="keyword">public</span> <span class="keyword">void</span> scroll(<span class="keyword">int</span> itemsToScroll, <span class="keyword">int</span> time) {
    <span class="comment">//如果有滚动强制停止</span>
    scroller.forceFinished(<span class="keyword">true</span>);

    lastScrollY = scrollingOffset;
    <span class="keyword">int</span> offset = itemsToScroll * getItemHeight();

    <span class="comment">/*
     * 给定 一个开始点, 滚动距离, 滚动间隔, 开始滚动
     * 
     * 参数解析 : 
     * 1. 开始的 x 轴位置
     * 2. 开始的 y 轴位置
     * 3. 要滚动 x 轴距离
     * 4. 要滚动 y 轴距离
     * 5. 滚动花费的时间
     */</span>
    scroller.startScroll(<span class="number"></span>, lastScrollY, <span class="number"></span>, offset - lastScrollY, time);
    setNextMessage(MESSAGE_SCROLL);

    <span class="comment">//设置开始滚动状态, 并回调滚动监听器方法</span>
    startScrolling();
  }

}
## 7. Activity 主界面

```

package cn.org.octopus.wheelview;

import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.Button; import android.widget.LinearLayout; import cn.org.octopus.wheelview.widget.ArrayWheelAdapter; import cn.org.octopus.wheelview.widget.OnWheelChangedListener; import cn.org.octopus.wheelview.widget.OnWheelScrollListener; import cn.org.octopus.wheelview.widget.WheelView;

public class MainActivity extends Activity{

public static final String TAG = “octopus.activity”;

private static Button bt_click;

public String province[] = new String[] { ” 河北省 “, ” 山西省 “, ” 内蒙古 “, ” 辽宁省 “, ” 吉林省 “, ” 黑龙江 “, ” 江苏省 “ };

public String city[][] = new String[][] { new String[] {” 石家庄 “, “唐山”, “秦皇岛”, “邯郸”, “邢台”, “保定”, “张家口”, “承德”, “沧州”, “廊坊”, “衡水”}, new String[] {“太原”, “大同”, “阳泉”, “长治”, “晋城”, “朔州”, “晋中”, “运城”, “忻州”, “临汾”, “吕梁”}, new String[] {“呼和浩特”, “包头”, “乌海”, “赤峰”, “通辽”, “鄂尔多斯”, “呼伦贝尔”, “巴彦淖尔”, “乌兰察布”, “兴安”, “锡林郭勒”, “阿拉善”}, new String[] {“沈阳”, “大连”, “鞍山”, “抚顺”, “本溪”, “丹东”, “锦州”, “营口”, “阜新”, “辽阳”, “盘锦”, “铁岭”, “朝阳”, “葫芦岛”}, new String[] {“长春”, “吉林”, “四平”, “辽源”, “通化”, “白山”, “松原”, “白城”, “延边”}, new String[] {“哈尔滨”, “齐齐哈尔”, “鸡西”, “鹤岗”, “双鸭山”, “大庆”, “伊春”, “佳木斯”, “七台河”, “牡丹江”, “黑河”, “绥化”, “大兴安岭”}, new String[] {“南京”, “无锡”, “徐州”, “常州”, “苏州”, “南通”, “连云港”, “淮安”, “盐城”, “扬州”, “镇江”, “泰州”, “宿迁”} };

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);

<span class="keyword">if</span> (savedInstanceState == <span class="keyword">null</span>) {
  getFragmentManager().beginTransaction()
      .add(R.id.container, <span class="keyword">new</span> PlaceholderFragment()).commit();
}

}

/*

  • 点击事件 */ public void onClick(View view) { showSelectDialog(this, “选择地点”, province, city); }

private void showSelectDialog(Context context, String title, final String[] left, final String[][] right) { //创建对话框 AlertDialog dialog = new AlertDialog.Builder(context).create(); //为对话框设置标题 dialog.setTitle(title); //创建对话框内容, 创建一个 LinearLayout LinearLayout llContent = new LinearLayout(context); //将创建的 LinearLayout 设置成横向的 llContent.setOrientation(LinearLayout.HORIZONTAL); //创建 WheelView 组件 final WheelView wheelLeft = new WheelView(context); //设置 WheelView 组件最多显示 5 个元素 wheelLeft.setVisibleItems(5); //设置 WheelView 元素是否循环滚动 wheelLeft.setCyclic(false); //设置 WheelView 适配器 wheelLeft.setAdapter(new ArrayWheelAdapter<String>(left)); //设置右侧的 WheelView final WheelView wheelRight = new WheelView(context); //设置右侧 WheelView 显示个数 wheelRight.setVisibleItems(5); //设置右侧 WheelView 元素是否循环滚动 wheelRight.setCyclic(true); //设置右侧 WheelView 的元素适配器 wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[])); //设置 LinearLayout 的布局参数 LinearLayout.LayoutParams paramsLeft = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 4); paramsLeft.gravity = Gravity.LEFT; LinearLayout.LayoutParams paramsRight = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 6); paramsRight.gravity = Gravity.RIGHT; //将 WheelView 对象放到左侧 LinearLayout 中 llContent.addView(wheelLeft, paramsLeft); //将 WheelView 对象放到 右侧 LinearLayout 中 llContent.addView(wheelRight, paramsRight);

<span class="comment">//为左侧的 WheelView 设置条目改变监听器</span>
wheelLeft.addChangingListener(<span class="keyword">new</span> OnWheelChangedListener() {
  <span class="annotation">@Override</span>
  <span class="keyword">public</span> <span class="keyword">void</span> onChanged(WheelView wheel, <span class="keyword">int</span> oldValue, <span class="keyword">int</span> newValue) {
    <span class="comment">//设置右侧的 WheelView 的适配器</span>
    wheelRight.setAdapter(<span class="keyword">new</span> ArrayWheelAdapter&lt;String&gt;(right[newValue]));
    wheelRight.setCurrentItem(right[newValue].length / <span class="number">2</span>);
  }
});

wheelLeft.addScrollingListener(<span class="keyword">new</span> OnWheelScrollListener() {
  
  <span class="annotation">@Override</span>
  <span class="keyword">public</span> <span class="keyword">void</span> onScrollingStarted(WheelView wheel) {
    <span class="comment">// TODO Auto-generated method stub</span>
    
  }
  
  <span class="annotation">@Override</span>
  <span class="keyword">public</span> <span class="keyword">void</span> onScrollingFinished(WheelView wheel) {
    <span class="comment">// TODO Auto-generated method stub</span>
    
  }
});

<span class="comment">//设置对话框点击事件 积极</span>
dialog.setButton(AlertDialog.BUTTON_POSITIVE, <span class="string">"确定"</span>, <span class="keyword">new</span> DialogInterface.OnClickListener() {
  <span class="annotation">@Override</span>
  <span class="keyword">public</span> <span class="keyword">void</span> onClick(DialogInterface dialog, <span class="keyword">int</span> which) {
    <span class="keyword">int</span> leftPosition = wheelLeft.getCurrentItem();
    String vLeft = left[leftPosition];
    String vRight = right[leftPosition][wheelRight.getCurrentItem()];
    bt_click.setText(vLeft + <span class="string">"-"</span> + vRight);
    dialog.dismiss();
  }
});

<span class="comment">//设置对话框点击事件 消极</span>
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, <span class="string">"取消"</span>, <span class="keyword">new</span> DialogInterface.OnClickListener() {
  <span class="annotation">@Override</span>
  <span class="keyword">public</span> <span class="keyword">void</span> onClick(DialogInterface dialog, <span class="keyword">int</span> which) {
    dialog.dismiss();
  }
});
<span class="comment">//将 LinearLayout 设置到 对话框中</span>
dialog.setView(llContent);
<span class="comment">//显示对话框</span>
<span class="keyword">if</span> (!dialog.isShowing()) {
  dialog.show();
}

}

@Override public boolean onCreateOptionsMenu(Menu menu) {

<span class="comment">// Inflate the menu; this adds items to the action bar if it is present.</span>
getMenuInflater().inflate(R.menu.main, menu);
<span class="keyword">return</span> <span class="keyword">true</span>;

}

@Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); }

/**

  • A placeholder fragment containing a simple view. */ public static class PlaceholderFragment extends Fragment {
<span class="keyword">public</span> PlaceholderFragment() {
}

<span class="annotation">@Override</span>
<span class="keyword">public</span> View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
  View rootView = inflater.inflate(R.layout.fragment_main, container,
      <span class="keyword">false</span>);
  bt_click = (Button)rootView.findViewById(R.id.bt_click);
  <span class="keyword">return</span> rootView;
}

}

}

    
    

      **博客地址  **:  [http://blog.csdn.net/shulianghan/article/details/41520569#t17](http://blog.csdn.net/shulianghan/article/details/41520569#t17)
    

    
    

      代码下载 :
    

    
    

      &#8212;  **GitHub **:  [https://github.com/han1202012/WheelViewDemo.git](https://github.com/han1202012/WheelViewDemo.git)
    

    
    

      &#8212;  **CSDN **:  [http://download.csdn.net/detail/han1202012/8208997](http://download.csdn.net/detail/han1202012/8208997) ;
    

    
    <div>
      

        代码下载 :
      

      
      

        &#8212;  **GitHub **:  [https://github.com/han1202012/WheelViewDemo.git](https://github.com/han1202012/WheelViewDemo.git)
      

      
      

        &#8212;  **CSDN **:  [http://download.csdn.net/detail/han1202012/8208997](http://download.csdn.net/detail/han1202012/8208997) ;
      

    </div>
    
    <div>
      

        **博客地址  **:  [http://blog.csdn.net/shulianghan/article/details/41520569#t17](http://blog.csdn.net/shulianghan/article/details/41520569#t17)
      

      
      

        代码下载 :
      

      
      

        &#8212;  **GitHub **:  [https://github.com/han1202012/WheelViewDemo.git](https://github.com/han1202012/WheelViewDemo.git)
      

      
      

        &#8212;  **CSDN **:  [http://download.csdn.net/detail/han1202012/8208997](http://download.csdn.net/detail/han1202012/8208997) ;
      

    </div>
    
    <div>
      

        **博客地址  **:  [http://blog.csdn.net/shulianghan/article/details/41520569#t17](http://blog.csdn.net/shulianghan/article/details/41520569#t17)
      

      
      

        代码下载 :
      

      
      

        &#8212;  **GitHub **:  [https://github.com/han1202012/WheelViewDemo.git](https://github.com/han1202012/WheelViewDemo.git)
      

      
      

        &#8212;  **CSDN **:  [http://download.csdn.net/detail/han1202012/8208997](http://download.csdn.net/detail/han1202012/8208997) ;
      

    </div>
  </div>
</div>

💬 评论