Android 实现自定义的WheelView选择器

最近有项目中需要用到一个这个滚动轮,

刚拿到原型一看,这不就是一个ListView么?但是当我用ListView写完后,我发现有问题,
1.ListView不会整条滚动,
2.无法准确的获取第一数据

后来网上查找,发现有类似的控件,那么就不用自己实现了,直接使用。

现在很多地方都用到了滚轮布局WheelView,比如在选择生日的时候,风格类似系统提供的DatePickerDialog,开源的控件也有很多,不过大部分都是根据当前项目的需求绘制的界面,因此我就自己写了一款比较符合自己项目的WheelView。
首先这个控件有以下的需求:
1、能够循环滚动,当向上或者向下滑动到临界值的时候,则循环开始滚动
2、中间的一块有一块半透明的选择区,滑动结束时,哪一块在这个选择区,就选择这快。
3、继承自View进行绘制

然后进行一些关键点的讲解:
1、整体控件继承自View,在onDraw中进行绘制。整体包含三个模块,整个View、每一块的条目、中间选择区的条目(额外绘制一块灰色区域)。
2、通过动态设置或者默认设置的可显示条目数,在最上和最下再各加入一块,意思就是一共绘制showCount+2个条目。
3、当最上面的条目数滑动超过条目高度的一半时,进行动态条目更新:将最下面的条目删除加入第一个条目、将第一个条目删除加入最下面的条目。
4、外界可设置条目显示数、字体大小、颜色、选择区提示文字(图中那个年字)、默认选择项、padding补白等等。
5、在onTouchEvent中,得到手指滑动的渐变值,动态更新当前所有的条目。
6、在onMeasure中动态计算宽度,所有条目的宽度、高度、起始Y坐标等等。
7、通过当前条目和被选择条目的坐标,超过一半则视为被选择,并且滑动到对应的位置。

下面的是WheelView代码,主要是计算初始值、得到外面设置的值:

WheelView

/** 
 * 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);  
    }  
}  

WheelItem

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);  
    }  
}  

WheelSelect

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)
.setSelectLocation(false)  //让选中位置为第一行
.listener(new WheelView.OnWheelViewItemSelectListener() {  
    @Override  
    public void onItemSelect(int index) {  
        Log.d("cc", "current select:" + wheelView.getSelectItem() + " index :" + index + ",result=" + lists.get(index));  
    }  
})
.build();  

在实现后,当更改选中背景颜色为白色时,发现,看不到选中项,查看代码,
WheelView中应该先绘制选中的背景,然后再绘制每一个Item

   @Override
    protected void onDraw(Canvas canvas) {
        //绘制缩影
        if (wheelSelect != null) {
            wheelSelect.onDraw(canvas);
        }
        //绘制每一项item
        for (WheelItem item : wheelItems) {
            item.onDraw(canvas);
        }

    }

原文地址:http://blog.csdn.net/cc_lova_wxf/article/details/51063381
推荐一个类似的项目:https://github.com/Bigkoo/Android-PickerView

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值