首先呈上效果图

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

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

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

  1. Android自己定义组件之日历控件-精美日历实现(内容、样式可扩展)

    需求 我们知道.Android系统本身有自带的日历控件,网络上也有非常多开源的日历控件资源.可是这些日历控件往往样式较单一.API较多.不易于在实际项目中扩展并实现出符合详细样式风格的,内容可定制的效 ...

  2. Android自己定义View之组合控件 ---- LED数字时钟

    先上图 LEDView效果如图所看到的. 之前看到一篇博客使用两个TextView实现了该效果.于是我想用自己定义控件的方式实现一个LEDView.使用时就可以直接使用该控件. 採用组合控件的方式,将 ...

  3. Android仿iPhone 滚轮控件 实现

    Android_开发 实用滚轮效果选择数字http://blog.csdn.net/zhangtengyuan23/article/details/8653771 Android仿iPhone滚轮控件 ...

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

    ios 8 的时间滚轮控件实现了扁平化,带来很好用户体验,android没有现成控件,小弟不才,数学与算法知识不过关,顾十分苦恼,幸好在github上找到sai大神实现代码,甚为欣喜,顾把学习这个控件 ...

  5. Android开发技巧——自定义控件之组合控件

    Android开发技巧--自定义控件之组合控件 我准备在接下来一段时间,写一系列有关Android自定义控件的博客,包括如何进行各种自定义,并分享一下我所知道的其中的技巧,注意点等. 还是那句老话,尽 ...

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

    Android 带清除功能的输入框控件ClearEditText,仿IOS的输入框 标签: Android清除功能EditText仿IOS的输入框 2013-09-04 17:33 70865人阅读  ...

  7. Android support library支持包常用控件介绍(二)

    谷歌官方推出Material Design 设计理念已经有段时间了,为支持更方便的实现 Material Design设计效果,官方给出了Android support design library ...

  8. [APP] Android 开发笔记 004-Android常用基本控件使用说明

    TextView 文本框 EditText控件 Button 与 ImageButton ImageView RadioButton CheckBox复选框 TextView 文本框 ,用于显示文本的 ...

  9. (转载)Android UI设计之AlertDialog弹窗控件

    Android UI设计之AlertDialog弹窗控件 作者:qq_27630169 字体:[增加 减小] 类型:转载 时间:2016-08-18我要评论 这篇文章主要为大家详细介绍了Android ...

随机推荐

  1. doc下设置永久环境变量的好方法

    http://www-2w.blog.163.com/blog/static/97931518201021211123267/ 需要查看命令具体实现:setx machine “%path%”. 配置 ...

  2. dev c++ 提示没有iostream.h文件

    dev c++ 提示没有iostream.h文件 解决办法路径没有打通最好是这样写:#include <iostream>using namespace std;int main(int ...

  3. POJ 1952

    BUY LOW, BUY LOWER Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 7748   Accepted: 267 ...

  4. ThinkPHP5杂技(二)

    不要使用数据库查询嵌套 if (!$listA = Db::name('coin') ->field('id,symbol') ->where('id', 'IN', logic('All ...

  5. 【bzoj1283】序列 线性规划与费用流

    题目描述 给出一个长度为 的正整数序列Ci,求一个子序列,使得原序列中任意长度为 的子串中被选出的元素不超过K(K,M<=100) 个,并且选出的元素之和最大. 输入 第1行三个数N,m,k. ...

  6. LCT(Link-Cut Tree)

    Link-cut tree(LCT)[可以理解为树链剖分+splay] 给出如下定义: access(x):访问x节点 perferred child:若以x为根的子树中最后被访问的节点在以x的儿子y ...

  7. 刷题总结——骑士的旅行(bzoj4336 树链剖分套权值线段树)

    题目: Description 在一片古老的土地上,有一个繁荣的文明. 这片大地几乎被森林覆盖,有N座城坐落其中.巧合的是,这N座城由恰好N-1条双 向道路连接起来,使得任意两座城都是连通的.也就是说 ...

  8. 337. House Robber III(包含I和II)

    198. House Robber You are a professional robber planning to rob houses along a street. Each house ha ...

  9. centos7配置国内yum源

    文章目录 1.什么是yum仓库? 2.yum仓库配置 2.1.阿里镜像仓库配置 2.1.1.配置步骤 2.1.2.epel源 安装和配置 2.1.3.查看yum源 2.2.配置 清华大学镜像仓库 1. ...

  10. APUE 学习笔记(三) 文件和目录

    1. 文件类型 文件类型信息包含在 struct stat 里的 st_mode 成员 (1)普通文件,unix内核并不区分文本文件和二进制文件 (2)目录文件,这种文件包含了其他文件的名字以及指向这 ...