源码下载:https://github.com/victorfan336/WheelView 喜欢的话就star下吧。
前言
当初写这个控件基于三个原因:
- 想找个控件来练练手,再次熟悉熟悉自定义view
- 最近开始流行学习kotlin语言了,我也已经学习了一段时间,想练练手
- 最近发现公司的滚轮控件出现了一个bug
先上效果图:
一、使用
1.1 特点
* 完全使用自定义的view编写完成,我看过有些人不是完全基于view写的
* 实现了三种滚轮模式:
* 循环模式:
* 居中显示模式;
* 从头开始显示
* 自己处理了滚动事件和快速滑动事件
* 处理了边界检测和弹性效果
* 通过adapter快速添加数据
导入:compile 'com.victor.library:wheelview:1.0.7@aar'
1.新增itemHeight属性配置;
2.解决UI拖出可见范围后,有时回弹不准的问题,是由于没有做四舍五入的问题导致的;
3.拓展滚动监听方法,回传wheelview;
4.新增设置当前选择位置和获取当前选择位置方法:
public void setCurrItem(int index);
public int getSelectedItem();
java版本已在公司APP中使用!
1.2 定义了三个可配置属性
<attr name="textColor" format="color"/>
<attr name="textSize" format="dimension" />
<attr name="dragOut" format="boolean" />
<attr name="itemheight" format="dimension" />
1.3 在xml中配置
<com.victor.library.wheelview.WheelView
android:id="@+id/wheelview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:focusable="true"
android:gravity="center"
app:dragOut="true"
app:textColor="@color/black"
app:textSize="12sp"
/>
1.4 自定义Adapter
只需要实现IWheelviewAdapter即可public interface IWheelviewAdapter<T> {
String getItemeTitle(int i);
int getCount();
T get(int index);
void clear();
}
只要实现以上接口方法即可,下面示范一个自定义的Adapter:
public class WheelviewAdapter implements IWheelviewAdapter {
private List<String> mList;
public WheelviewAdapter(List<String> list) {
mList = list;
}
@Override
public String getItemeTitle(int i) {
if (mList != null) {
return mList.get(i);
} else {
return "";
}
}
@Override
public int getCount() {
if (mList != null) {
return mList.size();
} else {
return 0;
}
}
@Override
public String get(int index) {
if (mList != null && index >= 0 && index < mList.size()) {
return mList.get(index);
} else {
return null;
}
}
@Override
public void clear() {
if (mList != null) {
mList.clear();
}
}
}
1.5 在代码中配置
private String[] provides = {"天津市", "北京市", "黑龙江省", "江苏省", "浙江省", "安徽省",
"福建省", "江西省", "山东省", "河南省", "湖北省", "湖南省", "广东省"};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayList<String> provider = new ArrayList<>();
for (String name : provides) {
provider.add(name);
}
wheelView1 = (WheelView) findViewById(R.id.wheelview1);
IWheelviewAdapter providerAdapter = new WheelviewAdapter(provider);
wheelView1.setAdapter(providerAdapter);
// 设置滚动选择监听
wheelView1.setWheelScrollListener(new WheelView.WheelScrollListener() {
@Override
public void onChanged(WheelView wheelView, int selected, Object bean) {
Toast.makeText(DemoActivity.this, bean + "被选中了第" + selected, Toast.LENGTH_SHORT).show();
}
});
}
设置对齐模式:默认是WheelViewCenterMode居中显示
wheelView1.setMode(WheelView.getStartModeInstance(wheelView1));
wheelView1.setMode(WheelView.getCenterModeInstance(wheelView1));
wheelView1.setMode(WheelView.getRecycleModeInstance(wheelView1));
二、源码分析
2.1自定义流程
自定义流程主要包括:
1)接收xml属性配置;
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WheelView, defStyleAttr, 0); int count = typedArray.getIndexCount(); for (int i = 0; i < count; i++) { int attr = typedArray.getIndex(i); if (attr == R.styleable.WheelView_textColor) { textColor = typedArray.getColor(attr, 0x000000); } else if (attr == R.styleable.WheelView_textSize) { textSize = typedArray.getDimension(attr, 19f); } else if (attr == R.styleable.WheelView_dragOut) { canDragOutBorder = typedArray.getBoolean(attr, true); } else if (attr == R.styleable.WheelView_itemHeight) { eachItemHeight = (int) typedArray.getDimension(attr, 12); } } typedArray.recycle();
2)实现public voidonMeasure(intwidthMeasureSpec, intheightMeasureSpec);
@Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthMOde = MeasureSpec.getMode((widthMeasureSpec)); int heightM = MeasureSpec.getSize(heightMeasureSpec); int widthM = MeasureSpec.getSize(widthMeasureSpec); if (heightMode == MeasureSpec.EXACTLY && widthMOde == MeasureSpec.EXACTLY) { setMeasuredDimension(widthM, heightM); } else if (heightMode == MeasureSpec.EXACTLY) { setMeasuredDimension(600, heightM); } else if (widthMOde == MeasureSpec.EXACTLY) { setMeasuredDimension(widthM, 600); } else { setMeasuredDimension(400, 600); } }
3)实现public voidonDraw(Canvas canvas);
@Override public void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(0xffffff); clipView(canvas);// 指定绘制区域 drawText(canvas);// 绘制文本内容 drawLine(canvas);// 绘制两根分割线 drawShadows(canvas);// 绘制阴影遮罩 }
关于怎么自定义控件,请参考:http://blog.csdn.net/column/details/androidcustomview.html
2.2 onTouch事件
自定义onTouch主要实现:
1)边界检查
2)滚动效果处理
3)滚动定位
4)选中位置回调
2.3 onFling事件
通过手势实现onFling()方法来处理快速滑动效果,不然快速时界面会出现卡顿。代码中是通过scroller来处理滚动的。
@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { int dy = (int) (-vTracker.getYVelocity() / 8); // 边距检测 if (getScrollY() + dy <= wheelViewMode.getTopMaxScrollHeight()) { dy = wheelViewMode.getTopMaxScrollHeight() - getScrollY(); } if (getScrollY() + dy >= wheelViewMode.getBottomMaxScrollHeight()) { dy = wheelViewMode.getBottomMaxScrollHeight() - getScrollY(); } // 取整每次onfling的距离 int scrollDy = getScrollY() % eachItemHeight; if (scrollDy + dy % eachItemHeight > eachItemHeight / 2) { dy += eachItemHeight - (scrollDy + dy % eachItemHeight); } else if (scrollDy + dy % eachItemHeight > 0 && scrollDy + dy % eachItemHeight <= eachItemHeight / 2) { dy -= scrollDy + dy % eachItemHeight; } else if (scrollDy + dy % eachItemHeight < 0 && scrollDy + dy % eachItemHeight >= -eachItemHeight / 2) { dy -= scrollDy + dy % eachItemHeight; } else if (scrollDy + dy % eachItemHeight < -eachItemHeight / 2) { dy -= scrollDy + dy % eachItemHeight + eachItemHeight; } Log.e(TAG, "GestureDetector: dy = " + dy + " -- getScrollY() = " + getScrollY()); scroller.startScroll(0, getScrollY(), 0, dy, 400); invalidate(); return true; }
首先,我们拿到vTracker当前y方向上的速度,换算成我们需要滚动的距离,我这里处理比较简单,每次去的距离是-vTracker.getYVelocity() / 8;负号的话只是方向问题;
其次,检测快速滚动dy距离后,是否已经滚出了屏幕;
最后,取整滚动的距离,因为onFling方法是在松手后才会执行的,所以为了确保滚动后能够让文本信息还是居中显示,松后前滚动的距离和快速滑动的距离dy之和必须是eachItemHeight的倍数。
最终再通过scroller滚动界面,来达到快速滚动的效果,scroller可以设置弹性动画,这就是使用scroller很大的一个好处。
2.4 三种显示模式
显示模式目前处理了三种,当然你也可以还有其他的显示方式。
显示模式主要实现以下接口即可:
public abstract class IWheelViewMode { int eachItemHeight; int childrenSize; public int getEachItemHeight() { return eachItemHeight; } public void setEachItemHeight(int eachItemHeight) { this.eachItemHeight = eachItemHeight; } public int getChildrenSize() { return childrenSize; } public void setChildrenSize(int childrenSize) { this.childrenSize = childrenSize; } public IWheelViewMode(int eachItemHeight, int childrenSize) { this.eachItemHeight = eachItemHeight; this.childrenSize = childrenSize; } public abstract int getSelectedIndex(int baseIndex);// 将滚动的位置换算成当前的选中位置 public abstract int getTopMaxScrollHeight(); // 向上最大滚动距离 public abstract int getBottomMaxScrollHeight(); // 想下最大滚动距离 public abstract float getTextDrawY(int height, int index, Paint paint); // 绘制text的Y方向的位置 public float getCenterY(int height, Paint paint) { return (height - paint.getFontMetrics().bottom - paint.getFontMetrics().top) / 2; } }
getSelectedIndex(int baseIndex):baseIndex是scrollY滚动后的当前位置,
float offset = 0.5f; if (getScrollY() < 0) { offset = -0.5f; } int moveIndex = (int) (getScrollY() * 1f / eachItemHeight + offset); int selected = wheelViewMode.getSelectedIndex(moveIndex);
代码很容易,scrollY / eachItemHeight就是滚动了多少个item,然后四舍五入,因为这是在松手后调用的,后面可能还有onFling方法会执行。
1)WheelViewStartMode:当前默认位置是0,返回的参数和baseIndex参数是一样,也就是说从第一个文本内容开始显示;向下滚动2,则返回2;
public class WheelViewStartMode extends IWheelViewMode { public WheelViewStartMode(int eachItemHeight, int childrenSize) { super(eachItemHeight, childrenSize); } @Override public int getSelectedIndex(int baseIndex) { return baseIndex; } @Override public int getTopMaxScrollHeight() { return 0; } @Override public int getBottomMaxScrollHeight() { return eachItemHeight * (childrenSize - 1); } @Override public float getTextDrawY(int height, int index, Paint paint) { return (getCenterY(height, paint) + index * eachItemHeight); } }
2)WheelViewCenterMode:默认显示位置为(childrenSize - 1) / 2 ;所以滚动后的位置就是baseIndex + (childrenSize- 1) /2就很好理解了。
public class WheelViewCenterMode extends IWheelViewMode { public WheelViewCenterMode(int eachItemHeight, int childrenSize) { super(eachItemHeight, childrenSize); } @Override public int getSelectedIndex(int baseIndex) { return baseIndex + (childrenSize - 1) / 2; } @Override public int getTopMaxScrollHeight() { return (childrenSize - 1) / 2 * (-eachItemHeight); } @Override public int getBottomMaxScrollHeight() { return childrenSize / 2 * eachItemHeight; } @Override public float getTextDrawY(int height, int index, Paint paint) { return (getCenterY(height, paint) + (index - (childrenSize - 1) / 2) * eachItemHeight); } }
3)WheelViewRecycleMode:默认显示位置为0,因为除数不能为0,所以避免出错,多加了个判断。
public class WheelViewRecycleMode extends IWheelViewMode { public WheelViewRecycleMode(int eachItemHeight, int childrenSize) { super(eachItemHeight, childrenSize); } @Override public int getSelectedIndex(int baseIndex) { int index = baseIndex; while (index <= 0) { index += childrenSize; } if (childrenSize == 0) { Log.e("Wheelview", "WheelViewRecycleMode childrenSize == 0"); } return index % (childrenSize == 0?1:childrenSize); } @Override public int getTopMaxScrollHeight() { return Integer.MIN_VALUE; } @Override public int getBottomMaxScrollHeight() { return Integer.MAX_VALUE; } @Override public float getTextDrawY(int height, int index, Paint paint) { return (getCenterY(height, paint) + index * eachItemHeight); } }
2.5 边界检查