基於WheelView 組件分析自定義組件

下面的內容是我轉載的,我只是換了一個位置,存到了自己的blog,希望作者勿怪!


博客地址 http://blog.csdn.net/shulianghan/article/details/41520569


代碼下載 : 

-- GitHub : https://github.com/han1202012/WheelViewDemo.git 

-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;



博客總結 :

 

博文內容 : 本文完整地分析了 WheelView 所有的源碼, 包括其適配器類型兩種回調接口 (選中條目改變回調, 和開始結束滾動回調), 以及詳細的分析了 WheelView 主題源碼, 其中 組件寬高測量手勢監聽器添加, 以及精準的繪圖方法是主要目的, 花了將近1周時間, 感覺很值, 在這裏分享給大家;


WheelView 使用方法 創建 WheelView 組件 --> 設置顯示條目數 --> 設置循環 --> 設置適配器 --> 設置監聽器 ;


自定義組件寬高獲取策略 : MeasureSpec 最大模式 取 默認值 和 給定值中較小的那個未定義模式取默認值精準模式取 給定值;


自定義組件維護各種回調監聽器策略 : 維護集合, 將監聽器置於集合中, 回調接口時遍歷集合元素, 回調每個元素的接口方法;


自定義組件手勢監聽器添加方法 : 創建手勢監聽器, 將手勢監聽器傳入手勢探測器, 在 onTouchEvent() 方法中回調手勢監聽器的 onTouchEvent()方法;



一. WheelView 簡介



1. WheelView 效果


在 Android 中實現類似與 iOS 的 WheelView 控件 : 如圖 




2. WheelView 使用流程



(1) 基本流程簡介

 

獲取組件 --> 設置顯示條目數 --> 設置循環 --> 設置適配器 --> 設置條目改變監聽器 --> 設置滾動監聽器


a. 創建 WheelView 組件 : 使用 構造方法 或者 從佈局文件獲取 WheelView 組件;

b. 設置顯示條目數 : 調用 WheelView 組件對象的 setVisibleItems 方法 設置;

c. 設置是否循環 : 設置 WheelView 是否循環, 調用 setCyclic() 方法設置;

d. 設置適配器 : 調用 WheelView 組件的 setAdapter() 方法設置;

e. 設置條目改變監聽器 : 調用 WheelView 組件對象的 addChangingListener() 方法設置;

f. 設置滾動監聽器 : 調用 WheelView 組件對象的 addScrollingListener() 方法設置;



(2) 代碼實例


a. 創建 WheelView 對象 : 

[java] view plain copy
  1. //創建 WheelView 組件  
  2. final WheelView wheelLeft = new WheelView(context);  

b. 設置 WheelView 顯示條目數 : 

[java] view plain copy
  1. //設置 WheelView 組件最多顯示 5 個元素  
  2. wheelLeft.setVisibleItems(5);  

c. 設置 WheelView 是否滾動循環 : 

[java] view plain copy
  1. //設置 WheelView 元素是否循環滾動  
  2. wheelLeft.setCyclic(false);  

d. 設置 WheelView 適配器 

[java] view plain copy
  1. //設置 WheelView 適配器  
  2. wheelLeft.setAdapter(new ArrayWheelAdapter<String>(left));  

e. 設置條目改變監聽器 : 

[java] view plain copy
  1. //爲左側的 WheelView 設置條目改變監聽器  
  2. wheelLeft.addChangingListener(new OnWheelChangedListener() {  
  3.     @Override  
  4.     public void onChanged(WheelView wheel, int oldValue, int newValue) {  
  5.         //設置右側的 WheelView 的適配器  
  6.         wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[newValue]));  
  7.         wheelRight.setCurrentItem(right[newValue].length / 2);  
  8.     }  
  9. });  

f. 設置滾動監聽器 : 

[java] view plain copy
  1.      wheelLeft.addScrollingListener(new OnWheelScrollListener() {  
  2.   
  3. @Override  
  4. public void onScrollingStarted(WheelView wheel) {  
  5.     // TODO Auto-generated method stub  
  6.       
  7. }  
  8.   
  9. @Override  
  10. public void onScrollingFinished(WheelView wheel) {  
  11.     // TODO Auto-generated method stub  
  12.       
  13. }  
  14. );  




二. WheelView  適配器 監聽器 相關接口分析



1. 適配器 分析



這裏定義了一個適配器接口, 以及兩個適配器類, 一個用於任意類型的數據集適配, 一個用於數字適配;


適配器操作 : 在 WheelView.Java 中通過 setAdapter(WheelAdapter adapter) 和 getAdapter() 方法設置 獲取 適配器;

-- 適配器常用操作 : 在 WheelView 中定義了 getItem(), getItemsCount(), getMaxmiumLength() 方法獲取 適配器的相關信息;

[java] view plain copy
  1. /** 
  2.  * 獲取該 WheelView 的適配器 
  3.  *  
  4.  * @return  
  5.  *      返回適配器 
  6.  */  
  7. public WheelAdapter getAdapter() {  
  8.     return adapter;  
  9. }  
  10.   
  11. /** 
  12.  * 設置適配器 
  13.  *  
  14.  * @param adapter 
  15.  *            要設置的適配器 
  16.  */  
  17. public void setAdapter(WheelAdapter adapter) {  
  18.     this.adapter = adapter;  
  19.     invalidateLayouts();  
  20.     invalidate();  
  21. }  




(1) 適配器接口 ( interface WheelAdapter )


適配器接口 : WheelAdapter;

-- 接口作用 : 該接口是所有適配器的接口, 適配器類都需要實現該接口;


接口抽象方法介紹 : 

-- getItemsCount() : 獲取適配器數據集合中元素個數;

[java] view plain copy
  1. /** 
  2.  * 獲取條目的個數 
  3.  *  
  4.  * @return  
  5.  *      WheelView 的條目個數 
  6.  */  
  7. public int getItemsCount();  


-- getItem(int index) : 獲取適配器集合的中指定索引元素;

[java] view plain copy
  1. /** 
  2.  * 根據索引位置獲取 WheelView 的條目 
  3.  *  
  4.  * @param index 
  5.  *            條目的索引 
  6.  * @return  
  7.  *      WheelView 上顯示的條目的值 
  8.  */  
  9. public String getItem(int index);  


-- getMaximumLength() : 獲取 WheelView 在界面上的顯示寬度;

[java] view plain copy
  1. /** 
  2.  * 獲取條目的最大長度. 用來定義 WheelView 的寬度. 如果返回 -1, 就會使用默認寬度 
  3.  *  
  4.  * @return  
  5.  *      條目的最大寬度 或者 -1 
  6.  */  
  7. public int getMaximumLength();  



(2) 數組適配器 ( class ArrayWheelAdapter<T> implements WheelAdapter )


適配器作用 : 該適配器可以傳入任何數據類型的數組, 可以是 字符串數組, 也可以是任何對象的數組, 傳入的數組作爲適配器的數據源;


成員變量分析 

-- 數據源 

[java] view plain copy
  1. /** 適配器的數據源 */  
  2. private T items[];  


-- WheelView 最大寬度 

[java] view plain copy
  1. /** WheelView 的寬度 */  
  2. private int length;  



構造方法分析 

-- ArrayWheelAdapter(T items[], int length) : 傳入 T 類型 對象數組, 以及 WheelView 的寬度;

[java] view plain copy
  1. /** 
  2.  * 構造方法 
  3.  *  
  4.  * @param items 
  5.  *            適配器數據源 集合 T 類型的數組 
  6.  * @param length 
  7.  *            適配器數據源 集合 T 數組長度 
  8.  */  
  9. public ArrayWheelAdapter(T items[], int length) {  
  10.     this.items = items;  
  11.     this.length = length;  
  12. }  


-- ArrayWheelAdapter(T items[]) : 傳入 T 類型對象數組, 寬度使用默認的寬度;

[java] view plain copy
  1. /** 
  2.  * 構造方法 
  3.  *  
  4.  * @param items 
  5.  *            適配器數據源集合 T 類型數組 
  6.  */  
  7. public ArrayWheelAdapter(T items[]) {  
  8.     this(items, DEFAULT_LENGTH);  
  9. }  


實現的父類方法分析 :

--  getItem(int index) : 根據索引獲取數組中對應位置的對象的字符串類型;

[java] view plain copy
  1. @Override  
  2. public String getItem(int index) {  
  3.     //如果這個索引值合法, 就返回 item 數組對應的元素的字符串形式  
  4.     if (index >= 0 && index < items.length) {  
  5.         return items[index].toString();  
  6.     }  
  7.     return null;  
  8. }  


-- getItemsCount() : 獲取數據集廣大小, 直接返回數組大小;

[java] view plain copy
  1. @Override  
  2. public int getItemsCount() {  
  3.     //返回 item 數組的長度  
  4.     return items.length;  
  5. }  


-- getMaximumLength() : 獲取 WheelView 的最大寬度;

[java] view plain copy
  1. @Override  
  2. public int getMaximumLength() {  
  3.     //返回 item 元素的寬度  
  4.     return length;  
  5. }  



(3) 數字適配器 ( class NumericWheelAdapter implements WheelAdapter )


NumericWheelAdapter 適配器作用 : 數字作爲 WheelView 適配器的顯示值;



成員變量分析 : 

-- 最小值 : WheelView 數值顯示的最小值;

[java] view plain copy
  1. /** 設置的最小值 */  
  2. private int minValue;  


-- 最大值 : WheelView 數值顯示的最大值;

[java] view plain copy
  1. /** 設置的最大值 */  
  2. private int maxValue;  

-- 格式化字符串 : 用於字符串的格式化;

[java] view plain copy
  1. /** 格式化字符串, 用於格式化 貨幣, 科學計數, 十六進制 等格式 */  
  2. private String format;  


構造方法分析 : 

-- NumericWheelAdapter() : 默認的構造方法, 使用默認的最大最小值;

[java] view plain copy
  1. /** 
  2.  * 默認的構造方法, 使用默認的最大最小值 
  3.  */  
  4. public NumericWheelAdapter() {  
  5.     this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE);  
  6. }  


-- NumericWheelAdapter(int minValue, int maxValue) : 傳入一個最大最小值;

[java] view plain copy
  1. /** 
  2.  * 構造方法 
  3.  *  
  4.  * @param minValue 
  5.  *            最小值 
  6.  * @param maxValue 
  7.  *            最大值 
  8.  */  
  9. public NumericWheelAdapter(int minValue, int maxValue) {  
  10.     this(minValue, maxValue, null);  
  11. }  


-- NumericWheelAdapter(int minValue, int maxValue, String format) : 傳入最大最小值, 以及數字格式化方式;

[java] view plain copy
  1. /** 
  2.  * 構造方法 
  3.  *  
  4.  * @param minValue 
  5.  *            最小值 
  6.  * @param maxValue 
  7.  *            最大值 
  8.  * @param format 
  9.  *            格式化字符串 
  10.  */  
  11. public NumericWheelAdapter(int minValue, int maxValue, String format) {  
  12.     this.minValue = minValue;  
  13.     this.maxValue = maxValue;  
  14.     this.format = format;  
  15. }  


實現的父類方法 : 

-- 獲取條目 : 如果需要格式化, 先進行格式化;

[java] view plain copy
  1. @Override  
  2. public String getItem(int index) {  
  3.     String result = "";  
  4.     if (index >= 0 && index < getItemsCount()) {  
  5.         int value = minValue + index;  
  6.         //如果 format 不爲 null, 那麼格式化字符串, 如果爲 null, 直接返回數字  
  7.         if(format != null){  
  8.             result = String.format(format, value);  
  9.         }else{  
  10.             result = Integer.toString(value);  
  11.         }  
  12.         return result;  
  13.     }  
  14.     return null;  
  15. }  

-- 獲取元素個數 : 

[java] view plain copy
  1. @Override  
  2. public int getItemsCount() {  
  3.     //返回數字總個數  
  4.     return maxValue - minValue + 1;  
  5. }  

-- 獲取 WheelView 最大寬度 

[java] view plain copy
  1. @Override  
  2. public int getMaximumLength() {  
  3.     //獲取 最大值 和 最小值 中的 較大的數字  
  4.     int max = Math.max(Math.abs(maxValue), Math.abs(minValue));  
  5.     //獲取這個數字 的 字符串形式的 字符串長度  
  6.     int maxLen = Integer.toString(max).length();  
  7.     if (minValue < 0) {  
  8.         maxLen++;  
  9.     }  
  10.     return maxLen;  
  11. }  



2. 監聽器相關接口



(1) 條目改變監聽器 ( interface OnWheelChangedListener )


監聽器作用 : 在 WheelView 條目改變的時候, 回調該監聽器的接口方法, 執行條目改變對應的操作;


接口方法介紹 : 

-- onChanged(WheelView wheel, int oldValue, int newValue) : 傳入 WheelView 組件對象, 以及 舊的 和 新的 條目值索引;

[java] view plain copy
  1. /** 
  2.  * 當前條目改變時回調該方法 
  3.  *  
  4.  * @param wheel 
  5.  *            條目改變的 WheelView 對象 
  6.  * @param oldValue 
  7.  *            WheelView 舊的條目值 
  8.  * @param newValue 
  9.  *            WheelView 新的條目值 
  10.  */  
  11. void onChanged(WheelView wheel, int oldValue, int newValue);  



(2) 滾動監聽器 ( interface OnWheelScrollListener )


滾動監聽器作用 : 在 WheelView 滾動動作 開始 和 結束的時候回調對應的方法, 在對應方法中進行相應的操作;



接口方法介紹 

-- 開始滾動方法 : 在滾動開始的時候回調該方法;

[java] view plain copy
  1. /** 
  2.  * 在 WheelView 滾動開始的時候回調該接口 
  3.  *  
  4.  * @param wheel 
  5.  *            開始滾動的 WheelView 對象 
  6.  */  
  7. void onScrollingStarted(WheelView wheel);  


-- 停止滾動方法 : 在滾動結束的時候回調該方法;

[java] view plain copy
  1. /** 
  2.  * 在 WheelView 滾動結束的時候回調該接口 
  3.  *  
  4.  * @param wheel 
  5.  *            結束滾動的 WheelView 對象 
  6.  */  
  7. void onScrollingFinished(WheelView wheel);  



三. WheelView 解析



1. 觸摸 點擊 手勢 動作操作控制組件 模塊



(1) 創建手勢監聽器


手勢監聽器創建及對應方法 : 

-- onDown(MotionEvent e) : 在按下的時候回調該方法, e 參數是按下的事件;

-- onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) : 滾動的時候回調該方法, e1 滾動第一次按下事件, e2 當前滾動的觸摸事件, X 上一次滾動到這一次滾動 x 軸距離, Y 上一次滾動到這一次滾動 y 軸距離;

-- onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) : 快速急衝滾動時回調的方法, e1 e2 與上面參數相同, velocityX 是手勢在 x 軸的速度, velocityY 是手勢在 y 軸的速度;

-- 代碼示例 : 

[java] view plain copy
  1.     /* 
  2.      * 手勢監聽器監聽到 滾動操作後回調 
  3.      *  
  4.      * 參數解析 :  
  5.      * MotionEvent e1 : 觸發滾動時第一次按下的事件 
  6.      * MotionEvent e2 : 觸發當前滾動的移動事件 
  7.      * float distanceX : 自從上一次調用 該方法 到這一次 x 軸滾動的距離,  
  8.      *              注意不是 e1 到 e2 的距離, e1 到 e2 的距離是從開始滾動到現在的滾動距離 
  9.      * float distanceY : 自從上一次回調該方法到這一次 y 軸滾動的距離 
  10.      *  
  11.      * 返回值 : 如果事件成功觸發, 執行完了方法中的操作, 返回true, 否則返回 false  
  12.      * (non-Javadoc) 
  13.      * @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float) 
  14.      */  
  15.     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {  
  16.         //開始滾動, 並回調滾動監聽器集合中監聽器的 開始滾動方法  
  17.         startScrolling();  
  18.         doScroll((int) -distanceY);  
  19.         return true;  
  20.     }  
  21.   
  22.     /* 
  23.      * 當一個急衝手勢發生後 回調該方法, 會計算出該手勢在 x 軸 y 軸的速率 
  24.      *  
  25.      * 參數解析 :  
  26.      * -- MotionEvent e1 : 急衝動作的第一次觸摸事件; 
  27.      * -- MotionEvent e2 : 急衝動作的移動發生的時候的觸摸事件; 
  28.      * -- float velocityX : x 軸的速率 
  29.      * -- float velocityY : y 軸的速率 
  30.      *  
  31.      * 返回值 : 如果執行完畢返回 true, 否則返回false, 這個就是自己定義的 
  32.      *  
  33.      * (non-Javadoc) 
  34.      * @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float) 
  35.      */  
  36.     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {  
  37.         //計算上一次的 y 軸位置, 當前的條目高度 加上 剩餘的 不夠一行高度的那部分  
  38.         lastScrollY = currentItem * getItemHeight() + scrollingOffset;  
  39.         //如果可以循環最大值是無限大, 不能循環就是條目數的高度值  
  40.         int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();  
  41.         int minY = isCyclic ? -maxY : 0;  
  42.         /* 
  43.          * Scroll 開始根據一個急衝手勢滾動, 滾動的距離與初速度有關 
  44.          * 參數介紹 :  
  45.          * -- int startX : 開始時的 X軸位置 
  46.          * -- int startY : 開始時的 y軸位置 
  47.          * -- int velocityX : 急衝手勢的 x 軸的初速度, 單位 px/s 
  48.          * -- int velocityY : 急衝手勢的 y 軸的初速度, 單位 px/s 
  49.          * -- int minX : x 軸滾動的最小值 
  50.          * -- int maxX : x 軸滾動的最大值 
  51.          * -- int minY : y 軸滾動的最小值 
  52.          * -- int maxY : y 軸滾動的最大值 
  53.          */  
  54.         scroller.fling(0, lastScrollY, 0, (int) -velocityY / 200, minY, maxY);  
  55.         setNextMessage(MESSAGE_SCROLL);  
  56.         return true;  
  57.     }  
  58. };  



(2) 創建手勢探測器


手勢探測器創建 : 調用 其構造函數, 傳入 上下文對象 和 手勢監聽器對象;

-- 禁止長按操作 : 調用 setIsLongpressEnabled(false) 方法, 禁止長按操作, 因爲 長按操作會屏蔽滾動事件;

[java] view plain copy
  1. //創建一個手勢處理  
  2.    gestureDetector = new GestureDetector(context, gestureListener);  
  3.    /* 
  4.     * 是否允許長按操作,  
  5.     * 如果設置爲 true 用戶按下不鬆開, 會返回一個長按事件,  
  6.     * 如果設置爲 false, 按下不鬆開滑動的話 會收到滾動事件. 
  7.     */  
  8.    gestureDetector.setIsLongpressEnabled(false);  



(3) 將手勢探測器 與 組件結合


關聯手勢探測器 與 組件 : 在組件的 onTouchEvent(MotionEvent event) 方法中, 調用手勢探測器的 gestureDetector.onTouchEvent(event) 方法即可;

[java] view plain copy
  1. /* 
  2.  * 繼承自 View 的觸摸事件, 當出現觸摸事件的時候, 就會回調該方法 
  3.  * (non-Javadoc) 
  4.  * @see android.view.View#onTouchEvent(android.view.MotionEvent) 
  5.  */  
  6. @Override  
  7. public boolean onTouchEvent(MotionEvent event) {  
  8.     //獲取適配器  
  9.     WheelAdapter adapter = getAdapter();  
  10.     if (adapter == null) {  
  11.         return true;  
  12.     }  
  13.   
  14.     /* 
  15.      * gestureDetector.onTouchEvent(event) : 分析給定的動作, 如果可用, 調用 手勢檢測器的 onTouchEvent 方法 
  16.      * -- 參數解析 : ev , 觸摸事件 
  17.      * -- 返回值 : 如果手勢監聽器成功執行了該方法, 返回true, 如果執行出現意外 返回 false; 
  18.      */  
  19.     if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {  
  20.         justify();  
  21.     }  
  22.     return true;  
  23. }  




2. Scroller 簡介



(1) Scroller 簡介 


Scroller 通用作用 : Scroller 組件並不是一個佈局組件, 該組件是運行在後臺的, 通過一些方法設定 Scroller 對象 的操作 或者 動畫, 然後讓 Scroller 運行在後臺中 用於模擬滾動操作, 在適當的時機 獲取該對象的座標信息, 這些信息是在後臺運算出來的;


Scroller 在本 View 中作用 : Android 的這個自定義的 WheelView 組件, 可以平滑的滾動, 當我們做一個加速滑動時, 會根據速度計算出滑動的距離, 這些數據都是在 Scroller 中計算出來的;



(2) 設定 Scroller 對象的動作參數 


終止滾動 : 

-- 終止滾動 跳轉到目標位置 : 終止平緩的動畫, 直接跳轉到最終的 x y 軸的座標位置;

[java] view plain copy
  1. public void abortAnimation()  

-- 終止滾動 停止在當前位置 : 強行結束 Scroll 的滾動;

[java] view plain copy
  1. public final void forceFinished(boolean finished)  


設置滾動參數 : 

-- 設置最終 x 軸座標 : 

[java] view plain copy
  1. public void setFinalX(int newX)  

-- 設置最終 y 軸座標 : 

[java] view plain copy
  1. public void setFinalY(int newY)  

-- 設置滾動摩擦力 : 

[java] view plain copy
  1. public final void setFriction(float friction)  


設置動作 

-- 開始滾動 : 傳入參數 開始 x 位置, 開始 y 位置, x 軸滾動距離, y 軸滾動距離;

[java] view plain copy
  1. public void startScroll(int startX, int startY, int dx, int dy)  
-- 開始滾動 設定時間 : 最後一個參數是時間, 單位是 ms;

[java] view plain copy
  1. public void startScroll(int startX, int startY, int dx, int dy, int duration)  
-- 急衝滾動 : 根據一個 急衝 手勢進行滾動, 傳入參數 : x軸開始位置, y軸開始位置, x 軸速度, y 軸速度, x 軸最小速度, x 軸最大速度, y 軸最小速度, y 軸最大速度;

[java] view plain copy
  1. public void fling(int startX, int startY, int velocityX, int velocityY,  
  2.             int minX, int maxX, int minY, int maxY)  


延長滾動時間 : 延長滾動的時間, 讓滾動滾的更遠一些;

[java] view plain copy
  1. public void extendDuration(int extend)  



(3) 獲取 Scroll 後臺運行參數 



獲取當前數據 : 

-- 獲取當前 x 軸座標 : 

[java] view plain copy
  1. public final int getCurrX()  

-- 獲取當前 y 軸座標 : 

[java] view plain copy
  1. public final int getCurrY()  

-- 獲取當前速度 : 

[java] view plain copy
  1. public float getCurrVelocity()  


獲取開始結束時的數據  : 

-- 獲取開始 x 軸座標 : 

[java] view plain copy
  1. public final int getStartX()  

-- 獲取開始 y 軸座標 : 

[java] view plain copy
  1. public final int getStartY()  

-- 獲取最終 x 軸座標 : 該參數只在急衝滾動時有效;

[java] view plain copy
  1. public final int getFinalX()  

-- 獲取最終 y 軸座標 : 該參數只在急衝滾動時有效;

[java] view plain copy
  1. public final int getFinalY()  


查看是否滾動完畢 : 

[java] view plain copy
  1. public final boolean isFinished()  


獲取從開始滾動到現在的時間 : 

[java] view plain copy
  1. public int timePassed()  


獲取新位置 : 調用該方法可以獲取新位置, 如果返回 true 說明動畫還沒執行完畢;

[java] view plain copy
  1. public boolean computeScrollOffset()  



(4) Scroll 在 WheelView 中的運用


Scroller 創建 

[java] view plain copy
  1. //使用默認的 時間 和 插入器 創建一個滾動器  
  2. scroller = new Scroller(context);  


手勢監聽器 SimpleOnGestureListener 對象中的 onDown() 方法 : 如果滾動還在執行, 那麼強行停止 Scroller 滾動;

[java] view plain copy
  1. //按下操作  
  2.    public boolean onDown(MotionEvent e) {  
  3.     //如果滾動在執行  
  4.        if (isScrollingPerformed) {  
  5.         //滾動強制停止, 按下的時候不能繼續滾動  
  6.            scroller.forceFinished(true);  
  7.            //清理信息  
  8.            clearMessages();  
  9.            return true;  
  10.        }  
  11.        return false;  
  12.    }  


當手勢監聽器 SimpleOnGestureListener 對象中有急衝動作時 onFling() 方法中 : 手勢監聽器監聽到了 急衝動作, 那麼 Scroller 也進行對應操作;

[java] view plain copy
  1. public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {  
  2.     //計算上一次的 y 軸位置, 當前的條目高度 加上 剩餘的 不夠一行高度的那部分  
  3.     lastScrollY = currentItem * getItemHeight() + scrollingOffset;  
  4.     //如果可以循環最大值是無限大, 不能循環就是條目數的高度值  
  5.     int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();  
  6.     int minY = isCyclic ? -maxY : 0;  
  7.     /* 
  8.      * Scroll 開始根據一個急衝手勢滾動, 滾動的距離與初速度有關 
  9.      * 參數介紹 :  
  10.      * -- int startX : 開始時的 X軸位置 
  11.      * -- int startY : 開始時的 y軸位置 
  12.      * -- int velocityX : 急衝手勢的 x 軸的初速度, 單位 px/s 
  13.      * -- int velocityY : 急衝手勢的 y 軸的初速度, 單位 px/s 
  14.      * -- int minX : x 軸滾動的最小值 
  15.      * -- int maxX : x 軸滾動的最大值 
  16.      * -- int minY : y 軸滾動的最小值 
  17.      * -- int maxY : y 軸滾動的最大值 
  18.      */  
  19.     scroller.fling(0, lastScrollY, 0, (int) -velocityY / 200, minY, maxY);  
  20.     setNextMessage(MESSAGE_SCROLL);  
  21.     return true;  
  22. }  


動畫控制 Handler 中 : 

-- 滾動 : 獲取當前 Scroller 的 y 軸位置, 與上一次的 y 軸位置對比, 如果 間距 delta 不爲0, 就滾動;  

-- 查看是否停止 : 如果現在距離 到 最終距離 小於最小滾動距離, 強制停止;

-- 執行 msg.what 指令 : 如果需要停止, 強制停止, 否則調整座標;

[java] view plain copy
  1. /** 
  2.  * 動畫控制器 
  3.  *  animation handler 
  4.  *   
  5.  *  可能會造成內存泄露 : 添加註解 HandlerLeak 
  6.  *  Handler 類應該應該爲static類型,否則有可能造成泄露。 
  7.  *  在程序消息隊列中排隊的消息保持了對目標Handler類的應用。 
  8.  *  如果Handler是個內部類,那 麼它也會保持它所在的外部類的引用。 
  9.  *  爲了避免泄露這個外部類,應該將Handler聲明爲static嵌套類,並且使用對外部類的弱應用。 
  10.  */  
  11. @SuppressLint("HandlerLeak")  
  12. vate Handler animationHandler = new Handler() {  
  13.     public void handleMessage(Message msg) {  
  14.         //回調該方法獲取當前位置, 如果返回true, 說明動畫還沒有執行完畢  
  15.         scroller.computeScrollOffset();  
  16.         //獲取當前 y 位置  
  17.         int currY = scroller.getCurrY();  
  18.         //獲取已經滾動了的位置, 使用上一次位置 減去 當前位置  
  19.         int delta = lastScrollY - currY;  
  20.         lastScrollY = currY;  
  21.         if (delta != 0) {  
  22.             //改變值不爲 0 , 繼續滾動  
  23.             doScroll(delta);  
  24.         }  
  25.   
  26.         /* 
  27.          * 如果滾動到了指定的位置, 滾動還沒有停止 
  28.          * 這時需要強制停止 
  29.          */  
  30.         if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) {  
  31.             currY = scroller.getFinalY();  
  32.             scroller.forceFinished(true);  
  33.         }  
  34.           
  35.         /* 
  36.          * 如果滾動沒有停止 
  37.          * 再向 Handler 發送一個停止 
  38.          */  
  39.         if (!scroller.isFinished()) {  
  40.             animationHandler.sendEmptyMessage(msg.what);  
  41.         } else if (msg.what == MESSAGE_SCROLL) {  
  42.             justify();  
  43.         } else {  
  44.             finishScrolling();  
  45.         }  
  46.     }  
  47. };  




3. StaticLayout 佈局容器



(1) StaticLayout 解析


StaticLayout 解析 : 該組件用於顯示文本, 一旦該文本被顯示後, 就不能再編輯, 如果想要修改文本, 使用 DynamicLayout 佈局即可; 

-- 使用場景 : 一般情況下不會使用該組件, 當想要自定義組件 或者 想要使用 Canvas 繪製文本時 才使用該佈局;



常用方法解析 : 

-- 獲取底部 Padding : 獲取底部 到最後一行文字的 間隔, 單位是 px;

[java] view plain copy
  1. public int getBottomPadding()  

-- 獲取頂部 Padding : 

[java] view plain copy
  1. public int getTopPadding()  

-- 獲取省略個數 : 獲取某一行需要省略的字符個數;

[java] view plain copy
  1. public int getEllipsisCount(int line)  
