【Android 应用开发】 自定义组件 宽高适配方法, 手势监听器操作组件, 回调接口维护策略, 绘制方法分析 -- 基于 WheelView 组件分析自定义组件
博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569
代码下载 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;
博客总结 :
博文内容 : 本文完整地分析了 WheelView 所有的源码, 包括其适配器类型, 两种回调接口 (选中条目改变回调, 和开始结束滚动回调), 以及详细的分析了 WheelView 主题源码, 其中 组件宽高测量, 手势监听器添加, 以及精准的绘图方法是主要目的, 花了将近1周时间, 感觉很值, 在这里分享给大家;
WheelView 使用方法 : 创建 WheelView 组件 --> 设置显示条目数 --> 设置循环 --> 设置适配器 --> 设置监听器 ;
自定义组件宽高获取策略 : MeasureSpec 最大模式 取 默认值 和 给定值中较小的那个, 未定义模式取默认值, 精准模式取 给定值;
自定义组件维护各种回调监听器策略 : 维护集合, 将监听器置于集合中, 回调接口时遍历集合元素, 回调每个元素的接口方法;
自定义组件手势监听器添加方法 : 创建手势监听器, 将手势监听器传入手势探测器, 在 onTouchEvent() 方法中回调手势监听器的 onTouchEvent()方法;
一. WheelView 简介
1. WheelView 效果
在 Android 中实现类似与 IOS 的 WheelView 控件 : 如图
2. WheelView 使用流程
(1) 基本流程简介
获取组件 --> 设置显示条目数 --> 设置循环 --> 设置适配器 --> 设置条目改变监听器 --> 设置滚动监听器
a. 创建 WheelView 组件 : 使用 构造方法 或者 从布局文件获取 WheelView 组件;
b. 设置显示条目数 : 调用 WheelView 组件对象的 setVisibleItems 方法 设置;
c. 设置是否循环 : 设置 WheelView 是否循环, 调用 setCyclic() 方法设置;
d. 设置适配器 : 调用 WheelView 组件的 setAdapter() 方法设置;
e. 设置条目改变监听器 : 调用 WheelView 组件对象的 addChangingListener() 方法设置;
f. 设置滚动监听器 : 调用 WheelView 组件对象的 addScrollingListener() 方法设置;
(2) 代码实例
a. 创建 WheelView 对象 :
//创建 WheelView 组件final WheelView wheelLeft = new WheelView(context);
b. 设置 WheelView 显示条目数 :
//设置 WheelView 组件最多显示 5 个元素wheelLeft.setVisibleItems(5);
c. 设置 WheelView 是否滚动循环 :
//设置 WheelView 元素是否循环滚动wheelLeft.setCyclic(false);
d. 设置 WheelView 适配器 :
//设置 WheelView 适配器wheelLeft.setAdapter(new ArrayWheelAdapter<String>(left));
e. 设置条目改变监听器 :
//为左侧的 WheelView 设置条目改变监听器wheelLeft.addChangingListener(new OnWheelChangedListener() {@Overridepublic void onChanged(WheelView wheel, int oldValue, int newValue) {//设置右侧的 WheelView 的适配器wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[newValue]));wheelRight.setCurrentItem(right[newValue].length / 2);}});
f. 设置滚动监听器 :
wheelLeft.addScrollingListener(new OnWheelScrollListener() {@Overridepublic void onScrollingStarted(WheelView wheel) {// TODO Auto-generated method stub}@Overridepublic void onScrollingFinished(WheelView wheel) {// TODO Auto-generated method stub}});
二. WheelView 适配器 监听器 相关接口分析
1. 适配器 分析
这里定义了一个适配器接口, 以及两个适配器类, 一个用于任意类型的数据集适配, 一个用于数字适配;
适配器操作 : 在 WheelView.java 中通过 setAdapter(WheelAdapter adapter) 和 getAdapter() 方法设置 获取 适配器;
-- 适配器常用操作 : 在 WheelView 中定义了 getItem(), getItemsCount(), getMaxmiumLength() 方法获取 适配器的相关信息;
/*** 获取该 WheelView 的适配器* * @return * 返回适配器*/public WheelAdapter getAdapter() {return adapter;}/*** 设置适配器* * @param adapter* 要设置的适配器*/public void setAdapter(WheelAdapter adapter) {this.adapter = adapter;invalidateLayouts();invalidate();}
(1) 适配器接口 ( interface WheelAdapter )
适配器接口 : WheelAdapter;
-- 接口作用 : 该接口是所有适配器的接口, 适配器类都需要实现该接口;
接口抽象方法介绍 :
-- getItemsCount() : 获取适配器数据集合中元素个数;
/*** 获取条目的个数* * @return * WheelView 的条目个数*/public int getItemsCount();
-- getItem(int index) : 获取适配器集合的中指定索引元素;
/*** 根据索引位置获取 WheelView 的条目* * @param index* 条目的索引* @return * WheelView 上显示的条目的值*/public String getItem(int index);
-- getMaximumLength() : 获取 WheelView 在界面上的显示宽度;
/*** 获取条目的最大长度. 用来定义 WheelView 的宽度. 如果返回 -1, 就会使用默认宽度* * @return * 条目的最大宽度 或者 -1*/public int getMaximumLength();
(2) 数组适配器 ( class ArrayWheelAdapter<T> implements WheelAdapter )
适配器作用 : 该适配器可以传入任何数据类型的数组, 可以是 字符串数组, 也可以是任何对象的数组, 传入的数组作为适配器的数据源;
成员变量分析 :
-- 数据源 :
/** 适配器的数据源 */private T items[];
-- WheelView 最大宽度 :
/** WheelView 的宽度 */private int length;
构造方法分析 :
-- ArrayWheelAdapter(T items[], int length) : 传入 T 类型 对象数组, 以及 WheelView 的宽度;
/*** 构造方法* * @param items* 适配器数据源 集合 T 类型的数组* @param length* 适配器数据源 集合 T 数组长度*/public ArrayWheelAdapter(T items[], int length) {this.items = items;this.length = length;}
-- ArrayWheelAdapter(T items[]) : 传入 T 类型对象数组, 宽度使用默认的宽度;
/*** 构造方法* * @param items* 适配器数据源集合 T 类型数组*/public ArrayWheelAdapter(T items[]) {this(items, DEFAULT_LENGTH);}
实现的父类方法分析 :
-- getItem(int index) : 根据索引获取数组中对应位置的对象的字符串类型;
@Overridepublic String getItem(int index) {//如果这个索引值合法, 就返回 item 数组对应的元素的字符串形式if (index >= 0 && index < items.length) {return items[index].toString();}return null;}
-- getItemsCount() : 获取数据集广大小, 直接返回数组大小;
@Overridepublic int getItemsCount() {//返回 item 数组的长度return items.length;}
-- getMaximumLength() : 获取 WheelView 的最大宽度;
@Overridepublic int getMaximumLength() {//返回 item 元素的宽度return length;}
(3) 数字适配器 ( class NumericWheelAdapter implements WheelAdapter )
NumericWheelAdapter 适配器作用 : 数字作为 WheelView 适配器的显示值;
成员变量分析 :
-- 最小值 : WheelView 数值显示的最小值;
/** 设置的最小值 */private int minValue;
-- 最大值 : WheelView 数值显示的最大值;
/** 设置的最大值 */private int maxValue;
-- 格式化字符串 : 用于字符串的格式化;
/** 格式化字符串, 用于格式化 货币, 科学计数, 十六进制 等格式 */private String format;
构造方法分析 :
-- NumericWheelAdapter() : 默认的构造方法, 使用默认的最大最小值;
/*** 默认的构造方法, 使用默认的最大最小值*/public NumericWheelAdapter() {this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE);}
-- NumericWheelAdapter(int minValue, int maxValue) : 传入一个最大最小值;
/*** 构造方法* * @param minValue* 最小值* @param maxValue* 最大值*/public NumericWheelAdapter(int minValue, int maxValue) {this(minValue, maxValue, null);}
-- NumericWheelAdapter(int minValue, int maxValue, String format) : 传入最大最小值, 以及数字格式化方式;
/*** 构造方法* * @param minValue* 最小值* @param maxValue* 最大值* @param format* 格式化字符串*/public NumericWheelAdapter(int minValue, int maxValue, String format) {this.minValue = minValue;this.maxValue = maxValue;this.format = format;}
实现的父类方法 :
-- 获取条目 : 如果需要格式化, 先进行格式化;
@Overridepublic String getItem(int index) {String result = "";if (index >= 0 && index < getItemsCount()) {int value = minValue + index;//如果 format 不为 null, 那么格式化字符串, 如果为 null, 直接返回数字if(format != null){result = String.format(format, value);}else{result = Integer.toString(value);}return result;}return null;}
-- 获取元素个数 :
@Overridepublic int getItemsCount() {//返回数字总个数return maxValue - minValue + 1;}
-- 获取 WheelView 最大宽度 :
@Overridepublic int getMaximumLength() {//获取 最大值 和 最小值 中的 较大的数字int max = Math.max(Math.abs(maxValue), Math.abs(minValue));//获取这个数字 的 字符串形式的 字符串长度int maxLen = Integer.toString(max).length();if (minValue < 0) {maxLen++;}return maxLen;}
2. 监听器相关接口
(1) 条目改变监听器 ( interface OnWheelChangedListener )
监听器作用 : 在 WheelView 条目改变的时候, 回调该监听器的接口方法, 执行条目改变对应的操作;
接口方法介绍 :
-- onChanged(WheelView wheel, int oldValue, int newValue) : 传入 WheelView 组件对象, 以及 旧的 和 新的 条目值索引;
/*** 当前条目改变时回调该方法* * @param wheel* 条目改变的 WheelView 对象* @param oldValue* WheelView 旧的条目值* @param newValue* WheelView 新的条目值*/void onChanged(WheelView wheel, int oldValue, int newValue);
(2) 滚动监听器 ( interface OnWheelScrollListener )
滚动监听器作用 : 在 WheelView 滚动动作 开始 和 结束的时候回调对应的方法, 在对应方法中进行相应的操作;
接口方法介绍 :
-- 开始滚动方法 : 在滚动开始的时候回调该方法;
/*** 在 WheelView 滚动开始的时候回调该接口* * @param wheel* 开始滚动的 WheelView 对象*/void onScrollingStarted(WheelView wheel);
-- 停止滚动方法 : 在滚动结束的时候回调该方法;
/*** 在 WheelView 滚动结束的时候回调该接口* * @param wheel* 结束滚动的 WheelView 对象*/void onScrollingFinished(WheelView wheel);
三. WheelView 解析
1. 触摸 点击 手势 动作操作控制组件 模块
(1) 创建手势监听器
手势监听器创建及对应方法 :
-- onDown(MotionEvent e) : 在按下的时候回调该方法, e 参数是按下的事件;
-- onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) : 滚动的时候回调该方法, e1 滚动第一次按下事件, e2 当前滚动的触摸事件, X 上一次滚动到这一次滚动 x 轴距离, Y 上一次滚动到这一次滚动 y 轴距离;
-- onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) : 快速急冲滚动时回调的方法, e1 e2 与上面参数相同, velocityX 是手势在 x 轴的速度, velocityY 是手势在 y 轴的速度;
-- 代码示例 :
/** 手势监听器监听到 滚动操作后回调* * 参数解析 : * MotionEvent e1 : 触发滚动时第一次按下的事件* MotionEvent e2 : 触发当前滚动的移动事件* float distanceX : 自从上一次调用 该方法 到这一次 x 轴滚动的距离, * 注意不是 e1 到 e2 的距离, e1 到 e2 的距离是从开始滚动到现在的滚动距离* float distanceY : 自从上一次回调该方法到这一次 y 轴滚动的距离* * 返回值 : 如果事件成功触发, 执行完了方法中的操作, 返回true, 否则返回 false * (non-Javadoc)* @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float)*/public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//开始滚动, 并回调滚动监听器集合中监听器的 开始滚动方法startScrolling();doScroll((int) -distanceY);return true;}/** 当一个急冲手势发生后 回调该方法, 会计算出该手势在 x 轴 y 轴的速率* * 参数解析 : * -- MotionEvent e1 : 急冲动作的第一次触摸事件;* -- MotionEvent e2 : 急冲动作的移动发生的时候的触摸事件;* -- float velocityX : x 轴的速率* -- float velocityY : y 轴的速率* * 返回值 : 如果执行完毕返回 true, 否则返回false, 这个就是自己定义的* * (non-Javadoc)* @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float)*/public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {//计算上一次的 y 轴位置, 当前的条目高度 加上 剩余的 不够一行高度的那部分lastScrollY = currentItem * getItemHeight() + scrollingOffset;//如果可以循环最大值是无限大, 不能循环就是条目数的高度值int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();int minY = isCyclic ? -maxY : 0;/** Scroll 开始根据一个急冲手势滚动, 滚动的距离与初速度有关* 参数介绍 : * -- int startX : 开始时的 X轴位置* -- int startY : 开始时的 y轴位置* -- int velocityX : 急冲手势的 x 轴的初速度, 单位 px/s* -- int velocityY : 急冲手势的 y 轴的初速度, 单位 px/s* -- int minX : x 轴滚动的最小值* -- int maxX : x 轴滚动的最大值* -- int minY : y 轴滚动的最小值* -- int maxY : y 轴滚动的最大值*/scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);setNextMessage(MESSAGE_SCROLL);return true;}};
(2) 创建手势探测器
手势探测器创建 : 调用 其构造函数, 传入 上下文对象 和 手势监听器对象;
-- 禁止长按操作 : 调用 setIsLongpressEnabled(false) 方法, 禁止长按操作, 因为 长按操作会屏蔽滚动事件;
//创建一个手势处理gestureDetector = new GestureDetector(context, gestureListener);/** 是否允许长按操作, * 如果设置为 true 用户按下不松开, 会返回一个长按事件, * 如果设置为 false, 按下不松开滑动的话 会收到滚动事件.*/gestureDetector.setIsLongpressEnabled(false);
(3) 将手势探测器 与 组件结合
关联手势探测器 与 组件 : 在组件的 onTouchEvent(MotionEvent event) 方法中, 调用手势探测器的 gestureDetector.onTouchEvent(event) 方法即可;
/** 继承自 View 的触摸事件, 当出现触摸事件的时候, 就会回调该方法* (non-Javadoc)* @see android.view.View#onTouchEvent(android.view.MotionEvent)*/@Overridepublic boolean onTouchEvent(MotionEvent event) {//获取适配器WheelAdapter adapter = getAdapter();if (adapter == null) {return true;}/** gestureDetector.onTouchEvent(event) : 分析给定的动作, 如果可用, 调用 手势检测器的 onTouchEvent 方法* -- 参数解析 : ev , 触摸事件* -- 返回值 : 如果手势监听器成功执行了该方法, 返回true, 如果执行出现意外 返回 false;*/if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {justify();}return true;}
2. Scroller 简介
(1) Scroller 简介
Scroller 通用作用 : Scroller 组件并不是一个布局组件, 该组件是运行在后台的, 通过一些方法设定 Scroller 对象 的操作 或者 动画, 然后让 Scroller 运行在后台中 用于模拟滚动操作, 在适当的时机 获取该对象的坐标信息, 这些信息是在后台运算出来的;
Scroller 在本 View 中作用 : Android 的这个自定义的 WheelView 组件, 可以平滑的滚动, 当我们做一个加速滑动时, 会根据速度计算出滑动的距离, 这些数据都是在 Scroller 中计算出来的;
(2) 设定 Scroller 对象的动作参数
终止滚动 :
-- 终止滚动 跳转到目标位置 : 终止平缓的动画, 直接跳转到最终的 x y 轴的坐标位置;
public void abortAnimation()
-- 终止滚动 停止在当前位置 : 强行结束 Scroll 的滚动;
public final void forceFinished(boolean finished)
设置滚动参数 :
-- 设置最终 x 轴坐标 :
public void setFinalX(int newX)
-- 设置最终 y 轴坐标 :
public void setFinalY(int newY)
-- 设置滚动摩擦力 :
public final void setFriction(float friction)
设置动作 :
-- 开始滚动 : 传入参数 开始 x 位置, 开始 y 位置, x 轴滚动距离, y 轴滚动距离;
public void startScroll(int startX, int startY, int dx, int dy)-- 开始滚动 设定时间 : 最后一个参数是时间, 单位是 ms;
public void startScroll(int startX, int startY, int dx, int dy, int duration)-- 急冲滚动 : 根据一个 急冲 手势进行滚动, 传入参数 : x轴开始位置, y轴开始位置, x 轴速度, y 轴速度, x 轴最小速度, x 轴最大速度, y 轴最小速度, y 轴最大速度;
public void fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY)
延长滚动时间 : 延长滚动的时间, 让滚动滚的更远一些;
public void extendDuration(int extend)
(3) 获取 Scroll 后台运行参数
获取当前数据 :
-- 获取当前 x 轴坐标 :
public final int getCurrX()
-- 获取当前 y 轴坐标 :
public final int getCurrY()
-- 获取当前速度 :
public float getCurrVelocity()
获取开始结束时的数据 :
-- 获取开始 x 轴坐标 :
public final int getStartX()
-- 获取开始 y 轴坐标 :
public final int getStartY()
-- 获取最终 x 轴坐标 : 该参数只在急冲滚动时有效;
public final int getFinalX()
-- 获取最终 y 轴坐标 : 该参数只在急冲滚动时有效;
public final int getFinalY()
查看是否滚动完毕 :
public final boolean isFinished()
获取从开始滚动到现在的时间 :
public int timePassed()
获取新位置 : 调用该方法可以获取新位置, 如果返回 true 说明动画还没执行完毕;
public boolean computeScrollOffset()
(4) Scroll 在 WheelView 中的运用
Scroller 创建 :
//使用默认的 时间 和 插入器 创建一个滚动器scroller = new Scroller(context);
手势监听器 SimpleOnGestureListener 对象中的 onDown() 方法 : 如果滚动还在执行, 那么强行停止 Scroller 滚动;
//按下操作public boolean onDown(MotionEvent e) {//如果滚动在执行if (isScrollingPerformed) {//滚动强制停止, 按下的时候不能继续滚动scroller.forceFinished(true);//清理信息clearMessages();return true;}return false;}
当手势监听器 SimpleOnGestureListener 对象中有急冲动作时 onFling() 方法中 : 手势监听器监听到了 急冲动作, 那么 Scroller 也进行对应操作;
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {//计算上一次的 y 轴位置, 当前的条目高度 加上 剩余的 不够一行高度的那部分lastScrollY = currentItem * getItemHeight() + scrollingOffset;//如果可以循环最大值是无限大, 不能循环就是条目数的高度值int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();int minY = isCyclic ? -maxY : 0;/** Scroll 开始根据一个急冲手势滚动, 滚动的距离与初速度有关* 参数介绍 : * -- int startX : 开始时的 X轴位置* -- int startY : 开始时的 y轴位置* -- int velocityX : 急冲手势的 x 轴的初速度, 单位 px/s* -- int velocityY : 急冲手势的 y 轴的初速度, 单位 px/s* -- int minX : x 轴滚动的最小值* -- int maxX : x 轴滚动的最大值* -- int minY : y 轴滚动的最小值* -- int maxY : y 轴滚动的最大值*/scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);setNextMessage(MESSAGE_SCROLL);return true;}
动画控制 Handler 中 :
-- 滚动 : 获取当前 Scroller 的 y 轴位置, 与上一次的 y 轴位置对比, 如果 间距 delta 不为0, 就滚动;
-- 查看是否停止 : 如果现在距离 到 最终距离 小于最小滚动距离, 强制停止;
-- 执行 msg.what 指令 : 如果需要停止, 强制停止, 否则调整坐标;
/*** 动画控制器* animation handler* * 可能会造成内存泄露 : 添加注解 HandlerLeak* Handler 类应该应该为static类型,否则有可能造成泄露。* 在程序消息队列中排队的消息保持了对目标Handler类的应用。* 如果Handler是个内部类,那 么它也会保持它所在的外部类的引用。* 为了避免泄露这个外部类,应该将Handler声明为static嵌套类,并且使用对外部类的弱应用。*/@SuppressLint("HandlerLeak")private Handler animationHandler = new Handler() {public void handleMessage(Message msg) {//回调该方法获取当前位置, 如果返回true, 说明动画还没有执行完毕scroller.computeScrollOffset();//获取当前 y 位置int currY = scroller.getCurrY();//获取已经滚动了的位置, 使用上一次位置 减去 当前位置int delta = lastScrollY - currY;lastScrollY = currY;if (delta != 0) {//改变值不为 0 , 继续滚动doScroll(delta);}/** 如果滚动到了指定的位置, 滚动还没有停止* 这时需要强制停止*/if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) {currY = scroller.getFinalY();scroller.forceFinished(true);}/** 如果滚动没有停止* 再向 Handler 发送一个停止*/if (!scroller.isFinished()) {animationHandler.sendEmptyMessage(msg.what);} else if (msg.what == MESSAGE_SCROLL) {justify();} else {finishScrolling();}}};
3. StaticLayout 布局容器
(1) StaticLayout 解析
StaticLayout 解析 : 该组件用于显示文本, 一旦该文本被显示后, 就不能再编辑, 如果想要修改文本, 使用 DynamicLayout 布局即可;
-- 使用场景 : 一般情况下不会使用该组件, 当想要自定义组件 或者 想要使用 Canvas 绘制文本时 才使用该布局;
常用方法解析 :
-- 获取底部 Padding : 获取底部 到最后一行文字的 间隔, 单位是 px;
public int getBottomPadding()
-- 获取顶部 Padding :
public int getTopPadding() -- 获取省略个数 : 获取某一行需要省略的字符个数;
public int getEllipsisCount(int line)-- 获取省略开始位置 : 获取某一行要省略的字符串的第一个位置索引;
public int getEllipsisStart(int line)-- 获取省略的宽度 : 获取某一行省略字符串的宽度, 单位 px;
public int getEllipsisStart(int line)-- 获取是否处理特殊符号 :
public boolean getLineContainsTab(int line)-- 获取文字的行数 :
public int getLineCount()-- 获取顶部位置 : 获取某一行顶部的位置;
public int getLineTop(int line)-- 获取某一行底部位置 :
public int getLineDescent(int line)-- 获取行的方向 : 字符串从左至右 还是从右至左;
public final Directions getLineDirections(int line)-- 获取某行第一个字符索引 : 获取的是 某一行 第一个字符 在整个字符串的索引;
public int getLineStart(int line)-- 获取该行段落方向 : 获取该行文字方向, 左至右 或者 右至左;
public int getParagraphDirection(int line)-- 获取某个垂直位置显示的行数 :
public int getLineForVertical(int vertical)
(2) 布局显示
布局创建 :
-- 三种布局 : WheelView 中涉及到了三种 StaticLayout 布局, 普通条目布局 itemLayout, 选中条目布局 valueLayout, 标签布局 labelLayout;
-- 创建时机 : 在 View 组件 每次 onMeasure() 和 onDraw() 方法中都要重新创建对应布局;
-- 创建布局源码 :
/*** 创建布局* * @param widthItems* 布局条目宽度* @param widthLabel* label 宽度*/private void createLayouts(int widthItems, int widthLabel) {/** 创建普通条目布局* 如果 普通条目布局 为 null 或者 普通条目布局的宽度 大于 传入的宽度, 这时需要重新创建布局* 如果 普通条目布局存在, 并且其宽度小于传入的宽度, 此时需要将*/if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {/** android.text.StaticLayout.StaticLayout(* CharSequence source, TextPaint paint, * int width, Alignment align, * float spacingmult, float spacingadd, boolean includepad)* 传入参数介绍 : * CharSequence source : 需要分行显示的字符串* TextPaint paint : 绘制字符串的画笔* int width : 条目的宽度* Alignment align : Layout 的对齐方式, ALIGN_CENTER 居中对齐, ALIGN_NORMAL 左对齐, Alignment.ALIGN_OPPOSITE 右对齐* float spacingmult : 行间距, 1.5f 代表 1.5 倍字体高度* float spacingadd : 基础行距上增加多少 , 真实行间距 等于 spacingmult 和 spacingadd 的和* boolean includepad : */itemsLayout = new StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,ADDITIONAL_ITEM_HEIGHT, false);} else {//调用 Layout 内置的方法 increaseWidthTo 将宽度提升到指定的宽度itemsLayout.increaseWidthTo(widthItems);}/** 创建选中条目*/if (!isScrollingPerformed && (valueLayout == null || valueLayout.getWidth() > widthItems)) {String text = getAdapter() != null ? getAdapter().getItem(currentItem) : null;valueLayout = new StaticLayout(text != null ? text : "", valuePaint, widthItems,widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,ADDITIONAL_ITEM_HEIGHT, false);} else if (isScrollingPerformed) {valueLayout = null;} else {valueLayout.increaseWidthTo(widthItems);}/** 创建标签条目*/if (widthLabel > 0) {if (labelLayout == null || labelLayout.getWidth() > widthLabel) {labelLayout = new StaticLayout(label, valuePaint, widthLabel, Layout.Alignment.ALIGN_NORMAL, 1,ADDITIONAL_ITEM_HEIGHT, false);} else {labelLayout.increaseWidthTo(widthLabel);}}}
4. 监听器管理
监听器集合维护 :
-- 定义监听器集合 : 在 View 组件中 定义一个 List 集合, 集合中存放 监听器元素;
/** 条目改变监听器集合 封装了条目改变方法, 当条目改变时回调 */private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>();/** 条目滚动监听器集合, 该监听器封装了 开始滚动方法, 结束滚动方法 */private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>();
-- 提供对监听器集合的添加删除接口 : 提供 对集合 进行 添加 和 删除的接口;
/*** 添加 WheelView 选择的元素改变监听器* * @param listener* the listener*/public void addChangingListener(OnWheelChangedListener listener) {changingListeners.add(listener);}/*** 移除 WheelView 元素改变监听器* * @param listener* the listener*/public void removeChangingListener(OnWheelChangedListener listener) {changingListeners.remove(listener);}
-- 调用监听器接口 :
/*** 回调元素改变监听器集合的元素改变监听器元素的元素改变方法* * @param oldValue* 旧的 WheelView选中的值* @param newValue* 新的 WheelView选中的值*/protected void notifyChangingListeners(int oldValue, int newValue) {for (OnWheelChangedListener listener : changingListeners) {listener.onChanged(this, oldValue, newValue);}}
5. 自定义 View 对象的宽高
(1) onMeasure 方法 MeasureSpec 模式解析
常规处理方法 : 组件的宽高有三种情况, widthMeasureSpec 有三种模式 最大模式, 精准模式, 未定义模式;
-- 最大模式 : 在 组件的宽或高 warp_content 属性时, 会使用最大模式;
-- 精准模式 : 当给组件宽 或者高 定义一个值 或者 使用 match_parent 时, 会使用精准模式;
处理宽高的常规代码 :
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);//获取宽度 和 高度的模式 和 大小int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);Log.i(TAG, "宽度 : widthMode : " + getMode(widthMode) + " , widthSize : " + widthSize + "\n" + "高度 : heightMode : " + getMode(heightMode) + " , heightSize : " + heightSize);int width = 0;int height = 0;/** 精准模式* 精准模式下 高度就是精确的高度*/if (heightMode == MeasureSpec.EXACTLY) {height = heightSize;//未定义模式 和 最大模式} else {//未定义模式下 获取布局需要的高度height = 100;//最大模式下 获取 布局高度 和 布局所需高度的最小值if (heightMode == MeasureSpec.AT_MOST) {height = Math.min(height, heightSize);}}if (widthMode == MeasureSpec.EXACTLY) {width = widthSize;} else {width = 100;if (heightMode == MeasureSpec.AT_MOST) {width = Math.min(width, widthSize);}}Log.i(TAG, "最终结果 : 宽度 : " + width + " , 高度 : " + height);setMeasuredDimension(width, height);}public String getMode(int mode) {String modeName = "";if(mode == MeasureSpec.EXACTLY){modeName = "精准模式";}else if(mode == MeasureSpec.AT_MOST){modeName = "最大模式";}else if(mode == MeasureSpec.UNSPECIFIED){modeName = "未定义模式";}return modeName;}
(2) 测试上述代码
使用下面的自定义组件测试 :
package cn.org.octopus.wheelview;import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.util.AttributeSet; import android.util.Log; import android.view.View;public class MyView extends View {public static final String TAG = "octopus.my.view";public MyView(Context context, AttributeSet attrs) {super(context, attrs);}public MyView(Context context) {super(context);}public MyView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);//获取宽度 和 高度的模式 和 大小int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);Log.i(TAG, "宽度 : widthMode : " + getMode(widthMode) + " , widthSize : " + widthSize + "\n" + "高度 : heightMode : " + getMode(heightMode) + " , heightSize : " + heightSize);int width = 0;int height = 0;/** 精准模式* 精准模式下 高度就是精确的高度*/if (heightMode == MeasureSpec.EXACTLY) {height = heightSize;//未定义模式 和 最大模式} else {//未定义模式下 获取布局需要的高度height = 100;//最大模式下 获取 布局高度 和 布局所需高度的最小值if (heightMode == MeasureSpec.AT_MOST) {height = Math.min(height, heightSize);}}if (widthMode == MeasureSpec.EXACTLY) {width = widthSize;} else {width = 100;if (heightMode == MeasureSpec.AT_MOST) {width = Math.min(width, widthSize);}}Log.i(TAG, "最终结果 : 宽度 : " + width + " , 高度 : " + height);setMeasuredDimension(width, height);}public String getMode(int mode) {String modeName = "";if(mode == MeasureSpec.EXACTLY){modeName = "精准模式";}else if(mode == MeasureSpec.AT_MOST){modeName = "最大模式";}else if(mode == MeasureSpec.UNSPECIFIED){modeName = "未定义模式";}return modeName;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawColor(Color.BLUE);}}
给定具体值情况 :
-- 组件信息 :
<cn.org.octopus.wheelview.MyViewandroid:layout_width="300dip"android:layout_height="300dip"/>-- 日志信息 :
11-30 01:40:24.304: I/octopus.my.view(2609): 宽度 : widthMode : 精准模式 , widthSize : 450 11-30 01:40:24.304: I/octopus.my.view(2609): 高度 : heightMode : 最大模式 , heightSize : 850 11-30 01:40:24.304: I/octopus.my.view(2609): 最终结果 : 宽度 : 450 , 高度 : 100 11-30 01:40:24.304: I/octopus.my.view(2609): 宽度 : widthMode : 精准模式 , widthSize : 450 11-30 01:40:24.304: I/octopus.my.view(2609): 高度 : heightMode : 精准模式 , heightSize : 450 11-30 01:40:24.304: I/octopus.my.view(2609): 最终结果 : 宽度 : 450 , 高度 : 450 11-30 01:40:24.335: I/octopus.my.view(2609): 宽度 : widthMode : 精准模式 , widthSize : 450 11-30 01:40:24.335: I/octopus.my.view(2609): 高度 : heightMode : 最大模式 , heightSize : 850 11-30 01:40:24.335: I/octopus.my.view(2609): 最终结果 : 宽度 : 450 , 高度 : 100 11-30 01:40:24.335: I/octopus.my.view(2609): 宽度 : widthMode : 精准模式 , widthSize : 450 11-30 01:40:24.335: I/octopus.my.view(2609): 高度 : heightMode : 精准模式 , heightSize : 450 11-30 01:40:24.335: I/octopus.my.view(2609): 最终结果 : 宽度 : 450 , 高度 : 450
-- 组件信息 :
<cn.org.octopus.wheelview.MyViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"/>-- 日志信息 :
11-30 01:37:47.351: I/octopus.my.view(1803): 宽度 : widthMode : 最大模式 , widthSize : 492 11-30 01:37:47.351: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 850 11-30 01:37:47.351: I/octopus.my.view(1803): 最终结果 : 宽度 : 100 , 高度 : 100 11-30 01:37:47.351: I/octopus.my.view(1803): 宽度 : widthMode : 精准模式 , widthSize : 100 11-30 01:37:47.351: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 802 11-30 01:37:47.351: I/octopus.my.view(1803): 最终结果 : 宽度 : 100 , 高度 : 100 11-30 01:37:47.390: I/octopus.my.view(1803): 宽度 : widthMode : 最大模式 , widthSize : 492 11-30 01:37:47.390: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 850 11-30 01:37:47.390: I/octopus.my.view(1803): 最终结果 : 宽度 : 100 , 高度 : 100 11-30 01:37:47.390: I/octopus.my.view(1803): 宽度 : widthMode : 精准模式 , widthSize : 100 11-30 01:37:47.390: I/octopus.my.view(1803): 高度 : heightMode : 最大模式 , heightSize : 802 11-30 01:37:47.390: I/octopus.my.view(1803): 最终结果 : 宽度 : 100 , 高度 : 100
match_parent 情况 :
-- 组件信息 :
<cn.org.octopus.wheelview.MyViewandroid:layout_width="match_parent"android:layout_height="match_parent"/>
-- 日志信息 :
11-30 01:39:08.296: I/octopus.my.view(2249): 宽度 : widthMode : 精准模式 , widthSize : 492 11-30 01:39:08.296: I/octopus.my.view(2249): 高度 : heightMode : 精准模式 , heightSize : 850 11-30 01:39:08.296: I/octopus.my.view(2249): 最终结果 : 宽度 : 492 , 高度 : 850 11-30 01:39:08.296: I/octopus.my.view(2249): 宽度 : widthMode : 精准模式 , widthSize : 492 11-30 01:39:08.296: I/octopus.my.view(2249): 高度 : heightMode : 精准模式 , heightSize : 802 11-30 01:39:08.296: I/octopus.my.view(2249): 最终结果 : 宽度 : 492 , 高度 : 802 11-30 01:39:08.328: I/octopus.my.view(2249): 宽度 : widthMode : 精准模式 , widthSize : 492 11-30 01:39:08.328: I/octopus.my.view(2249): 高度 : heightMode : 精准模式 , heightSize : 850 11-30 01:39:08.328: I/octopus.my.view(2249): 最终结果 : 宽度 : 492 , 高度 : 850 11-30 01:39:08.328: I/octopus.my.view(2249): 宽度 : widthMode : 精准模式 , widthSize : 492 11-30 01:39:08.328: I/octopus.my.view(2249): 高度 : heightMode : 精准模式 , heightSize : 802 11-30 01:39:08.328: I/octopus.my.view(2249): 最终结果 : 宽度 : 492 , 高度 : 802
博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569#t17
代码下载 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;
四. 详细代码
1. WheelAdapter
package cn.org.octopus.wheelview.widget;/*** WheelView 适配器接口* @author han_shuliang(octopus_truth@163.com)**/ public interface WheelAdapter {/*** 获取条目的个数* * @return * WheelView 的条目个数*/public int getItemsCount();/*** 根据索引位置获取 WheelView 的条目* * @param index* 条目的索引* @return * WheelView 上显示的条目的值*/public String getItem(int index);/*** 获取条目的最大长度. 用来定义 WheelView 的宽度. 如果返回 -1, 就会使用默认宽度* * @return * 条目的最大宽度 或者 -1*/public int getMaximumLength(); }
2. ArrayWheelAdapter
package cn.org.octopus.wheelview.widget;/*** WheelView 的适配器类* * @param <T>* 元素类型*/ public class ArrayWheelAdapter<T> implements WheelAdapter {/** 适配器的 元素集合(数据源) 默认长度为 -1 */public static final int DEFAULT_LENGTH = -1;/** 适配器的数据源 */private T items[];/** WheelView 的宽度 */private int length;/*** 构造方法* * @param items* 适配器数据源 集合 T 类型的数组* @param length* 适配器数据源 集合 T 数组长度*/public ArrayWheelAdapter(T items[], int length) {this.items = items;this.length = length;}/*** 构造方法* * @param items* 适配器数据源集合 T 类型数组*/public ArrayWheelAdapter(T items[]) {this(items, DEFAULT_LENGTH);}@Overridepublic String getItem(int index) {//如果这个索引值合法, 就返回 item 数组对应的元素的字符串形式if (index >= 0 && index < items.length) {return items[index].toString();}return null;}@Overridepublic int getItemsCount() {//返回 item 数组的长度return items.length;}@Overridepublic int getMaximumLength() {//返回 item 元素的宽度return length;}}
3. NumericWheelAdapter
package cn.org.octopus.wheelview.widget;/*** 显示数字的 WheelAdapter*/ public class NumericWheelAdapter implements WheelAdapter {/** 默认最小值 */public static final int DEFAULT_MAX_VALUE = 9;/** 默认最大值 */private static final int DEFAULT_MIN_VALUE = 0;/** 设置的最小值 */private int minValue;/** 设置的最大值 */private int maxValue;/** 格式化字符串, 用于格式化 货币, 科学计数, 十六进制 等格式 */private String format;/*** 默认的构造方法, 使用默认的最大最小值*/public NumericWheelAdapter() {this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE);}/*** 构造方法* * @param minValue* 最小值* @param maxValue* 最大值*/public NumericWheelAdapter(int minValue, int maxValue) {this(minValue, maxValue, null);}/*** 构造方法* * @param minValue* 最小值* @param maxValue* 最大值* @param format* 格式化字符串*/public NumericWheelAdapter(int minValue, int maxValue, String format) {this.minValue = minValue;this.maxValue = maxValue;this.format = format;}@Overridepublic String getItem(int index) {String result = "";if (index >= 0 && index < getItemsCount()) {int value = minValue + index;//如果 format 不为 null, 那么格式化字符串, 如果为 null, 直接返回数字if(format != null){result = String.format(format, value);}else{result = Integer.toString(value);}return result;}return null;}@Overridepublic int getItemsCount() {//返回数字总个数return maxValue - minValue + 1;}@Overridepublic int getMaximumLength() {//获取 最大值 和 最小值 中的 较大的数字int max = Math.max(Math.abs(maxValue), Math.abs(minValue));//获取这个数字 的 字符串形式的 字符串长度int maxLen = Integer.toString(max).length();if (minValue < 0) {maxLen++;}return maxLen;} }
4. OnWheelChangedListener
package cn.org.octopus.wheelview.widget;/*** 条目改变监听器*/ public interface OnWheelChangedListener {/*** 当前条目改变时回调该方法* * @param wheel* 条目改变的 WheelView 对象* @param oldValue* WheelView 旧的条目值* @param newValue* WheelView 新的条目值*/void onChanged(WheelView wheel, int oldValue, int newValue); }
5. OnWheelScrollListener
package cn.org.octopus.wheelview.widget;/*** WheelView 滚动监听器*/ public interface OnWheelScrollListener {/*** 在 WheelView 滚动开始的时候回调该接口* * @param wheel* 开始滚动的 WheelView 对象*/void onScrollingStarted(WheelView wheel);/*** 在 WheelView 滚动结束的时候回调该接口* * @param wheel* 结束滚动的 WheelView 对象*/void onScrollingFinished(WheelView wheel); }
6. WheelView
package cn.org.octopus.wheelview.widget;import java.util.LinkedList; import java.util.List;import cn.org.octopus.wheelview.R; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.GradientDrawable.Orientation; import android.os.Handler; import android.os.Message; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.MotionEvent; import android.view.View; import android.view.animation.Interpolator; import android.widget.Scroller;/*** WheelView 主对象*/ public class WheelView extends View {/** 滚动花费时间 Scrolling duration */private static final int SCROLLING_DURATION = 400;/** 最小的滚动值, 每次最少滚动一个单位 */private static final int MIN_DELTA_FOR_SCROLLING = 1;/** 当前条目中的文字颜色 */private static final int VALUE_TEXT_COLOR = 0xF0FF6347;/** 非当前条目的文字颜色 */private static final int ITEMS_TEXT_COLOR = 0xFF000000;/** 顶部和底部的阴影颜色 *///private static final int[] SHADOWS_COLORS = new int[] { 0xFF5436EE, 0x0012CEAE, 0x0012CEAE };private static final int[] SHADOWS_COLORS = new int[] { 0xFF111111, 0x00AAAAAA, 0x00AAAAAA };/** 额外的条目高度 Additional items height (is added to standard text item height) */private static final int ADDITIONAL_ITEM_HEIGHT = 15;/** 字体大小 */private static final int TEXT_SIZE = 24;/** 顶部 和 底部 条目的隐藏大小, * 如果是正数 会隐藏一部份, * 0 顶部 和 底部的字正好紧贴 边缘, * 负数时 顶部和底部 与 字有一定间距 */private static final int ITEM_OFFSET = TEXT_SIZE / 5;/** Additional width for items layout */private static final int ADDITIONAL_ITEMS_SPACE = 10;/** Label offset */private static final int LABEL_OFFSET = 8;/** Left and right padding value */private static final int PADDING = 10;/** 默认的可显示的条目数 */private static final int DEF_VISIBLE_ITEMS = 5;/** WheelView 适配器 */private WheelAdapter adapter = null;/** 当前显示的条目索引 */private int currentItem = 0;/** 条目宽度 */private int itemsWidth = 0;/** 标签宽度 */private int labelWidth = 0;/** 可见的条目数 */private int visibleItems = DEF_VISIBLE_ITEMS;/** 条目高度 */private int itemHeight = 0;/** 绘制普通条目画笔 */private TextPaint itemsPaint;/** 绘制选中条目画笔 */private TextPaint valuePaint;/** 普通条目布局* StaticLayout 布局用于控制 TextView 组件, 一般情况下不会直接使用该组件, * 除非你自定义一个组件 或者 想要直接调用 Canvas.drawText() 方法* */private StaticLayout itemsLayout;private StaticLayout labelLayout;/** 选中条目布局 */private StaticLayout valueLayout;/** 标签 在选中条目的右边出现 */private String label;/** 选中条目的背景图片 */private Drawable centerDrawable;/** 顶部阴影图片 */private GradientDrawable topShadow;/** 底部阴影图片 */private GradientDrawable bottomShadow;/** 是否在滚动 */private boolean isScrollingPerformed;/** 滚动的位置 */private int scrollingOffset;/** 手势检测器 */private GestureDetector gestureDetector;/** * Scroll 类封装了滚动动作. * 开发者可以使用 Scroll 或者 Scroll 实现类 去收集产生一个滚动动画所需要的数据, 返回一个急冲滑动的手势.* 该对象可以追踪随着时间推移滚动的偏移量, 但是这些对象不会自动向 View 对象提供这些位置.* 如果想要使滚动动画看起来比较平滑, 开发者需要在适当的时机 获取 和 使用新的坐标; * */private Scroller scroller;/** 之前所在的 y 轴位置 */private int lastScrollY;/** 是否循环 */boolean isCyclic = false;/** 条目改变监听器集合 封装了条目改变方法, 当条目改变时回调 */private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>();/** 条目滚动监听器集合, 该监听器封装了 开始滚动方法, 结束滚动方法 */private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>();/*** 构造方法*/public WheelView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);initData(context);}/*** 构造方法*/public WheelView(Context context, AttributeSet attrs) {super(context, attrs);initData(context);}/*** 构造方法*/public WheelView(Context context) {super(context);initData(context);}/*** 初始化数据* * @param context* 上下文对象*/private void initData(Context context) {//创建一个手势处理gestureDetector = new GestureDetector(context, gestureListener);/** 是否允许长按操作, * 如果设置为 true 用户按下不松开, 会返回一个长按事件, * 如果设置为 false, 按下不松开滑动的话 会收到滚动事件.*/gestureDetector.setIsLongpressEnabled(false);//使用默认的 时间 和 插入器 创建一个滚动器scroller = new Scroller(context);}/*** 获取该 WheelView 的适配器* * @return * 返回适配器*/public WheelAdapter getAdapter() {return adapter;}/*** 设置适配器* * @param adapter* 要设置的适配器*/public void setAdapter(WheelAdapter adapter) {this.adapter = adapter;invalidateLayouts();invalidate();}/*** 设置 Scroll 的插入器* * @param interpolator* the interpolator*/public void setInterpolator(Interpolator interpolator) {//强制停止滚动scroller.forceFinished(true);//创建一个 Scroll 对象scroller = new Scroller(getContext(), interpolator);}/*** 获取课件条目数* * @return the count of visible items*/public int getVisibleItems() {return visibleItems;}/*** 设置可见条目数* * @param count* the new count*/public void setVisibleItems(int count) {visibleItems = count;invalidate();}/*** 获取标签* * @return the label*/public String getLabel() {return label;}/*** 设置标签* * @param newLabel* the label to set*/public void setLabel(String newLabel) {if (label == null || !label.equals(newLabel)) {label = newLabel;labelLayout = null;invalidate();}}/*** 添加 WheelView 选择的元素改变监听器* * @param listener* the listener*/public void addChangingListener(OnWheelChangedListener listener) {changingListeners.add(listener);}/*** 移除 WheelView 元素改变监听器* * @param listener* the listener*/public void removeChangingListener(OnWheelChangedListener listener) {changingListeners.remove(listener);}/*** 回调元素改变监听器集合的元素改变监听器元素的元素改变方法* * @param oldValue* 旧的 WheelView选中的值* @param newValue* 新的 WheelView选中的值*/protected void notifyChangingListeners(int oldValue, int newValue) {for (OnWheelChangedListener listener : changingListeners) {listener.onChanged(this, oldValue, newValue);}}/*** 添加 WheelView 滚动监听器* * @param listener* the listener*/public void addScrollingListener(OnWheelScrollListener listener) {scrollingListeners.add(listener);}/*** 移除 WheelView 滚动监听器* * @param listener* the listener*/public void removeScrollingListener(OnWheelScrollListener listener) {scrollingListeners.remove(listener);}/*** 通知监听器开始滚动*/protected void notifyScrollingListenersAboutStart() {for (OnWheelScrollListener listener : scrollingListeners) {//回调开始滚动方法listener.onScrollingStarted(this);}}/*** 通知监听器结束滚动*/protected void notifyScrollingListenersAboutEnd() {for (OnWheelScrollListener listener : scrollingListeners) {//回调滚动结束方法listener.onScrollingFinished(this);}}/*** 获取当前选中元素的索引* * @return * 当前元素索引*/public int getCurrentItem() {return currentItem;}/*** 设置当前元素的位置, 如果索引是错误的 不进行任何操作* -- 需要考虑该 WheelView 是否能循环* -- 根据是否需要滚动动画来确定是 ①滚动到目的位置 还是 ②晴空所有条目然后重绘* * @param index* 要设置的元素索引值* @param animated* 动画标志位*/public void setCurrentItem(int index, boolean animated) {//如果没有适配器或者元素个数为0 直接返回if (adapter == null || adapter.getItemsCount() == 0) {return; // throw?}//目标索引小于 0 或者大于 元素索引最大值(个数 -1)if (index < 0 || index >= adapter.getItemsCount()) {//入股WheelView 可循环, 修正索引值, 如果不可循环直接返回if (isCyclic) {while (index < 0) {index += adapter.getItemsCount();}index %= adapter.getItemsCount();} else {return; // throw?}}//如果当前的索引不是传入的 索引if (index != currentItem) {/** 如果需要动画, 就滚动到目标位置* 如果不需要动画, 重新设置布局*/if (animated) {/** 开始滚动, 每个元素滚动间隔 400 ms, 滚动次数是 目标索引值 减去 当前索引值, 这是滚动的真实方法*/scroll(index - currentItem, SCROLLING_DURATION);} else {//所有布局设置为 null, 滚动位置设置为 0invalidateLayouts();int old = currentItem;currentItem = index;//便利回调元素改变监听器集合中的监听器元素中的元素改变方法notifyChangingListeners(old, currentItem);//重绘invalidate();}}}/*** 设置当前选中的条目, 没有动画, 当索引出错不做任何操作* * @param index* 要设置的索引*/public void setCurrentItem(int index) {setCurrentItem(index, false);}/*** 获取 WheelView 是否可以循环* -- 如果可循环 : 第一个之前是最后一个, 最后一个之后是第一个;* -- 如果不可循环 : 到第一个就不能上翻, 最后一个不能下翻 * * @return*/public boolean isCyclic() {return isCyclic;}/*** 设置 WheelView 循环标志* * @param isCyclic* the flag to set*/public void setCyclic(boolean isCyclic) {this.isCyclic = isCyclic;invalidate();invalidateLayouts();}/*** 使布局无效* 将 选中条目 和 普通条目设置为 null, 滚动位置设置为0*/private void invalidateLayouts() {itemsLayout = null;valueLayout = null;scrollingOffset = 0;}/*** 初始化资源*/private void initResourcesIfNecessary() {/** 设置绘制普通条目的画笔, 允许抗拒齿, 允许 fake-bold* 设置文字大小为 24*/if (itemsPaint == null) {itemsPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FAKE_BOLD_TEXT_FLAG);itemsPaint.setTextSize(TEXT_SIZE);}/** 设置绘制选中条目的画笔* 设置文字大小 24*/if (valuePaint == null) {valuePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FAKE_BOLD_TEXT_FLAG | Paint.DITHER_FLAG);valuePaint.setTextSize(TEXT_SIZE);valuePaint.setShadowLayer(0.1f, 0, 0.1f, 0xFFC0C0C0);}//选中的条目背景if (centerDrawable == null) {centerDrawable = getContext().getResources().getDrawable(R.drawable.wheel_val);}//创建顶部阴影图片if (topShadow == null) {/** 构造方法中传入颜色渐变方向* 阴影颜色*/topShadow = new GradientDrawable(Orientation.TOP_BOTTOM, SHADOWS_COLORS);}//创建底部阴影图片if (bottomShadow == null) {bottomShadow = new GradientDrawable(Orientation.BOTTOM_TOP, SHADOWS_COLORS);}/** 设置 View 组件的背景*/setBackgroundResource(R.drawable.wheel_bg);}/*** 计算布局期望的高度* * @param layout* 组件的布局的* @return * 布局需要的高度*/private int getDesiredHeight(Layout layout) {if (layout == null) {return 0;}/** 布局需要的高度是 条目个数 * 可见条目数 减去 顶部和底部隐藏的一部份 减去 额外的条目高度*/int desired = getItemHeight() * visibleItems - ITEM_OFFSET * 2 - ADDITIONAL_ITEM_HEIGHT;// 将计算的布局高度 与 最小高度比较, 取最大值desired = Math.max(desired, getSuggestedMinimumHeight());return desired;}/*** 根据条目获取字符串* * @param index* 条目索引* @return * 条目显示的字符串*/private String getTextItem(int index) {if (adapter == null || adapter.getItemsCount() == 0) {return null;}//适配器显示的字符串个数int count = adapter.getItemsCount();//考虑 index 小于 0 的情况if ((index < 0 || index >= count) && !isCyclic) {return null;} else {while (index < 0) {index = count + index;}}//index 大于 0index %= count;return adapter.getItem(index);}/*** 根据当前值创建 字符串* * @param useCurrentValue* 是否在滚动* @return the text* 生成的字符串*/private String buildText(boolean useCurrentValue) {//创建字符串容器StringBuilder itemsText = new StringBuilder();//计算出显示的条目相对位置, 例如显示 5个, 第 3 个是正中见选中的布局int addItems = visibleItems / 2 + 1;/** 遍历显示的条目* 获取当前显示条目 上下 各 addItems 个文本, 将该文本添加到显示文本中去* 如果不是最后一个 都加上回车*/for (int i = currentItem - addItems; i <= currentItem + addItems; i++) {//如果在滚动if (useCurrentValue || i != currentItem) {String text = getTextItem(i);if (text != null) {itemsText.append(text);}}if (i < currentItem + addItems) {itemsText.append("\n");}}return itemsText.toString();}/*** 返回 条目的字符串* * @return * 条目最大宽度*/private int getMaxTextLength() {WheelAdapter adapter = getAdapter();if (adapter == null) {return 0;}//如果获取的最大条目宽度不为 -1, 可以直接返回该条目宽度int adapterLength = adapter.getMaximumLength();if (adapterLength > 0) {return adapterLength;}String maxText = null;int addItems = visibleItems / 2;/** 遍历当前显示的条目, 获取字符串长度最长的那个, 返回这个最长的字符串长度*/for (int i = Math.max(currentItem - addItems, 0); i < Math.min(currentItem + visibleItems,adapter.getItemsCount()); i++) {String text = adapter.getItem(i);if (text != null && (maxText == null || maxText.length() < text.length())) {maxText = text;}}return maxText != null ? maxText.length() : 0;}/*** 获取每个条目的高度* * @return * 条目的高度*/private int getItemHeight() {//如果条目高度不为 0, 直接返回if (itemHeight != 0) {return itemHeight;//如果条目的高度为 0, 并且普通条目布局不为null, 条目个数大于 2 } else if (itemsLayout != null && itemsLayout.getLineCount() > 2) {/** itemsLayout.getLineTop(2) : 获取顶部第二行上面的垂直(y轴)位置, 如果行数等于*/itemHeight = itemsLayout.getLineTop(2) - itemsLayout.getLineTop(1);return itemHeight;}//如果上面都不符合, 使用整体高度处以 显示条目数return getHeight() / visibleItems;}/*** 计算宽度并创建文字布局* * @param widthSize* 输入的布局宽度* @param mode* 布局模式* @return * 计算的宽度*/private int calculateLayoutWidth(int widthSize, int mode) {initResourcesIfNecessary();int width = widthSize;//获取最长的条目显示字符串字符个数int maxLength = getMaxTextLength();if (maxLength > 0) {/** 使用方法 FloatMath.ceil() 方法有以下警告* Use java.lang.Math#ceil instead of android.util.FloatMath#ceil() since it is faster as of API 8*///float textWidth = FloatMath.ceil(Layout.getDesiredWidth("0", itemsPaint));//向上取整 计算一个字符串宽度float textWidth = (float) Math.ceil(Layout.getDesiredWidth("0", itemsPaint));//获取字符串总的宽度itemsWidth = (int) (maxLength * textWidth);} else {itemsWidth = 0;}//总宽度加上一些间距itemsWidth += ADDITIONAL_ITEMS_SPACE; // make it some more//计算 label 的长度labelWidth = 0;if (label != null && label.length() > 0) {labelWidth = (int) Math.ceil(Layout.getDesiredWidth(label, valuePaint));//labelWidth = (int) FloatMath.ceil(Layout.getDesiredWidth(label, valuePaint));}boolean recalculate = false;//精准模式if (mode == MeasureSpec.EXACTLY) {//精准模式下, 宽度就是给定的宽度width = widthSize;recalculate = true;} else {//未定义模式width = itemsWidth + labelWidth + 2 * PADDING;if (labelWidth > 0) {width += LABEL_OFFSET;}// 获取 ( 计算出来的宽度 与 最小宽度的 ) 最大值width = Math.max(width, getSuggestedMinimumWidth());//最大模式 如果 给定的宽度 小于 计算出来的宽度, 那么使用最小的宽度 ( 给定宽度 | 计算出来的宽度 )if (mode == MeasureSpec.AT_MOST && widthSize < width) {width = widthSize;recalculate = true;}}/** 重新计算宽度 , 如果宽度是给定的宽度, 不是我们计算出来的宽度, 需要重新进行计算* 重新计算的宽度是用于* * 计算 itemsWidth , 这个与返回的 宽度无关, 与创建布局有关*/if (recalculate) {int pureWidth = width - LABEL_OFFSET - 2 * PADDING;if (pureWidth <= 0) {itemsWidth = labelWidth = 0;}if (labelWidth > 0) {double newWidthItems = (double) itemsWidth * pureWidth / (itemsWidth + labelWidth);itemsWidth = (int) newWidthItems;labelWidth = pureWidth - itemsWidth;} else {itemsWidth = pureWidth + LABEL_OFFSET; // no label}}if (itemsWidth > 0) {//创建布局createLayouts(itemsWidth, labelWidth);}return width;}/*** 创建布局* * @param widthItems* 布局条目宽度* @param widthLabel* label 宽度*/private void createLayouts(int widthItems, int widthLabel) {/** 创建普通条目布局* 如果 普通条目布局 为 null 或者 普通条目布局的宽度 大于 传入的宽度, 这时需要重新创建布局* 如果 普通条目布局存在, 并且其宽度小于传入的宽度, 此时需要将*/if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {/** android.text.StaticLayout.StaticLayout(* CharSequence source, TextPaint paint, * int width, Alignment align, * float spacingmult, float spacingadd, boolean includepad)* 传入参数介绍 : * CharSequence source : 需要分行显示的字符串* TextPaint paint : 绘制字符串的画笔* int width : 条目的宽度* Alignment align : Layout 的对齐方式, ALIGN_CENTER 居中对齐, ALIGN_NORMAL 左对齐, Alignment.ALIGN_OPPOSITE 右对齐* float spacingmult : 行间距, 1.5f 代表 1.5 倍字体高度* float spacingadd : 基础行距上增加多少 , 真实行间距 等于 spacingmult 和 spacingadd 的和* boolean includepad : */itemsLayout = new StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,ADDITIONAL_ITEM_HEIGHT, false);} else {//调用 Layout 内置的方法 increaseWidthTo 将宽度提升到指定的宽度itemsLayout.increaseWidthTo(widthItems);}/** 创建选中条目*/if (!isScrollingPerformed && (valueLayout == null || valueLayout.getWidth() > widthItems)) {String text = getAdapter() != null ? getAdapter().getItem(currentItem) : null;valueLayout = new StaticLayout(text != null ? text : "", valuePaint, widthItems,widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER, 1,ADDITIONAL_ITEM_HEIGHT, false);} else if (isScrollingPerformed) {valueLayout = null;} else {valueLayout.increaseWidthTo(widthItems);}/** 创建标签条目*/if (widthLabel > 0) {if (labelLayout == null || labelLayout.getWidth() > widthLabel) {labelLayout = new StaticLayout(label, valuePaint, widthLabel, Layout.Alignment.ALIGN_NORMAL, 1,ADDITIONAL_ITEM_HEIGHT, false);} else {labelLayout.increaseWidthTo(widthLabel);}}}/** 测量组件大小* (non-Javadoc)* @see android.view.View#onMeasure(int, int)*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//获取宽度 和 高度的模式 和 大小int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);//宽度就是 计算的布局的宽度int width = calculateLayoutWidth(widthSize, widthMode);int height;/** 精准模式* 精准模式下 高度就是精确的高度*/if (heightMode == MeasureSpec.EXACTLY) {height = heightSize;//未定义模式 和 最大模式} else {//未定义模式下 获取布局需要的高度height = getDesiredHeight(itemsLayout);//最大模式下 获取 布局高度 和 布局所需高度的最小值if (heightMode == MeasureSpec.AT_MOST) {height = Math.min(height, heightSize);}}//设置组件的宽和高setMeasuredDimension(width, height);}/** 绘制组件* (non-Javadoc)* @see android.view.View#onDraw(android.graphics.Canvas)*/@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//如果条目布局为 null, 就创建该布局if (itemsLayout == null) {/** 如果 条目宽度为0, 说明该宽度没有计算, 先计算, 计算完之后会创建布局* 如果 条目宽度 大于 0, 说明已经计算过宽度了, 直接创建布局*/if (itemsWidth == 0) {calculateLayoutWidth(getWidth(), MeasureSpec.EXACTLY);} else {//创建普通条目布局, 选中条目布局, 标签条目布局createLayouts(itemsWidth, labelWidth);}}//如果条目宽度大于0if (itemsWidth > 0) {canvas.save();// 使用平移方法忽略 填充的空间 和 顶部底部隐藏的一部份条目canvas.translate(PADDING, -ITEM_OFFSET);//绘制普通条目drawItems(canvas);//绘制选中条目drawValue(canvas);canvas.restore();}//在中心位置绘制drawCenterRect(canvas);//绘制阴影drawShadows(canvas);}/*** Draws shadows on top and bottom of control* * @param canvas* the canvas for drawing*/private void drawShadows(Canvas canvas) {topShadow.setBounds(0, 0, getWidth(), getHeight() / visibleItems);topShadow.draw(canvas);bottomShadow.setBounds(0, getHeight() - getHeight() / visibleItems, getWidth(), getHeight());bottomShadow.draw(canvas);}/*** 绘制选中条目* * @param canvas* 画布*/private void drawValue(Canvas canvas) {valuePaint.setColor(VALUE_TEXT_COLOR);//将当前 View 状态属性值 转为整型集合, 赋值给 普通条目布局的绘制属性valuePaint.drawableState = getDrawableState();Rect bounds = new Rect();//获取选中条目布局的边界itemsLayout.getLineBounds(visibleItems / 2, bounds);// 绘制标签if (labelLayout != null) {canvas.save();canvas.translate(itemsLayout.getWidth() + LABEL_OFFSET, bounds.top);labelLayout.draw(canvas);canvas.restore();}// 绘制选中条目if (valueLayout != null) {canvas.save();canvas.translate(0, bounds.top + scrollingOffset);valueLayout.draw(canvas);canvas.restore();}}/*** 绘制普通条目* * @param canvas* 画布*/private void drawItems(Canvas canvas) {canvas.save();//获取 y 轴 定点高度int top = itemsLayout.getLineTop(1);canvas.translate(0, -top + scrollingOffset);//设置画笔颜色itemsPaint.setColor(ITEMS_TEXT_COLOR);//将当前 View 状态属性值 转为整型集合, 赋值给 普通条目布局的绘制属性itemsPaint.drawableState = getDrawableState();//将布局绘制到画布上itemsLayout.draw(canvas);canvas.restore();}/*** 绘制当前选中条目的背景图片* * @param canvas* 画布*/private void drawCenterRect(Canvas canvas) {int center = getHeight() / 2;int offset = getItemHeight() / 2;centerDrawable.setBounds(0, center - offset, getWidth(), center + offset);centerDrawable.draw(canvas);}/** 继承自 View 的触摸事件, 当出现触摸事件的时候, 就会回调该方法* (non-Javadoc)* @see android.view.View#onTouchEvent(android.view.MotionEvent)*/@Overridepublic boolean onTouchEvent(MotionEvent event) {//获取适配器WheelAdapter adapter = getAdapter();if (adapter == null) {return true;}/** gestureDetector.onTouchEvent(event) : 分析给定的动作, 如果可用, 调用 手势检测器的 onTouchEvent 方法* -- 参数解析 : ev , 触摸事件* -- 返回值 : 如果手势监听器成功执行了该方法, 返回true, 如果执行出现意外 返回 false;*/if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {justify();}return true;}/*** 滚动 WheelView* * @param delta* 滚动的值*/private void doScroll(int delta) {scrollingOffset += delta;//计算滚动的条目数, 使用滚动的值 处于 单个条目高度, 注意计算整数值int count = scrollingOffset / getItemHeight();/** pos 是滚动后的目标元素索引* 计算当前位置, 当前条目数 减去 滚动的条目数* 注意 滚动条目数可正 可负*/int pos = currentItem - count;//如果是可循环的, 并且条目数大于0if (isCyclic && adapter.getItemsCount() > 0) {//设置循环, 如果位置小于0, 那么该位置就显示最后一个元素while (pos < 0) {pos += adapter.getItemsCount();}//如果位置正无限大, 模条目数 取余pos %= adapter.getItemsCount();// (前提 : 不可循环 条目数大于0, 可循环 条目数小于0, 条目数小于0, 不可循环) , 如果滚动在执行} else if (isScrollingPerformed) {//位置一旦小于0, 计算的位置就赋值为 0, 条目滚动数为0if (pos < 0) {count = currentItem;pos = 0;//位置大于条目数的时候, 当前位置等于(条目数 - 1), 条目滚动数等于 当前位置 减去 (条目数 - 1)} else if (pos >= adapter.getItemsCount()) {count = currentItem - adapter.getItemsCount() + 1;pos = adapter.getItemsCount() - 1;}} else {// fix positionpos = Math.max(pos, 0);pos = Math.min(pos, adapter.getItemsCount() - 1);}//滚动的高度int offset = scrollingOffset;/** 如果当前位置不是滚动后的目标位置, 就将当前位置设置为目标位置* 否则就重绘组件*/if (pos != currentItem) {setCurrentItem(pos, false);} else {//重绘组件invalidate();}// 将滚动后剩余的小数部分保存scrollingOffset = offset - count * getItemHeight();if (scrollingOffset > getHeight()) {scrollingOffset = scrollingOffset % getHeight() + getHeight();}}/*** 手势监听器*/private SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() {//按下操作public boolean onDown(MotionEvent e) {//如果滚动在执行if (isScrollingPerformed) {//滚动强制停止, 按下的时候不能继续滚动scroller.forceFinished(true);//清理信息clearMessages();return true;}return false;}/** 手势监听器监听到 滚动操作后回调* * 参数解析 : * MotionEvent e1 : 触发滚动时第一次按下的事件* MotionEvent e2 : 触发当前滚动的移动事件* float distanceX : 自从上一次调用 该方法 到这一次 x 轴滚动的距离, * 注意不是 e1 到 e2 的距离, e1 到 e2 的距离是从开始滚动到现在的滚动距离* float distanceY : 自从上一次回调该方法到这一次 y 轴滚动的距离* * 返回值 : 如果事件成功触发, 执行完了方法中的操作, 返回true, 否则返回 false * (non-Javadoc)* @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float)*/public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//开始滚动, 并回调滚动监听器集合中监听器的 开始滚动方法startScrolling();doScroll((int) -distanceY);return true;}/** 当一个急冲手势发生后 回调该方法, 会计算出该手势在 x 轴 y 轴的速率* * 参数解析 : * -- MotionEvent e1 : 急冲动作的第一次触摸事件;* -- MotionEvent e2 : 急冲动作的移动发生的时候的触摸事件;* -- float velocityX : x 轴的速率* -- float velocityY : y 轴的速率* * 返回值 : 如果执行完毕返回 true, 否则返回false, 这个就是自己定义的* * (non-Javadoc)* @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float)*/public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {//计算上一次的 y 轴位置, 当前的条目高度 加上 剩余的 不够一行高度的那部分lastScrollY = currentItem * getItemHeight() + scrollingOffset;//如果可以循环最大值是无限大, 不能循环就是条目数的高度值int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();int minY = isCyclic ? -maxY : 0;/** Scroll 开始根据一个急冲手势滚动, 滚动的距离与初速度有关* 参数介绍 : * -- int startX : 开始时的 X轴位置* -- int startY : 开始时的 y轴位置* -- int velocityX : 急冲手势的 x 轴的初速度, 单位 px/s* -- int velocityY : 急冲手势的 y 轴的初速度, 单位 px/s* -- int minX : x 轴滚动的最小值* -- int maxX : x 轴滚动的最大值* -- int minY : y 轴滚动的最小值* -- int maxY : y 轴滚动的最大值*/scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);setNextMessage(MESSAGE_SCROLL);return true;}};// Handler 中的 Message 信息/** 滚动信息 */private final int MESSAGE_SCROLL = 0;/** 调整信息 */private final int MESSAGE_JUSTIFY = 1;/*** 清空之前的 Handler 队列, 发送下一个消息到 Handler 中* * @param message* 要发送的消息*/private void setNextMessage(int message) {//清空 Handler 队列中的 what 消息clearMessages();//发送消息到 Handler 中animationHandler.sendEmptyMessage(message);}/*** 清空队列中的信息*/private void clearMessages() {//删除 Handler 执行队列中的滚动操作animationHandler.removeMessages(MESSAGE_SCROLL);animationHandler.removeMessages(MESSAGE_JUSTIFY);}/*** 动画控制器* animation handler* * 可能会造成内存泄露 : 添加注解 HandlerLeak* Handler 类应该应该为static类型,否则有可能造成泄露。* 在程序消息队列中排队的消息保持了对目标Handler类的应用。* 如果Handler是个内部类,那 么它也会保持它所在的外部类的引用。* 为了避免泄露这个外部类,应该将Handler声明为static嵌套类,并且使用对外部类的弱应用。*/@SuppressLint("HandlerLeak")private Handler animationHandler = new Handler() {public void handleMessage(Message msg) {//回调该方法获取当前位置, 如果返回true, 说明动画还没有执行完毕scroller.computeScrollOffset();//获取当前 y 位置int currY = scroller.getCurrY();//获取已经滚动了的位置, 使用上一次位置 减去 当前位置int delta = lastScrollY - currY;lastScrollY = currY;if (delta != 0) {//改变值不为 0 , 继续滚动doScroll(delta);}/** 如果滚动到了指定的位置, 滚动还没有停止* 这时需要强制停止*/if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) {currY = scroller.getFinalY();scroller.forceFinished(true);}/** 如果滚动没有停止* 再向 Handler 发送一个停止*/if (!scroller.isFinished()) {animationHandler.sendEmptyMessage(msg.what);} else if (msg.what == MESSAGE_SCROLL) {justify();} else {finishScrolling();}}};/*** 调整 WheelView*/private void justify() {if (adapter == null) {return;}//上一次的 y 轴的位置为 0lastScrollY = 0;int offset = scrollingOffset;int itemHeight = getItemHeight();/** 当滚动补偿 大于 0, 说明还有没有滚动的部分, needToIncrease 是 当前条目是否小于条目数* 如果 滚动补偿不大于 0, needToIncrease 是当前条目是否大于 0*/boolean needToIncrease = offset > 0 ? currentItem < adapter.getItemsCount() : currentItem > 0;if ((isCyclic || needToIncrease) && Math.abs((float) offset) > (float) itemHeight / 2) {if (offset < 0)offset += itemHeight + MIN_DELTA_FOR_SCROLLING;elseoffset -= itemHeight + MIN_DELTA_FOR_SCROLLING;}if (Math.abs(offset) > MIN_DELTA_FOR_SCROLLING) {scroller.startScroll(0, 0, 0, offset, SCROLLING_DURATION);setNextMessage(MESSAGE_JUSTIFY);} else {finishScrolling();}}/*** WheelView 开始滚动*/private void startScrolling() {//如果没有滚动, 将滚动状态 isScrollingPerformed 设为 trueif (!isScrollingPerformed) {isScrollingPerformed = true;//通知监听器开始滚动 回调所有的 滚动监听集合中 的 开始滚动方法notifyScrollingListenersAboutStart();}}/*** 结束滚动* 设置滚动状态为 false, 回调滚动监听器的停止滚动方法*/void finishScrolling() {if (isScrollingPerformed) {notifyScrollingListenersAboutEnd();isScrollingPerformed = false;}//设置布局无效invalidateLayouts();//重绘布局invalidate();}/*** 滚动 WheelView* * @param itemsToSkip* 滚动的元素个数* @param time* 每次滚动的间隔*/public void scroll(int itemsToScroll, int time) {//如果有滚动强制停止scroller.forceFinished(true);lastScrollY = scrollingOffset;int offset = itemsToScroll * getItemHeight();/** 给定 一个开始点, 滚动距离, 滚动间隔, 开始滚动* * 参数解析 : * 1. 开始的 x 轴位置* 2. 开始的 y 轴位置* 3. 要滚动 x 轴距离* 4. 要滚动 y 轴距离* 5. 滚动花费的时间*/scroller.startScroll(0, lastScrollY, 0, offset - lastScrollY, time);setNextMessage(MESSAGE_SCROLL);//设置开始滚动状态, 并回调滚动监听器方法startScrolling();}}
7. Activity 主界面
package cn.org.octopus.wheelview;import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.Button; import android.widget.LinearLayout; import cn.org.octopus.wheelview.widget.ArrayWheelAdapter; import cn.org.octopus.wheelview.widget.OnWheelChangedListener; import cn.org.octopus.wheelview.widget.OnWheelScrollListener; import cn.org.octopus.wheelview.widget.WheelView;public class MainActivity extends Activity{public static final String TAG = "octopus.activity";private static Button bt_click;public String province[] = new String[] { " 河北省 ", " 山西省 ", " 内蒙古 ", " 辽宁省 ", " 吉林省 ", " 黑龙江 ", " 江苏省 " };public String city[][] = new String[][] {new String[] {" 石家庄 ", "唐山", "秦皇岛", "邯郸", "邢台", "保定", "张家口", "承德", "沧州", "廊坊", "衡水"},new String[] {"太原", "大同", "阳泉", "长治", "晋城", "朔州", "晋中", "运城", "忻州", "临汾", "吕梁"},new String[] {"呼和浩特", "包头", "乌海", "赤峰", "通辽", "鄂尔多斯", "呼伦贝尔", "巴彦淖尔", "乌兰察布", "兴安", "锡林郭勒", "阿拉善"},new String[] {"沈阳", "大连", "鞍山", "抚顺", "本溪", "丹东", "锦州", "营口", "阜新", "辽阳", "盘锦", "铁岭", "朝阳", "葫芦岛"},new String[] {"长春", "吉林", "四平", "辽源", "通化", "白山", "松原", "白城", "延边"},new String[] {"哈尔滨", "齐齐哈尔", "鸡西", "鹤岗", "双鸭山", "大庆", "伊春", "佳木斯", "七台河", "牡丹江", "黑河", "绥化", "大兴安岭"},new String[] {"南京", "无锡", "徐州", "常州", "苏州", "南通", "连云港", "淮安", "盐城", "扬州", "镇江", "泰州", "宿迁"} };@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);if (savedInstanceState == null) {getFragmentManager().beginTransaction().add(R.id.container, new PlaceholderFragment()).commit();}}/** 点击事件*/public void onClick(View view) {showSelectDialog(this, "选择地点", province, city);}private void showSelectDialog(Context context, String title, final String[] left, final String[][] right) {//创建对话框AlertDialog dialog = new AlertDialog.Builder(context).create();//为对话框设置标题dialog.setTitle(title);//创建对话框内容, 创建一个 LinearLayout LinearLayout llContent = new LinearLayout(context);//将创建的 LinearLayout 设置成横向的llContent.setOrientation(LinearLayout.HORIZONTAL);//创建 WheelView 组件final WheelView wheelLeft = new WheelView(context);//设置 WheelView 组件最多显示 5 个元素wheelLeft.setVisibleItems(5);//设置 WheelView 元素是否循环滚动wheelLeft.setCyclic(false);//设置 WheelView 适配器wheelLeft.setAdapter(new ArrayWheelAdapter<String>(left));//设置右侧的 WheelViewfinal WheelView wheelRight = new WheelView(context);//设置右侧 WheelView 显示个数wheelRight.setVisibleItems(5);//设置右侧 WheelView 元素是否循环滚动wheelRight.setCyclic(true);//设置右侧 WheelView 的元素适配器wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[0]));//设置 LinearLayout 的布局参数LinearLayout.LayoutParams paramsLeft = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT, 4);paramsLeft.gravity = Gravity.LEFT;LinearLayout.LayoutParams paramsRight = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT, 6);paramsRight.gravity = Gravity.RIGHT;//将 WheelView 对象放到左侧 LinearLayout 中llContent.addView(wheelLeft, paramsLeft);//将 WheelView 对象放到 右侧 LinearLayout 中llContent.addView(wheelRight, paramsRight);//为左侧的 WheelView 设置条目改变监听器wheelLeft.addChangingListener(new OnWheelChangedListener() {@Overridepublic void onChanged(WheelView wheel, int oldValue, int newValue) {//设置右侧的 WheelView 的适配器wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[newValue]));wheelRight.setCurrentItem(right[newValue].length / 2);}});wheelLeft.addScrollingListener(new OnWheelScrollListener() {@Overridepublic void onScrollingStarted(WheelView wheel) {// TODO Auto-generated method stub}@Overridepublic void onScrollingFinished(WheelView wheel) {// TODO Auto-generated method stub}});//设置对话框点击事件 积极dialog.setButton(AlertDialog.BUTTON_POSITIVE, "确定", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {int leftPosition = wheelLeft.getCurrentItem();String vLeft = left[leftPosition];String vRight = right[leftPosition][wheelRight.getCurrentItem()];bt_click.setText(vLeft + "-" + vRight);dialog.dismiss();}});//设置对话框点击事件 消极dialog.setButton(AlertDialog.BUTTON_NEGATIVE, "取消", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {dialog.dismiss();}});//将 LinearLayout 设置到 对话框中dialog.setView(llContent);//显示对话框if (!dialog.isShowing()) {dialog.show();}}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {// Inflate the menu; this adds items to the action bar if it is present.getMenuInflater().inflate(R.menu.main, menu);return true;}@Overridepublic boolean onOptionsItemSelected(MenuItem item) {// Handle action bar item clicks here. The action bar will// automatically handle clicks on the Home/Up button, so long// as you specify a parent activity in AndroidManifest.xml.int id = item.getItemId();if (id == R.id.action_settings) {return true;}return super.onOptionsItemSelected(item);}/*** A placeholder fragment containing a simple view.*/public static class PlaceholderFragment extends Fragment {public PlaceholderFragment() {}@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {View rootView = inflater.inflate(R.layout.fragment_main, container,false);bt_click = (Button)rootView.findViewById(R.id.bt_click);return rootView;}}}
博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569#t17
代码下载 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;
代码下载 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;
博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569#t17
代码下载 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;
博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569#t17
代码下载 :
-- GitHub : https://github.com/han1202012/WheelViewDemo.git
-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;
总结
以上是生活随笔为你收集整理的【Android 应用开发】 自定义组件 宽高适配方法, 手势监听器操作组件, 回调接口维护策略, 绘制方法分析 -- 基于 WheelView 组件分析自定义组件的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 博客技术资料整理
- 下一篇: 【Android 应用开发】 Andro