下面的內容是我轉載的,我只是換了一個位置,存到了自己的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 對象 :
- //創建 WheelView 組件
- final WheelView wheelLeft = new WheelView(context);
b. 設置 WheelView 顯示條目數 :
- //設置 WheelView 組件最多顯示 5 個元素
- wheelLeft.setVisibleItems(5);
c. 設置 WheelView 是否滾動循環 :
- //設置 WheelView 元素是否循環滾動
- wheelLeft.setCyclic(false);
d. 設置 WheelView 適配器 :
- //設置 WheelView 適配器
- wheelLeft.setAdapter(new ArrayWheelAdapter<String>(left));
e. 設置條目改變監聽器 :
- //爲左側的 WheelView 設置條目改變監聽器
- wheelLeft.addChangingListener(new OnWheelChangedListener() {
- @Override
- public void onChanged(WheelView wheel, int oldValue, int newValue) {
- //設置右側的 WheelView 的適配器
- wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[newValue]));
- wheelRight.setCurrentItem(right[newValue].length / 2);
- }
- });
f. 設置滾動監聽器 :
- wheelLeft.addScrollingListener(new OnWheelScrollListener() {
- @Override
- public void onScrollingStarted(WheelView wheel) {
- // TODO Auto-generated method stub
- }
- @Override
- public void onScrollingFinished(WheelView wheel) {
- // TODO Auto-generated method stub
- }
- );
二. WheelView 適配器 監聽器 相關接口分析
1. 適配器 分析
這裏定義了一個適配器接口, 以及兩個適配器類, 一個用於任意類型的數據集適配, 一個用於數字適配;
適配器操作 : 在 WheelView.Java 中通過 setAdapter(WheelAdapter adapter) 和 getAdapter() 方法設置 獲取 適配器;
-- 適配器常用操作 : 在 WheelView 中定義了 getItem(), getItemsCount(), getMaxmiumLength() 方法獲取 適配器的相關信息;
- /**
- * 獲取該 WheelView 的適配器
- *
- * @return
- * 返回適配器
- */
- public WheelAdapter getAdapter() {
- return adapter;
- }
- /**
- * 設置適配器
- *
- * @param adapter
- * 要設置的適配器
- */
- public void setAdapter(WheelAdapter adapter) {
- this.adapter = adapter;
- invalidateLayouts();
- invalidate();
- }
(1) 適配器接口 ( interface WheelAdapter )
適配器接口 : WheelAdapter;
-- 接口作用 : 該接口是所有適配器的接口, 適配器類都需要實現該接口;
接口抽象方法介紹 :
-- getItemsCount() : 獲取適配器數據集合中元素個數;
- /**
- * 獲取條目的個數
- *
- * @return
- * WheelView 的條目個數
- */
- public int getItemsCount();
-- getItem(int index) : 獲取適配器集合的中指定索引元素;
- /**
- * 根據索引位置獲取 WheelView 的條目
- *
- * @param index
- * 條目的索引
- * @return
- * WheelView 上顯示的條目的值
- */
- public String getItem(int index);
-- getMaximumLength() : 獲取 WheelView 在界面上的顯示寬度;
- /**
- * 獲取條目的最大長度. 用來定義 WheelView 的寬度. 如果返回 -1, 就會使用默認寬度
- *
- * @return
- * 條目的最大寬度 或者 -1
- */
- public int getMaximumLength();
(2) 數組適配器 ( class ArrayWheelAdapter<T> implements WheelAdapter )
適配器作用 : 該適配器可以傳入任何數據類型的數組, 可以是 字符串數組, 也可以是任何對象的數組, 傳入的數組作爲適配器的數據源;
成員變量分析 :
-- 數據源 :
- /** 適配器的數據源 */
- private T items[];
-- WheelView 最大寬度 :
- /** WheelView 的寬度 */
- private int length;
構造方法分析 :
-- ArrayWheelAdapter(T items[], int length) : 傳入 T 類型 對象數組, 以及 WheelView 的寬度;
- /**
- * 構造方法
- *
- * @param items
- * 適配器數據源 集合 T 類型的數組
- * @param length
- * 適配器數據源 集合 T 數組長度
- */
- public ArrayWheelAdapter(T items[], int length) {
- this.items = items;
- this.length = length;
- }
-- ArrayWheelAdapter(T items[]) : 傳入 T 類型對象數組, 寬度使用默認的寬度;
- /**
- * 構造方法
- *
- * @param items
- * 適配器數據源集合 T 類型數組
- */
- public ArrayWheelAdapter(T items[]) {
- this(items, DEFAULT_LENGTH);
- }
實現的父類方法分析 :
-- getItem(int index) : 根據索引獲取數組中對應位置的對象的字符串類型;
- @Override
- public String getItem(int index) {
- //如果這個索引值合法, 就返回 item 數組對應的元素的字符串形式
- if (index >= 0 && index < items.length) {
- return items[index].toString();
- }
- return null;
- }
-- getItemsCount() : 獲取數據集廣大小, 直接返回數組大小;
- @Override
- public int getItemsCount() {
- //返回 item 數組的長度
- return items.length;
- }
-- getMaximumLength() : 獲取 WheelView 的最大寬度;
- @Override
- public int getMaximumLength() {
- //返回 item 元素的寬度
- return length;
- }
(3) 數字適配器 ( class NumericWheelAdapter implements WheelAdapter )
NumericWheelAdapter 適配器作用 : 數字作爲 WheelView 適配器的顯示值;
成員變量分析 :
-- 最小值 : WheelView 數值顯示的最小值;
- /** 設置的最小值 */
- private int minValue;
-- 最大值 : WheelView 數值顯示的最大值;
- /** 設置的最大值 */
- private int maxValue;
-- 格式化字符串 : 用於字符串的格式化;
- /** 格式化字符串, 用於格式化 貨幣, 科學計數, 十六進制 等格式 */
- private String format;
構造方法分析 :
-- NumericWheelAdapter() : 默認的構造方法, 使用默認的最大最小值;
- /**
- * 默認的構造方法, 使用默認的最大最小值
- */
- public NumericWheelAdapter() {
- this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE);
- }
-- NumericWheelAdapter(int minValue, int maxValue) : 傳入一個最大最小值;
- /**
- * 構造方法
- *
- * @param minValue
- * 最小值
- * @param maxValue
- * 最大值
- */
- public NumericWheelAdapter(int minValue, int maxValue) {
- this(minValue, maxValue, null);
- }
-- NumericWheelAdapter(int minValue, int maxValue, String format) : 傳入最大最小值, 以及數字格式化方式;
- /**
- * 構造方法
- *
- * @param minValue
- * 最小值
- * @param maxValue
- * 最大值
- * @param format
- * 格式化字符串
- */
- public NumericWheelAdapter(int minValue, int maxValue, String format) {
- this.minValue = minValue;
- this.maxValue = maxValue;
- this.format = format;
- }
實現的父類方法 :
-- 獲取條目 : 如果需要格式化, 先進行格式化;
- @Override
- public String getItem(int index) {
- String result = "";
- if (index >= 0 && index < getItemsCount()) {
- int value = minValue + index;
- //如果 format 不爲 null, 那麼格式化字符串, 如果爲 null, 直接返回數字
- if(format != null){
- result = String.format(format, value);
- }else{
- result = Integer.toString(value);
- }
- return result;
- }
- return null;
- }
-- 獲取元素個數 :
- @Override
- public int getItemsCount() {
- //返回數字總個數
- return maxValue - minValue + 1;
- }
-- 獲取 WheelView 最大寬度 :
- @Override
- public int getMaximumLength() {
- //獲取 最大值 和 最小值 中的 較大的數字
- int max = Math.max(Math.abs(maxValue), Math.abs(minValue));
- //獲取這個數字 的 字符串形式的 字符串長度
- int maxLen = Integer.toString(max).length();
- if (minValue < 0) {
- maxLen++;
- }
- return maxLen;
- }
2. 監聽器相關接口
(1) 條目改變監聽器 ( interface OnWheelChangedListener )
監聽器作用 : 在 WheelView 條目改變的時候, 回調該監聽器的接口方法, 執行條目改變對應的操作;
接口方法介紹 :
-- onChanged(WheelView wheel, int oldValue, int newValue) : 傳入 WheelView 組件對象, 以及 舊的 和 新的 條目值索引;
- /**
- * 當前條目改變時回調該方法
- *
- * @param wheel
- * 條目改變的 WheelView 對象
- * @param oldValue
- * WheelView 舊的條目值
- * @param newValue
- * WheelView 新的條目值
- */
- void onChanged(WheelView wheel, int oldValue, int newValue);
(2) 滾動監聽器 ( interface OnWheelScrollListener )
滾動監聽器作用 : 在 WheelView 滾動動作 開始 和 結束的時候回調對應的方法, 在對應方法中進行相應的操作;
接口方法介紹 :
-- 開始滾動方法 : 在滾動開始的時候回調該方法;
- /**
- * 在 WheelView 滾動開始的時候回調該接口
- *
- * @param wheel
- * 開始滾動的 WheelView 對象
- */
- void onScrollingStarted(WheelView wheel);
-- 停止滾動方法 : 在滾動結束的時候回調該方法;
- /**
- * 在 WheelView 滾動結束的時候回調該接口
- *
- * @param wheel
- * 結束滾動的 WheelView 對象
- */
- 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 軸的速度;
-- 代碼示例 :
- /*
- * 手勢監聽器監聽到 滾動操作後回調
- *
- * 參數解析 :
- * MotionEvent e1 : 觸發滾動時第一次按下的事件
- * MotionEvent e2 : 觸發當前滾動的移動事件
- * float distanceX : 自從上一次調用 該方法 到這一次 x 軸滾動的距離,
- * 注意不是 e1 到 e2 的距離, e1 到 e2 的距離是從開始滾動到現在的滾動距離
- * float distanceY : 自從上一次回調該方法到這一次 y 軸滾動的距離
- *
- * 返回值 : 如果事件成功觸發, 執行完了方法中的操作, 返回true, 否則返回 false
- * (non-Javadoc)
- * @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float)
- */
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- //開始滾動, 並回調滾動監聽器集合中監聽器的 開始滾動方法
- startScrolling();
- doScroll((int) -distanceY);
- return true;
- }
- /*
- * 當一個急衝手勢發生後 回調該方法, 會計算出該手勢在 x 軸 y 軸的速率
- *
- * 參數解析 :
- * -- MotionEvent e1 : 急衝動作的第一次觸摸事件;
- * -- MotionEvent e2 : 急衝動作的移動發生的時候的觸摸事件;
- * -- float velocityX : x 軸的速率
- * -- float velocityY : y 軸的速率
- *
- * 返回值 : 如果執行完畢返回 true, 否則返回false, 這個就是自己定義的
- *
- * (non-Javadoc)
- * @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float)
- */
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- //計算上一次的 y 軸位置, 當前的條目高度 加上 剩餘的 不夠一行高度的那部分
- lastScrollY = currentItem * getItemHeight() + scrollingOffset;
- //如果可以循環最大值是無限大, 不能循環就是條目數的高度值
- int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();
- int minY = isCyclic ? -maxY : 0;
- /*
- * Scroll 開始根據一個急衝手勢滾動, 滾動的距離與初速度有關
- * 參數介紹 :
- * -- int startX : 開始時的 X軸位置
- * -- int startY : 開始時的 y軸位置
- * -- int velocityX : 急衝手勢的 x 軸的初速度, 單位 px/s
- * -- int velocityY : 急衝手勢的 y 軸的初速度, 單位 px/s
- * -- int minX : x 軸滾動的最小值
- * -- int maxX : x 軸滾動的最大值
- * -- int minY : y 軸滾動的最小值
- * -- int maxY : y 軸滾動的最大值
- */
- scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);
- setNextMessage(MESSAGE_SCROLL);
- return true;
- }
- };
(2) 創建手勢探測器
手勢探測器創建 : 調用 其構造函數, 傳入 上下文對象 和 手勢監聽器對象;
-- 禁止長按操作 : 調用 setIsLongpressEnabled(false) 方法, 禁止長按操作, 因爲 長按操作會屏蔽滾動事件;
- //創建一個手勢處理
- gestureDetector = new GestureDetector(context, gestureListener);
- /*
- * 是否允許長按操作,
- * 如果設置爲 true 用戶按下不鬆開, 會返回一個長按事件,
- * 如果設置爲 false, 按下不鬆開滑動的話 會收到滾動事件.
- */
- gestureDetector.setIsLongpressEnabled(false);
(3) 將手勢探測器 與 組件結合
關聯手勢探測器 與 組件 : 在組件的 onTouchEvent(MotionEvent event) 方法中, 調用手勢探測器的 gestureDetector.onTouchEvent(event) 方法即可;
- /*
- * 繼承自 View 的觸摸事件, 當出現觸摸事件的時候, 就會回調該方法
- * (non-Javadoc)
- * @see android.view.View#onTouchEvent(android.view.MotionEvent)
- */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- //獲取適配器
- WheelAdapter adapter = getAdapter();
- if (adapter == null) {
- return true;
- }
- /*
- * gestureDetector.onTouchEvent(event) : 分析給定的動作, 如果可用, 調用 手勢檢測器的 onTouchEvent 方法
- * -- 參數解析 : ev , 觸摸事件
- * -- 返回值 : 如果手勢監聽器成功執行了該方法, 返回true, 如果執行出現意外 返回 false;
- */
- if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {
- justify();
- }
- return true;
- }
2. Scroller 簡介
(1) Scroller 簡介
Scroller 通用作用 : Scroller 組件並不是一個佈局組件, 該組件是運行在後臺的, 通過一些方法設定 Scroller 對象 的操作 或者 動畫, 然後讓 Scroller 運行在後臺中 用於模擬滾動操作, 在適當的時機 獲取該對象的座標信息, 這些信息是在後臺運算出來的;
Scroller 在本 View 中作用 : Android 的這個自定義的 WheelView 組件, 可以平滑的滾動, 當我們做一個加速滑動時, 會根據速度計算出滑動的距離, 這些數據都是在 Scroller 中計算出來的;
(2) 設定 Scroller 對象的動作參數
終止滾動 :
-- 終止滾動 跳轉到目標位置 : 終止平緩的動畫, 直接跳轉到最終的 x y 軸的座標位置;
- public void abortAnimation()
-- 終止滾動 停止在當前位置 : 強行結束 Scroll 的滾動;
- public final void forceFinished(boolean finished)
設置滾動參數 :
-- 設置最終 x 軸座標 :
- public void setFinalX(int newX)
-- 設置最終 y 軸座標 :
- public void setFinalY(int newY)
-- 設置滾動摩擦力 :
- public final void setFriction(float friction)
設置動作 :
-- 開始滾動 : 傳入參數 開始 x 位置, 開始 y 位置, x 軸滾動距離, y 軸滾動距離;
- public void startScroll(int startX, int startY, int dx, int dy)
- public void startScroll(int startX, int startY, int dx, int dy, int duration)
- public void fling(int startX, int startY, int velocityX, int velocityY,
- int minX, int maxX, int minY, int maxY)
延長滾動時間 : 延長滾動的時間, 讓滾動滾的更遠一些;
- public void extendDuration(int extend)
(3) 獲取 Scroll 後臺運行參數
獲取當前數據 :
-- 獲取當前 x 軸座標 :
- public final int getCurrX()
-- 獲取當前 y 軸座標 :
- public final int getCurrY()
-- 獲取當前速度 :
- public float getCurrVelocity()
獲取開始結束時的數據 :
-- 獲取開始 x 軸座標 :
- public final int getStartX()
-- 獲取開始 y 軸座標 :
- public final int getStartY()
-- 獲取最終 x 軸座標 : 該參數只在急衝滾動時有效;
- public final int getFinalX()
-- 獲取最終 y 軸座標 : 該參數只在急衝滾動時有效;
- public final int getFinalY()
查看是否滾動完畢 :
- public final boolean isFinished()
獲取從開始滾動到現在的時間 :
- public int timePassed()
獲取新位置 : 調用該方法可以獲取新位置, 如果返回 true 說明動畫還沒執行完畢;
- public boolean computeScrollOffset()
(4) Scroll 在 WheelView 中的運用
Scroller 創建 :
- //使用默認的 時間 和 插入器 創建一個滾動器
- scroller = new Scroller(context);
手勢監聽器 SimpleOnGestureListener 對象中的 onDown() 方法 : 如果滾動還在執行, 那麼強行停止 Scroller 滾動;
- //按下操作
- public boolean onDown(MotionEvent e) {
- //如果滾動在執行
- if (isScrollingPerformed) {
- //滾動強制停止, 按下的時候不能繼續滾動
- scroller.forceFinished(true);
- //清理信息
- clearMessages();
- return true;
- }
- return false;
- }
當手勢監聽器 SimpleOnGestureListener 對象中有急衝動作時 onFling() 方法中 : 手勢監聽器監聽到了 急衝動作, 那麼 Scroller 也進行對應操作;
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- //計算上一次的 y 軸位置, 當前的條目高度 加上 剩餘的 不夠一行高度的那部分
- lastScrollY = currentItem * getItemHeight() + scrollingOffset;
- //如果可以循環最大值是無限大, 不能循環就是條目數的高度值
- int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();
- int minY = isCyclic ? -maxY : 0;
- /*
- * Scroll 開始根據一個急衝手勢滾動, 滾動的距離與初速度有關
- * 參數介紹 :
- * -- int startX : 開始時的 X軸位置
- * -- int startY : 開始時的 y軸位置
- * -- int velocityX : 急衝手勢的 x 軸的初速度, 單位 px/s
- * -- int velocityY : 急衝手勢的 y 軸的初速度, 單位 px/s
- * -- int minX : x 軸滾動的最小值
- * -- int maxX : x 軸滾動的最大值
- * -- int minY : y 軸滾動的最小值
- * -- int maxY : y 軸滾動的最大值
- */
- scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);
- setNextMessage(MESSAGE_SCROLL);
- return true;
- }
動畫控制 Handler 中 :
-- 滾動 : 獲取當前 Scroller 的 y 軸位置, 與上一次的 y 軸位置對比, 如果 間距 delta 不爲0, 就滾動;
-- 查看是否停止 : 如果現在距離 到 最終距離 小於最小滾動距離, 強制停止;
-- 執行 msg.what 指令 : 如果需要停止, 強制停止, 否則調整座標;
- /**
- * 動畫控制器
- * animation handler
- *
- * 可能會造成內存泄露 : 添加註解 HandlerLeak
- * Handler 類應該應該爲static類型,否則有可能造成泄露。
- * 在程序消息隊列中排隊的消息保持了對目標Handler類的應用。
- * 如果Handler是個內部類,那 麼它也會保持它所在的外部類的引用。
- * 爲了避免泄露這個外部類,應該將Handler聲明爲static嵌套類,並且使用對外部類的弱應用。
- */
- @SuppressLint("HandlerLeak")
- vate Handler animationHandler = new Handler() {
- public void handleMessage(Message msg) {
- //回調該方法獲取當前位置, 如果返回true, 說明動畫還沒有執行完畢
- scroller.computeScrollOffset();
- //獲取當前 y 位置
- int currY = scroller.getCurrY();
- //獲取已經滾動了的位置, 使用上一次位置 減去 當前位置
- int delta = lastScrollY - currY;
- lastScrollY = currY;
- if (delta != 0) {
- //改變值不爲 0 , 繼續滾動
- doScroll(delta);
- }
- /*
- * 如果滾動到了指定的位置, 滾動還沒有停止
- * 這時需要強制停止
- */
- if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) {
- currY = scroller.getFinalY();
- scroller.forceFinished(true);
- }
- /*
- * 如果滾動沒有停止
- * 再向 Handler 發送一個停止
- */
- if (!scroller.isFinished()) {
- animationHandler.sendEmptyMessage(msg.what);
- } else if (msg.what == MESSAGE_SCROLL) {
- justify();
- } else {
- finishScrolling();
- }
- }
- };
3. StaticLayout 佈局容器
(1) StaticLayout 解析
StaticLayout 解析 : 該組件用於顯示文本, 一旦該文本被顯示後, 就不能再編輯, 如果想要修改文本, 使用 DynamicLayout 佈局即可;
-- 使用場景 : 一般情況下不會使用該組件, 當想要自定義組件 或者 想要使用 Canvas 繪製文本時 才使用該佈局;
常用方法解析 :
-- 獲取底部 Padding : 獲取底部 到最後一行文字的 間隔, 單位是 px;
- public int getBottomPadding()
-- 獲取頂部 Padding :
- public int getTopPadding()
-- 獲取省略個數 : 獲取某一行需要省略的字符個數;
- public int getEllipsisCount(int line)
- public int getEllipsisStart(int line)
- public int getEllipsisStart(int line)
- public boolean getLineContainsTab(int line)
- public int getLineCount()
- public int getLineTop(int line)
- public int getLineDescent(int line)
- public final Directions getLineDirections(int line)
- public int getLineStart(int line)
- public int getParagraphDirection(int line)
- public int getLineForVertical(int vertical)
(2) 佈局顯示
佈局創建 :
-- 三種佈局 : WheelView 中涉及到了三種 StaticLayout 佈局, 普通條目佈局 itemLayout, 選中條目佈局 valueLayout, 標籤佈局 labelLayout;
-- 創建時機 : 在 View 組件 每次 onMeasure() 和 onDraw() 方法中都要重新創建對應佈局;
-- 創建佈局源碼 :
- /**
- * 創建佈局
- *
- * @param widthItems
- * 佈局條目寬度
- * @param widthLabel
- * label 寬度
- */
- private void createLayouts(int widthItems, int widthLabel) {
- /*
- * 創建普通條目佈局
- * 如果 普通條目佈局 爲 null 或者 普通條目佈局的寬度 大於 傳入的寬度, 這時需要重新創建佈局
- * 如果 普通條目佈局存在, 並且其寬度小於傳入的寬度, 此時需要將
- */
- if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {
- /*
- * android.text.StaticLayout.StaticLayout(
- * CharSequence source, TextPaint paint,
- * int width, Alignment align,
- * float spacingmult, float spacingadd, boolean includepad)
- * 傳入參數介紹 :
- * CharSequence source : 需要分行顯示的字符串
- * TextPaint paint : 繪製字符串的畫筆
- * int width : 條目的寬度
- * Alignment align : Layout 的對齊方式, ALIGN_CENTER 居中對齊, ALIGN_NORMAL 左對齊, Alignment.ALIGN_OPPOSITE 右對齊
- * float spacingmult : 行間距, 1.5f 代表 1.5 倍字體高度
- * float spacingadd : 基礎行距上增加多少 , 真實行間距 等於 spacingmult 和 spacingadd 的和
- * boolean includepad :
- */
- itemsLayout = new StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,
- widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,
- ADDITIONAL_ITEM_HEIGHT, false);
- } else {
- //調用 Layout 內置的方法 increaseWidthTo 將寬度提升到指定的寬度
- itemsLayout.increaseWidthTo(widthItems);
- }
- /*
- * 創建選中條目
- */
- if (!isScrollingPerformed && (valueLayout == null || valueLayout.getWidth() > widthItems)) {
- String text = getAdapter() != null ? getAdapter().getItem(currentItem) : null;
- valueLayout = new StaticLayout(text != null ? text : "", valuePaint, widthItems,
- widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,
- ADDITIONAL_ITEM_HEIGHT, false);
- } else if (isScrollingPerformed) {
- valueLayout = null;
- } else {
- valueLayout.increaseWidthTo(widthItems);
- }
- /*
- * 創建標籤條目
- */
- if (widthLabel > 0) {
- if (labelLayout == null || labelLayout.getWidth() > widthLabel) {
- labelLayout = new StaticLayout(label, valuePaint, widthLabel, Layout.Alignment.ALIGN_NORMAL, 1,
- ADDITIONAL_ITEM_HEIGHT, false);
- } else {
- labelLayout.increaseWidthTo(widthLabel);
- }
- }
- }
4. 監聽器管理
監聽器集合維護 :
-- 定義監聽器集合 : 在 View 組件中 定義一個 List 集合, 集合中存放 監聽器元素;
- /** 條目改變監聽器集合 封裝了條目改變方法, 當條目改變時回調 */
- private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>();
- /** 條目滾動監聽器集合, 該監聽器封裝了 開始滾動方法, 結束滾動方法 */
- private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>();
-- 提供對監聽器集合的添加刪除接口 : 提供 對集合 進行 添加 和 刪除的接口;
- /**
- * 添加 WheelView 選擇的元素改變監聽器
- *
- * @param listener
- * the listener
- */
- public void addChangingListener(OnWheelChangedListener listener) {
- changingListeners.add(listener);
- }
- /**
- * 移除 WheelView 元素改變監聽器
- *
- * @param listener
- * the listener
- */
- public void removeChangingListener(OnWheelChangedListener listener) {
- changingListeners.remove(listener);
- }
-- 調用監聽器接口 :
- /**
- * 回調元素改變監聽器集合的元素改變監聽器元素的元素改變方法
- *
- * @param oldValue
- * 舊的 WheelView選中的值
- * @param newValue
- * 新的 WheelView選中的值
- */
- protected void notifyChangingListeners(int oldValue, int newValue) {
- for (OnWheelChangedListener listener : changingListeners) {
- listener.onChanged(this, oldValue, newValue);
- }
- }
5. 自定義 View 對象的寬高
(1) onMeasure 方法 MeasureSpec 模式解析
常規處理方法 : 組件的寬高有三種情況, widthMeasureSpec 有三種模式 最大模式, 精準模式, 未定義模式;
-- 最大模式 : 在 組件的寬或高 warp_content 屬性時, 會使用最大模式;
-- 精準模式 : 當給組件寬 或者高 定義一個值 或者 使用 match_parent 時, 會使用精準模式;
處理寬高的常規代碼 :
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- //獲取寬度 和 高度的模式 和 大小
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
- Log.i(TAG, "寬度 : widthMode : " + getMode(widthMode) + " , widthSize : " + widthSize + "\n"
- + "高度 : heightMode : " + getMode(heightMode) + " , heightSize : " + heightSize);
- int width = 0;
- int height = 0;
- /*
- * 精準模式
- * 精準模式下 高度就是精確的高度
- */
- if (heightMode == MeasureSpec.EXACTLY) {
- height = heightSize;
- //未定義模式 和 最大模式
- } else {
- //未定義模式下 獲取佈局需要的高度
- height = 100;
- //最大模式下 獲取 佈局高度 和 佈局所需高度的最小值
- if (heightMode == MeasureSpec.AT_MOST) {
- height = Math.min(height, heightSize);
- }
- }
- if (widthMode == MeasureSpec.EXACTLY) {
- width = widthSize;
- } else {
- width = 100;
- if (heightMode == MeasureSpec.AT_MOST) {
- width = Math.min(width, widthSize);
- }
- }
- Log.i(TAG, "最終結果 : 寬度 : " + width + " , 高度 : " + height);
- setMeasuredDimension(width, height);
- }
- public String getMode(int mode) {
- String modeName = "";
- if(mode == MeasureSpec.EXACTLY){
- modeName = "精準模式";
- }else if(mode == MeasureSpec.AT_MOST){
- modeName = "最大模式";
- }else if(mode == MeasureSpec.UNSPECIFIED){
- modeName = "未定義模式";
- }
- return modeName;
- }
(2) 測試上述代碼
使用下面的自定義組件測試 :
- package cn.org.octopus.wheelview;
- import android.content.Context;
- import android.graphics.Canvas;
- import android.graphics.Color;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.view.View;
- public class MyView extends View {
- public static final String TAG = "octopus.my.view";
- public MyView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public MyView(Context context) {
- super(context);
- }
- public MyView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- //獲取寬度 和 高度的模式 和 大小
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
- Log.i(TAG, "寬度 : widthMode : " + getMode(widthMode) + " , widthSize : " + widthSize + "\n"
- + "高度 : heightMode : " + getMode(heightMode) + " , heightSize : " + heightSize);
- int width = 0;
- int height = 0;
- /*
- * 精準模式
- * 精準模式下 高度就是精確的高度
- */
- if (heightMode == MeasureSpec.EXACTLY) {
- height = heightSize;
- //未定義模式 和 最大模式
- } else {
- //未定義模式下 獲取佈局需要的高度
- height = 100;
- //最大模式下 獲取 佈局高度 和 佈局所需高度的最小值
- if (heightMode == MeasureSpec.AT_MOST) {
- height = Math.min(height, heightSize);
- }
- }
- if (widthMode == MeasureSpec.EXACTLY) {
- width = widthSize;
- } else {
- width = 100;
- if (heightMode == MeasureSpec.AT_MOST) {
- width = Math.min(width, widthSize);
- }
- }
- Log.i(TAG, "最終結果 : 寬度 : " + width + " , 高度 : " + height);
- setMeasuredDimension(width, height);
- }
- public String getMode(int mode) {
- String modeName = "";
- if(mode == MeasureSpec.EXACTLY){
- modeName = "精準模式";
- }else if(mode == MeasureSpec.AT_MOST){
- modeName = "最大模式";
- }else if(mode == MeasureSpec.UNSPECIFIED){
- modeName = "未定義模式";
- }
- return modeName;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- canvas.drawColor(Color.BLUE);
- }
- }
給定具體值情況 :
-- 組件信息 :
- <cn.org.octopus.wheelview.MyView
- android:layout_width="300dip"
- android:layout_height="300dip"/>
- 11-30 01:40:24.304: I/octopus.my.view(2609): 寬度 : widthMode : 精準模式 , widthSize : 450
- 11-30 01:40:24.304: I/octopus.my.view(2609): 高度 : heightMode : 最大模式 , heightSize : 850
- 11-30 01:40:24.304: I/octopus.my.view(2609): 最終結果 : 寬度 : 450 , 高度 : 100
- 11-30 01:40:24.304: I/octopus.my.view(2609): 寬度 : widthMode : 精準模式 , widthSize : 450
- 11-30 01:40:24.304: I/octopus.my.view(2609): 高度 : heightMode : 精準模式 , heightSize : 450
- 11-30 01:40:24.304: I/octopus.my.view(2609): 最終結果 : 寬度 : 450 , 高度 : 450
- 11-30 01:40:24.335: I/octopus.my.view(2609): 寬度 : widthMode : 精準模式 , widthSize : 450
- 11-30 01:40:24.335: I/octopus.my.view(2609): 高度 : heightMode : 最大模式 , heightSize : 850
- 11-30 01:40:24.335: I/octopus.my.view(2609): 最終結果 : 寬度 : 450 , 高度 : 100
- 11-30 01:40:24.335: I/octopus.my.view(2609): 寬度 : widthMode : 精準模式 , widthSize : 450
- 11-30 01:40:24.335: I/octopus.my.view(2609): 高度 : heightMode : 精準模式 , heightSize : 450
- 11-30 01:40:24.335: I/octopus.my.view(2609): 最終結果 : 寬度 : 450 , 高度 : 450
-- 組件信息 :
- <cn.org.octopus.wheelview.MyView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- 11-30 01:37:47.351: I/octopus.my.view(1803): 寬度 : widthMode : 最大模式 , widthSize : 492
- 11-30 01:37:47.351: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 850
- 11-30 01:37:47.351: I/octopus.my.view(1803): 最終結果 : 寬度 : 100 , 高度 : 100
- 11-30 01:37:47.351: I/octopus.my.view(1803): 寬度 : widthMode : 精準模式 , widthSize : 100
- 11-30 01:37:47.351: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 802
- 11-30 01:37:47.351: I/octopus.my.view(1803): 最終結果 : 寬度 : 100 , 高度 : 100
- 11-30 01:37:47.390: I/octopus.my.view(1803): 寬度 : widthMode : 最大模式 , widthSize : 492
- 11-30 01:37:47.390: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 850
- 11-30 01:37:47.390: I/octopus.my.view(1803): 最終結果 : 寬度 : 100 , 高度 : 100
- 11-30 01:37:47.390: I/octopus.my.view(1803): 寬度 : widthMode : 精準模式 , widthSize : 100
- 11-30 01:37:47.390: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 802
- 11-30 01:37:47.390: I/octopus.my.view(1803): 最終結果 : 寬度 : 100 , 高度 : 100
match_parent 情況 :
-- 組件信息 :
- <cn.org.octopus.wheelview.MyView
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
-- 日誌信息 :
- 11-30 01:39:08.296: I/octopus.my.view(2249): 寬度 : widthMode : 精準模式 , widthSize : 492
- 11-30 01:39:08.296: I/octopus.my.view(2249): 高度 : heightMode : 精準模式 , heightSize : 850
- 11-30 01:39:08.296: I/octopus.my.view(2249): 最終結果 : 寬度 : 492 , 高度 : 850
- 11-30 01:39:08.296: I/octopus.my.view(2249): 寬度 : widthMode : 精準模式 , widthSize : 492
- 11-30 01:39:08.296: I/octopus.my.view(2249): 高度 : heightMode : 精準模式 , heightSize : 802
- 11-30 01:39:08.296: I/octopus.my.view(2249): 最終結果 : 寬度 : 492 , 高度 : 802
- 11-30 01:39:08.328: I/octopus.my.view(2249): 寬度 : widthMode : 精準模式 , widthSize : 492
- 11-30 01:39:08.328: I/octopus.my.view(2249): 高度 : heightMode : 精準模式 , heightSize : 850
- 11-30 01:39:08.328: I/octopus.my.view(2249): 最終結果 : 寬度 : 492 , 高度 : 850
- 11-30 01:39:08.328: I/octopus.my.view(2249): 寬度 : widthMode : 精準模式 , widthSize : 492
- 11-30 01:39:08.328: I/octopus.my.view(2249): 高度 : heightMode : 精準模式 , heightSize : 802
- 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
- package cn.org.octopus.wheelview.widget;
- /**
- * WheelView 適配器接口
- * @author han_shuliang([email protected])
- *
- */
- public interface WheelAdapter {
- /**
- * 獲取條目的個數
- *
- * @return
- * WheelView 的條目個數
- */
- public int getItemsCount();
- /**
- * 根據索引位置獲取 WheelView 的條目
- *
- * @param index
- * 條目的索引
- * @return
- * WheelView 上顯示的條目的值
- */
- public String getItem(int index);
- /**
- * 獲取條目的最大長度. 用來定義 WheelView 的寬度. 如果返回 -1, 就會使用默認寬度
- *
- * @return
- * 條目的最大寬度 或者 -1
- */
- public int getMaximumLength();
- }
2. ArrayWheelAdapter
- package cn.org.octopus.wheelview.widget;
- /**
- * WheelView 的適配器類
- *
- * @param <T>
- * 元素類型
- */
- public class ArrayWheelAdapter<T> implements WheelAdapter {
- /** 適配器的 元素集合(數據源) 默認長度爲 -1 */
- public static final int DEFAULT_LENGTH = -1;
- /** 適配器的數據源 */
- private T items[];
- /** WheelView 的寬度 */
- private int length;
- /**
- * 構造方法
- *
- * @param items
- * 適配器數據源 集合 T 類型的數組
- * @param length
- * 適配器數據源 集合 T 數組長度
- */
- public ArrayWheelAdapter(T items[], int length) {
- this.items = items;
- this.length = length;
- }
- /**
- * 構造方法
- *
- * @param items
- * 適配器數據源集合 T 類型數組
- */
- public ArrayWheelAdapter(T items[]) {
- this(items, DEFAULT_LENGTH);
- }
- @Override
- public String getItem(int index) {
- //如果這個索引值合法, 就返回 item 數組對應的元素的字符串形式
- if (index >= 0 && index < items.length) {
- return items[index].toString();
- }
- return null;
- }
- @Override
- public int getItemsCount() {
- //返回 item 數組的長度
- return items.length;
- }
- @Override
- public int getMaximumLength() {
- //返回 item 元素的寬度
- return length;
- }
- }
3. NumericWheelAdapter
- package cn.org.octopus.wheelview.widget;
- /**
- * 顯示數字的 WheelAdapter
- */
- public class NumericWheelAdapter implements WheelAdapter {
- /** 默認最小值 */
- public static final int DEFAULT_MAX_VALUE = 9;
- /** 默認最大值 */
- private static final int DEFAULT_MIN_VALUE = 0;
- /** 設置的最小值 */
- private int minValue;
- /** 設置的最大值 */
- private int maxValue;
- /** 格式化字符串, 用於格式化 貨幣, 科學計數, 十六進制 等格式 */
- private String format;
- /**
- * 默認的構造方法, 使用默認的最大最小值
- */
- public NumericWheelAdapter() {
- this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE);
- }
- /**
- * 構造方法
- *
- * @param minValue
- * 最小值
- * @param maxValue
- * 最大值
- */
- public NumericWheelAdapter(int minValue, int maxValue) {
- this(minValue, maxValue, null);
- }
- /**
- * 構造方法
- *
- * @param minValue
- * 最小值
- * @param maxValue
- * 最大值
- * @param format
- * 格式化字符串
- */
- public NumericWheelAdapter(int minValue, int maxValue, String format) {
- this.minValue = minValue;
- this.maxValue = maxValue;
- this.format = format;
- }
- @Override
- public String getItem(int index) {
- String result = "";
- if (index >= 0 && index < getItemsCount()) {
- int value = minValue + index;
- //如果 format 不爲 null, 那麼格式化字符串, 如果爲 null, 直接返回數字
- if(format != null){
- result = String.format(format, value);
- }else{
- result = Integer.toString(value);
- }
- return result;
- }
- return null;
- }
- @Override
- public int getItemsCount() {
- //返回數字總個數
- return maxValue - minValue + 1;
- }
- @Override
- public int getMaximumLength() {
- //獲取 最大值 和 最小值 中的 較大的數字
- int max = Math.max(Math.abs(maxValue), Math.abs(minValue));
- //獲取這個數字 的 字符串形式的 字符串長度
- int maxLen = Integer.toString(max).length();
- if (minValue < 0) {
- maxLen++;
- }
- return maxLen;
- }
- }
4. OnWheelChangedListener
- package cn.org.octopus.wheelview.widget;
- /**
- * 條目改變監聽器
- */
- public interface OnWheelChangedListener {
- /**
- * 當前條目改變時回調該方法
- *
- * @param wheel
- * 條目改變的 WheelView 對象
- * @param oldValue
- * WheelView 舊的條目值
- * @param newValue
- * WheelView 新的條目值
- */
- void onChanged(WheelView wheel, int oldValue, int newValue);
- }
5. OnWheelScrollListener
- package cn.org.octopus.wheelview.widget;
- /**
- * WheelView 滾動監聽器
- */
- public interface OnWheelScrollListener {
- /**
- * 在 WheelView 滾動開始的時候回調該接口
- *
- * @param wheel
- * 開始滾動的 WheelView 對象
- */
- void onScrollingStarted(WheelView wheel);
- /**
- * 在 WheelView 滾動結束的時候回調該接口
- *
- * @param wheel
- * 結束滾動的 WheelView 對象
- */
- void onScrollingFinished(WheelView wheel);
- }
6. WheelView
- package cn.org.octopus.wheelview.widget;
- import java.util.LinkedList;
- import java.util.List;
- import cn.org.octopus.wheelview.R;
- import android.annotation.SuppressLint;
- import android.content.Context;
- import android.graphics.Canvas;
- import android.graphics.Paint;
- import android.graphics.Rect;
- import android.graphics.drawable.Drawable;
- import android.graphics.drawable.GradientDrawable;
- import android.graphics.drawable.GradientDrawable.Orientation;
- import android.os.Handler;
- import android.os.Message;
- import android.text.Layout;
- import android.text.StaticLayout;
- import android.text.TextPaint;
- import android.util.AttributeSet;
- import android.view.GestureDetector;
- import android.view.GestureDetector.SimpleOnGestureListener;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.animation.Interpolator;
- import android.widget.Scroller;
- /**
- * WheelView 主對象
- */
- public class WheelView extends View {
- /** 滾動花費時間 Scrolling duration */
- private static final int SCROLLING_DURATION = 400;
- /** 最小的滾動值, 每次最少滾動一個單位 */
- private static final int MIN_DELTA_FOR_SCROLLING = 1;
- /** 當前條目中的文字顏色 */
- private static final int VALUE_TEXT_COLOR = 0xF0FF6347;
- /** 非當前條目的文字顏色 */
- private static final int ITEMS_TEXT_COLOR = 0xFF000000;
- /** 頂部和底部的陰影顏色 */
- //private static final int[] SHADOWS_COLORS = new int[] { 0xFF5436EE, 0x0012CEAE, 0x0012CEAE };
- private static final int[] SHADOWS_COLORS = new int[] { 0xFF111111, 0x00AAAAAA, 0x00AAAAAA };
- /** 額外的條目高度 Additional items height (is added to standard text item height) */
- private static final int ADDITIONAL_ITEM_HEIGHT = 15;
- /** 字體大小 */
- private static final int TEXT_SIZE = 24;
- /** 頂部 和 底部 條目的隱藏大小,
- * 如果是正數 會隱藏一部份,
- * 0 頂部 和 底部的字正好緊貼 邊緣,
- * 負數時 頂部和底部 與 字有一定間距 */
- private static final int ITEM_OFFSET = TEXT_SIZE / 5;
- /** Additional width for items layout */
- private static final int ADDITIONAL_ITEMS_SPACE = 10;
- /** Label offset */
- private static final int LABEL_OFFSET = 8;
- /** Left and right padding value */
- private static final int PADDING = 10;
- /** 默認的可顯示的條目數 */
- private static final int DEF_VISIBLE_ITEMS = 5;
- /** WheelView 適配器 */
- private WheelAdapter adapter = null;
- /** 當前顯示的條目索引 */
- private int currentItem = 0;
- /** 條目寬度 */
- private int itemsWidth = 0;
- /** 標籤寬度 */
- private int labelWidth = 0;
- /** 可見的條目數 */
- private int visibleItems = DEF_VISIBLE_ITEMS;
- /** 條目高度 */
- private int itemHeight = 0;
- /** 繪製普通條目畫筆 */
- private TextPaint itemsPaint;
- /** 繪製選中條目畫筆 */
- private TextPaint valuePaint;
- /** 普通條目佈局
- * StaticLayout 佈局用於控制 TextView 組件, 一般情況下不會直接使用該組件,
- * 除非你自定義一個組件 或者 想要直接調用 Canvas.drawText() 方法
- * */
- private StaticLayout itemsLayout;
- private StaticLayout labelLayout;
- /** 選中條目佈局 */
- private StaticLayout valueLayout;
- /** 標籤 在選中條目的右邊出現 */
- private String label;
- /** 選中條目的背景圖片 */
- private Drawable centerDrawable;
- /** 頂部陰影圖片 */
- private GradientDrawable topShadow;
- /** 底部陰影圖片 */
- private GradientDrawable bottomShadow;
- /** 是否在滾動 */
- private boolean isScrollingPerformed;
- /** 滾動的位置 */
- private int scrollingOffset;
- /** 手勢檢測器 */
- private GestureDetector gestureDetector;
- /**
- * Scroll 類封裝了滾動動作.
- * 開發者可以使用 Scroll 或者 Scroll 實現類 去收集產生一個滾動動畫所需要的數據, 返回一個急衝滑動的手勢.
- * 該對象可以追蹤隨着時間推移滾動的偏移量, 但是這些對象不會自動向 View 對象提供這些位置.
- * 如果想要使滾動動畫看起來比較平滑, 開發者需要在適當的時機 獲取 和 使用新的座標;
- * */
- private Scroller scroller;
- /** 之前所在的 y 軸位置 */
- private int lastScrollY;
- /** 是否循環 */
- boolean isCyclic = false;
- /** 條目改變監聽器集合 封裝了條目改變方法, 當條目改變時回調 */
- private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>();
- /** 條目滾動監聽器集合, 該監聽器封裝了 開始滾動方法, 結束滾動方法 */
- private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>();
- /**
- * 構造方法
- */
- public WheelView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initData(context);
- }
- /**
- * 構造方法
- */
- public WheelView(Context context, AttributeSet attrs) {
- super(context, attrs);
- initData(context);
- }
- /**
- * 構造方法
- */
- public WheelView(Context context) {
- super(context);
- initData(context);
- }
- /**
- * 初始化數據
- *
- * @param context
- * 上下文對象
- */
- private void initData(Context context) {
- //創建一個手勢處理
- gestureDetector = new GestureDetector(context, gestureListener);
- /*
- * 是否允許長按操作,
- * 如果設置爲 true 用戶按下不鬆開, 會返回一個長按事件,
- * 如果設置爲 false, 按下不鬆開滑動的話 會收到滾動事件.
- */
- gestureDetector.setIsLongpressEnabled(false);
- //使用默認的 時間 和 插入器 創建一個滾動器
- scroller = new Scroller(context);
- }
- /**
- * 獲取該 WheelView 的適配器
- *
- * @return
- * 返回適配器
- */
- public WheelAdapter getAdapter() {
- return adapter;
- }
- /**
- * 設置適配器
- *
- * @param adapter
- * 要設置的適配器
- */
- public void setAdapter(WheelAdapter adapter) {
- this.adapter = adapter;
- invalidateLayouts();
- invalidate();
- }
- /**
- * 設置 Scroll 的插入器
- *
- * @param interpolator
- * the interpolator
- */
- public void setInterpolator(Interpolator interpolator) {
- //強制停止滾動
- scroller.forceFinished(true);
- //創建一個 Scroll 對象
- scroller = new Scroller(getContext(), interpolator);
- }
- /**
- * 獲取課件條目數
- *
- * @return the count of visible items
- */
- public int getVisibleItems() {
- return visibleItems;
- }
- /**
- * 設置可見條目數
- *
- * @param count
- * the new count
- */
- public void setVisibleItems(int count) {
- visibleItems = count;
- invalidate();
- }
- /**
- * 獲取標籤
- *
- * @return the label
- */
- public String getLabel() {
- return label;
- }
- /**
- * 設置標籤
- *
- * @param newLabel
- * the label to set
- */
- public void setLabel(String newLabel) {
- if (label == null || !label.equals(newLabel)) {
- label = newLabel;
- labelLayout = null;
- invalidate();
- }
- }
- /**
- * 添加 WheelView 選擇的元素改變監聽器
- *
- * @param listener
- * the listener
- */
- public void addChangingListener(OnWheelChangedListener listener) {
- changingListeners.add(listener);
- }
- /**
- * 移除 WheelView 元素改變監聽器
- *
- * @param listener
- * the listener
- */
- public void removeChangingListener(OnWheelChangedListener listener) {
- changingListeners.remove(listener);
- }
- /**
- * 回調元素改變監聽器集合的元素改變監聽器元素的元素改變方法
- *
- * @param oldValue
- * 舊的 WheelView選中的值
- * @param newValue
- * 新的 WheelView選中的值
- */
- protected void notifyChangingListeners(int oldValue, int newValue) {
- for (OnWheelChangedListener listener : changingListeners) {
- listener.onChanged(this, oldValue, newValue);
- }
- }
- /**
- * 添加 WheelView 滾動監聽器
- *
- * @param listener
- * the listener
- */
- public void addScrollingListener(OnWheelScrollListener listener) {
- scrollingListeners.add(listener);
- }
- /**
- * 移除 WheelView 滾動監聽器
- *
- * @param listener
- * the listener
- */
- public void removeScrollingListener(OnWheelScrollListener listener) {
- scrollingListeners.remove(listener);
- }
- /**
- * 通知監聽器開始滾動
- */
- protected void notifyScrollingListenersAboutStart() {
- for (OnWheelScrollListener listener : scrollingListeners) {
- //回調開始滾動方法
- listener.onScrollingStarted(this);
- }
- }
- /**
- * 通知監聽器結束滾動
- */
- protected void notifyScrollingListenersAboutEnd() {
- for (OnWheelScrollListener listener : scrollingListeners) {
- //回調滾動結束方法
- listener.onScrollingFinished(this);
- }
- }
- /**
- * 獲取當前選中元素的索引
- *
- * @return
- * 當前元素索引
- */
- public int getCurrentItem() {
- return currentItem;
- }
- /**
- * 設置當前元素的位置, 如果索引是錯誤的 不進行任何操作
- * -- 需要考慮該 WheelView 是否能循環
- * -- 根據是否需要滾動動畫來確定是 ①滾動到目的位置 還是 ②晴空所有條目然後重繪
- *
- * @param index
- * 要設置的元素索引值
- * @param animated
- * 動畫標誌位
- */
- public void setCurrentItem(int index, boolean animated) {
- //如果沒有適配器或者元素個數爲0 直接返回
- if (adapter == null || adapter.getItemsCount() == 0) {
- return; // throw?
- }
- //目標索引小於 0 或者大於 元素索引最大值(個數 -1)
- if (index < 0 || index >= adapter.getItemsCount()) {
- //入股WheelView 可循環, 修正索引值, 如果不可循環直接返回
- if (isCyclic) {
- while (index < 0) {
- index += adapter.getItemsCount();
- }
- index %= adapter.getItemsCount();
- } else {
- return; // throw?
- }
- }
- //如果當前的索引不是傳入的 索引
- if (index != currentItem) {
- /*
- * 如果需要動畫, 就滾動到目標位置
- * 如果不需要動畫, 重新設置佈局
- */
- if (animated) {
- /*
- * 開始滾動, 每個元素滾動間隔 400 ms, 滾動次數是 目標索引值 減去 當前索引值, 這是滾動的真實方法
- */
- scroll(index - currentItem, SCROLLING_DURATION);
- } else {
- //所有佈局設置爲 null, 滾動位置設置爲 0
- invalidateLayouts();
- int old = currentItem;
- currentItem = index;
- //便利回調元素改變監聽器集合中的監聽器元素中的元素改變方法
- notifyChangingListeners(old, currentItem);
- //重繪
- invalidate();
- }
- }
- }
- /**
- * 設置當前選中的條目, 沒有動畫, 當索引出錯不做任何操作
- *
- * @param index
- * 要設置的索引
- */
- public void setCurrentItem(int index) {
- setCurrentItem(index, false);
- }
- /**
- * 獲取 WheelView 是否可以循環
- * -- 如果可循環 : 第一個之前是最後一個, 最後一個之後是第一個;
- * -- 如果不可循環 : 到第一個就不能上翻, 最後一個不能下翻
- *
- * @return
- */
- public boolean isCyclic() {
- return isCyclic;
- }
- /**
- * 設置 WheelView 循環標誌
- *
- * @param isCyclic
- * the flag to set
- */
- public void setCyclic(boolean isCyclic) {
- this.isCyclic = isCyclic;
- invalidate();
- invalidateLayouts();
- }
- /**
- * 使佈局無效
- * 將 選中條目 和 普通條目設置爲 null, 滾動位置設置爲0
- */
- private void invalidateLayouts() {
- itemsLayout = null;
- valueLayout = null;
- scrollingOffset = 0;
- }
- /**
- * 初始化資源
- */
- private void initResourcesIfNecessary() {
- /*
- * 設置繪製普通條目的畫筆, 允許抗拒齒, 允許 fake-bold
- * 設置文字大小爲 24
- */
- if (itemsPaint == null) {
- itemsPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FAKE_BOLD_TEXT_FLAG);
- itemsPaint.setTextSize(TEXT_SIZE);
- }
- /*
- * 設置繪製選中條目的畫筆
- * 設置文字大小 24
- */
- if (valuePaint == null) {
- valuePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FAKE_BOLD_TEXT_FLAG | Paint.DITHER_FLAG);
- valuePaint.setTextSize(TEXT_SIZE);
- valuePaint.setShadowLayer(0.1f, 0, 0.1f, 0xFFC0C0C0);
- }
- //選中的條目背景
- if (centerDrawable == null) {
- centerDrawable = getContext().getResources().getDrawable(R.drawable.wheel_val);
- }
- //創建頂部陰影圖片
- if (topShadow == null) {
- /*
- * 構造方法中傳入顏色漸變方向
- * 陰影顏色
- */
- topShadow = new GradientDrawable(Orientation.TOP_BOTTOM, SHADOWS_COLORS);
- }
- //創建底部陰影圖片
- if (bottomShadow == null) {
- bottomShadow = new GradientDrawable(Orientation.BOTTOM_TOP, SHADOWS_COLORS);
- }
- /*
- * 設置 View 組件的背景
- */
- setBackgroundResource(R.drawable.wheel_bg);
- }
- /**
- * 計算佈局期望的高度
- *
- * @param layout
- * 組件的佈局的
- * @return
- * 佈局需要的高度
- */
- private int getDesiredHeight(Layout layout) {
- if (layout == null) {
- return 0;
- }
- /*
- * 佈局需要的高度是 條目個數 * 可見條目數 減去 頂部和底部隱藏的一部份 減去 額外的條目高度
- */
- int desired = getItemHeight() * visibleItems - ITEM_OFFSET * 2 - ADDITIONAL_ITEM_HEIGHT;
- // 將計算的佈局高度 與 最小高度比較, 取最大值
- desired = Math.max(desired, getSuggestedMinimumHeight());
- return desired;
- }
- /**
- * 根據條目獲取字符串
- *
- * @param index
- * 條目索引
- * @return
- * 條目顯示的字符串
- */
- private String getTextItem(int index) {
- if (adapter == null || adapter.getItemsCount() == 0) {
- return null;
- }
- //適配器顯示的字符串個數
- int count = adapter.getItemsCount();
- //考慮 index 小於 0 的情況
- if ((index < 0 || index >= count) && !isCyclic) {
- return null;
- } else {
- while (index < 0) {
- index = count + index;
- }
- }
- //index 大於 0
- index %= count;
- return adapter.getItem(index);
- }
- /**
- * 根據當前值創建 字符串
- *
- * @param useCurrentValue
- * 是否在滾動
- * @return the text
- * 生成的字符串
- */
- private String buildText(boolean useCurrentValue) {
- //創建字符串容器
- StringBuilder itemsText = new StringBuilder();
- //計算出顯示的條目相對位置, 例如顯示 5個, 第 3 個是正中見選中的佈局
- int addItems = visibleItems / 2 + 1;
- /*
- * 遍歷顯示的條目
- * 獲取當前顯示條目 上下 各 addItems 個文本, 將該文本添加到顯示文本中去
- * 如果不是最後一個 都加上回車
- */
- for (int i = currentItem - addItems; i <= currentItem + addItems; i++) {
- //如果在滾動
- if (useCurrentValue || i != currentItem) {
- String text = getTextItem(i);
- if (text != null) {
- itemsText.append(text);
- }
- }
- if (i < currentItem + addItems) {
- itemsText.append("\n");
- }
- }
- return itemsText.toString();
- }
- /**
- * 返回 條目的字符串
- *
- * @return
- * 條目最大寬度
- */
- private int getMaxTextLength() {
- WheelAdapter adapter = getAdapter();
- if (adapter == null) {
- return 0;
- }
- //如果獲取的最大條目寬度不爲 -1, 可以直接返回該條目寬度
- int adapterLength = adapter.getMaximumLength();
- if (adapterLength > 0) {
- return adapterLength;
- }
- String maxText = null;
- int addItems = visibleItems / 2;
- /*
- * 遍歷當前顯示的條目, 獲取字符串長度最長的那個, 返回這個最長的字符串長度
- */
- for (int i = Math.max(currentItem - addItems, 0); i < Math.min(currentItem + visibleItems,
- adapter.getItemsCount()); i++) {
- String text = adapter.getItem(i);
- if (text != null && (maxText == null || maxText.length() < text.length())) {
- maxText = text;
- }
- }
- return maxText != null ? maxText.length() : 0;
- }
- /**
- * 獲取每個條目的高度
- *
- * @return
- * 條目的高度
- */
- private int getItemHeight() {
- //如果條目高度不爲 0, 直接返回
- if (itemHeight != 0) {
- return itemHeight;
- //如果條目的高度爲 0, 並且普通條目佈局不爲null, 條目個數大於 2
- } else if (itemsLayout != null && itemsLayout.getLineCount() > 2) {
- /*
- * itemsLayout.getLineTop(2) : 獲取頂部第二行上面的垂直(y軸)位置, 如果行數等於
- */
- itemHeight = itemsLayout.getLineTop(2) - itemsLayout.getLineTop(1);
- return itemHeight;
- }
- //如果上面都不符合, 使用整體高度處以 顯示條目數
- return getHeight() / visibleItems;
- }
- /**
- * 計算寬度並創建文字佈局
- *
- * @param widthSize
- * 輸入的佈局寬度
- * @param mode
- * 佈局模式
- * @return
- * 計算的寬度
- */
- private int calculateLayoutWidth(int widthSize, int mode) {
- initResourcesIfNecessary();
- int width = widthSize;
- //獲取最長的條目顯示字符串字符個數
- int maxLength = getMaxTextLength();
- if (maxLength > 0) {
- /*
- * 使用方法 FloatMath.ceil() 方法有以下警告
- * Use java.lang.Math#ceil instead of android.util.FloatMath#ceil() since it is faster as of API 8
- */
- //float textWidth = FloatMath.ceil(Layout.getDesiredWidth("0", itemsPaint));
- //向上取整 計算一個字符串寬度
- float textWidth = (float) Math.ceil(Layout.getDesiredWidth("0", itemsPaint));
- //獲取字符串總的寬度
- itemsWidth = (int) (maxLength * textWidth);
- } else {
- itemsWidth = 0;
- }
- //總寬度加上一些間距
- itemsWidth += ADDITIONAL_ITEMS_SPACE; // make it some more
- //計算 label 的長度
- labelWidth = 0;
- if (label != null && label.length() > 0) {
- labelWidth = (int) Math.ceil(Layout.getDesiredWidth(label, valuePaint));
- //labelWidth = (int) FloatMath.ceil(Layout.getDesiredWidth(label, valuePaint));
- }
- boolean recalculate = false;
- //精準模式
- if (mode == MeasureSpec.EXACTLY) {
- //精準模式下, 寬度就是給定的寬度
- width = widthSize;
- recalculate = true;
- } else {
- //未定義模式
- width = itemsWidth + labelWidth + 2 * PADDING;
- if (labelWidth > 0) {
- width += LABEL_OFFSET;
- }
- // 獲取 ( 計算出來的寬度 與 最小寬度的 ) 最大值
- width = Math.max(width, getSuggestedMinimumWidth());
- //最大模式 如果 給定的寬度 小於 計算出來的寬度, 那麼使用最小的寬度 ( 給定寬度 | 計算出來的寬度 )
- if (mode == MeasureSpec.AT_MOST && widthSize < width) {
- width = widthSize;
- recalculate = true;
- }
- }
- /*
- * 重新計算寬度 , 如果寬度是給定的寬度, 不是我們計算出來的寬度, 需要重新進行計算
- * 重新計算的寬度是用於
- *
- * 計算 itemsWidth , 這個與返回的 寬度無關, 與創建佈局有關
- */
- if (recalculate) {
- int pureWidth = width - LABEL_OFFSET - 2 * PADDING;
- if (pureWidth <= 0) {
- itemsWidth = labelWidth = 0;
- }
- if (labelWidth > 0) {
- double newWidthItems = (double) itemsWidth * pureWidth / (itemsWidth + labelWidth);
- itemsWidth = (int) newWidthItems;
- labelWidth = pureWidth - itemsWidth;
- } else {
- itemsWidth = pureWidth + LABEL_OFFSET; // no label
- }
- }
- if (itemsWidth > 0) {
- //創建佈局
- createLayouts(itemsWidth, labelWidth);
- }
- return width;
- }
- /**
- * 創建佈局
- *
- * @param widthItems
- * 佈局條目寬度
- * @param widthLabel
- * label 寬度
- */
- private void createLayouts(int widthItems, int widthLabel) {
- /*
- * 創建普通條目佈局
- * 如果 普通條目佈局 爲 null 或者 普通條目佈局的寬度 大於 傳入的寬度, 這時需要重新創建佈局
- * 如果 普通條目佈局存在, 並且其寬度小於傳入的寬度, 此時需要將
- */
- if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {
- /*
- * android.text.StaticLayout.StaticLayout(
- * CharSequence source, TextPaint paint,
- * int width, Alignment align,
- * float spacingmult, float spacingadd, boolean includepad)
- * 傳入參數介紹 :
- * CharSequence source : 需要分行顯示的字符串
- * TextPaint paint : 繪製字符串的畫筆
- * int width : 條目的寬度
- * Alignment align : Layout 的對齊方式, ALIGN_CENTER 居中對齊, ALIGN_NORMAL 左對齊, Alignment.ALIGN_OPPOSITE 右對齊
- * float spacingmult : 行間距, 1.5f 代表 1.5 倍字體高度
- * float spacingadd : 基礎行距上增加多少 , 真實行間距 等於 spacingmult 和 spacingadd 的和
- * boolean includepad :
- */
- itemsLayout = new StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,
- widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,
- ADDITIONAL_ITEM_HEIGHT, false);
- } else {
- //調用 Layout 內置的方法 increaseWidthTo 將寬度提升到指定的寬度
- itemsLayout.increaseWidthTo(widthItems);
- }
- /*
- * 創建選中條目
- */
- if (!isScrollingPerformed && (valueLayout == null || valueLayout.getWidth() > widthItems)) {
- String text = getAdapter() != null ? getAdapter().getItem(currentItem) : null;
- valueLayout = new StaticLayout(text != null ? text : "", valuePaint, widthItems,
- widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,
- ADDITIONAL_ITEM_HEIGHT, false);
- } else if (isScrollingPerformed) {
- valueLayout = null;
- } else {
- valueLayout.increaseWidthTo(widthItems);
- }
- /*
- * 創建標籤條目
- */
- if (widthLabel > 0) {
- if (labelLayout == null || labelLayout.getWidth() > widthLabel) {
- labelLayout = new StaticLayout(label, valuePaint, widthLabel, Layout.Alignment.ALIGN_NORMAL, 1,
- ADDITIONAL_ITEM_HEIGHT, false);
- } else {
- labelLayout.increaseWidthTo(widthLabel);
- }
- }
- }
- /*
- * 測量組件大小
- * (non-Javadoc)
- * @see android.view.View#onMeasure(int, int)
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- //獲取寬度 和 高度的模式 和 大小
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
- //寬度就是 計算的佈局的寬度
- int width = calculateLayoutWidth(widthSize, widthMode);
- int height;
- /*
- * 精準模式
- * 精準模式下 高度就是精確的高度
- */
- if (heightMode == MeasureSpec.EXACTLY) {
- height = heightSize;
- //未定義模式 和 最大模式
- } else {
- //未定義模式下 獲取佈局需要的高度
- height = getDesiredHeight(itemsLayout);
- //最大模式下 獲取 佈局高度 和 佈局所需高度的最小值
- if (heightMode == MeasureSpec.AT_MOST) {
- height = Math.min(height, heightSize);
- }
- }
- //設置組件的寬和高
- setMeasuredDimension(width, height);
- }
- /*
- * 繪製組件
- * (non-Javadoc)
- * @see android.view.View#onDraw(android.graphics.Canvas)
- */
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //如果條目佈局爲 null, 就創建該佈局
- if (itemsLayout == null) {
- /*
- * 如果 條目寬度爲0, 說明該寬度沒有計算, 先計算, 計算完之後會創建佈局
- * 如果 條目寬度 大於 0, 說明已經計算過寬度了, 直接創建佈局
- */
- if (itemsWidth == 0) {
- calculateLayoutWidth(getWidth(), MeasureSpec.EXACTLY);
- } else {
- //創建普通條目佈局, 選中條目佈局, 標籤條目佈局
- createLayouts(itemsWidth, labelWidth);
- }
- }
- //如果條目寬度大於0
- if (itemsWidth > 0) {
- canvas.save();
- // 使用平移方法忽略 填充的空間 和 頂部底部隱藏的一部份條目
- canvas.translate(PADDING, -ITEM_OFFSET);
- //繪製普通條目
- drawItems(canvas);
- //繪製選中條目
- drawValue(canvas);
- canvas.restore();
- }
- //在中心位置繪製
- drawCenterRect(canvas);
- //繪製陰影
- drawShadows(canvas);
- }
- /**
- * Draws shadows on top and bottom of control
- *
- * @param canvas
- * the canvas for drawing
- */
- private void drawShadows(Canvas canvas) {
- topShadow.setBounds(0, 0, getWidth(), getHeight() / visibleItems);
- topShadow.draw(canvas);
- bottomShadow.setBounds(0, getHeight() - getHeight() / visibleItems, getWidth(), getHeight());
- bottomShadow.draw(canvas);
- }
- /**
- * 繪製選中條目
- *
- * @param canvas
- * 畫布
- */
- private void drawValue(Canvas canvas) {
- valuePaint.setColor(VALUE_TEXT_COLOR);
- //將當前 View 狀態屬性值 轉爲整型集合, 賦值給 普通條目佈局的繪製屬性
- valuePaint.drawableState = getDrawableState();
- Rect bounds = new Rect();
- //獲取選中條目佈局的邊界
- itemsLayout.getLineBounds(visibleItems / 2, bounds);
- // 繪製標籤
- if (labelLayout != null) {
- canvas.save();
- canvas.translate(itemsLayout.getWidth() + LABEL_OFFSET, bounds.top);
- labelLayout.draw(canvas);
- canvas.restore();
- }
- // 繪製選中條目
- if (valueLayout != null) {
- canvas.save();
- canvas.translate(0, bounds.top + scrollingOffset);
- valueLayout.draw(canvas);
- canvas.restore();
- }
- }
- /**
- * 繪製普通條目
- *
- * @param canvas
- * 畫布
- */
- private void drawItems(Canvas canvas) {
- canvas.save();
- //獲取 y 軸 定點高度
- int top = itemsLayout.getLineTop(1);
- canvas.translate(0, -top + scrollingOffset);
- //設置畫筆顏色
- itemsPaint.setColor(ITEMS_TEXT_COLOR);
- //將當前 View 狀態屬性值 轉爲整型集合, 賦值給 普通條目佈局的繪製屬性
- itemsPaint.drawableState = getDrawableState();
- //將佈局繪製到畫布上
- itemsLayout.draw(canvas);
- canvas.restore();
- }
- /**
- * 繪製當前選中條目的背景圖片
- *
- * @param canvas
- * 畫布
- */
- private void drawCenterRect(Canvas canvas) {
- int center = getHeight() / 2;
- int offset = getItemHeight() / 2;
- centerDrawable.setBounds(0, center - offset, getWidth(), center + offset);
- centerDrawable.draw(canvas);
- }
- /*
- * 繼承自 View 的觸摸事件, 當出現觸摸事件的時候, 就會回調該方法
- * (non-Javadoc)
- * @see android.view.View#onTouchEvent(android.view.MotionEvent)
- */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- //獲取適配器
- WheelAdapter adapter = getAdapter();
- if (adapter == null) {
- return true;
- }
- /*
- * gestureDetector.onTouchEvent(event) : 分析給定的動作, 如果可用, 調用 手勢檢測器的 onTouchEvent 方法
- * -- 參數解析 : ev , 觸摸事件
- * -- 返回值 : 如果手勢監聽器成功執行了該方法, 返回true, 如果執行出現意外 返回 false;
- */
- if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {
- justify();
- }
- return true;
- }
- /**
- * 滾動 WheelView
- *
- * @param delta
- * 滾動的值
- */
- private void doScroll(int delta) {
- scrollingOffset += delta;
- //計算滾動的條目數, 使用滾動的值 處於 單個條目高度, 注意計算整數值
- int count = scrollingOffset / getItemHeight();
- /*
- * pos 是滾動後的目標元素索引
- * 計算當前位置, 當前條目數 減去 滾動的條目數
- * 注意 滾動條目數可正 可負
- */
- int pos = currentItem - count;
- //如果是可循環的, 並且條目數大於0
- if (isCyclic && adapter.getItemsCount() > 0) {
- //設置循環, 如果位置小於0, 那麼該位置就顯示最後一個元素
- while (pos < 0) {
- pos += adapter.getItemsCount();
- }
- //如果位置正無限大, 模條目數 取餘
- pos %= adapter.getItemsCount();
- // (前提 : 不可循環 條目數大於0, 可循環 條目數小於0, 條目數小於0, 不可循環) , 如果滾動在執行
- } else if (isScrollingPerformed) {
- //位置一旦小於0, 計算的位置就賦值爲 0, 條目滾動數爲0
- if (pos < 0) {
- count = currentItem;
- pos = 0;
- //位置大於條目數的時候, 當前位置等於(條目數 - 1), 條目滾動數等於 當前位置 減去 (條目數 - 1)
- } else if (pos >= adapter.getItemsCount()) {
- count = currentItem - adapter.getItemsCount() + 1;
- pos = adapter.getItemsCount() - 1;
- }
- } else {
- // fix position
- pos = Math.max(pos, 0);
- pos = Math.min(pos, adapter.getItemsCount() - 1);
- }
- //滾動的高度
- int offset = scrollingOffset;
- /*
- * 如果當前位置不是滾動後的目標位置, 就將當前位置設置爲目標位置
- * 否則就重繪組件
- */
- if (pos != currentItem) {
- setCurrentItem(pos, false);
- } else {
- //重繪組件
- invalidate();
- }
- // 將滾動後剩餘的小數部分保存
- scrollingOffset = offset - count * getItemHeight();
- if (scrollingOffset > getHeight()) {
- scrollingOffset = scrollingOffset % getHeight() + getHeight();
- }
- }
- /**
- * 手勢監聽器
- */
- private SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() {
- //按下操作
- public boolean onDown(MotionEvent e) {
- //如果滾動在執行
- if (isScrollingPerformed) {
- //滾動強制停止, 按下的時候不能繼續滾動
- scroller.forceFinished(true);
- //清理信息
- clearMessages();
- return true;
- }
- return false;
- }
- /*
- * 手勢監聽器監聽到 滾動操作後回調
- *
- * 參數解析 :
- * MotionEvent e1 : 觸發滾動時第一次按下的事件
- * MotionEvent e2 : 觸發當前滾動的移動事件
- * float distanceX : 自從上一次調用 該方法 到這一次 x 軸滾動的距離,
- * 注意不是 e1 到 e2 的距離, e1 到 e2 的距離是從開始滾動到現在的滾動距離
- * float distanceY : 自從上一次回調該方法到這一次 y 軸滾動的距離
- *
- * 返回值 : 如果事件成功觸發, 執行完了方法中的操作, 返回true, 否則返回 false
- * (non-Javadoc)
- * @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float)
- */
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- //開始滾動, 並回調滾動監聽器集合中監聽器的 開始滾動方法
- startScrolling();
- doScroll((int) -distanceY);
- return true;
- }
- /*
- * 當一個急衝手勢發生後 回調該方法, 會計算出該手勢在 x 軸 y 軸的速率
- *
- * 參數解析 :
- * -- MotionEvent e1 : 急衝動作的第一次觸摸事件;
- * -- MotionEvent e2 : 急衝動作的移動發生的時候的觸摸事件;
- * -- float velocityX : x 軸的速率
- * -- float velocityY : y 軸的速率
- *
- * 返回值 : 如果執行完畢返回 true, 否則返回false, 這個就是自己定義的
- *
- * (non-Javadoc)
- * @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float)
- */
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- //計算上一次的 y 軸位置, 當前的條目高度 加上 剩餘的 不夠一行高度的那部分
- lastScrollY = currentItem * getItemHeight() + scrollingOffset;
- //如果可以循環最大值是無限大, 不能循環就是條目數的高度值
- int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();
- int minY = isCyclic ? -maxY : 0;
- /*
- * Scroll 開始根據一個急衝手勢滾動, 滾動的距離與初速度有關
- * 參數介紹 :
- * -- int startX : 開始時的 X軸位置
- * -- int startY : 開始時的 y軸位置
- * -- int velocityX : 急衝手勢的 x 軸的初速度, 單位 px/s
- * -- int velocityY : 急衝手勢的 y 軸的初速度, 單位 px/s
- * -- int minX : x 軸滾動的最小值
- * -- int maxX : x 軸滾動的最大值
- * -- int minY : y 軸滾動的最小值
- * -- int maxY : y 軸滾動的最大值
- */
- scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);
- setNextMessage(MESSAGE_SCROLL);
- return true;
- }
- };
- // Handler 中的 Message 信息
- /** 滾動信息 */
- private final int MESSAGE_SCROLL = 0;
- /** 調整信息 */
- private final int MESSAGE_JUSTIFY = 1;
- /**
- * 清空之前的 Handler 隊列, 發送下一個消息到 Handler 中
- *
- * @param message
- * 要發送的消息
- */
- private void setNextMessage(int message) {
- //清空 Handler 隊列中的 what 消息
- clearMessages();
- //發送消息到 Handler 中
- animationHandler.sendEmptyMessage(message);
- }
- /**
- * 清空隊列中的信息
- */
- private void clearMessages() {
- //刪除 Handler 執行隊列中的滾動操作
- animationHandler.removeMessages(MESSAGE_SCROLL);
- animationHandler.removeMessages(MESSAGE_JUSTIFY);
- }
- /**
- * 動畫控制器
- * animation handler
- *
- * 可能會造成內存泄露 : 添加註解 HandlerLeak
- * Handler 類應該應該爲static類型,否則有可能造成泄露。
- * 在程序消息隊列中排隊的消息保持了對目標Handler類的應用。
- * 如果Handler是個內部類,那 麼它也會保持它所在的外部類的引用。
- * 爲了避免泄露這個外部類,應該將Handler聲明爲static嵌套類,並且使用對外部類的弱應用。
- */
- @SuppressLint("HandlerLeak")
- private Handler animationHandler = new Handler() {
- public void handleMessage(Message msg) {
- //回調該方法獲取當前位置, 如果返回true, 說明動畫還沒有執行完畢
- scroller.computeScrollOffset();
- //獲取當前 y 位置
- int currY = scroller.getCurrY();
- //獲取已經滾動了的位置, 使用上一次位置 減去 當前位置
- int delta = lastScrollY - currY;
- lastScrollY = currY;
- if (delta != 0) {
- //改變值不爲 0 , 繼續滾動
- doScroll(delta);
- }
- /*
- * 如果滾動到了指定的位置, 滾動還沒有停止
- * 這時需要強制停止
- */
- if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) {
- currY = scroller.getFinalY();
- scroller.forceFinished(true);
- }
- /*
- * 如果滾動沒有停止
- * 再向 Handler 發送一個停止
- */
- if (!scroller.isFinished()) {
- animationHandler.sendEmptyMessage(msg.what);
- } else if (msg.what == MESSAGE_SCROLL) {
- justify();
- } else {
- finishScrolling();
- }
- }
- };
- /**
- * 調整 WheelView
- */
- private void justify() {
- if (adapter == null) {
- return;
- }
- //上一次的 y 軸的位置爲 0
- lastScrollY = 0;
- int offset = scrollingOffset;
- int itemHeight = getItemHeight();
- /*
- * 當滾動補償 大於 0, 說明還有沒有滾動的部分, needToIncrease 是 當前條目是否小於條目數
- * 如果 滾動補償不大於 0, needToIncrease 是當前條目是否大於 0
- */
- boolean needToIncrease = offset > 0 ? currentItem < adapter.getItemsCount() : currentItem > 0;
- if ((isCyclic || needToIncrease) && Math.abs((float) offset) > (float) itemHeight / 2) {
- if (offset < 0)
- offset += itemHeight + MIN_DELTA_FOR_SCROLLING;
- else
- offset -= itemHeight + MIN_DELTA_FOR_SCROLLING;
- }
- if (Math.abs(offset) > MIN_DELTA_FOR_SCROLLING) {
- scroller.startScroll(0, 0, 0, offset, SCROLLING_DURATION);
- setNextMessage(MESSAGE_JUSTIFY);
- } else {
- finishScrolling();
- }
- }
- /**
- * WheelView 開始滾動
- */
- private void startScrolling() {
- //如果沒有滾動, 將滾動狀態 isScrollingPerformed 設爲 true
- if (!isScrollingPerformed) {
- isScrollingPerformed = true;
- //通知監聽器開始滾動 回調所有的 滾動監聽集合中 的 開始滾動方法
- notifyScrollingListenersAboutStart();
- }
- }
- /**
- * 結束滾動
- * 設置滾動狀態爲 false, 回調滾動監聽器的停止滾動方法
- */
- void finishScrolling() {
- if (isScrollingPerformed) {
- notifyScrollingListenersAboutEnd();
- isScrollingPerformed = false;
- }
- //設置佈局無效
- invalidateLayouts();
- //重繪佈局
- invalidate();
- }
- /**
- * 滾動 WheelView
- *
- * @param itemsToSkip
- * 滾動的元素個數
- * @param time
- * 每次滾動的間隔
- */
- public void scroll(int itemsToScroll, int time) {
- //如果有滾動強制停止
- scroller.forceFinished(true);
- lastScrollY = scrollingOffset;
- int offset = itemsToScroll * getItemHeight();
- /*
- * 給定 一個開始點, 滾動距離, 滾動間隔, 開始滾動
- *
- * 參數解析 :
- * 1. 開始的 x 軸位置
- * 2. 開始的 y 軸位置
- * 3. 要滾動 x 軸距離
- * 4. 要滾動 y 軸距離
- * 5. 滾動花費的時間
- */
- scroller.startScroll(0, lastScrollY, 0, offset - lastScrollY, time);
- setNextMessage(MESSAGE_SCROLL);
- //設置開始滾動狀態, 並回調滾動監聽器方法
- startScrolling();
- }
- }
7. Activity 主界面
- package cn.org.octopus.wheelview;
- import android.app.Activity;
- import android.app.AlertDialog;
- import android.app.Fragment;
- import android.content.Context;
- import android.content.DialogInterface;
- import android.os.Bundle;
- import android.view.Gravity;
- import android.view.LayoutInflater;
- import android.view.Menu;
- import android.view.MenuItem;
- import android.view.View;
- import android.view.ViewGroup;
- import android.view.ViewGroup.LayoutParams;
- import android.widget.Button;
- import android.widget.LinearLayout;
- import cn.org.octopus.wheelview.widget.ArrayWheelAdapter;
- import cn.org.octopus.wheelview.widget.OnWheelChangedListener;
- import cn.org.octopus.wheelview.widget.OnWheelScrollListener;
- import cn.org.octopus.wheelview.widget.WheelView;
- public class MainActivity extends Activity{
- public static final String TAG = "octopus.activity";
- private static Button bt_click;
- public String province[] = new String[] { " 河北省 ", " 山西省 ", " 內蒙古 ", " 遼寧省 ", " 吉林省 ", " 黑龍江 ", " 江蘇省 " };
- public String city[][] = new String[][] {
- new String[] {" 石家莊 ", "唐山", "秦皇島", "邯鄲", "邢臺", "保定", "張家口", "承德", "滄州", "廊坊", "衡水"},
- new String[] {"太原", "大同", "陽泉", "長治", "晉城", "朔州", "晉中", "運城", "忻州", "臨汾", "呂梁"},
- new String[] {"呼和浩特", "包頭", "烏海", "赤峯", "通遼", "鄂爾多斯", "呼倫貝爾", "巴彥淖爾", "烏蘭察布", "興安", "錫林郭勒", "阿拉善"},
- new String[] {"瀋陽", "大連", "鞍山", "撫順", "本溪", "丹東", "錦州", "營口", "阜新", "遼陽", "盤錦", "鐵嶺", "朝陽", "葫蘆島"},
- new String[] {"長春", "吉林", "四平", "遼源", "通化", "白山", "松原", "白城", "延邊"},
- new String[] {"哈爾濱", "齊齊哈爾", "雞西", "鶴崗", "雙鴨山", "大慶", "伊春", "佳木斯", "七臺河", "牡丹江", "黑河", "綏化", "大興安嶺"},
- new String[] {"南京", "無錫", "徐州", "常州", "蘇州", "南通", "連雲港", "淮安", "鹽城", "揚州", "鎮江", "泰州", "宿遷"} };
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- if (savedInstanceState == null) {
- getFragmentManager().beginTransaction()
- .add(R.id.container, new PlaceholderFragment()).commit();
- }
- }
- /*
- * 點擊事件
- */
- public void onClick(View view) {
- showSelectDialog(this, "選擇地點", province, city);
- }
- private void showSelectDialog(Context context, String title, final String[] left, final String[][] right) {
- //創建對話框
- AlertDialog dialog = new AlertDialog.Builder(context).create();
- //爲對話框設置標題
- dialog.setTitle(title);
- //創建對話框內容, 創建一個 LinearLayout
- LinearLayout llContent = new LinearLayout(context);
- //將創建的 LinearLayout 設置成橫向的
- llContent.setOrientation(LinearLayout.HORIZONTAL);
- //創建 WheelView 組件
- final WheelView wheelLeft = new WheelView(context);
- //設置 WheelView 組件最多顯示 5 個元素
- wheelLeft.setVisibleItems(5);
- //設置 WheelView 元素是否循環滾動
- wheelLeft.setCyclic(false);
- //設置 WheelView 適配器
- wheelLeft.setAdapter(new ArrayWheelAdapter<String>(left));
- //設置右側的 WheelView
- final WheelView wheelRight = new WheelView(context);
- //設置右側 WheelView 顯示個數
- wheelRight.setVisibleItems(5);
- //設置右側 WheelView 元素是否循環滾動
- wheelRight.setCyclic(true);
- //設置右側 WheelView 的元素適配器
- wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[0]));
- //設置 LinearLayout 的佈局參數
- LinearLayout.LayoutParams paramsLeft = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT, 4);
- paramsLeft.gravity = Gravity.LEFT;
- LinearLayout.LayoutParams paramsRight = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT, 6);
- paramsRight.gravity = Gravity.RIGHT;
- //將 WheelView 對象放到左側 LinearLayout 中
- llContent.addView(wheelLeft, paramsLeft);
- //將 WheelView 對象放到 右側 LinearLayout 中
- llContent.addView(wheelRight, paramsRight);
- //爲左側的 WheelView 設置條目改變監聽器
- wheelLeft.addChangingListener(new OnWheelChangedListener() {
- @Override
- public void onChanged(WheelView wheel, int oldValue, int newValue) {
- //設置右側的 WheelView 的適配器
- wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[newValue]));
- wheelRight.setCurrentItem(right[newValue].length / 2);
- }
- });
- wheelLeft.addScrollingListener(new OnWheelScrollListener() {
- @Override
- public void onScrollingStarted(WheelView wheel) {
- // TODO Auto-generated method stub
- }
- @Override
- public void onScrollingFinished(WheelView wheel) {
- // TODO Auto-generated method stub
- }
- });
- //設置對話框點擊事件 積極
- dialog.setButton(AlertDialog.BUTTON_POSITIVE, "確定", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- int leftPosition = wheelLeft.getCurrentItem();
- String vLeft = left[leftPosition];
- String vRight = right[leftPosition][wheelRight.getCurrentItem()];
- bt_click.setText(vLeft + "-" + vRight);
- dialog.dismiss();
- }
- });
- //設置對話框點擊事件 消極
- dialog.setButton(AlertDialog.BUTTON_NEGATIVE, "取消", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- });
- //將 LinearLayout 設置到 對話框中
- dialog.setView(llContent);
- //顯示對話框
- if (!dialog.isShowing()) {
- dialog.show();
- }
- }
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.main, menu);
- return true;
- }
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- // Handle action bar item clicks here. The action bar will
- // automatically handle clicks on the Home/Up button, so long
- // as you specify a parent activity in AndroidManifest.xml.
- int id = item.getItemId();
- if (id == R.id.action_settings) {
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
- /**
- * A placeholder fragment containing a simple view.
- */
- public static class PlaceholderFragment extends Fragment {
- public PlaceholderFragment() {
- }
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_main, container,
- false);
- bt_click = (Button)rootView.findViewById(R.id.bt_click);
- return rootView;
- }
- }
- }
博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569#t17
代碼下載 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;
代碼下載 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;
博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569#t17
代碼下載 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;
博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569#t17
代碼下載 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;