Android自定義實現迴圈滾輪控制元件WheelView
阿新 • • 發佈:2019-02-20
首先呈上效果圖
現在很多地方都用到了滾輪佈局WheelView,比如在選擇生日的時候,風格類似系統提供的DatePickerDialog,開源的控制元件也有很多,不過大部分都是根據當前專案的需求繪製的介面,因此我就自己寫了一款比較符合自己專案的WheelView。
首先這個控制元件有以下的需求:
1、能夠迴圈滾動,當向上或者向下滑動到臨界值的時候,則迴圈開始滾動
2、中間的一塊有一塊半透明的選擇區,滑動結束時,哪一塊在這個選擇區,就選擇這快。
3、繼承自View進行繪製
然後進行一些關鍵點的講解:
1、整體控制元件繼承自View,在onDraw中進行繪製。整體包含三個模組,整個View、每一塊的條目、中間選擇區的條目(額外繪製一塊灰色區域)。
2、通過動態設定或者預設設定的可顯示條目數,在最上和最下再各加入一塊,意思就是一共繪製showCount+2個條目。
3、當最上面的條目數滑動超過條目高度的一半時,進行動態條目更新:將最下面的條目刪除加入第一個條目、將第一個條目刪除加入最下面的條目。
4、外界可設定條目顯示數、字型大小、顏色、選擇區提示文字(圖中那個年字)、預設選擇項、padding補白等等。
5、在onTouchEvent中,得到手指滑動的漸變值,動態更新當前所有的條目。
6、在onMeasure中動態計算寬度,所有條目的寬度、高度、起始Y座標等等。
7、通過當前條目和被選擇條目的座標,超過一半則視為被選擇,並且滑動到對應的位置。
下面的是WheelView程式碼,主要是計算初始值、得到外面設定的值:
然後是每一個條目類,根據當前的座標進行繪製,根據漸變值改變座標等:package cc.wxf.view.wheel; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; import java.util.List; /** * Created by ccwxf on 2016/3/31. */ public class WheelView extends View { public static final int FONT_COLOR = Color.BLACK; public static final int FONT_SIZE = 30; public static final int PADDING = 10; public static final int SHOW_COUNT = 3; public static final int SELECT = 0; //總體寬度、高度、Item的高度 private int width; private int height; private int itemHeight; //需要顯示的行數 private int showCount = SHOW_COUNT; //當前預設選擇的位置 private int select = SELECT; //字型顏色、大小、補白 private int fontColor = FONT_COLOR; private int fontSize = FONT_SIZE; private int padding = PADDING; //文字列表 private List<String> lists; //選中項的輔助文字,可為空 private String selectTip; //每一項Item和選中項 private List<WheelItem> wheelItems = new ArrayList<WheelItem>(); private WheelSelect wheelSelect = null; //手點選的Y座標 private float mTouchY; //監聽器 private OnWheelViewItemSelectListener listener; public WheelView(Context context) { super(context); } public WheelView(Context context, AttributeSet attrs) { super(context, attrs); } public WheelView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 設定字型的顏色,不設定的話預設為黑色 * @param fontColor * @return */ public WheelView fontColor(int fontColor){ this.fontColor = fontColor; return this; } /** * 設定字型的大小,不設定的話預設為30 * @param fontSize * @return */ public WheelView fontSize(int fontSize){ this.fontSize = fontSize; return this; } /** * 設定文字到上下兩邊的補白,不合適的話預設為10 * @param padding * @return */ public WheelView padding(int padding){ this.padding = padding; return this; } /** * 設定選中項的複製文字,可以不設定 * @param selectTip * @return */ public WheelView selectTip(String selectTip){ this.selectTip = selectTip; return this; } /** * 設定文字列表,必須且必須在build方法之前設定 * @param lists * @return */ public WheelView lists(List<String> lists){ this.lists = lists; return this; } /** * 設定顯示行數,不設定的話預設為3 * @param showCount * @return */ public WheelView showCount(int showCount){ if(showCount % 2 == 0){ throw new IllegalStateException("the showCount must be odd"); } this.showCount = showCount; return this; } /** * 設定預設選中的文字的索引,不設定預設為0 * @param select * @return */ public WheelView select(int select){ this.select = select; return this; } /** * 最後呼叫的方法,判斷是否有必要函式沒有被呼叫 * @return */ public WheelView build(){ if(lists == null){ throw new IllegalStateException("this method must invoke after the method [lists]"); } return this; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //得到總體寬度 width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); // 得到每一個Item的高度 Paint mPaint = new Paint(); mPaint.setTextSize(fontSize); Paint.FontMetrics metrics = mPaint.getFontMetrics(); itemHeight = (int) (metrics.bottom - metrics.top) + 2 * padding; //初始化每一個WheelItem initWheelItems(width, itemHeight); //初始化WheelSelect wheelSelect = new WheelSelect(showCount / 2 * itemHeight, width, itemHeight, selectTip, fontColor, fontSize, padding); //得到所有的高度 height = itemHeight * showCount; super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } /** * 建立顯示個數+2個WheelItem * @param width * @param itemHeight */ private void initWheelItems(int width, int itemHeight) { wheelItems.clear(); for(int i = 0; i < showCount + 2; i++){ int startY = itemHeight * (i - 1); int stringIndex = select - showCount / 2 - 1 + i; if(stringIndex < 0){ stringIndex = lists.size() + stringIndex; } wheelItems.add(new WheelItem(startY, width, itemHeight, fontColor, fontSize, lists.get(stringIndex))); } } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: mTouchY = event.getY(); return true; case MotionEvent.ACTION_MOVE: float dy = event.getY() - mTouchY; mTouchY = event.getY(); handleMove(dy); break; case MotionEvent.ACTION_UP: handleUp(); break; } return super.onTouchEvent(event); } /** * 處理移動操作 * @param dy */ private void handleMove(float dy) { //調整座標 for(WheelItem item : wheelItems){ item.adjust(dy); } invalidate(); //調整 adjust(); } /** * 處理擡起操作 */ private void handleUp(){ int index = -1; //得到應該選擇的那一項 for(int i = 0; i < wheelItems.size(); i++){ WheelItem item = wheelItems.get(i); //如果startY在selectItem的中點上面,則將該項作為選擇項 if(item.getStartY() > wheelSelect.getStartY() && item.getStartY() < (wheelSelect.getStartY() + itemHeight / 2)){ index = i; break; } //如果startY在selectItem的中點下面,則將上一項作為選擇項 if(item.getStartY() >= (wheelSelect.getStartY() + itemHeight / 2) && item.getStartY() < (wheelSelect.getStartY() + itemHeight)){ index = i - 1; break; } } //如果沒找到或者其他因素,直接返回 if(index == -1){ return; } //得到偏移的位移 float dy = wheelSelect.getStartY() - wheelItems.get(index).getStartY(); //調整座標 for(WheelItem item : wheelItems){ item.adjust(dy); } invalidate(); // 調整 adjust(); //設定選擇項 int stringIndex = lists.indexOf(wheelItems.get(index).getText()); if(stringIndex != -1){ select = stringIndex; if(listener != null){ listener.onItemSelect(select); } } } /** * 調整Item移動和迴圈顯示 */ private void adjust(){ //如果向下滑動超出半個Item的高度,則調整容器 if(wheelItems.get(0).getStartY() >= -itemHeight / 2 ){ //移除最後一個Item重用 WheelItem item = wheelItems.remove(wheelItems.size() - 1); //設定起點Y座標 item.setStartY(wheelItems.get(0).getStartY() - itemHeight); //得到文字在容器中的索引 int index = lists.indexOf(wheelItems.get(0).getText()); if(index == -1){ return; } index -= 1; if(index < 0){ index = lists.size() + index; } //設定文字 item.setText(lists.get(index)); //新增到最開始 wheelItems.add(0, item); invalidate(); return; } //如果向上滑超出半個Item的高度,則調整容器 if(wheelItems.get(0).getStartY() <= (-itemHeight / 2 - itemHeight)){ //移除第一個Item重用 WheelItem item = wheelItems.remove(0); //設定起點Y座標 item.setStartY(wheelItems.get(wheelItems.size() - 1).getStartY() + itemHeight); //得到文字在容器中的索引 int index = lists.indexOf(wheelItems.get(wheelItems.size() - 1).getText()); if(index == -1){ return; } index += 1; if(index >= lists.size()){ index = 0; } //設定文字 item.setText(lists.get(index)); //新增到最後面 wheelItems.add(item); invalidate(); return; } } /** * 得到當前的選擇項 */ public int getSelectItem(){ return select; } @Override protected void onDraw(Canvas canvas) { //繪製每一項Item for(WheelItem item : wheelItems){ item.onDraw(canvas); } //繪製陰影 if(wheelSelect != null){ wheelSelect.onDraw(canvas); } } /** * 設定監聽器 * @param listener * @return */ public WheelView listener(OnWheelViewItemSelectListener listener){ this.listener = listener; return this; } public interface OnWheelViewItemSelectListener{ void onItemSelect(int index); } }
package cc.wxf.view.wheel;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
/**
* Created by ccwxf on 2016/3/31.
*/
public class WheelItem {
// 起點Y座標、寬度、高度
private float startY;
private int width;
private int height;
//四點座標
private RectF rect = new RectF();
//字型大小、顏色
private int fontColor;
private int fontSize;
private String text;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public WheelItem(float startY, int width, int height, int fontColor, int fontSize, String text) {
this.startY = startY;
this.width = width;
this.height = height;
this.fontColor = fontColor;
this.fontSize = fontSize;
this.text = text;
adjust(0);
}
/**
* 根據Y座標的變化值,調整四點座標值
* @param dy
*/
public void adjust(float dy){
startY += dy;
rect.left = 0;
rect.top = startY;
rect.right = width;
rect.bottom = startY + height;
}
public float getStartY() {
return startY;
}
/**
* 直接設定Y座標屬性,調整四點座標屬性
* @param startY
*/
public void setStartY(float startY) {
this.startY = startY;
rect.left = 0;
rect.top = startY;
rect.right = width;
rect.bottom = startY + height;
}
public void setText(String text) {
this.text = text;
}
public String getText() {
return text;
}
public void onDraw(Canvas mCanvas){
//設定鋼筆屬性
mPaint.setTextSize(fontSize);
mPaint.setColor(fontColor);
//得到字型的寬度
int textWidth = (int)mPaint.measureText(text);
//drawText的繪製起點是左下角,y軸起點為baseLine
Paint.FontMetrics metrics = mPaint.getFontMetrics();
int baseLine = (int)(rect.centerY() + (metrics.bottom - metrics.top) / 2 - metrics.bottom);
//居中繪製
mCanvas.drawText(text, rect.centerX() - textWidth / 2, baseLine, mPaint);
}
}
最後是選擇項,就是額外得在中間區域繪製一塊灰色區域:
package cc.wxf.view.wheel;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
/**
* Created by ccwxf on 2016/4/1.
*/
public class WheelSelect {
//黑框背景顏色
public static final int COLOR_BACKGROUND = Color.parseColor("#77777777");
//黑框的Y座標起點、寬度、高度
private int startY;
private int width;
private int height;
//四點座標
private Rect rect = new Rect();
//需要選擇文字的顏色、大小、補白
private String selectText;
private int fontColor;
private int fontSize;
private int padding;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public WheelSelect(int startY, int width, int height, String selectText, int fontColor, int fontSize, int padding) {
this.startY = startY;
this.width = width;
this.height = height;
this.selectText = selectText;
this.fontColor = fontColor;
this.fontSize = fontSize;
this.padding = padding;
rect.left = 0;
rect.top = startY;
rect.right = width;
rect.bottom = startY + height;
}
public int getStartY() {
return startY;
}
public void setStartY(int startY) {
this.startY = startY;
}
public void onDraw(Canvas mCanvas) {
//繪製背景
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(COLOR_BACKGROUND);
mCanvas.drawRect(rect, mPaint);
//繪製提醒文字
if(selectText != null){
//設定鋼筆屬性
mPaint.setTextSize(fontSize);
mPaint.setColor(fontColor);
//得到字型的寬度
int textWidth = (int)mPaint.measureText(selectText);
//drawText的繪製起點是左下角,y軸起點為baseLine
Paint.FontMetrics metrics = mPaint.getFontMetrics();
int baseLine = (int)(rect.centerY() + (metrics.bottom - metrics.top) / 2 - metrics.bottom);
//在靠右邊繪製文字
mCanvas.drawText(selectText, rect.right - padding - textWidth, baseLine, mPaint);
}
}
}
原始碼就三個檔案,很簡單,註釋也很詳細,接下來就是使用檔案了:
final WheelView wheelView = (WheelView) findViewById(R.id.wheelView);
final List<String> lists = new ArrayList<>();
for(int i = 0; i < 20; i++){
lists.add("test:" + i);
}
wheelView.lists(lists).fontSize(35).showCount(5).selectTip("年").select(0).listener(new WheelView.OnWheelViewItemSelectListener() {
@Override
public void onItemSelect(int index) {
Log.d("cc", "current select:" + wheelView.getSelectItem() + " index :" + index + ",result=" + lists.get(index));
}
}).build();
這個控制元件說簡單也簡單,說複雜也挺複雜,從最基礎的onDraw實現,可以非常高靈活度地定製各自的需求。
demo工程就不提供了,使用非常簡單。