Android自定義實(shí)現(xiàn)循環(huán)滾輪控件WheelView
首先呈上Android循環(huán)滾輪效果圖:

現(xiàn)在很多地方都用到了滾輪布局WheelView,比如在選擇生日的時(shí)候,風(fēng)格類似系統(tǒng)提供的DatePickerDialog,開源的控件也有很多,不過大部分都是根據(jù)當(dāng)前項(xiàng)目的需求繪制的界面,因此我就自己寫了一款比較符合自己項(xiàng)目的WheelView。
首先這個(gè)控件有以下的需求:
1、能夠循環(huán)滾動(dòng),當(dāng)向上或者向下滑動(dòng)到臨界值的時(shí)候,則循環(huán)開始滾動(dòng)
2、中間的一塊有一塊半透明的選擇區(qū),滑動(dòng)結(jié)束時(shí),哪一塊在這個(gè)選擇區(qū),就選擇這快。
3、繼承自View進(jìn)行繪制
然后進(jìn)行一些關(guān)鍵點(diǎn)的講解:
1、整體控件繼承自View,在onDraw中進(jìn)行繪制。整體包含三個(gè)模塊,整個(gè)View、每一塊的條目、中間選擇區(qū)的條目(額外繪制一塊灰色區(qū)域)。
2、通過動(dòng)態(tài)設(shè)置或者默認(rèn)設(shè)置的可顯示條目數(shù),在最上和最下再各加入一塊,意思就是一共繪制showCount+2個(gè)條目。
3、當(dāng)最上面的條目數(shù)滑動(dòng)超過條目高度的一半時(shí),進(jìn)行動(dòng)態(tài)條目更新:將最下面的條目刪除加入第一個(gè)條目、將第一個(gè)條目刪除加入最下面的條目。
4、外界可設(shè)置條目顯示數(shù)、字體大小、顏色、選擇區(qū)提示文字(圖中那個(gè)年字)、默認(rèn)選擇項(xiàng)、padding補(bǔ)白等等。
5、在onTouchEvent中,得到手指滑動(dòng)的漸變值,動(dòng)態(tài)更新當(dāng)前所有的條目。
6、在onMeasure中動(dòng)態(tài)計(jì)算寬度,所有條目的寬度、高度、起始Y坐標(biāo)等等。
7、通過當(dāng)前條目和被選擇條目的坐標(biāo),超過一半則視為被選擇,并且滑動(dòng)到對應(yīng)的位置。
下面的是WheelView代碼,主要是計(jì)算初始值、得到外面設(shè)置的值:
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;
//需要顯示的行數(shù)
private int showCount = SHOW_COUNT;
//當(dāng)前默認(rèn)選擇的位置
private int select = SELECT;
//字體顏色、大小、補(bǔ)白
private int fontColor = FONT_COLOR;
private int fontSize = FONT_SIZE;
private int padding = PADDING;
//文本列表
private List<String> lists;
//選中項(xiàng)的輔助文本,可為空
private String selectTip;
//每一項(xiàng)Item和選中項(xiàng)
private List<WheelItem> wheelItems = new ArrayList<WheelItem>();
private WheelSelect wheelSelect = null;
//手點(diǎn)擊的Y坐標(biāo)
private float mTouchY;
//監(jiān)聽器
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);
}
/**
* 設(shè)置字體的顏色,不設(shè)置的話默認(rèn)為黑色
* @param fontColor
* @return
*/
public WheelView fontColor(int fontColor){
this.fontColor = fontColor;
return this;
}
/**
* 設(shè)置字體的大小,不設(shè)置的話默認(rèn)為30
* @param fontSize
* @return
*/
public WheelView fontSize(int fontSize){
this.fontSize = fontSize;
return this;
}
/**
* 設(shè)置文本到上下兩邊的補(bǔ)白,不合適的話默認(rèn)為10
* @param padding
* @return
*/
public WheelView padding(int padding){
this.padding = padding;
return this;
}
/**
* 設(shè)置選中項(xiàng)的復(fù)制文本,可以不設(shè)置
* @param selectTip
* @return
*/
public WheelView selectTip(String selectTip){
this.selectTip = selectTip;
return this;
}
/**
* 設(shè)置文本列表,必須且必須在build方法之前設(shè)置
* @param lists
* @return
*/
public WheelView lists(List<String> lists){
this.lists = lists;
return this;
}
/**
* 設(shè)置顯示行數(shù),不設(shè)置的話默認(rèn)為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;
}
/**
* 設(shè)置默認(rèn)選中的文本的索引,不設(shè)置默認(rèn)為0
* @param select
* @return
*/
public WheelView select(int select){
this.select = select;
return this;
}
/**
* 最后調(diào)用的方法,判斷是否有必要函數(shù)沒有被調(diào)用
* @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();
// 得到每一個(gè)Item的高度
Paint mPaint = new Paint();
mPaint.setTextSize(fontSize);
Paint.FontMetrics metrics = mPaint.getFontMetrics();
itemHeight = (int) (metrics.bottom - metrics.top) + 2 * padding;
//初始化每一個(gè)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));
}
/**
* 創(chuàng)建顯示個(gè)數(shù)+2個(gè)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);
}
/**
* 處理移動(dòng)操作
* @param dy
*/
private void handleMove(float dy) {
//調(diào)整坐標(biāo)
for(WheelItem item : wheelItems){
item.adjust(dy);
}
invalidate();
//調(diào)整
adjust();
}
/**
* 處理抬起操作
*/
private void handleUp(){
int index = -1;
//得到應(yīng)該選擇的那一項(xiàng)
for(int i = 0; i < wheelItems.size(); i++){
WheelItem item = wheelItems.get(i);
//如果startY在selectItem的中點(diǎn)上面,則將該項(xiàng)作為選擇項(xiàng)
if(item.getStartY() > wheelSelect.getStartY() && item.getStartY() < (wheelSelect.getStartY() + itemHeight / 2)){
index = i;
break;
}
//如果startY在selectItem的中點(diǎn)下面,則將上一項(xiàng)作為選擇項(xiàng)
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();
//調(diào)整坐標(biāo)
for(WheelItem item : wheelItems){
item.adjust(dy);
}
invalidate();
// 調(diào)整
adjust();
//設(shè)置選擇項(xiàng)
int stringIndex = lists.indexOf(wheelItems.get(index).getText());
if(stringIndex != -1){
select = stringIndex;
if(listener != null){
listener.onItemSelect(select);
}
}
}
/**
* 調(diào)整Item移動(dòng)和循環(huán)顯示
*/
private void adjust(){
//如果向下滑動(dòng)超出半個(gè)Item的高度,則調(diào)整容器
if(wheelItems.get(0).getStartY() >= -itemHeight / 2 ){
//移除最后一個(gè)Item重用
WheelItem item = wheelItems.remove(wheelItems.size() - 1);
//設(shè)置起點(diǎn)Y坐標(biāo)
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;
}
//設(shè)置文本
item.setText(lists.get(index));
//添加到最開始
wheelItems.add(0, item);
invalidate();
return;
}
//如果向上滑超出半個(gè)Item的高度,則調(diào)整容器
if(wheelItems.get(0).getStartY() <= (-itemHeight / 2 - itemHeight)){
//移除第一個(gè)Item重用
WheelItem item = wheelItems.remove(0);
//設(shè)置起點(diǎn)Y坐標(biāo)
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;
}
//設(shè)置文本
item.setText(lists.get(index));
//添加到最后面
wheelItems.add(item);
invalidate();
return;
}
}
/**
* 得到當(dāng)前的選擇項(xiàng)
*/
public int getSelectItem(){
return select;
}
@Override
protected void onDraw(Canvas canvas) {
//繪制每一項(xiàng)Item
for(WheelItem item : wheelItems){
item.onDraw(canvas);
}
//繪制陰影
if(wheelSelect != null){
wheelSelect.onDraw(canvas);
}
}
/**
* 設(shè)置監(jiān)聽器
* @param listener
* @return
*/
public WheelView listener(OnWheelViewItemSelectListener listener){
this.listener = listener;
return this;
}
public interface OnWheelViewItemSelectListener{
void onItemSelect(int index);
}
}
然后是每一個(gè)條目類,根據(jù)當(dāng)前的坐標(biāo)進(jìn)行繪制,根據(jù)漸變值改變坐標(biāo)等:
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 {
// 起點(diǎn)Y坐標(biāo)、寬度、高度
private float startY;
private int width;
private int height;
//四點(diǎn)坐標(biāo)
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);
}
/**
* 根據(jù)Y坐標(biāo)的變化值,調(diào)整四點(diǎn)坐標(biāo)值
* @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;
}
/**
* 直接設(shè)置Y坐標(biāo)屬性,調(diào)整四點(diǎn)坐標(biāo)屬性
* @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){
//設(shè)置鋼筆屬性
mPaint.setTextSize(fontSize);
mPaint.setColor(fontColor);
//得到字體的寬度
int textWidth = (int)mPaint.measureText(text);
//drawText的繪制起點(diǎn)是左下角,y軸起點(diǎn)為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);
}
}
最后是選擇項(xiàng),就是額外得在中間區(qū)域繪制一塊灰色區(qū)域:
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坐標(biāo)起點(diǎn)、寬度、高度
private int startY;
private int width;
private int height;
//四點(diǎn)坐標(biāo)
private Rect rect = new Rect();
//需要選擇文本的顏色、大小、補(bǔ)白
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){
//設(shè)置鋼筆屬性
mPaint.setTextSize(fontSize);
mPaint.setColor(fontColor);
//得到字體的寬度
int textWidth = (int)mPaint.measureText(selectText);
//drawText的繪制起點(diǎn)是左下角,y軸起點(diǎn)為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);
}
}
}
源代碼就三個(gè)文件,很簡單,注釋也很詳細(xì),接下來就是使用文件了:
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();
這個(gè)控件說簡單也簡單,說復(fù)雜也挺復(fù)雜,從最基礎(chǔ)的onDraw實(shí)現(xiàn),可以非常高靈活度地定制各自的需求。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android開發(fā)之BroadcastReceiver用法實(shí)例分析
這篇文章主要介紹了Android開發(fā)之BroadcastReceiver用法,實(shí)例分析了Android中廣播的相關(guān)使用技巧,需要的朋友可以參考下2015-05-05
Android開發(fā)使用RecyclerView添加點(diǎn)擊事件實(shí)例詳解
這篇文章主要為大家介紹了Android開發(fā)使用RecyclerView添加點(diǎn)擊事件實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
Android截取指定View為圖片的實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Android截取指定View為圖片的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06
Android用于校驗(yàn)集合參數(shù)的小封裝示例
本篇文章主要介紹了Android-用于校驗(yàn)集合參數(shù)的小封裝示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10
Android圖片三級緩存的原理及其實(shí)現(xiàn)
本篇文章主要介紹了Android圖片三級緩存的原理及其實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
Android編程實(shí)現(xiàn)給Button添加圖片和文字的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)給Button添加圖片和文字的方法,涉及Android針對按鈕元素屬性的相關(guān)操作技巧,需要的朋友可以參考下2015-11-11
基于android startActivityForResult的學(xué)習(xí)心得總結(jié)
本篇文章是對android中的startActivityForResult進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
Android 滑動(dòng)監(jiān)聽RecyclerView線性流+左右劃刪除+上下移動(dòng)
這篇文章主要介紹了Android 滑動(dòng)監(jiān)聽RecyclerView線性流+左右劃刪除+上下移動(dòng)的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09