-- 獲取省略開始位置 : 獲取某一行要省略的字符串的第一個位置索引;

[java] view plain copy
  1. public int getEllipsisStart(int line)  
-- 獲取省略的寬度 : 獲取某一行省略字符串的寬度, 單位 px;

[java] view plain copy
  1. public int getEllipsisStart(int line)  
-- 獲取是否處理特殊符號 : 

[java] view plain copy
  1. public boolean getLineContainsTab(int line)  
-- 獲取文字的行數 : 

[java] view plain copy
  1. public int getLineCount()  
-- 獲取頂部位置 : 獲取某一行頂部的位置;

[java] view plain copy
  1. public int getLineTop(int line)  
-- 獲取某一行底部位置 : 

[java] view plain copy
  1. public int getLineDescent(int line)  
-- 獲取行的方向 : 字符串從左至右 還是從右至左;

[java] view plain copy
  1. public final Directions getLineDirections(int line)  
-- 獲取某行第一個字符索引 : 獲取的是 某一行 第一個字符 在整個字符串的索引;

[java] view plain copy
  1. public int getLineStart(int line)  
-- 獲取該行段落方向 : 獲取該行文字方向, 左至右 或者 右至左;

[java] view plain copy
  1. public int getParagraphDirection(int line)  
-- 獲取某個垂直位置顯示的行數 : 

[java] view plain copy
  1. public int getLineForVertical(int vertical)  



(2) 佈局顯示


佈局創建 : 

-- 三種佈局 : WheelView 中涉及到了三種 StaticLayout 佈局, 普通條目佈局 itemLayout, 選中條目佈局 valueLayout, 標籤佈局 labelLayout;

-- 創建時機 : 在 View 組件 每次 onMeasure() 和 onDraw() 方法中都要重新創建對應佈局;

-- 創建佈局源碼 : 

[java] view plain copy
  1. /** 
  2.  * 創建佈局 
  3.  *  
  4.  * @param widthItems 
  5.  *            佈局條目寬度 
  6.  * @param widthLabel 
  7.  *            label 寬度 
  8.  */  
  9. private void createLayouts(int widthItems, int widthLabel) {  
  10.     /* 
  11.      * 創建普通條目佈局 
  12.      * 如果 普通條目佈局 爲 null 或者 普通條目佈局的寬度 大於 傳入的寬度, 這時需要重新創建佈局 
  13.      * 如果 普通條目佈局存在, 並且其寬度小於傳入的寬度, 此時需要將 
  14.      */  
  15.     if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {  
  16.           
  17.         /* 
  18.          * android.text.StaticLayout.StaticLayout( 
  19.          * CharSequence source, TextPaint paint,  
  20.          * int width, Alignment align,  
  21.          * float spacingmult, float spacingadd, boolean includepad) 
  22.          * 傳入參數介紹 :  
  23.          * CharSequence source : 需要分行顯示的字符串 
  24.          * TextPaint paint : 繪製字符串的畫筆 
  25.          * int width : 條目的寬度 
  26.          * Alignment align : Layout 的對齊方式, ALIGN_CENTER 居中對齊, ALIGN_NORMAL 左對齊, Alignment.ALIGN_OPPOSITE 右對齊 
  27.          * float spacingmult : 行間距, 1.5f 代表 1.5 倍字體高度 
  28.          * float spacingadd : 基礎行距上增加多少 , 真實行間距 等於 spacingmult 和 spacingadd 的和 
  29.          * boolean includepad :  
  30.          */  
  31.         itemsLayout = new StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,  
  32.                 widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,  
  33.                 ADDITIONAL_ITEM_HEIGHT, false);  
  34.     } else {  
  35.         //調用 Layout 內置的方法 increaseWidthTo 將寬度提升到指定的寬度  
  36.         itemsLayout.increaseWidthTo(widthItems);  
  37.     }  
  38.   
  39.     /* 
  40.      * 創建選中條目 
  41.      */  
  42.     if (!isScrollingPerformed && (valueLayout == null || valueLayout.getWidth() > widthItems)) {  
  43.         String text = getAdapter() != null ? getAdapter().getItem(currentItem) : null;  
  44.         valueLayout = new StaticLayout(text != null ? text : "", valuePaint, widthItems,  
  45.                 widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,  
  46.                 ADDITIONAL_ITEM_HEIGHT, false);  
  47.     } else if (isScrollingPerformed) {  
  48.         valueLayout = null;  
  49.     } else {  
  50.         valueLayout.increaseWidthTo(widthItems);  
  51.     }  
  52.   
  53.     /* 
  54.      * 創建標籤條目 
  55.      */  
  56.     if (widthLabel > 0) {  
  57.         if (labelLayout == null || labelLayout.getWidth() > widthLabel) {  
  58.             labelLayout = new StaticLayout(label, valuePaint, widthLabel, Layout.Alignment.ALIGN_NORMAL, 1,  
  59.                     ADDITIONAL_ITEM_HEIGHT, false);  
  60.         } else {  
  61.             labelLayout.increaseWidthTo(widthLabel);  
  62.         }  
  63.     }  
  64. }  



4. 監聽器管理



監聽器集合維護 

-- 定義監聽器集合 : 在 View 組件中 定義一個 List 集合, 集合中存放 監聽器元素;

[java] view plain copy
  1. /** 條目改變監聽器集合  封裝了條目改變方法, 當條目改變時回調 */  
  2. private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>();  
  3. /** 條目滾動監聽器集合, 該監聽器封裝了 開始滾動方法, 結束滾動方法 */  
  4. private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>();  


-- 提供對監聽器集合的添加刪除接口 : 提供 對集合 進行 添加 和 刪除的接口;

[java] view plain copy
  1. /** 
  2.  * 添加 WheelView 選擇的元素改變監聽器 
  3.  *  
  4.  * @param listener 
  5.  *            the listener 
  6.  */  
  7. public void addChangingListener(OnWheelChangedListener listener) {  
  8.     changingListeners.add(listener);  
  9. }  
  10.   
  11. /** 
  12.  * 移除 WheelView 元素改變監聽器 
  13.  *  
  14.  * @param listener 
  15.  *            the listener 
  16.  */  
  17. public void removeChangingListener(OnWheelChangedListener listener) {  
  18.     changingListeners.remove(listener);  
  19. }  

-- 調用監聽器接口 : 

[java] view plain copy
  1. /** 
  2.  * 回調元素改變監聽器集合的元素改變監聽器元素的元素改變方法 
  3.  *  
  4.  * @param oldValue 
  5.  *            舊的 WheelView選中的值 
  6.  * @param newValue 
  7.  *            新的 WheelView選中的值 
  8.  */  
  9. protected void notifyChangingListeners(int oldValue, int newValue) {  
  10.     for (OnWheelChangedListener listener : changingListeners) {  
  11.         listener.onChanged(this, oldValue, newValue);  
  12.     }  
  13. }  



5. 自定義 View 對象的寬高 



(1) onMeasure 方法 MeasureSpec 模式解析


常規處理方法 : 組件的寬高有三種情況, widthMeasureSpec 有三種模式 最大模式, 精準模式, 未定義模式;

-- 最大模式 : 在 組件的寬或高 warp_content 屬性時, 會使用最大模式;

-- 精準模式 : 當給組件寬 或者高 定義一個值 或者 使用 match_parent 時, 會使用精準模式;


處理寬高的常規代碼 

[java] view plain copy
  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  4.       
  5.     //獲取寬度 和 高度的模式 和 大小  
  6.        int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
  7.        int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
  8.        int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
  9.        int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
  10.          
  11.        Log.i(TAG, "寬度 : widthMode : " + getMode(widthMode) + " , widthSize : " + widthSize + "\n"   
  12.             + "高度 : heightMode : " + getMode(heightMode) + " , heightSize : " + heightSize);  
  13.          
  14.        int width = 0;  
  15.        int height = 0;  
  16.        /* 
  17.         * 精準模式 
  18.         *       精準模式下 高度就是精確的高度 
  19.         */  
  20.        if (heightMode == MeasureSpec.EXACTLY) {  
  21.            height = heightSize;  
  22.        //未定義模式 和 最大模式  
  23.        } else {  
  24.         //未定義模式下 獲取佈局需要的高度  
  25.            height = 100;  
  26.   
  27.            //最大模式下 獲取 佈局高度 和 佈局所需高度的最小值  
  28.            if (heightMode == MeasureSpec.AT_MOST) {  
  29.                height = Math.min(height, heightSize);  
  30.            }  
  31.        }  
  32.          
  33.        if (widthMode == MeasureSpec.EXACTLY) {  
  34.            width = widthSize;  
  35.        } else {  
  36.            width = 100;  
  37.            if (heightMode == MeasureSpec.AT_MOST) {  
  38.                width = Math.min(width, widthSize);  
  39.            }  
  40.        }  
  41.   
  42.        Log.i(TAG, "最終結果 : 寬度 : " + width + " , 高度 : " + height);  
  43.          
  44.        setMeasuredDimension(width, height);  
  45.       
  46. }  
  47.   
  48.   
  49. public String getMode(int mode) {  
  50.     String modeName = "";  
  51.     if(mode == MeasureSpec.EXACTLY){  
  52.         modeName = "精準模式";  
  53.     }else if(mode == MeasureSpec.AT_MOST){  
  54.         modeName = "最大模式";  
  55.     }else if(mode == MeasureSpec.UNSPECIFIED){  
  56.         modeName = "未定義模式";  
  57.     }  
  58.           
  59.     return modeName;  
  60. }  



(2) 測試上述代碼


使用下面的自定義組件測試 : 

[java] view plain copy
  1. package cn.org.octopus.wheelview;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Canvas;  
  5. import android.graphics.Color;  
  6. import android.util.AttributeSet;  
  7. import android.util.Log;  
  8. import android.view.View;  
  9.   
  10. public class MyView extends View {  
  11.   
  12.     public static final String TAG = "octopus.my.view";  
  13.       
  14.     public MyView(Context context, AttributeSet attrs) {  
  15.         super(context, attrs);  
  16.     }  
  17.   
  18.     public MyView(Context context) {  
  19.         super(context);  
  20.     }  
  21.   
  22.     public MyView(Context context, AttributeSet attrs, int defStyle) {  
  23.         super(context, attrs, defStyle);  
  24.     }  
  25.       
  26.       
  27.       
  28.       
  29.     @Override  
  30.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  31.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  32.           
  33.         //獲取寬度 和 高度的模式 和 大小  
  34.         int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
  35.         int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
  36.         int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
  37.         int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
  38.           
  39.         Log.i(TAG, "寬度 : widthMode : " + getMode(widthMode) + " , widthSize : " + widthSize + "\n"   
  40.                 + "高度 : heightMode : " + getMode(heightMode) + " , heightSize : " + heightSize);  
  41.           
  42.         int width = 0;  
  43.         int height = 0;  
  44.         /* 
  45.          * 精準模式 
  46.          *      精準模式下 高度就是精確的高度 
  47.          */  
  48.         if (heightMode == MeasureSpec.EXACTLY) {  
  49.             height = heightSize;  
  50.         //未定義模式 和 最大模式  
  51.         } else {  
  52.             //未定義模式下 獲取佈局需要的高度  
  53.             height = 100;  
  54.   
  55.             //最大模式下 獲取 佈局高度 和 佈局所需高度的最小值  
  56.             if (heightMode == MeasureSpec.AT_MOST) {  
  57.                 height = Math.min(height, heightSize);  
  58.             }  
  59.         }  
  60.           
  61.         if (widthMode == MeasureSpec.EXACTLY) {  
  62.             width = widthSize;  
  63.         } else {  
  64.             width = 100;  
  65.             if (heightMode == MeasureSpec.AT_MOST) {  
  66.                 width = Math.min(width, widthSize);  
  67.             }  
  68.         }  
  69.   
  70.         Log.i(TAG, "最終結果 : 寬度 : " + width + " , 高度 : " + height);  
  71.           
  72.         setMeasuredDimension(width, height);  
  73.           
  74.     }  
  75.       
  76.       
  77.     public String getMode(int mode) {  
  78.         String modeName = "";  
  79.         if(mode == MeasureSpec.EXACTLY){  
  80.             modeName = "精準模式";  
  81.         }else if(mode == MeasureSpec.AT_MOST){  
  82.             modeName = "最大模式";  
  83.         }else if(mode == MeasureSpec.UNSPECIFIED){  
  84.             modeName = "未定義模式";  
  85.         }  
  86.               
  87.         return modeName;  
  88.     }  
  89.       
  90.     @Override  
  91.     protected void onDraw(Canvas canvas) {  
  92.         super.onDraw(canvas);  
  93.           
  94.         canvas.drawColor(Color.BLUE);  
  95.     }  
  96.   
  97. }  



給定具體值情況 : 

-- 組件信息 : 

[html] view plain copy
  1. <cn.org.octopus.wheelview.MyView  
  2.     android:layout_width="300dip"  
  3.     android:layout_height="300dip"/>  
-- 日誌信息 : 

[plain] view plain copy
  1. 11-30 01:40:24.304: I/octopus.my.view(2609): 寬度 : widthMode : 精準模式 , widthSize : 450  
  2. 11-30 01:40:24.304: I/octopus.my.view(2609): 高度 : heightMode : 最大模式 , heightSize : 850  
  3. 11-30 01:40:24.304: I/octopus.my.view(2609): 最終結果 : 寬度 : 450 , 高度 : 100  
  4. 11-30 01:40:24.304: I/octopus.my.view(2609): 寬度 : widthMode : 精準模式 , widthSize : 450  
  5. 11-30 01:40:24.304: I/octopus.my.view(2609): 高度 : heightMode : 精準模式 , heightSize : 450  
  6. 11-30 01:40:24.304: I/octopus.my.view(2609): 最終結果 : 寬度 : 450 , 高度 : 450  
  7. 11-30 01:40:24.335: I/octopus.my.view(2609): 寬度 : widthMode : 精準模式 , widthSize : 450  
  8. 11-30 01:40:24.335: I/octopus.my.view(2609): 高度 : heightMode : 最大模式 , heightSize : 850  
  9. 11-30 01:40:24.335: I/octopus.my.view(2609): 最終結果 : 寬度 : 450 , 高度 : 100  
  10. 11-30 01:40:24.335: I/octopus.my.view(2609): 寬度 : widthMode : 精準模式 , widthSize : 450  
  11. 11-30 01:40:24.335: I/octopus.my.view(2609): 高度 : heightMode : 精準模式 , heightSize : 450  
  12. 11-30 01:40:24.335: I/octopus.my.view(2609): 最終結果 : 寬度 : 450 , 高度 : 450  



warp_content 情況 : 

-- 組件信息 : 

