【Android 应用开发】 自定义组件 宽高适配方法, 手势监听器操作组件, 回调接口维护策略, 绘制方法分析 -- 基于 WheelView 组件分析自定
来源:程序员人生 发布时间:2014-12-12 08:33:09 阅读次数:4516次
博客地址 : 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()方法;
1. 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() {
@Override
public 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() {
@Override
public void onScrollingStarted(WheelView wheel) {
// TODO Auto-generated method stub
}
@Override
public void onScrollingFinished(WheelView wheel) {
// TODO Auto-generated method stub
}
});
2. WheelView 适配器 监听器 相干接口分析
1. 适配器 分析
这里定义了1个适配器接口, 和两个适配器类, 1个用于任意类型的数据集适配, 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 的宽度. 如果返回 ⑴, 就会使用默许宽度
*
* @return
* 条目的最大宽度 或 ⑴
*/
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) : 根据索引获得数组中对应位置的对象的字符串类型;
@Override
public String getItem(int index) {
//如果这个索引值合法, 就返回 item 数组对应的元素的字符串情势
if (index >= 0 && index < items.length) {
return items[index].toString();
}
return null;
}
-- getItemsCount() : 获得数据集广大小, 直接返回数组大小;
@Override
public int getItemsCount() {
//返回 item 数组的长度
return items.length;
}
-- getMaximumLength() : 获得 WheelView 的最大宽度;
@Override
public int getMaximumLength() {
//返回 item 元素的宽度
return length;
}
(3) 数字适配器 ( class NumericWheelAdapter implements WheelAdapter )
NumericWheelAdapter 适配器作用 : 数字作为 WheelView 适配器的显示值;
成员变量分析 :
-- 最小值 : WheelView 数值显示的最小值;
/** 设置的最小值 */
private int minValue;
-- 最大值 : WheelView 数值显示的最大值;
/** 设置的最大值 */
private int maxValue;
--
格式化字符串 : 用于字符串的格式化;
/** 格式化字符串, 用于格式化 货币, 科学计数, 106进制 等格式 */
private String format;
构造方法分析 :
-- NumericWheelAdapter() : 默许的构造方法, 使用默许的最大最小值;
/**
* 默许的构造方法, 使用默许的最大最小值
*/
public NumericWheelAdapter() {
this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE);
}
-- NumericWheelAdapter(int minValue, int maxValue) : 传入1个最大最小值;
/**
* 构造方法
*
* @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;
}
实现的父类方法 :
-- 获得条目 : 如果需要格式化, 先进行格式化;
@Override
public 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;
}
-- 获得元素个数 :
@Override
public int getItemsCount() {
//返回数字总个数
return maxValue - minValue + 1;
}
-- 获得 WheelView 最大宽度 :
@Override
public 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);
3. WheelView 解析
1. 触摸 点击 手势 动作操作控制组件 模块
(1) 创建手势监听器
手势监听器创建及对应方法 :
-- onDown(MotionEvent e) : 在按下的时候回调该方法, e 参数是按下的事件;
-- onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) : 转动的时候回调该方法, e1 转动第1次按下事件, e2 当前转动的触摸事件, X 上1次转动到这1次转动 x 轴距离, Y 上1次转动到这1次转动 y 轴距离;
-- onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) : 快速急冲转动时回调的方法, e1 e2 与上面参数相同, velocityX 是手势在 x 轴的速度, velocityY 是手势在 y 轴的速度;
-- 代码示例 :
/*
* 手势监听器监听到 转动操作后回调
*
* 参数解析 :
* MotionEvent e1 : 触发转动时第1次按下的事件
* MotionEvent e2 : 触发当前转动的移动事件
* float distanceX : 自从上1次调用 该方法 到这1次 x 轴转动的距离,
* 注意不是 e1 到 e2 的距离, e1 到 e2 的距离是从开始转动到现在的转动距离
* float distanceY : 自从上1次回调该方法到这1次 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;
}
/*
* 当1个急冲手势产生后 回调该方法, 会计算出该手势在 x 轴 y 轴的速率
*
* 参数解析 :
* -- MotionEvent e1 : 急冲动作的第1次触摸事件;
* -- 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) {
//计算上1次的 y 轴位置, 当前的条目高度 加上 剩余的 不够1行高度的那部份
lastScrollY = currentItem * getItemHeight() + scrollingOffset;
//如果可以循环最大值是无穷大, 不能循环就是条目数的高度值
int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();
int minY = isCyclic ? -maxY : 0;
/*
* Scroll 开始根据1个急冲手势转动, 转动的距离与初速度有关
* 参数介绍 :
* -- 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) 方法, 制止长按操作, 由于 长按操作会屏蔽转动事件;
//创建1个手势处理
gestureDetector = new GestureDetector(context, gestureListener);
/*
* 是不是允许长按操作,
* 如果设置为 true 用户按下不松开, 会返回1个长按事件,
* 如果设置为 false, 按下不松开滑动的话 会收到转动事件.
*/
gestureDetector.setIsLongpressEnabled(false);
(3) 将手势探测器 与 组件结合
关联手势探测器 与 组件 : 在组件的 onTouchEvent(MotionEvent event) 方法中, 调用手势探测器的 gestureDetector.onTouchEvent(event) 方法便可;
/*
* 继承自 View 的触摸事件, 当出现触摸事件的时候, 就会回调该方法
* (non-Javadoc)
* @see android.view.View#onTouchEvent(android.view.MotionEvent)
*/
@Override
public 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 组件其实不是1个布局组件, 该组件是运行在后台的, 通过1些方法设定 Scroller 对象 的操作 或 动画, 然后让 Scroller 运行在后台中 用于摹拟转动操作, 在适当的时机 获得该对象的坐标信息, 这些信息是在后台运算出来的;
Scroller 在本 View 中作用 : Android 的这个自定义的 WheelView 组件, 可以平滑的转动, 当我们做1个加速滑动时, 会根据速度计算出滑动的距离, 这些数据都是在 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)
--
开始转动 设定时间 : 最后1个参数是时间, 单位是 ms;
public void startScroll(int startX, int startY, int dx, int dy, int duration)
--
急冲转动 : 根据1个 急冲 手势进行转动, 传入参数 : 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)
延长转动时间 : 延长转动的时间, 让转动滚的更远1些;
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 创建 :
//使用默许的 时间 和 插入器 创建1个转动器
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) {
//计算上1次的 y 轴位置, 当前的条目高度 加上 剩余的 不够1行高度的那部份
lastScrollY = currentItem * getItemHeight() + scrollingOffset;
//如果可以循环最大值是无穷大, 不能循环就是条目数的高度值
int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();
int minY = isCyclic ? -maxY : 0;
/*
* Scroll 开始根据1个急冲手势转动, 转动的距离与初速度有关
* 参数介绍 :
* -- 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 轴位置, 与上1次的 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();
//获得已转动了的位置, 使用上1次位置 减去 当前位置
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 发送1个停止
*/
if (!scroller.isFinished()) {
animationHandler.sendEmptyMessage(msg.what);
} else if (msg.what == MESSAGE_SCROLL) {
justify();
} else {
finishScrolling();
}
}
};
3. StaticLayout 布局容器
(1) StaticLayout 解析
StaticLayout 解析 : 该组件用于显示文本, 1旦该文本被显示后, 就不能再编辑, 如果想要修改文本, 使用 DynamicLayout 布局便可;
-- 使用处景 : 1般情况下不会使用该组件, 当想要自定义组件 或 想要使用 Canvas 绘制文本时 才使用该布局;
经常使用方法解析 :
-- 获得底部 Padding : 获得底部 到最后1行文字的 间隔, 单位是 px;
public int getBottomPadding()
-- 获得顶部 Padding :
public int getTopPadding()
--
获得省略个数 : 获得某1行需要省略的字符个数;
public int getEllipsisCount(int line)
--
获得省略开始位置 : 获得某1行要省略的字符串的第1个位置索引;
public int getEllipsisStart(int line)
--
获得省略的宽度 : 获得某1行省略字符串的宽度, 单位 px;
public int getEllipsisStart(int line)
--
获得是不是处理特殊符号 :
public boolean getLineContainsTab(int line)
--
获得文字的行数 :
public int getLineCount()
--
获得顶部位置 : 获得某1行顶部的位置;
public int getLineTop(int line)
--
获得某1行底部位置 :
public int getLineDescent(int line)
--
获得行的方向 : 字符串从左至右 还是从右至左;
public final Directions getLineDirections(int line)
--
获得某行第1个字符索引 : 获得的是 某1行 第1个字符 在全部字符串的索引;
public int getLineStart(int line)
--
获得该行段落方向 : 获得该行文字方向, 左至右 或 右至左;
public int getParagraphDirection(int line)
--
获得某个垂直位置显示的行数 :
public int getLineForVertical(int vertical)
(2) 布局显示
布局创建 :
-- 3种布局 : WheelView 中触及到了3种 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 组件中 定义1个 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 模式解析
常规处理方法 : 组件的宽高有3种情况, widthMeasureSpec 有3种模式 最大模式, 精准模式, 未定义模式;
-- 最大模式 : 在 组件的宽或高 warp_content 属性时, 会使用最大模式;
-- 精准模式 : 当给组件宽 或高 定义1个值 或 使用 match_parent 时, 会使用精准模式;
处理宽高的常规代码 :
@Override
protected 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 + "
"
+ "高度 : 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);
}
@Override
protected 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 + "
"
+ "高度 : 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
生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
------分隔线----------------------------
------分隔线----------------------------