Android自己定义实现循环滚轮控件WheelView

首先呈上效果图

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" >

如今非常多地方都用到了滚轮布局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);
        }
    }
}

源码就三个文件,非常easy。凝视也非常具体,接下来就是使用文件了:

        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实现,能够很高灵活度地定制各自的需求。

demoproject就不提供了,使用很easy。

时间: 2024-11-05 17:26:16

Android自己定义实现循环滚轮控件WheelView的相关文章

Android自定义实现循环滚轮控件WheelView

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

老猪带你玩转自定义控件三——sai大神带我实现ios 8 时间滚轮控件

ios 8 的时间滚轮控件实现了扁平化,带来很好用户体验,android没有现成控件,小弟不才,数学与算法知识不过关,顾十分苦恼,幸好在github上找到sai大神实现代码,甚为欣喜,顾把学习这个控件点滴记录下来,分享给大家.项目原地址https://github.com/saiwu-bigkoo/Android-PickerView. ios 8 滚轮的效果: 而sai大神控件的效果: 哎,妈呀是不是效果95%相识啊. 好了,废话少说,谈谈我从这个控件中收获的心得. 首先,我们要高瞻远瞩看一下

【读书笔记-《Android游戏编程之从零开始》】5.Android 游戏开发常用的系统控件(ProgressBar、Seekbar)

3.7 ProgressBar ProgressBar类官方文档地址:http://developer.android.com/reference/android/widget/ProgressBar.html 在Android应用开发中,ProgressBar(运行进度条)是比较常用到的组件,例如下载进度.安装程序进度.加载资源进度显示等.在Android中提供了两种样式来分别表示在不同状态下显示的进度条,下面来实现这两种样式.默认进度条是圆形,通过style属性来指定系统进度条的大小:sty

【读书笔记-《Android游戏编程之从零开始》】3.Android 游戏开发常用的系统控件(Button、Layout、ImageButton)

3.1 Button Button这控件不用多说,就是一个按钮,主要是点击后进行相应事件的响应. 给组件添加ID属性:定义格式为 android:id="@+id/name",这里的name是自定义的,不是索引变量."@+"表示新声明,"@"表示引用,例如:"@+id/tv" 表示新声明一个id,是id名为tv的组件:"@id/tv" 表示引用id名为tv的组件. 给按钮添加点击事件响应  想知道按钮是否被

Android 带清除功能的输入框控件ClearEditText,仿IOS的输入框

转载请注明出处http://blog.csdn.net/xiaanming/article/details/11066685 今天给大家带来一个很实用的小控件ClearEditText,就是在Android系统的输入框右边加入一个小图标,点击小图标可以清除输入框里面的内容,IOS上面直接设置某个属性就可以实现这一功能,但是Android原生EditText不具备此功能,所以要想实现这一功能我们需要重写EditText,接下来就带大家来实现这一小小的功能 我们知道,我们可以为我们的输入框在上下左右

如果写一个android桌面滑动切换屏幕的控件(一)

首先这个控件应该是继承ViewGroup: 初始化: public class MyGroup extends ViewGroup{ private Scroller mScroller; private float mOriMotionX; private float mLastMotionX; private VelocityTracker mVelocityTracker; private int mTouchState = TOUCH_STATE_REST; private static

【读书笔记-《Android游戏编程之从零开始》】8.Android 游戏开发常用的系统控件(系统控件常见问题)

Android 中常用的计量单位Android有时候需要一些计量单位,比如在布局Layout文件中可能需要指定具体单位等.常用的计量单位有:px.dip(dp).sp,以及一些不常用的pt.in.mm.下面详细介绍下这些计量单位之间的区别和联系.in:英寸(长度单位):mm:毫米(长度单位):pt:磅/点,1/72英寸(一个标准的长度单位):sp:全名 scaled pixels-best for text size,放大像素,与刻度无关,可以根据用户的字体大小就行缩放,主要用来处理字体的大小:

(转载) Android 带清除功能的输入框控件ClearEditText,仿IOS的输入框

Android 带清除功能的输入框控件ClearEditText,仿IOS的输入框 标签: Android清除功能EditText仿IOS的输入框 2013-09-04 17:33 70865人阅读 评论(57) 收藏 举报  分类: Android UI设计(7)  版权声明:本文为博主原创文章,未经博主允许不得转载. 转载请注明出处http://blog.csdn.net/xiaanming/article/details/11066685 今天给大家带来一个很实用的小控件ClearEdit

【读书笔记-《Android游戏编程之从零开始》】6.Android 游戏开发常用的系统控件(TabHost、ListView)

3.9 TabSpec与TabHost TabHost类官方文档地址:http://developer.android.com/reference/android/widget/TabHost.html Android 实现tab视图有2种方法,一种是在布局页面中定义<tabhost>标签,另一种就是继承tabactivity.但是我比较喜欢第二种方式,应为如果页面比较复杂的话你的XML文件会写得比较庞大,用第二种方式XML页面相对要简洁得多. <?xml version="1