[html] view plain copy
  1. <cn.org.octopus.wheelview.MyView  
  2.     android:layout_width="wrap_content"  
  3.     android:layout_height="wrap_content"/>  
-- 日誌信息 : 

[html] view plain copy
  1. 11-30 01:37:47.351: I/octopus.my.view(1803): 寬度 : widthMode : 最大模式 , widthSize : 492  
  2. 11-30 01:37:47.351: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 850  
  3. 11-30 01:37:47.351: I/octopus.my.view(1803): 最終結果 : 寬度 : 100 , 高度 : 100  
  4. 11-30 01:37:47.351: I/octopus.my.view(1803): 寬度 : widthMode : 精準模式 , widthSize : 100  
  5. 11-30 01:37:47.351: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 802  
  6. 11-30 01:37:47.351: I/octopus.my.view(1803): 最終結果 : 寬度 : 100 , 高度 : 100  
  7. 11-30 01:37:47.390: I/octopus.my.view(1803): 寬度 : widthMode : 最大模式 , widthSize : 492  
  8. 11-30 01:37:47.390: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 850  
  9. 11-30 01:37:47.390: I/octopus.my.view(1803): 最終結果 : 寬度 : 100 , 高度 : 100  
  10. 11-30 01:37:47.390: I/octopus.my.view(1803): 寬度 : widthMode : 精準模式 , widthSize : 100  
  11. 11-30 01:37:47.390: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 802  
  12. 11-30 01:37:47.390: I/octopus.my.view(1803): 最終結果 : 寬度 : 100 , 高度 : 100  



match_parent 情況 : 

-- 組件信息 : 

[html] view plain copy
  1. <cn.org.octopus.wheelview.MyView  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"/>  

-- 日誌信息 : 

[html] view plain copy
  1. 11-30 01:39:08.296: I/octopus.my.view(2249): 寬度 : widthMode : 精準模式 , widthSize : 492  
  2. 11-30 01:39:08.296: I/octopus.my.view(2249): 高度 : heightMode : 精準模式 , heightSize : 850  
  3. 11-30 01:39:08.296: I/octopus.my.view(2249): 最終結果 : 寬度 : 492 , 高度 : 850  
  4. 11-30 01:39:08.296: I/octopus.my.view(2249): 寬度 : widthMode : 精準模式 , widthSize : 492  
  5. 11-30 01:39:08.296: I/octopus.my.view(2249): 高度 : heightMode : 精準模式 , heightSize : 802  
  6. 11-30 01:39:08.296: I/octopus.my.view(2249): 最終結果 : 寬度 : 492 , 高度 : 802  
  7. 11-30 01:39:08.328: I/octopus.my.view(2249): 寬度 : widthMode : 精準模式 , widthSize : 492  
  8. 11-30 01:39:08.328: I/octopus.my.view(2249): 高度 : heightMode : 精準模式 , heightSize : 850  
  9. 11-30 01:39:08.328: I/octopus.my.view(2249): 最終結果 : 寬度 : 492 , 高度 : 850  
  10. 11-30 01:39:08.328: I/octopus.my.view(2249): 寬度 : widthMode : 精準模式 , widthSize : 492  
  11. 11-30 01:39:08.328: I/octopus.my.view(2249): 高度 : heightMode : 精準模式 , heightSize : 802  
  12. 11-30 01:39:08.328: I/octopus.my.view(2249): 最終結果 : 寬度 : 492 , 高度 : 802  


博客地址 http://blog.csdn.net/shulianghan/article/details/41520569#t17


代碼下載 : 

-- GitHub : https://github.com/han1202012/WheelViewDemo.git 

-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;




四. 詳細代碼 



1. WheelAdapter


[java] view plain copy
  1. package cn.org.octopus.wheelview.widget;  
  2.   
  3. /** 
  4.  * WheelView 適配器接口 
  5.  * @author han_shuliang([email protected]) 
  6.  * 
  7.  */  
  8. public interface WheelAdapter {  
  9.     /** 
  10.      * 獲取條目的個數 
  11.      *  
  12.      * @return  
  13.      *      WheelView 的條目個數 
  14.      */  
  15.     public int getItemsCount();  
  16.   
  17.     /** 
  18.      * 根據索引位置獲取 WheelView 的條目 
  19.      *  
  20.      * @param index 
  21.      *            條目的索引 
  22.      * @return  
  23.      *      WheelView 上顯示的條目的值 
  24.      */  
  25.     public String getItem(int index);  
  26.   
  27.     /** 
  28.      * 獲取條目的最大長度. 用來定義 WheelView 的寬度. 如果返回 -1, 就會使用默認寬度 
  29.      *  
  30.      * @return  
  31.      *      條目的最大寬度 或者 -1 
  32.      */  
  33.     public int getMaximumLength();  
  34. }  



2. ArrayWheelAdapter



[java] view plain copy
  1. package cn.org.octopus.wheelview.widget;  
  2.   
  3. /** 
  4.  * WheelView 的適配器類 
  5.  *  
  6.  * @param <T> 
  7.  *            元素類型 
  8.  */  
  9. public class ArrayWheelAdapter<T> implements WheelAdapter {  
  10.   
  11.     /** 適配器的 元素集合(數據源) 默認長度爲 -1 */  
  12.     public static final int DEFAULT_LENGTH = -1;  
  13.   
  14.     /** 適配器的數據源 */  
  15.     private T items[];  
  16.     /** WheelView 的寬度 */  
  17.     private int length;  
  18.   
  19.     /** 
  20.      * 構造方法 
  21.      *  
  22.      * @param items 
  23.      *            適配器數據源 集合 T 類型的數組 
  24.      * @param length 
  25.      *            適配器數據源 集合 T 數組長度 
  26.      */  
  27.     public ArrayWheelAdapter(T items[], int length) {  
  28.         this.items = items;  
  29.         this.length = length;  
  30.     }  
  31.   
  32.     /** 
  33.      * 構造方法 
  34.      *  
  35.      * @param items 
  36.      *            適配器數據源集合 T 類型數組 
  37.      */  
  38.     public ArrayWheelAdapter(T items[]) {  
  39.         this(items, DEFAULT_LENGTH);  
  40.     }  
  41.   
  42.       
  43.     @Override  
  44.     public String getItem(int index) {  
  45.         //如果這個索引值合法, 就返回 item 數組對應的元素的字符串形式  
  46.         if (index >= 0 && index < items.length) {  
  47.             return items[index].toString();  
  48.         }  
  49.         return null;  
  50.     }  
  51.   
  52.     @Override  
  53.     public int getItemsCount() {  
  54.         //返回 item 數組的長度  
  55.         return items.length;  
  56.     }  
  57.   
  58.     @Override  
  59.     public int getMaximumLength() {  
  60.         //返回 item 元素的寬度  
  61.         return length;  
  62.     }  
  63.   
  64. }  


3. NumericWheelAdapter


[java] view plain copy
  1. package cn.org.octopus.wheelview.widget;  
  2.   
  3. /** 
  4.  * 顯示數字的 WheelAdapter 
  5.  */  
  6. public class NumericWheelAdapter implements WheelAdapter {  
  7.   
  8.     /** 默認最小值 */  
  9.     public static final int DEFAULT_MAX_VALUE = 9;  
  10.   
  11.     /** 默認最大值 */  
  12.     private static final int DEFAULT_MIN_VALUE = 0;  
  13.   
  14.     /** 設置的最小值 */  
  15.     private int minValue;  
  16.     /** 設置的最大值 */  
  17.     private int maxValue;  
  18.   
  19.     /** 格式化字符串, 用於格式化 貨幣, 科學計數, 十六進制 等格式 */  
  20.     private String format;  
  21.   
  22.     /** 
  23.      * 默認的構造方法, 使用默認的最大最小值 
  24.      */  
  25.     public NumericWheelAdapter() {  
  26.         this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE);  
  27.     }  
  28.   
  29.     /** 
  30.      * 構造方法 
  31.      *  
  32.      * @param minValue 
  33.      *            最小值 
  34.      * @param maxValue 
  35.      *            最大值 
  36.      */  
  37.     public NumericWheelAdapter(int minValue, int maxValue) {  
  38.         this(minValue, maxValue, null);  
  39.     }  
  40.   
  41.     /** 
  42.      * 構造方法 
  43.      *  
  44.      * @param minValue 
  45.      *            最小值 
  46.      * @param maxValue 
  47.      *            最大值 
  48.      * @param format 
  49.      *            格式化字符串 
  50.      */  
  51.     public NumericWheelAdapter(int minValue, int maxValue, String format) {  
  52.         this.minValue = minValue;  
  53.         this.maxValue = maxValue;  
  54.         this.format = format;  
  55.     }  
  56.   
  57.     @Override  
  58.     public String getItem(int index) {  
  59.         String result = "";  
  60.         if (index >= 0 && index < getItemsCount()) {  
  61.             int value = minValue + index;  
  62.             //如果 format 不爲 null, 那麼格式化字符串, 如果爲 null, 直接返回數字  
  63.             if(format != null){  
  64.                 result = String.format(format, value);  
  65.             }else{  
  66.                 result = Integer.toString(value);  
  67.             }  
  68.             return result;  
  69.         }  
  70.         return null;  
  71.     }  
  72.   
  73.     @Override  
  74.     public int getItemsCount() {  
  75.         //返回數字總個數  
  76.         return maxValue - minValue + 1;  
  77.     }  
  78.   
  79.     @Override  
  80.     public int getMaximumLength() {  
  81.         //獲取 最大值 和 最小值 中的 較大的數字  
  82.         int max = Math.max(Math.abs(maxValue), Math.abs(minValue));  
  83.         //獲取這個數字 的 字符串形式的 字符串長度  
  84.         int maxLen = Integer.toString(max).length();  
  85.         if (minValue < 0) {  
  86.             maxLen++;  
  87.         }  
  88.         return maxLen;  
  89.     }  
  90. }  



4. OnWheelChangedListener


[java] view plain copy
  1. package cn.org.octopus.wheelview.widget;  
  2.   
  3. /** 
  4.  * 條目改變監聽器 
  5.  */  
  6. public interface OnWheelChangedListener {  
  7.     /** 
  8.      * 當前條目改變時回調該方法 
  9.      *  
  10.      * @param wheel 
  11.      *            條目改變的 WheelView 對象 
  12.      * @param oldValue 
  13.      *            WheelView 舊的條目值 
  14.      * @param newValue 
  15.      *            WheelView 新的條目值 
  16.      */  
  17.     void onChanged(WheelView wheel, int oldValue, int newValue);  
  18. }  




5. OnWheelScrollListener



[java] view plain copy
  1. package cn.org.octopus.wheelview.widget;  
  2.   
  3. /** 
  4.  * WheelView 滾動監聽器 
  5.  */  
  6. public interface OnWheelScrollListener {  
  7.     /** 
  8.      * 在 WheelView 滾動開始的時候回調該接口 
  9.      *  
  10.      * @param wheel 
  11.      *            開始滾動的 WheelView 對象 
  12.      */  
  13.     void onScrollingStarted(WheelView wheel);  
  14.   
  15.     /** 
  16.      * 在 WheelView 滾動結束的時候回調該接口 
  17.      *  
  18.      * @param wheel 
  19.      *            結束滾動的 WheelView 對象 
  20.      */  
  21.     void onScrollingFinished(WheelView wheel);  
  22. }  



6. WheelView


[java] view plain copy
  1. package cn.org.octopus.wheelview.widget;  
  2.   
  3. import java.util.LinkedList;  
  4. import java.util.List;  
  5.   
  6. import cn.org.octopus.wheelview.R;  
  7. import android.annotation.SuppressLint;  
  8. import android.content.Context;  
  9. import android.graphics.Canvas;  
  10. import android.graphics.Paint;  
  11. import android.graphics.Rect;  
  12. import android.graphics.drawable.Drawable;  
  13. import android.graphics.drawable.GradientDrawable;  
  14. import android.graphics.drawable.GradientDrawable.Orientation;  
  15. import android.os.Handler;  
  16. import android.os.Message;  
  17. import android.text.Layout;  
  18. import android.text.StaticLayout;  
  19. import android.text.TextPaint;  
  20. import android.util.AttributeSet;  
  21. import android.view.GestureDetector;  
  22. import android.view.GestureDetector.SimpleOnGestureListener;  
  23. import android.view.MotionEvent;  
  24. import android.view.View;  
  25. import android.view.animation.Interpolator;  
  26. import android.widget.Scroller;  
  27.   
  28. /** 
  29.  * WheelView 主對象 
  30.  */  
  31. public class WheelView extends View {  
  32.     /** 滾動花費時間 Scrolling duration */  
  33.     private static final int SCROLLING_DURATION = 400;  
  34.   
  35.     /** 最小的滾動值, 每次最少滾動一個單位 */  
  36.     private static final int MIN_DELTA_FOR_SCROLLING = 1;  
  37.   
  38.     /** 當前條目中的文字顏色 */  
  39.     private static final int VALUE_TEXT_COLOR = 0xF0FF6347;  
  40.   
  41.     /** 非當前條目的文字顏色 */  
  42.     private static final int ITEMS_TEXT_COLOR = 0xFF000000;  
  43.   
  44.     /** 頂部和底部的陰影顏色 */  
  45.     //private static final int[] SHADOWS_COLORS = new int[] { 0xFF5436EE, 0x0012CEAE, 0x0012CEAE };  
  46.     private static final int[] SHADOWS_COLORS = new int[] { 0xFF1111110x00AAAAAA0x00AAAAAA };  
  47.   
  48.     /** 額外的條目高度 Additional items height (is added to standard text item height) */  
  49.     private static final int ADDITIONAL_ITEM_HEIGHT = 15;  
  50.   
  51.     /** 字體大小 */  
  52.     private static final int TEXT_SIZE = 24;  
  53.   
  54.     /** 頂部 和 底部 條目的隱藏大小,  
  55.      * 如果是正數 會隱藏一部份,  
  56.      * 0 頂部 和 底部的字正好緊貼 邊緣,  
  57.      * 負數時 頂部和底部 與 字有一定間距 */  
  58.     private static final int ITEM_OFFSET = TEXT_SIZE / 5;  
  59.   
  60.     /** Additional width for items layout */  
  61.     private static final int ADDITIONAL_ITEMS_SPACE = 10;  
  62.   
  63.     /** Label offset */  
  64.     private static final int LABEL_OFFSET = 8;  
  65.   
  66.     /** Left and right padding value */  
  67.     private static final int PADDING = 10;  
  68.   
  69.     /** 默認的可顯示的條目數 */  
  70.     private static final int DEF_VISIBLE_ITEMS = 5;  
  71.   
  72.     /** WheelView 適配器 */  
  73.     private WheelAdapter adapter = null;  
  74.     /** 當前顯示的條目索引 */  
  75.     private int currentItem = 0;  
  76.   
  77.     /** 條目寬度 */  
  78.     private int itemsWidth = 0;  
  79.     /** 標籤寬度 */  
  80.     private int labelWidth = 0;  
  81.   
  82.     /** 可見的條目數 */  
  83.     private int visibleItems = DEF_VISIBLE_ITEMS;  
  84.   
  85.     /** 條目高度 */  
  86.     private int itemHeight = 0;  
  87.   
  88.     /** 繪製普通條目畫筆 */  
  89.     private TextPaint itemsPaint;  
  90.     /** 繪製選中條目畫筆 */  
  91.     private TextPaint valuePaint;  
  92.   
  93.     /** 普通條目佈局 
  94.      * StaticLayout 佈局用於控制 TextView 組件, 一般情況下不會直接使用該組件,  
  95.      * 除非你自定義一個組件 或者 想要直接調用  Canvas.drawText() 方法 
  96.      *  */  
  97.     private StaticLayout itemsLayout;  
  98.     private StaticLayout labelLayout;  
  99.     /** 選中條目佈局 */  
  100.     private StaticLayout valueLayout;  
  101.   
  102.     /** 標籤 在選中條目的右邊出現 */  
  103.     private String label;  
  104.     /** 選中條目的背景圖片 */  
  105.     private Drawable centerDrawable;  
  106.   
  107.     /** 頂部陰影圖片 */  
  108.     private GradientDrawable topShadow;  
  109.     /** 底部陰影圖片 */  
  110.     private GradientDrawable bottomShadow;  
  111.   
  112.     /** 是否在滾動 */  
  113.     private boolean isScrollingPerformed;  
  114.     /** 滾動的位置 */  
  115.     private int scrollingOffset;  
  116.   
  117.     /** 手勢檢測器 */  
  118.     private GestureDetector gestureDetector;  
  119.     /**  
  120.      * Scroll 類封裝了滾動動作.  
  121.      * 開發者可以使用 Scroll 或者 Scroll 實現類 去收集產生一個滾動動畫所需要的數據, 返回一個急衝滑動的手勢. 
  122.      * 該對象可以追蹤隨着時間推移滾動的偏移量, 但是這些對象不會自動向 View 對象提供這些位置. 
  123.      * 如果想要使滾動動畫看起來比較平滑, 開發者需要在適當的時機  獲取 和 使用新的座標;  
  124.      *  */  
  125.     private Scroller scroller;  
  126.     /** 之前所在的 y 軸位置 */  
  127.     private int lastScrollY;  
  128.   
  129.     /** 是否循環 */  
  130.     boolean isCyclic = false;  
  131.   
  132.     /** 條目改變監聽器集合  封裝了條目改變方法, 當條目改變時回調 */  
  133.     private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>();  
  134.     /** 條目滾動監聽器集合, 該監聽器封裝了 開始滾動方法, 結束滾動方法 */  
  135.     private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>();  
  136.   
  137.     /** 
  138.      * 構造方法 
  139.      */  
  140.     public WheelView(Context context, AttributeSet attrs, int defStyle) {  
  141.         super(context, attrs, defStyle);  
  142.         initData(context);  
  143.     }  
  144.   
  145.     /** 
  146.      * 構造方法 
  147.      */  
  148.     public WheelView(Context context, AttributeSet attrs) {  
  149.         super(context, attrs);  
  150.         initData(context);  
  151.     }  
  152.   
  153.     /** 
  154.      * 構造方法 
  155.      */  
  156.     public WheelView(Context context) {  
  157.         super(context);  
  158.         initData(context);  
  159.     }  
  160.   
  161.     /** 
  162.      * 初始化數據 
  163.      *  
  164.      * @param context 
  165.      *            上下文對象 
  166.      */  
  167.     private void initData(Context context) {  
  168.         //創建一個手勢處理  
  169.         gestureDetector = new GestureDetector(context, gestureListener);  
  170.         /* 
  171.          * 是否允許長按操作,  
  172.          * 如果設置爲 true 用戶按下不鬆開, 會返回一個長按事件,  
  173.          * 如果設置爲 false, 按下不鬆開滑動的話 會收到滾動事件. 
  174.          */  
  175.         gestureDetector.setIsLongpressEnabled(false);  
  176.           
  177.         //使用默認的 時間 和 插入器 創建一個滾動器  
  178.         scroller = new Scroller(context);  
  179.     }  
  180.   
  181.     /** 
  182.      * 獲取該 WheelView 的適配器 
  183.      *  
  184.      * @return  
  185.      *      返回適配器 
  186.      */  
  187.     public WheelAdapter getAdapter() {  
  188.         return adapter;  
  189.     }  
  190.   
  191.     /** 
  192.      * 設置適配器 
  193.      *  
  194.      * @param adapter 
  195.      *            要設置的適配器 
  196.      */  
  197.     public void setAdapter(WheelAdapter adapter) {  
  198.         this.adapter = adapter;  
  199.         invalidateLayouts();  
  200.         invalidate();  
  201.     }  
  202.   
  203.     /** 
  204.      * 設置 Scroll 的插入器 
  205.      *  
  206.      * @param interpolator 
  207.      *            the interpolator 
  208.      */  
  209.     public void setInterpolator(Interpolator interpolator) {  
  210.         //強制停止滾動  
  211.         scroller.forceFinished(true);  
  212.         //創建一個 Scroll 對象  
  213.         scroller = new Scroller(getContext(), interpolator);  
  214.     }  
  215.   
  216.     /** 
  217.      * 獲取課件條目數 
  218.      *  
  219.      * @return the count of visible items 
  220.      */  
  221.     public int getVisibleItems() {  
  222.         return visibleItems;  
  223.     }  
  224.   
  225.     /** 
  226.      * 設置可見條目數 
  227.      *  
  228.      * @param count 
  229.      *            the new count 
  230.      */  
  231.     public void setVisibleItems(int count) {  
  232.         visibleItems = count;  
  233.         invalidate();  
  234.     }  
  235.   
  236.     /** 
  237.      * 獲取標籤 
  238.      *  
  239.      * @return the label 
  240.      */  
  241.     public String getLabel() {  
  242.         return label;  
  243.     }  
  244.   
  245.     /** 
  246.      * 設置標籤 
  247.      *  
  248.      * @param newLabel 
  249.      *            the label to set 
  250.      */  
  251.     public void setLabel(String newLabel) {  
  252.         if (label == null || !label.equals(newLabel)) {  
  253.             label = newLabel;  
  254.             labelLayout = null;  
  255.             invalidate();  
  256.         }  
  257.     }  
  258.   
  259.     /** 
  260.      * 添加 WheelView 選擇的元素改變監聽器 
  261.      *  
  262.      * @param listener 
  263.      *            the listener 
  264.      */  
  265.     public void addChangingListener(OnWheelChangedListener listener) {  
  266.         changingListeners.add(listener);  
  267.     }  
  268.   
  269.     /** 
  270.      * 移除 WheelView 元素改變監聽器 
  271.      *  
  272.      * @param listener 
  273.      *            the listener 
  274.      */  
  275.     public void removeChangingListener(OnWheelChangedListener listener) {  
  276.         changingListeners.remove(listener);  
  277.     }  
  278.   
  279.     /** 
  280.      * 回調元素改變監聽器集合的元素改變監聽器元素的元素改變方法 
  281.      *  
  282.      * @param oldValue 
  283.      *            舊的 WheelView選中的值 
  284.      * @param newValue 
  285.      *            新的 WheelView選中的值 
  286.      */  
  287.     protected void notifyChangingListeners(int oldValue, int newValue) {  
  288.         for (OnWheelChangedListener listener : changingListeners) {  
  289.             listener.onChanged(this, oldValue, newValue);  
  290.         }  
  291.     }  
  292.   
  293.     /** 
  294.      * 添加 WheelView 滾動監聽器 
  295.      *  
  296.      * @param listener 
  297.      *            the listener 
  298.      */  
  299.     public void addScrollingListener(OnWheelScrollListener listener) {  
  300.         scrollingListeners.add(listener);  
  301.     }  
  302.   
  303.     /** 
  304.      * 移除 WheelView 滾動監聽器 
  305.      *  
  306.      * @param listener 
  307.      *            the listener 
  308.      */  
  309.     public void removeScrollingListener(OnWheelScrollListener listener) {  
  310.         scrollingListeners.remove(listener);  
  311.     }  
  312.   
  313.     /** 
  314.      * 通知監聽器開始滾動 
  315.      */  
  316.     protected void notifyScrollingListenersAboutStart() {  
  317.         for (OnWheelScrollListener listener : scrollingListeners) {  
  318.             //回調開始滾動方法  
  319.             listener.onScrollingStarted(this);  
  320.         }  
  321.     }  
  322.   
  323.     /** 
  324.      * 通知監聽器結束滾動 
  325.      */  
  326.     protected void notifyScrollingListenersAboutEnd() {  
  327.         for (OnWheelScrollListener listener : scrollingListeners) {  
  328.             //回調滾動結束方法  
  329.             listener.onScrollingFinished(this);  
  330.         }  
  331.     }  
  332.   
  333.     /** 
  334.      * 獲取當前選中元素的索引 
  335.      *  
  336.      * @return  
  337.      *      當前元素索引 
  338.      */  
  339.     public int getCurrentItem() {  
  340.         return currentItem;  
  341.     }  
  342.   
  343.     /** 
  344.      * 設置當前元素的位置, 如果索引是錯誤的 不進行任何操作 
  345.      * -- 需要考慮該 WheelView 是否能循環 
  346.      * -- 根據是否需要滾動動畫來確定是 ①滾動到目的位置 還是 ②晴空所有條目然後重繪 
  347.      *  
  348.      * @param index 
  349.      *            要設置的元素索引值 
  350.      * @param animated 
  351.      *            動畫標誌位 
  352.      */  
  353.     public void setCurrentItem(int index, boolean animated) {  
  354.         //如果沒有適配器或者元素個數爲0 直接返回  
  355.         if (adapter == null || adapter.getItemsCount() == 0) {  
  356.             return// throw?  
  357.         }  
  358.         //目標索引小於 0 或者大於 元素索引最大值(個數 -1)  
  359.         if (index < 0 || index >= adapter.getItemsCount()) {  
  360.             //入股WheelView 可循環, 修正索引值, 如果不可循環直接返回  
  361.             if (isCyclic) {  
  362.                 while (index < 0) {  
  363.                     index += adapter.getItemsCount();  
  364.                 }  
  365.                 index %= adapter.getItemsCount();  
  366.             } else {  
  367.                 return// throw?  
  368.             }  
  369.         }  
  370.           
  371.         //如果當前的索引不是傳入的 索引  
  372.         if (index != currentItem) {  
  373.               
  374.             /* 
  375.              * 如果需要動畫, 就滾動到目標位置 
  376.              * 如果不需要動畫, 重新設置佈局 
  377.              */  
  378.             if (animated) {  
  379.                 /* 
  380.                  * 開始滾動, 每個元素滾動間隔 400 ms, 滾動次數是 目標索引值 減去 當前索引值, 這是滾動的真實方法 
  381.                  */  
  382.                 scroll(index - currentItem, SCROLLING_DURATION);  
  383.             } else {  
  384.                 //所有佈局設置爲 null, 滾動位置設置爲 0  
  385.                 invalidateLayouts();  
  386.   
  387.                 int old = currentItem;  
  388.                 currentItem = index;  
  389.   
  390.                 //便利回調元素改變監聽器集合中的監聽器元素中的元素改變方法  
  391.                 notifyChangingListeners(old, currentItem);  
  392.   
  393.                 //重繪  
  394.                 invalidate();  
  395.             }  
  396.         }  
  397.     }  
  398.   
  399.     /** 
  400.      * 設置當前選中的條目, 沒有動畫, 當索引出錯不做任何操作 
  401.      *  
  402.      * @param index 
  403.      *            要設置的索引 
  404.      */  
  405.     public void setCurrentItem(int index) {  
  406.         setCurrentItem(index, false);  
  407.     }  
  408.   
  409.     /** 
  410.      * 獲取 WheelView 是否可以循環 
  411.      * -- 如果可循環 : 第一個之前是最後一個, 最後一個之後是第一個; 
  412.      * -- 如果不可循環 : 到第一個就不能上翻, 最後一個不能下翻  
  413.      *  
  414.      * @return 
  415.      */  
  416.     public boolean isCyclic() {  
  417.         return isCyclic;  
  418.     }  
  419.   
  420.     /** 
  421.      * 設置 WheelView 循環標誌 
  422.      *  
  423.      * @param isCyclic 
  424.      *            the flag to set 
  425.      */  
  426.     public void setCyclic(boolean isCyclic) {  
  427.         this.isCyclic = isCyclic;  
  428.   
  429.         invalidate();  
  430.         invalidateLayouts();  
  431.     }  
  432.   
  433.     /** 
  434.      * 使佈局無效 
  435.      * 將 選中條目 和 普通條目設置爲 null, 滾動位置設置爲0 
  436.      */  
  437.     private void invalidateLayouts() {  
  438.         itemsLayout = null;  
  439.         valueLayout = null;  
  440.         scrollingOffset = 0;  
  441.     }  
  442.   
  443.     /** 
  444.      * 初始化資源 
  445.      */  
  446.     private void initResourcesIfNecessary() {  
  447.         /* 
  448.          * 設置繪製普通條目的畫筆, 允許抗拒齒, 允許 fake-bold 
  449.          * 設置文字大小爲 24 
  450.          */  
  451.         if (itemsPaint == null) {  
  452.             itemsPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FAKE_BOLD_TEXT_FLAG);  
  453.             itemsPaint.setTextSize(TEXT_SIZE);  
  454.         }  
  455.   
  456.         /* 
  457.          * 設置繪製選中條目的畫筆 
  458.          * 設置文字大小 24 
  459.          */  
  460.         if (valuePaint == null) {  
  461.             valuePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FAKE_BOLD_TEXT_FLAG | Paint.DITHER_FLAG);  
  462.             valuePaint.setTextSize(TEXT_SIZE);  
  463.             valuePaint.setShadowLayer(0.1f, 00.1f, 0xFFC0C0C0);  
  464.         }  
  465.   
  466.         //選中的條目背景  
  467.         if (centerDrawable == null) {  
  468.             centerDrawable = getContext().getResources().getDrawable(R.drawable.wheel_val);  
  469.         }  
  470.   
  471.         //創建頂部陰影圖片  
  472.         if (topShadow == null) {  
  473.             /* 
  474.              * 構造方法中傳入顏色漸變方向 
  475.              * 陰影顏色 
  476.              */  
  477.             topShadow = new GradientDrawable(Orientation.TOP_BOTTOM, SHADOWS_COLORS);  
  478.         }  
  479.   
  480.         //創建底部陰影圖片  
  481.         if (bottomShadow == null) {  
  482.             bottomShadow = new GradientDrawable(Orientation.BOTTOM_TOP, SHADOWS_COLORS);  
  483.         }  
  484.   
  485.         /* 
  486.          * 設置 View 組件的背景 
  487.          */  
  488.         setBackgroundResource(R.drawable.wheel_bg);  
  489.     }  
  490.   
  491.     /** 
  492.      * 計算佈局期望的高度 
  493.      *  
  494.      * @param layout 
  495.      *      組件的佈局的 
  496.      * @return  
  497.      *      佈局需要的高度 
  498.      */  
  499.     private int getDesiredHeight(Layout layout) {  
  500.         if (layout == null) {  
  501.             return 0;  
  502.         }  
  503.   
  504.         /* 
  505.          * 佈局需要的高度是 條目個數 * 可見條目數 減去 頂部和底部隱藏的一部份 減去 額外的條目高度 
  506.          */  
  507.         int desired = getItemHeight() * visibleItems - ITEM_OFFSET * 2 - ADDITIONAL_ITEM_HEIGHT;  
  508.   
  509.         // 將計算的佈局高度 與 最小高度比較, 取最大值  
  510.         desired = Math.max(desired, getSuggestedMinimumHeight());  
  511.   
  512.         return desired;  
  513.     }  
  514.   
  515.     /** 
  516.      * 根據條目獲取字符串 
  517.      *  
  518.      * @param index 
  519.      *            條目索引 
  520.      * @return  
  521.      *      條目顯示的字符串 
  522.      */  
  523.     private String getTextItem(int index) {  
  524.         if (adapter == null || adapter.getItemsCount() == 0) {  
  525.             return null;  
  526.         }  
  527.         //適配器顯示的字符串個數  
  528.         int count = adapter.getItemsCount();  
  529.           
  530.         //考慮 index 小於 0 的情況  
  531.         if ((index < 0 || index >= count) && !isCyclic) {  
  532.             return null;  
  533.         } else {  
  534.             while (index < 0) {  
  535.                 index = count + index;  
  536.             }  
  537.         }  
  538.   
  539.         //index 大於 0  
  540.         index %= count;  
  541.         return adapter.getItem(index);  
  542.     }  
  543.   
  544.     /** 
  545.      * 根據當前值創建 字符串 
  546.      *  
  547.      * @param useCurrentValue 
  548.      *      是否在滾動 
  549.      * @return the text 
  550.      *      生成的字符串 
  551.      */  
  552.     private String buildText(boolean useCurrentValue) {  
  553.         //創建字符串容器  
  554.         StringBuilder itemsText = new StringBuilder();  
  555.         //計算出顯示的條目相對位置, 例如顯示 5個, 第 3 個是正中見選中的佈局  
  556.         int addItems = visibleItems / 2 + 1;  
  557.   
  558.         /* 
  559.          * 遍歷顯示的條目 
  560.          * 獲取當前顯示條目 上下 各 addItems 個文本, 將該文本添加到顯示文本中去 
  561.          * 如果不是最後一個 都加上回車 
  562.          */  
  563.         for (int i = currentItem - addItems; i <= currentItem + addItems; i++) {  
  564.             //如果在滾動  
  565.             if (useCurrentValue || i != currentItem) {  
  566.                 String text = getTextItem(i);  
  567.                 if (text != null) {  
  568.                     itemsText.append(text);  
  569.                 }  
  570.             }  
  571.             if (i < currentItem + addItems) {  
  572.                 itemsText.append("\n");  
  573.             }  
  574.         }  
  575.   
  576.         return itemsText.toString();  
  577.     }  
  578.   
  579.     /** 
  580.      * 返回 條目的字符串 
  581.      *  
  582.      * @return  
  583.      *      條目最大寬度 
  584.      */  
  585.     private int getMaxTextLength() {  
  586.         WheelAdapter adapter = getAdapter();  
  587.         if (adapter == null) {  
  588.             return 0;  
  589.         }  
  590.   
  591.         //如果獲取的最大條目寬度不爲 -1, 可以直接返回該條目寬度  
  592.         int adapterLength = adapter.getMaximumLength();  
  593.         if (adapterLength > 0) {  
  594.             return adapterLength;  
  595.         }  
  596.   
  597.         String maxText = null;  
  598.         int addItems = visibleItems / 2;  
  599.         /* 
  600.          * 遍歷當前顯示的條目, 獲取字符串長度最長的那個, 返回這個最長的字符串長度 
  601.          */  
  602.         for (int i = Math.max(currentItem - addItems, 0); i < Math.min(currentItem + visibleItems,  
  603.                 adapter.getItemsCount()); i++) {  
  604.             String text = adapter.getItem(i);  
  605.             if (text != null && (maxText == null || maxText.length() < text.length())) {  
  606.                 maxText = text;  
  607.             }  
  608.         }  
  609.   
  610.         return maxText != null ? maxText.length() : 0;  
  611.     }  
  612.   
  613.     /** 
  614.      * 獲取每個條目的高度 
  615.      *  
  616.      * @return  
  617.      *      條目的高度 
  618.      */  
  619.     private int getItemHeight() {  
  620.         //如果條目高度不爲 0, 直接返回  
  621.         if (itemHeight != 0) {  
  622.             return itemHeight;  
  623.         //如果條目的高度爲 0, 並且普通條目佈局不爲null, 條目個數大於 2   
  624.         } else if (itemsLayout != null && itemsLayout.getLineCount() > 2) {  
  625.             /* 
  626.              * itemsLayout.getLineTop(2) : 獲取頂部第二行上面的垂直(y軸)位置, 如果行數等於 
  627.              */  
  628.             itemHeight = itemsLayout.getLineTop(2) - itemsLayout.getLineTop(1);  
  629.             return itemHeight;  
  630.         }  
  631.   
  632.         //如果上面都不符合, 使用整體高度處以 顯示條目數  
  633.         return getHeight() / visibleItems;  
  634.     }  
  635.   
  636.     /** 
  637.      * 計算寬度並創建文字佈局 
  638.      *  
  639.      * @param widthSize 
  640.      *            輸入的佈局寬度 
  641.      * @param mode 
  642.      *            佈局模式 
  643.      * @return  
  644.      *      計算的寬度 
  645.      */  
  646.     private int calculateLayoutWidth(int widthSize, int mode) {  
  647.         initResourcesIfNecessary();  
  648.   
  649.         int width = widthSize;  
  650.   
  651.         //獲取最長的條目顯示字符串字符個數  
  652.         int maxLength = getMaxTextLength();  
  653.           
  654.         if (maxLength > 0) {  
  655.             /* 
  656.              * 使用方法 FloatMath.ceil() 方法有以下警告 
  657.              * Use java.lang.Math#ceil instead of android.util.FloatMath#ceil() since it is faster as of API 8 
  658.              */  
  659.             //float textWidth = FloatMath.ceil(Layout.getDesiredWidth("0", itemsPaint));  
  660.             //向上取整  計算一個字符串寬度  
  661.             float textWidth = (float) Math.ceil(Layout.getDesiredWidth("0", itemsPaint));  
  662.               
  663.             //獲取字符串總的寬度  
  664.             itemsWidth = (int) (maxLength * textWidth);  
  665.         } else {  
  666.             itemsWidth = 0;  
  667.         }  
  668.           
  669.         //總寬度加上一些間距  
  670.         itemsWidth += ADDITIONAL_ITEMS_SPACE; // make it some more  
  671.   
  672.         //計算 label 的長度  
  673.         labelWidth = 0;  
  674.         if (label != null && label.length() > 0) {  
  675.             labelWidth = (int) Math.ceil(Layout.getDesiredWidth(label, valuePaint));  
  676.             //labelWidth = (int) FloatMath.ceil(Layout.getDesiredWidth(label, valuePaint));  
  677.         }  
  678.   
  679.         boolean recalculate = false;  
  680.         //精準模式  
  681.         if (mode == MeasureSpec.EXACTLY) {  
  682.             //精準模式下, 寬度就是給定的寬度  
  683.             width = widthSize;  
  684.             recalculate = true;  
  685.         } else {  
  686.             //未定義模式  
  687.             width = itemsWidth + labelWidth + 2 * PADDING;  
  688.             if (labelWidth > 0) {  
  689.                 width += LABEL_OFFSET;  
  690.             }  
  691.   
  692.             // 獲取 ( 計算出來的寬度 與 最小寬度的 ) 最大值  
  693.             width = Math.max(width, getSuggestedMinimumWidth());  
  694.   
  695.             //最大模式 如果 給定的寬度 小於 計算出來的寬度, 那麼使用最小的寬度 ( 給定寬度 | 計算出來的寬度 )  
  696.             if (mode == MeasureSpec.AT_MOST && widthSize < width) {  
  697.                 width = widthSize;  
  698.                 recalculate = true;  
  699.             }  
  700.         }  
  701.   
  702.         /* 
  703.          * 重新計算寬度 , 如果寬度是給定的寬度, 不是我們計算出來的寬度, 需要重新進行計算 
  704.          * 重新計算的寬度是用於 
  705.          *  
  706.          * 計算 itemsWidth , 這個與返回的 寬度無關, 與創建佈局有關 
  707.          */  
  708.         if (recalculate) {  
  709.             int pureWidth = width - LABEL_OFFSET - 2 * PADDING;  
  710.             if (pureWidth <= 0) {  
  711.                 itemsWidth = labelWidth = 0;  
  712.             }  
  713.             if (labelWidth > 0) {  
  714.                 double newWidthItems = (double) itemsWidth * pureWidth / (itemsWidth + labelWidth);  
  715.                 itemsWidth = (int) newWidthItems;  
  716.                 labelWidth = pureWidth - itemsWidth;  
  717.             } else {  
  718.                 itemsWidth = pureWidth + LABEL_OFFSET; // no label  
  719.             }  
  720.         }  
  721.   
  722.         if (itemsWidth > 0) {  
  723.             //創建佈局  
  724.             createLayouts(itemsWidth, labelWidth);  
  725.         }  
  726.   
  727.         return width;  
  728.     }  
  729.   
  730.     /** 
  731.      * 創建佈局 
  732.      *  
  733.      * @param widthItems 
  734.      *            佈局條目寬度 
  735.      * @param widthLabel 
  736.      *            label 寬度 
  737.      */  
  738.     private void createLayouts(int widthItems, int widthLabel) {  
  739.         /* 
  740.          * 創建普通條目佈局 
  741.          * 如果 普通條目佈局 爲 null 或者 普通條目佈局的寬度 大於 傳入的寬度, 這時需要重新創建佈局 
  742.          * 如果 普通條目佈局存在, 並且其寬度小於傳入的寬度, 此時需要將 
  743.          */  
  744.         if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {  
  745.               
  746.             /* 
  747.              * android.text.StaticLayout.StaticLayout( 
  748.              * CharSequence source, TextPaint paint,  
  749.              * int width, Alignment align,  
  750.              * float spacingmult, float spacingadd, boolean includepad) 
  751.              * 傳入參數介紹 :  
  752.              * CharSequence source : 需要分行顯示的字符串 
  753.              * TextPaint paint : 繪製字符串的畫筆 
  754.              * int width : 條目的寬度 
  755.              * Alignment align : Layout 的對齊方式, ALIGN_CENTER 居中對齊, ALIGN_NORMAL 左對齊, Alignment.ALIGN_OPPOSITE 右對齊 
  756.              * float spacingmult : 行間距, 1.5f 代表 1.5 倍字體高度 
  757.              * float spacingadd : 基礎行距上增加多少 , 真實行間距 等於 spacingmult 和 spacingadd 的和 
  758.              * boolean includepad :  
  759.              */  
  760.             itemsLayout = new StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,  
  761.                     widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,  
  762.                     ADDITIONAL_ITEM_HEIGHT, false);  
  763.         } else {  
  764.             //調用 Layout 內置的方法 increaseWidthTo 將寬度提升到指定的寬度  
  765.             itemsLayout.increaseWidthTo(widthItems);  
  766.         }  
  767.   
  768.         /* 
  769.          * 創建選中條目 
  770.          */  
  771.         if (!isScrollingPerformed && (valueLayout == null || valueLayout.getWidth() > widthItems)) {  
  772.             String text = getAdapter() != null ? getAdapter().getItem(currentItem) : null;  
  773.             valueLayout = new StaticLayout(text != null ? text : "", valuePaint, widthItems,  
  774.                     widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,  
  775.                     ADDITIONAL_ITEM_HEIGHT, false);  
  776.         } else if (isScrollingPerformed) {  
  777.             valueLayout = null;  
  778.         } else {  
  779.             valueLayout.increaseWidthTo(widthItems);  
  780.         }  
  781.   
  782.         /* 
  783.          * 創建標籤條目 
  784.          */  
  785.         if (widthLabel > 0) {  
  786.             if (labelLayout == null || labelLayout.getWidth() > widthLabel) {  
  787.                 labelLayout = new StaticLayout(label, valuePaint, widthLabel, Layout.Alignment.ALIGN_NORMAL, 1,  
  788.                         ADDITIONAL_ITEM_HEIGHT, false);  
  789.             } else {  
  790.                 labelLayout.increaseWidthTo(widthLabel);  
  791.             }  
  792.         }  
  793.     }  
  794.   
  795.     /* 
  796.      * 測量組件大小 
  797.      * (non-Javadoc) 
  798.      * @see android.view.View#onMeasure(int, int) 
  799.      */  
  800.     @Override  
  801.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  802.         //獲取寬度 和 高度的模式 和 大小  
  803.         int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
  804.         int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
  805.         int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
  806.         int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
  807.   
  808.         //寬度就是 計算的佈局的寬度  
  809.         int width = calculateLayoutWidth(widthSize, widthMode);  
  810.   
  811.         int height;  
  812.         /* 
  813.          * 精準模式 
  814.          *      精準模式下 高度就是精確的高度 
  815.          */  
  816.         if (heightMode == MeasureSpec.EXACTLY) {  
  817.             height = heightSize;  
  818.           
  819.         //未定義模式 和 最大模式  
  820.         } else {  
  821.             //未定義模式下 獲取佈局需要的高度  
  822.             height = getDesiredHeight(itemsLayout);  
  823.   
  824.             //最大模式下 獲取 佈局高度 和 佈局所需高度的最小值  
  825.             if (heightMode == MeasureSpec.AT_MOST) {  
  826.                 height = Math.min(height, heightSize);  
  827.             }  
  828.         }  
  829.   
  830.         //設置組件的寬和高  
  831.         setMeasuredDimension(width, height);  
  832.     }  
  833.   
  834.     /* 
  835.      * 繪製組件 
  836.      * (non-Javadoc) 
  837.      * @see android.view.View#onDraw(android.graphics.Canvas) 
  838.      */  
  839.     @Override  
  840.     protected void onDraw(Canvas canvas) {  
  841.         super.onDraw(canvas);  
  842.   
  843.         //如果條目佈局爲 null, 就創建該佈局  
  844.         if (itemsLayout == null) {  
  845.             /* 
  846.              * 如果 條目寬度爲0, 說明該寬度沒有計算, 先計算, 計算完之後會創建佈局 
  847.              * 如果 條目寬度 大於 0, 說明已經計算過寬度了, 直接創建佈局 
  848.              */  
  849.             if (itemsWidth == 0) {  
  850.                 calculateLayoutWidth(getWidth(), MeasureSpec.EXACTLY);  
  851.             } else {  
  852.                 //創建普通條目佈局, 選中條目佈局, 標籤條目佈局  
  853.                 createLayouts(itemsWidth, labelWidth);  
  854.             }  
  855.         }  
  856.   
  857.         //如果條目寬度大於0  
  858.         if (itemsWidth > 0) {  
  859.             canvas.save();  
  860.             // 使用平移方法忽略 填充的空間 和 頂部底部隱藏的一部份條目  
  861.             canvas.translate(PADDING, -ITEM_OFFSET);  
  862.             //繪製普通條目  
  863.             drawItems(canvas);  
  864.             //繪製選中條目  
  865.             drawValue(canvas);  
  866.             canvas.restore();  
  867.         }  
  868.   
  869.         //在中心位置繪製  
  870.         drawCenterRect(canvas);  
  871.         //繪製陰影  
  872.         drawShadows(canvas);  
  873.     }  
  874.   
  875.     /** 
  876.      * Draws shadows on top and bottom of control 
  877.      *  
  878.      * @param canvas 
  879.      *            the canvas for drawing 
  880.      */  
  881.     private void drawShadows(Canvas canvas) {  
  882.         topShadow.setBounds(00, getWidth(), getHeight() / visibleItems);  
  883.         topShadow.draw(canvas);  
  884.   
  885.         bottomShadow.setBounds(0, getHeight() - getHeight() / visibleItems, getWidth(), getHeight());  
  886.         bottomShadow.draw(canvas);  
  887.     }  
  888.   
  889.     /** 
  890.      * 繪製選中條目 
  891.      *  
  892.      * @param canvas 
  893.      *            畫布 
  894.      */  
  895.     private void drawValue(Canvas canvas) {  
  896.         valuePaint.setColor(VALUE_TEXT_COLOR);  
  897.           
  898.         //將當前 View 狀態屬性值 轉爲整型集合, 賦值給 普通條目佈局的繪製屬性  
  899.         valuePaint.drawableState = getDrawableState();  
  900.   
  901.         Rect bounds = new Rect();  
  902.         //獲取選中條目佈局的邊界  
  903.         itemsLayout.getLineBounds(visibleItems / 2, bounds);  
  904.   
  905.         // 繪製標籤  
  906.         if (labelLayout != null) {  
  907.             canvas.save();  
  908.             canvas.translate(itemsLayout.getWidth() + LABEL_OFFSET, bounds.top);  
  909.             labelLayout.draw(canvas);  
  910.             canvas.restore();  
  911.         }  
  912.   
  913.         // 繪製選中條目  
  914.         if (valueLayout != null) {  
  915.             canvas.save();  
  916.             canvas.translate(0, bounds.top + scrollingOffset);  
  917.             valueLayout.draw(canvas);  
  918.             canvas.restore();  
  919.         }  
  920.     }  
  921.   
  922.     /** 
  923.      * 繪製普通條目 
  924.      *  
  925.      * @param canvas 
  926.      *            畫布 
  927.      */  
  928.     private void drawItems(Canvas canvas) {  
  929.         canvas.save();  
  930.   
  931.         //獲取 y 軸 定點高度  
  932.         int top = itemsLayout.getLineTop(1);  
  933.         canvas.translate(0, -top + scrollingOffset);  
  934.   
  935.         //設置畫筆顏色  
  936.         itemsPaint.setColor(ITEMS_TEXT_COLOR);  
  937.         //將當前 View 狀態屬性值 轉爲整型集合, 賦值給 普通條目佈局的繪製屬性  
  938.         itemsPaint.drawableState = getDrawableState();  
  939.         //將佈局繪製到畫布上  
  940.         itemsLayout.draw(canvas);  
  941.   
  942.         canvas.restore();  
  943.     }  
  944.   
  945.     /** 
  946.      * 繪製當前選中條目的背景圖片 
  947.      *  
  948.      * @param canvas 
  949.      *            畫布 
  950.      */  
  951.     private void drawCenterRect(Canvas canvas) {  
  952.         int center = getHeight() / 2;  
  953.         int offset = getItemHeight() / 2;  
  954.         centerDrawable.setBounds(0, center - offset, getWidth(), center + offset);  
  955.         centerDrawable.draw(canvas);  
  956.     }  
  957.   
  958.     /* 
  959.      * 繼承自 View 的觸摸事件, 當出現觸摸事件的時候, 就會回調該方法 
  960.      * (non-Javadoc) 
  961.      * @see android.view.View#onTouchEvent(android.view.MotionEvent) 
  962.      */  
  963.     @Override  
  964.     public boolean onTouchEvent(MotionEvent event) {  
  965.         //獲取適配器  
  966.         WheelAdapter adapter = getAdapter();  
  967.         if (adapter == null) {  
  968.             return true;  
  969.         }  
  970.   
  971.         /* 
  972.          * gestureDetector.onTouchEvent(event) : 分析給定的動作, 如果可用, 調用 手勢檢測器的 onTouchEvent 方法 
  973.          * -- 參數解析 : ev , 觸摸事件 
  974.          * -- 返回值 : 如果手勢監聽器成功執行了該方法, 返回true, 如果執行出現意外 返回 false; 
  975.          */  
  976.         if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {  
  977.             justify();  
  978.         }  
  979.         return true;  
  980.     }  
  981.   
  982.     /** 
  983.      * 滾動 WheelView 
  984.      *  
  985.      * @param delta 
  986.      *            滾動的值 
  987.      */  
  988.     private void doScroll(int delta) {  
  989.         scrollingOffset += delta;  
  990.           
  991.         //計算滾動的條目數, 使用滾動的值 處於 單個條目高度, 注意計算整數值  
  992.         int count = scrollingOffset / getItemHeight();  
  993.         /* 
  994.          * pos 是滾動後的目標元素索引 
  995.          * 計算當前位置, 當前條目數 減去 滾動的條目數 
  996.          * 注意 滾動條目數可正 可負 
  997.          */  
  998.         int pos = currentItem - count;  
  999.         //如果是可循環的, 並且條目數大於0  
  1000.         if (isCyclic && adapter.getItemsCount() > 0) {  
  1001.             //設置循環, 如果位置小於0, 那麼該位置就顯示最後一個元素  
  1002.             while (pos < 0) {  
  1003.                 pos += adapter.getItemsCount();  
  1004.             }  
  1005.             //如果位置正無限大, 模條目數 取餘  
  1006.             pos %= adapter.getItemsCount();  
  1007.               
  1008.         // (前提 : 不可循環  條目數大於0, 可循環 條目數小於0, 條目數小於0, 不可循環) , 如果滾動在執行  
  1009.         } else if (isScrollingPerformed) {  
  1010.             //位置一旦小於0, 計算的位置就賦值爲 0, 條目滾動數爲0  
  1011.             if (pos < 0) {  
  1012.                 count = currentItem;  
  1013.                 pos = 0;  
  1014.                   
  1015.             //位置大於條目數的時候, 當前位置等於(條目數 - 1), 條目滾動數等於 當前位置 減去 (條目數 - 1)  
  1016.             } else if (pos >= adapter.getItemsCount()) {  
  1017.                 count = currentItem - adapter.getItemsCount() + 1;  
  1018.                 pos = adapter.getItemsCount() - 1;  
  1019.             }  
  1020.           
  1021.         } else {  
  1022.             // fix position  
  1023.             pos = Math.max(pos, 0);  
  1024.             pos = Math.min(pos, adapter.getItemsCount() - 1);  
  1025.         }  
  1026.   
  1027.         //滾動的高度  
  1028.         int offset = scrollingOffset;  
  1029.           
  1030.         /* 
  1031.          * 如果當前位置不是滾動後的目標位置, 就將當前位置設置爲目標位置 
  1032.          * 否則就重繪組件 
  1033.          */  
  1034.         if (pos != currentItem) {  
  1035.             setCurrentItem(pos, false);  
  1036.         } else {  
  1037.             //重繪組件  
  1038.             invalidate();  
  1039.         }  
  1040.   
  1041.         // 將滾動後剩餘的小數部分保存  
  1042.         scrollingOffset = offset - count * getItemHeight();  
  1043.         if (scrollingOffset > getHeight()) {  
  1044.             scrollingOffset = scrollingOffset % getHeight() + getHeight();  
  1045.         }  
  1046.     }  
  1047.   
  1048.     /** 
  1049.      * 手勢監聽器 
  1050.      */  
  1051.     private SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() {  
  1052.           
  1053.         //按下操作  
  1054.         public boolean onDown(MotionEvent e) {  
  1055.             //如果滾動在執行  
  1056.             if (isScrollingPerformed) {  
  1057.                 //滾動強制停止, 按下的時候不能繼續滾動  
  1058.                 scroller.forceFinished(true);  
  1059.                 //清理信息  
  1060.                 clearMessages();  
  1061.                 return true;  
  1062.             }  
  1063.             return false;  
  1064.         }  
  1065.   
  1066.         /* 
  1067.          * 手勢監聽器監聽到 滾動操作後回調 
  1068.          *  
  1069.          * 參數解析 :  
  1070.          * MotionEvent e1 : 觸發滾動時第一次按下的事件 
  1071.          * MotionEvent e2 : 觸發當前滾動的移動事件 
  1072.          * float distanceX : 自從上一次調用 該方法 到這一次 x 軸滾動的距離,  
  1073.          *              注意不是 e1 到 e2 的距離, e1 到 e2 的距離是從開始滾動到現在的滾動距離 
  1074.          * float distanceY : 自從上一次回調該方法到這一次 y 軸滾動的距離 
  1075.          *  
  1076.          * 返回值 : 如果事件成功觸發, 執行完了方法中的操作, 返回true, 否則返回 false  
  1077.          * (non-Javadoc) 
  1078.          * @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float) 
  1079.          */  
  1080.         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {  
  1081.             //開始滾動, 並回調滾動監聽器集合中監聽器的 開始滾動方法  
  1082.             startScrolling();  
  1083.             doScroll((int) -distanceY);  
  1084.             return true;  
  1085.         }  
  1086.   
  1087.         /* 
  1088.          * 當一個急衝手勢發生後 回調該方法, 會計算出該手勢在 x 軸 y 軸的速率 
  1089.          *  
  1090.          * 參數解析 :  
  1091.          * -- MotionEvent e1 : 急衝動作的第一次觸摸事件; 
  1092.          * -- MotionEvent e2 : 急衝動作的移動發生的時候的觸摸事件; 
  1093.          * -- float velocityX : x 軸的速率 
  1094.          * -- float velocityY : y 軸的速率 
  1095.          *  
  1096.          * 返回值 : 如果執行完畢返回 true, 否則返回false, 這個就是自己定義的 
  1097.          *  
  1098.          * (non-Javadoc) 
  1099.          * @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float) 
  1100.          */  
  1101.         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {  
  1102.             //計算上一次的 y 軸位置, 當前的條目高度 加上 剩餘的 不夠一行高度的那部分  
  1103.             lastScrollY = currentItem * getItemHeight() + scrollingOffset;  
  1104.             //如果可以循環最大值是無限大, 不能循環就是條目數的高度值  
  1105.             int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();  
  1106.             int minY = isCyclic ? -maxY : 0;  
  1107.             /* 
  1108.              * Scroll 開始根據一個急衝手勢滾動, 滾動的距離與初速度有關 
  1109.              * 參數介紹 :  
  1110.              * -- int startX : 開始時的 X軸位置 
  1111.              * -- int startY : 開始時的 y軸位置 
  1112.              * -- int velocityX : 急衝手勢的 x 軸的初速度, 單位 px/s 
  1113.              * -- int velocityY : 急衝手勢的 y 軸的初速度, 單位 px/s 
  1114.              * -- int minX : x 軸滾動的最小值 
  1115.              * -- int maxX : x 軸滾動的最大值 
  1116.              * -- int minY : y 軸滾動的最小值 
  1117.              * -- int maxY : y 軸滾動的最大值 
  1118.              */  
  1119.             scroller.fling(0, lastScrollY, 0, (int) -velocityY / 200, minY, maxY);  
  1120.             setNextMessage(MESSAGE_SCROLL);  
  1121.             return true;  
  1122.         }  
  1123.     };  
  1124.   
  1125.     // Handler 中的  Message 信息  
  1126.     /** 滾動信息 */  
  1127.     private final int MESSAGE_SCROLL = 0;  
  1128.     /** 調整信息 */  
  1129.     private final int MESSAGE_JUSTIFY = 1;  
  1130.   
  1131.     /** 
  1132.      * 清空之前的 Handler 隊列, 發送下一個消息到 Handler 中 
  1133.      *  
  1134.      * @param message 
  1135.      *            要發送的消息 
  1136.      */  
  1137.     private void setNextMessage(int message) {  
  1138.         //清空 Handler 隊列中的  what 消息  
  1139.         clearMessages();  
  1140.         //發送消息到 Handler 中  
  1141.         animationHandler.sendEmptyMessage(message);  
  1142.     }  
  1143.   
  1144.     /** 
  1145.      * 清空隊列中的信息 
  1146.      */  
  1147.     private void clearMessages() {  
  1148.         //刪除 Handler 執行隊列中的滾動操作  
  1149.         animationHandler.removeMessages(MESSAGE_SCROLL);  
  1150.         animationHandler.removeMessages(MESSAGE_JUSTIFY);  
  1151.     }  
  1152.   
  1153.     /** 
  1154.      * 動畫控制器 
  1155.      *  animation handler 
  1156.      *   
  1157.      *  可能會造成內存泄露 : 添加註解 HandlerLeak 
  1158.      *  Handler 類應該應該爲static類型,否則有可能造成泄露。 
  1159.      *  在程序消息隊列中排隊的消息保持了對目標Handler類的應用。 
  1160.      *  如果Handler是個內部類,那 麼它也會保持它所在的外部類的引用。 
  1161.      *  爲了避免泄露這個外部類,應該將Handler聲明爲static嵌套類,並且使用對外部類的弱應用。 
  1162.      */  
  1163.     @SuppressLint("HandlerLeak")  
  1164.     private Handler animationHandler = new Handler() {  
  1165.         public void handleMessage(Message msg) {  
  1166.             //回調該方法獲取當前位置, 如果返回true, 說明動畫還沒有執行完畢  
  1167.             scroller.computeScrollOffset();  
  1168.             //獲取當前 y 位置  
  1169.             int currY = scroller.getCurrY();  
  1170.             //獲取已經滾動了的位置, 使用上一次位置 減去 當前位置  
  1171.             int delta = lastScrollY - currY;  
  1172.             lastScrollY = currY;  
  1173.             if (delta != 0) {  
  1174.                 //改變值不爲 0 , 繼續滾動  
  1175.                 doScroll(delta);  
  1176.             }  
  1177.   
  1178.             /* 
  1179.              * 如果滾動到了指定的位置, 滾動還沒有停止 
  1180.              * 這時需要強制停止 
  1181.              */  
  1182.             if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) {  
  1183.                 currY = scroller.getFinalY();  
  1184.                 scroller.forceFinished(true);  
  1185.             }  
  1186.               
  1187.             /* 
  1188.              * 如果滾動沒有停止 
  1189.              * 再向 Handler 發送一個停止 
  1190.              */  
  1191.             if (!scroller.isFinished()) {  
  1192.                 animationHandler.sendEmptyMessage(msg.what);  
  1193.             } else if (msg.what == MESSAGE_SCROLL) {  
  1194.                 justify();  
  1195.             } else {  
  1196.                 finishScrolling();  
  1197.             }  
  1198.         }  
  1199.     };  
  1200.   
  1201.     /** 
  1202.      * 調整 WheelView 
  1203.      */  
  1204.     private void justify() {  
  1205.         if (adapter == null) {  
  1206.             return;  
  1207.         }  
  1208.         //上一次的 y 軸的位置爲 0  
  1209.         lastScrollY = 0;  
  1210.         int offset = scrollingOffset;  
  1211.         int itemHeight = getItemHeight();  
  1212.         /* 
  1213.          * 當滾動補償 大於 0, 說明還有沒有滾動的部分,  needToIncrease 是 當前條目是否小於條目數 
  1214.          * 如果 滾動補償不大於 0,  needToIncrease 是當前條目是否大於 0 
  1215.          */  
  1216.         boolean needToIncrease = offset > 0 ? currentItem < adapter.getItemsCount() : currentItem > 0;  
  1217.         if ((isCyclic || needToIncrease) && Math.abs((float) offset) > (float) itemHeight / 2) {  
  1218.             if (offset < 0)  
  1219.                 offset += itemHeight + MIN_DELTA_FOR_SCROLLING;  
  1220.             else  
  1221.                 offset -= itemHeight + MIN_DELTA_FOR_SCROLLING;  
  1222.         }  
  1223.         if (Math.abs(offset) > MIN_DELTA_FOR_SCROLLING) {  
  1224.             scroller.startScroll(000, offset, SCROLLING_DURATION);  
  1225.             setNextMessage(MESSAGE_JUSTIFY);  
  1226.         } else {  
  1227.             finishScrolling();  
  1228.         }  
  1229.     }  
  1230.   
  1231.     /** 
  1232.      * WheelView 開始滾動 
  1233.      */  
  1234.     private void startScrolling() {  
  1235.         //如果沒有滾動, 將滾動狀態 isScrollingPerformed 設爲 true  
  1236.         if (!isScrollingPerformed) {  
  1237.             isScrollingPerformed = true;  
  1238.             //通知監聽器開始滾動 回調所有的 滾動監聽集合中 的 開始滾動方法  
  1239.             notifyScrollingListenersAboutStart();  
  1240.         }  
  1241.     }  
  1242.   
  1243.     /** 
  1244.      * 結束滾動 
  1245.      *  設置滾動狀態爲 false, 回調滾動監聽器的停止滾動方法 
  1246.      */  
  1247.     void finishScrolling() {  
  1248.         if (isScrollingPerformed) {  
  1249.             notifyScrollingListenersAboutEnd();  
  1250.             isScrollingPerformed = false;  
  1251.         }  
  1252.         //設置佈局無效  
  1253.         invalidateLayouts();  
  1254.         //重繪佈局  
  1255.         invalidate();  
  1256.     }  
  1257.   
  1258.     /** 
  1259.      * 滾動 WheelView 
  1260.      *  
  1261.      * @param itemsToSkip 
  1262.      *            滾動的元素個數 
  1263.      * @param time 
  1264.      *            每次滾動的間隔 
  1265.      */  
  1266.     public void scroll(int itemsToScroll, int time) {  
  1267.         //如果有滾動強制停止  
  1268.         scroller.forceFinished(true);  
  1269.   
  1270.         lastScrollY = scrollingOffset;  
  1271.         int offset = itemsToScroll * getItemHeight();  
  1272.   
  1273.         /* 
  1274.          * 給定 一個開始點, 滾動距離, 滾動間隔, 開始滾動 
  1275.          *  
  1276.          * 參數解析 :  
  1277.          * 1. 開始的 x 軸位置 
  1278.          * 2. 開始的 y 軸位置 
  1279.          * 3. 要滾動 x 軸距離 
  1280.          * 4. 要滾動 y 軸距離 
  1281.          * 5. 滾動花費的時間 
  1282.          */  
  1283.         scroller.startScroll(0, lastScrollY, 0, offset - lastScrollY, time);  
  1284.         setNextMessage(MESSAGE_SCROLL);  
  1285.   
  1286.         //設置開始滾動狀態, 並回調滾動監聽器方法  
  1287.         startScrolling();  
  1288.     }  
  1289.   
  1290. }  



7. Activity 主界面 


[java] view plain copy
  1. package cn.org.octopus.wheelview;  
  2.   
  3. import android.app.Activity;  
  4. import android.app.AlertDialog;  
  5. import android.app.Fragment;  
  6. import android.content.Context;  
  7. import android.content.DialogInterface;  
  8. import android.os.Bundle;  
  9. import android.view.Gravity;  
  10. import android.view.LayoutInflater;  
  11. import android.view.Menu;  
  12. import android.view.MenuItem;  
  13. import android.view.View;  
  14. import android.view.ViewGroup;  
  15. import android.view.ViewGroup.LayoutParams;  
  16. import android.widget.Button;  
  17. import android.widget.LinearLayout;  
  18. import cn.org.octopus.wheelview.widget.ArrayWheelAdapter;  
  19. import cn.org.octopus.wheelview.widget.OnWheelChangedListener;  
  20. import cn.org.octopus.wheelview.widget.OnWheelScrollListener;  
  21. import cn.org.octopus.wheelview.widget.WheelView;  
  22.   
  23. public class MainActivity extends Activity{  
  24.   
  25.     public static final String TAG = "octopus.activity";  
  26.       
  27.     private static Button bt_click;  
  28.       
  29.     public String province[] = new String[] { "  河北省  ""  山西省  ""  內蒙古  ""  遼寧省  ""  吉林省  ""  黑龍江  ""  江蘇省  " };  
  30.   
  31.     public String city[][] = new String[][] {  
  32.             new String[] {"  石家莊  ""唐山""秦皇島""邯鄲""邢臺""保定""張家口""承德""滄州""廊坊""衡水"},  
  33.             new String[] {"太原""大同""陽泉""長治""晉城""朔州""晉中""運城""忻州""臨汾""呂梁"},  
  34.             new String[] {"呼和浩特""包頭""烏海""赤峯""通遼""鄂爾多斯""呼倫貝爾""巴彥淖爾""烏蘭察布""興安""錫林郭勒""阿拉善"},  
  35.             new String[] {"瀋陽""大連""鞍山""撫順""本溪""丹東""錦州""營口""阜新""遼陽""盤錦""鐵嶺""朝陽""葫蘆島"},  
  36.             new String[] {"長春""吉林""四平""遼源""通化""白山""松原""白城""延邊"},  
  37.             new String[] {"哈爾濱""齊齊哈爾""雞西""鶴崗""雙鴨山""大慶""伊春""佳木斯""七臺河""牡丹江""黑河""綏化""大興安嶺"},  
  38.             new String[] {"南京""無錫""徐州""常州""蘇州""南通""連雲港""淮安""鹽城""揚州""鎮江""泰州""宿遷"} };  
  39.       
  40.     @Override  
  41.     protected void onCreate(Bundle savedInstanceState) {  
  42.         super.onCreate(savedInstanceState);  
  43.         setContentView(R.layout.activity_main);  
  44.   
  45.         if (savedInstanceState == null) {  
  46.             getFragmentManager().beginTransaction()  
  47.                     .add(R.id.container, new PlaceholderFragment()).commit();  
  48.         }  
  49.     }  
  50.       
  51.     /* 
  52.      * 點擊事件 
  53.      */  
  54.     public void onClick(View view) {  
  55.         showSelectDialog(this"選擇地點", province, city);  
  56.     }  
  57.   
  58.       
  59.     private void showSelectDialog(Context context, String title, final String[] left, final String[][] right) {  
  60.         //創建對話框  
  61.         AlertDialog dialog = new AlertDialog.Builder(context).create();  
  62.         //爲對話框設置標題  
  63.         dialog.setTitle(title);  
  64.         //創建對話框內容, 創建一個 LinearLayout   
  65.         LinearLayout llContent = new LinearLayout(context);  
  66.         //將創建的 LinearLayout 設置成橫向的  
  67.         llContent.setOrientation(LinearLayout.HORIZONTAL);  
  68.         //創建 WheelView 組件  
  69.         final WheelView wheelLeft = new WheelView(context);  
  70.         //設置 WheelView 組件最多顯示 5 個元素  
  71.         wheelLeft.setVisibleItems(5);  
  72.         //設置 WheelView 元素是否循環滾動  
  73.         wheelLeft.setCyclic(false);  
  74.         //設置 WheelView 適配器  
  75.         wheelLeft.setAdapter(new ArrayWheelAdapter<String>(left));  
  76.         //設置右側的 WheelView  
  77.         final WheelView wheelRight = new WheelView(context);  
  78.         //設置右側 WheelView 顯示個數  
  79.         wheelRight.setVisibleItems(5);  
  80.         //設置右側 WheelView 元素是否循環滾動  
  81.         wheelRight.setCyclic(true);  
  82.         //設置右側 WheelView 的元素適配器  
  83.         wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[0]));  
  84.         //設置 LinearLayout 的佈局參數  
  85.         LinearLayout.LayoutParams paramsLeft = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,  
  86.                 LayoutParams.WRAP_CONTENT, 4);  
  87.         paramsLeft.gravity = Gravity.LEFT;  
  88.         LinearLayout.LayoutParams paramsRight = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,  
  89.                 LayoutParams.WRAP_CONTENT, 6);  
  90.         paramsRight.gravity = Gravity.RIGHT;  
  91.         //將 WheelView 對象放到左側 LinearLayout 中  
  92.         llContent.addView(wheelLeft, paramsLeft);  
  93.         //將 WheelView 對象放到 右側 LinearLayout 中  
  94.         llContent.addView(wheelRight, paramsRight);  
  95.           
  96.         //爲左側的 WheelView 設置條目改變監聽器  
  97.         wheelLeft.addChangingListener(new OnWheelChangedListener() {  
  98.             @Override  
  99.             public void onChanged(WheelView wheel, int oldValue, int newValue) {  
  100.                 //設置右側的 WheelView 的適配器  
  101.                 wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[newValue]));  
  102.                 wheelRight.setCurrentItem(right[newValue].length / 2);  
  103.             }  
  104.         });  
  105.           
  106.         wheelLeft.addScrollingListener(new OnWheelScrollListener() {  
  107.               
  108.             @Override  
  109.             public void onScrollingStarted(WheelView wheel) {  
  110.                 // TODO Auto-generated method stub  
  111.                   
  112.             }  
  113.               
  114.             @Override  
  115.             public void onScrollingFinished(WheelView wheel) {  
  116.                 // TODO Auto-generated method stub  
  117.                   
  118.             }  
  119.         });  
  120.           
  121.         //設置對話框點擊事件 積極  
  122.         dialog.setButton(AlertDialog.BUTTON_POSITIVE, "確定"new DialogInterface.OnClickListener() {  
  123.             @Override  
  124.             public void onClick(DialogInterface dialog, int which) {  
  125.                 int leftPosition = wheelLeft.getCurrentItem();  
  126.                 String vLeft = left[leftPosition];  
  127.                 String vRight = right[leftPosition][wheelRight.getCurrentItem()];  
  128.                 bt_click.setText(vLeft + "-" + vRight);  
  129.                 dialog.dismiss();  
  130.             }  
  131.         });  
  132.           
  133.         //設置對話框點擊事件 消極  
  134.         dialog.setButton(AlertDialog.BUTTON_NEGATIVE, "取消"new DialogInterface.OnClickListener() {  
  135.             @Override  
  136.             public void onClick(DialogInterface dialog, int which) {  
  137.                 dialog.dismiss();  
  138.             }  
  139.         });  
  140.         //將 LinearLayout 設置到 對話框中  
  141.         dialog.setView(llContent);  
  142.         //顯示對話框  
  143.         if (!dialog.isShowing()) {  
  144.             dialog.show();  
  145.         }  
  146.     }  
  147.       
  148.       
  149.     @Override  
  150.     public boolean onCreateOptionsMenu(Menu menu) {  
  151.   
  152.         // Inflate the menu; this adds items to the action bar if it is present.  
  153.         getMenuInflater().inflate(R.menu.main, menu);  
  154.         return true;  
  155.     }  
  156.   
  157.     @Override  
  158.     public boolean onOptionsItemSelected(MenuItem item) {  
  159.         // Handle action bar item clicks here. The action bar will  
  160.         // automatically handle clicks on the Home/Up button, so long  
  161.         // as you specify a parent activity in AndroidManifest.xml.  
  162.         int id = item.getItemId();  
  163.         if (id == R.id.action_settings) {  
  164.             return true;  
  165.         }  
  166.         return super.onOptionsItemSelected(item);  
  167.     }  
  168.   
  169.     /** 
  170.      * A placeholder fragment containing a simple view. 
  171.      */  
  172.     public static class PlaceholderFragment extends Fragment {  
  173.   
  174.         public PlaceholderFragment() {  
  175.         }  
  176.   
  177.         @Override  
  178.         public View onCreateView(LayoutInflater inflater, ViewGroup container,  
  179.                 Bundle savedInstanceState) {  
  180.             View rootView = inflater.inflate(R.layout.fragment_main, container,  
  181.                     false);  
  182.             bt_click = (Button)rootView.findViewById(R.id.bt_click);  
  183.             return rootView;  
  184.         }  
  185.     }  
  186.   
  187. }  


博客地址 http://blog.csdn.net/shulianghan/article/details/41520569#t17


代碼下載 : 

-- GitHub : https://github.com/han1202012/WheelViewDemo.git 

-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章