package cn.qqtheme.framework.widget; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.os.Handler; import android.os.Message; import android.support.annotation.ColorInt; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.text.TextUtils; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.TimerTask; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import cn.qqtheme.framework.entity.WheelItem; import cn.qqtheme.framework.util.ConvertUtils; import cn.qqtheme.framework.util.LogUtils; /** * 3D滚轮控件,参阅:http://blog.csdn.net/qq_22393017/article/details/59488906 * <p/> * Author:李玉江[QQ:1032694760] * DateTime:2015/12/15 09:45 基于ScrollView,参见https://github.com/wangjiegulu/WheelView * DateTime:2017/01/07 21:37 基于ListView,参见https://github.com/venshine/WheelView * DateTime:2017/04/28 21:10 基于View,参见https://github.com/weidongjian/androidWheelView * Builder:Android Studio * * @see WheelItem * @see DividerConfig * @see OnItemSelectListener */ public class WheelView extends View { public static final float LINE_SPACE_MULTIPLIER = 2.5F; public static final int TEXT_PADDING = -1; public static final int TEXT_SIZE = 16;//sp public static final int TEXT_COLOR_FOCUS = 0XFF0288CE; public static final int TEXT_COLOR_NORMAL = 0XFFBBBBBB; public static final int DIVIDER_COLOR = 0XFF83CDE6; public static final int DIVIDER_ALPHA = 220; public static final float DIVIDER_THICK = 2f;//px public static final int ITEM_OFF_SET = 3; private static final float ITEM_PADDING = 13f;//px,480X800的手机边距不能太大 private static final int ACTION_CLICK = 1;//点击 private static final int ACTION_FLING = 2;//滑翔 private static final int ACTION_DRAG = 3;//拖拽 private static final int VELOCITY_FLING = 5;//修改这个值可以改变滑行速度 private static final float SCALE_CONTENT = 0.8F;//非中间文字用此控制高度,压扁形成3D错觉 private MessageHandler handler; private GestureDetector gestureDetector; private OnItemSelectListener onItemSelectListener; private OnWheelListener onWheelListener; private boolean onlyShowCenterLabel = true;//附加单位是否仅仅只显示在选中项后面 private ScheduledFuture<?> mFuture; private Paint paintOuterText;//未选项画笔 private Paint paintCenterText;//选中项画笔 private Paint paintIndicator;//分割线画笔 private Paint paintShadow;//阴影画笔 private List<WheelItem> items = new ArrayList<>();//所有选项 private String label;//附加单位 private int maxTextWidth;//最大的文字宽 private int maxTextHeight;//最大的文字高 private int textSize = TEXT_SIZE;//文字大小,单位为sp private float itemHeight;//每行高度 private Typeface typeface = Typeface.DEFAULT;//字体样式 private int textColorOuter = TEXT_COLOR_NORMAL;//未选项文字颜色 private int textColorCenter = TEXT_COLOR_FOCUS;//选中项文字颜色 private DividerConfig dividerConfig = new DividerConfig(); private float lineSpaceMultiplier = LINE_SPACE_MULTIPLIER;//条目间距倍数,可用来设置上下间距 private int padding = TEXT_PADDING;//文字的左右边距,单位为px private boolean isLoop = true;//循环滚动 private float firstLineY;//第一条线Y坐标值 private float secondLineY;//第二条线Y坐标 private float totalScrollY = 0;//滚动总高度y值 private int initPosition = -1;//初始化默认选中项 private int selectedIndex;//选中项的索引 private int preCurrentIndex; private int visibleItemCount = ITEM_OFF_SET * 2 + 1;//绘制几个条目 private int measuredHeight;//控件高度 private int measuredWidth;//控件宽度 private int radius;//半径 private int offset = 0; private float previousY = 0; private long startTime = 0; private int widthMeasureSpec; private int gravity = Gravity.CENTER; private int drawCenterContentStart = 0;//中间选中文字开始绘制位置 private int drawOutContentStart = 0;//非中间文字开始绘制位置 private float centerContentOffset;//偏移量 private boolean useWeight = false;//使用比重还是包裹内容? public WheelView(Context context) { this(context, null); } public WheelView(Context context, AttributeSet attrs) { super(context, attrs); //屏幕密度:0.75、1.0、1.5、2.0、3.0,根据密度不同进行适配 float density = getResources().getDisplayMetrics().density; if (density < 1) { centerContentOffset = 2.4F; } else if (1 <= density && density < 2) { centerContentOffset = 3.6F; } else if (1 <= density && density < 2) { centerContentOffset = 4.5F; } else if (2 <= density && density < 3) { centerContentOffset = 6.0F; } else if (density >= 3) { centerContentOffset = density * 2.5F; } judgeLineSpace(); initView(context); } /** * 设置显示的选项个数,必须是奇数 */ public final void setVisibleItemCount(int count) { if (count % 2 == 0) { throw new IllegalArgumentException("must be odd"); } if (count != visibleItemCount) { visibleItemCount = count; } } /** * 设置是否禁用循环滚动 */ public final void setCycleDisable(boolean cycleDisable) { isLoop = !cycleDisable; } /** * 设置滚轮个数偏移量 */ public final void setOffset(@IntRange(from = 1, to = 5) int offset) { if (offset < 1 || offset > 5) { throw new IllegalArgumentException("must between 1 and 5"); } int count = offset * 2 + 1; if (offset % 2 == 0) { count += offset; } else { count += offset - 1; } setVisibleItemCount(count); } public final int getSelectedIndex() { return selectedIndex; } public final void setSelectedIndex(int index) { if (items == null || items.isEmpty()) { return; } int size = items.size(); if (index >= 0 && index < size && index != selectedIndex) { initPosition = index; totalScrollY = 0;//回归顶部,不然重设索引的话位置会偏移,就会显示出不对位置的数据 offset = 0; invalidate(); } } public final void setOnItemSelectListener(OnItemSelectListener onItemSelectListener) { this.onItemSelectListener = onItemSelectListener; } /** * @deprecated use {@link #setOnItemSelectListener(OnItemSelectListener)} instead */ @Deprecated public final void setOnWheelListener(OnWheelListener listener) { onWheelListener = listener; } public final void setItems(List<?> items) { this.items.clear(); for (Object item : items) { if (item instanceof WheelItem) { this.items.add((WheelItem) item); } else if (item instanceof CharSequence || item instanceof Number) { this.items.add(new StringItem(item.toString())); } else { throw new IllegalArgumentException("please implements " + WheelItem.class.getName()); } } remeasure(); invalidate(); } public final void setItems(List<?> items, int index) { setItems(items); setSelectedIndex(index); } public final void setItems(String[] list) { setItems(Arrays.asList(list)); } public final void setItems(List<String> list, String item) { int index = list.indexOf(item); if (index == -1) { index = 0; } setItems(list, index); } public final void setItems(String[] list, int index) { setItems(Arrays.asList(list), index); } public final void setItems(String[] items, String item) { setItems(Arrays.asList(items), item); } /** * 附加在右边的单位字符串 */ public final void setLabel(String label, boolean onlyShowCenterLabel) { this.label = label; this.onlyShowCenterLabel = onlyShowCenterLabel; } public final void setLabel(String label) { setLabel(label, true); } public final void setGravity(int gravity) { this.gravity = gravity; } public void setTextColor(@ColorInt int colorNormal, @ColorInt int colorFocus) { this.textColorOuter = colorNormal; this.textColorCenter = colorFocus; paintOuterText.setColor(colorNormal); paintCenterText.setColor(colorFocus); } public void setTextColor(@ColorInt int color) { this.textColorOuter = color; this.textColorCenter = color; paintOuterText.setColor(color); paintCenterText.setColor(color); } public final void setTypeface(Typeface font) { typeface = font; paintOuterText.setTypeface(typeface); paintCenterText.setTypeface(typeface); } public final void setTextSize(float size) { if (size > 0.0F) { textSize = (int) (getContext().getResources().getDisplayMetrics().density * size); paintOuterText.setTextSize(textSize); paintCenterText.setTextSize(textSize); } } public void setDividerColor(@ColorInt int color) { dividerConfig.setColor(color); paintIndicator.setColor(color); } /** * @deprecated use {{@link #setDividerConfig(DividerConfig)} instead */ @Deprecated public void setLineConfig(DividerConfig config) { setDividerConfig(config); } public void setDividerConfig(DividerConfig config) { if (null == config) { dividerConfig.setVisible(false); dividerConfig.setShadowVisible(false); return; } this.dividerConfig = config; paintIndicator.setColor(config.color); paintIndicator.setStrokeWidth(config.thick); paintIndicator.setAlpha(config.alpha); paintShadow.setColor(config.shadowColor); paintShadow.setAlpha(config.shadowAlpha); } public final void setLineSpaceMultiplier(@FloatRange(from = 2, to = 4) float multiplier) { lineSpaceMultiplier = multiplier; judgeLineSpace(); } public void setPadding(int padding) { this.padding = ConvertUtils.toPx(getContext(), padding); } public void setUseWeight(boolean useWeight) { this.useWeight = useWeight; } /** * 判断间距是否在有效范围内 */ private void judgeLineSpace() { if (lineSpaceMultiplier < 1.5f) { lineSpaceMultiplier = 1.5f; } else if (lineSpaceMultiplier > 4.0f) { lineSpaceMultiplier = 4.0f; } } private void initView(Context context) { handler = new MessageHandler(this); gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @Override public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { scrollBy(velocityY); return true; } }); gestureDetector.setIsLongpressEnabled(false); initPaints(); initDataForIDE(); } private void initPaints() { paintOuterText = new Paint(); paintOuterText.setAntiAlias(true); paintOuterText.setColor(textColorOuter); paintOuterText.setTypeface(typeface); paintOuterText.setTextSize(textSize); paintCenterText = new Paint(); paintCenterText.setAntiAlias(true); paintCenterText.setColor(textColorCenter); paintCenterText.setTextScaleX(1.1F); paintCenterText.setTypeface(typeface); paintCenterText.setTextSize(textSize); paintIndicator = new Paint(); paintIndicator.setAntiAlias(true); paintIndicator.setColor(dividerConfig.color); paintIndicator.setStrokeWidth(dividerConfig.thick); paintIndicator.setAlpha(dividerConfig.alpha); paintShadow = new Paint(); paintShadow.setAntiAlias(true); paintShadow.setColor(dividerConfig.shadowColor); paintShadow.setAlpha(dividerConfig.shadowAlpha); setLayerType(LAYER_TYPE_SOFTWARE, null); } private void initDataForIDE() { if (isInEditMode()) { setItems(new String[]{"李玉江", "男", "贵州", "穿青人"}); } } /** * 重新测量 */ private void remeasure() { if (items == null) { return; } measureTextWidthHeight(); //半圆的周长 int halfCircumference = (int) (itemHeight * (visibleItemCount - 1)); //整个圆的周长除以PI得到直径,这个直径用作控件的总高度 measuredHeight = (int) ((halfCircumference * 2) / Math.PI); //求出半径 radius = (int) (halfCircumference / Math.PI); ViewGroup.LayoutParams params = getLayoutParams(); //控件宽度 if (useWeight) { measuredWidth = MeasureSpec.getSize(widthMeasureSpec); } else if (params != null && params.width > 0) { measuredWidth = params.width; } else { measuredWidth = maxTextWidth; if (padding < 0) { padding = ConvertUtils.toPx(getContext(), ITEM_PADDING); } measuredWidth += padding * 2; if (!TextUtils.isEmpty(label)) { measuredWidth += obtainTextWidth(paintCenterText, label); } } LogUtils.debug("measuredWidth=" + measuredWidth + ",measuredHeight=" + measuredHeight); //计算两条横线 和 选中项画笔的基线Y位置 firstLineY = (measuredHeight - itemHeight) / 2.0F; secondLineY = (measuredHeight + itemHeight) / 2.0F; //初始化显示的item的position if (initPosition == -1) { if (isLoop) { initPosition = (items.size() + 1) / 2; } else { initPosition = 0; } } preCurrentIndex = initPosition; } /** * 计算最大length的Text的宽高度 */ private void measureTextWidthHeight() { Rect rect = new Rect(); for (int i = 0; i < items.size(); i++) { String s1 = obtainContentText(items.get(i)); paintCenterText.getTextBounds(s1, 0, s1.length(), rect); int textWidth = rect.width(); if (textWidth > maxTextWidth) { maxTextWidth = textWidth; } paintCenterText.getTextBounds("测试", 0, 2, rect); maxTextHeight = rect.height() + 2; } itemHeight = lineSpaceMultiplier * maxTextHeight; } /** * 平滑滚动的实现 */ private void smoothScroll(int actionType) { cancelFuture(); if (actionType == ACTION_FLING || actionType == ACTION_DRAG) { offset = (int) ((totalScrollY % itemHeight + itemHeight) % itemHeight); if ((float) offset > itemHeight / 2.0F) {//如果超过Item高度的一半,滚动到下一个Item去 offset = (int) (itemHeight - (float) offset); } else { offset = -offset; } } //停止的时候,位置有偏移,不是全部都能正确停止到中间位置的,这里把文字位置挪回中间去 SmoothScrollTimerTask command = new SmoothScrollTimerTask(this, offset); mFuture = Executors.newSingleThreadScheduledExecutor() .scheduleWithFixedDelay(command, 0, 10, TimeUnit.MILLISECONDS); } /** * 滚动惯性的实现 */ private void scrollBy(float velocityY) { cancelFuture(); InertiaTimerTask command = new InertiaTimerTask(this, velocityY); mFuture = Executors.newSingleThreadScheduledExecutor() .scheduleWithFixedDelay(command, 0, VELOCITY_FLING, TimeUnit.MILLISECONDS); } private void cancelFuture() { if (mFuture != null && !mFuture.isCancelled()) { mFuture.cancel(true); mFuture = null; } } private void itemSelectedCallback() { if (onItemSelectListener == null && onWheelListener == null) { return; } postDelayed(new Runnable() { @Override public void run() { if (onItemSelectListener != null) { onItemSelectListener.onSelected(selectedIndex); } if (onWheelListener != null) { onWheelListener.onSelected(true, selectedIndex, items.get(selectedIndex).getName()); } } }, 200L); } @Override protected void onDraw(Canvas canvas) { if (items == null || items.size() == 0) { return; } //可见项的数组 String[] visibleItemStrings = new String[visibleItemCount]; //滚动的Y值高度除去每行的高度,得到滚动了多少个项,即change数 int change = (int) (totalScrollY / itemHeight); //滚动中实际的预选中的item(即经过了中间位置的item) = 滑动前的位置 + 滑动相对位置 preCurrentIndex = initPosition + change % items.size(); if (!isLoop) {//不循环的情况 if (preCurrentIndex < 0) { preCurrentIndex = 0; } if (preCurrentIndex > items.size() - 1) { preCurrentIndex = items.size() - 1; } } else {//循环 if (preCurrentIndex < 0) {//举个例子:如果总数是5,preCurrentIndex = -1,那么preCurrentIndex按循环来说,其实是0的上面,也就是4的位置 preCurrentIndex = items.size() + preCurrentIndex; } if (preCurrentIndex > items.size() - 1) {//同理上面,自己脑补一下 preCurrentIndex = preCurrentIndex - items.size(); } } //跟滚动流畅度有关,总滑动距离与每个item高度取余,即并不是一格格的滚动,每个item不一定滚到对应Rect里的,这个item对应格子的偏移值 float itemHeightOffset = (totalScrollY % itemHeight); // 设置数组中每个元素的值 int counter = 0; while (counter < visibleItemCount) { int index = preCurrentIndex - (visibleItemCount / 2 - counter);//索引值,即当前在控件中间的item看作数据源的中间,计算出相对源数据源的index值 //判断是否循环,如果是循环数据源也使用相对循环的position获取对应的item值,如果不是循环则超出数据源范围使用""空白字符串填充,在界面上形成空白无数据的item项 if (isLoop) { index = getLoopMappingIndex(index); visibleItemStrings[counter] = items.get(index).getName(); } else if (index < 0) { visibleItemStrings[counter] = ""; } else if (index > items.size() - 1) { visibleItemStrings[counter] = ""; } else { visibleItemStrings[counter] = items.get(index).getName(); } counter++; } //绘制中间两条横线 if (dividerConfig.visible) { float ratio = dividerConfig.ratio; canvas.drawLine(measuredWidth * ratio, firstLineY, measuredWidth * (1 - ratio), firstLineY, paintIndicator); canvas.drawLine(measuredWidth * ratio, secondLineY, measuredWidth * (1 - ratio), secondLineY, paintIndicator); } if (dividerConfig.shadowVisible) { paintShadow.setColor(dividerConfig.shadowColor); paintShadow.setAlpha(dividerConfig.shadowAlpha); canvas.drawRect(0.0F, firstLineY, measuredWidth, secondLineY, paintShadow); } counter = 0; while (counter < visibleItemCount) { canvas.save(); // 弧长 L = itemHeight * counter - itemHeightOffset // 求弧度 α = L / r (弧长/半径) [0,π] double radian = ((itemHeight * counter - itemHeightOffset)) / radius; // 弧度转换成角度(把半圆以Y轴为轴心向右转90度,使其处于第一象限及第四象限 // angle [-90°,90°] float angle = (float) (90D - (radian / Math.PI) * 180D);//item第一项,从90度开始,逐渐递减到 -90度 // 计算取值可能有细微偏差,保证负90°到90°以外的不绘制 if (angle >= 90F || angle <= -90F) { canvas.restore(); } else { //获取内容文字 String contentText; //如果是label每项都显示的模式,并且item内容不为空、label也不为空 String tempStr = obtainContentText(visibleItemStrings[counter]); if (!onlyShowCenterLabel && !TextUtils.isEmpty(label) && !TextUtils.isEmpty(tempStr)) { contentText = tempStr + label; } else { contentText = tempStr; } remeasureTextSize(contentText); //计算开始绘制的位置 measuredCenterContentStart(contentText); measuredOutContentStart(contentText); float translateY = (float) (radius - Math.cos(radian) * radius - (Math.sin(radian) * maxTextHeight) / 2D); //根据Math.sin(radian)来更改canvas坐标系原点,然后缩放画布,使得文字高度进行缩放,形成弧形3d视觉差 canvas.translate(0.0F, translateY); canvas.scale(1.0F, (float) Math.sin(radian)); if (translateY <= firstLineY && maxTextHeight + translateY >= firstLineY) { // 条目经过第一条线 canvas.save(); canvas.clipRect(0, 0, measuredWidth, firstLineY - translateY); canvas.scale(1.0F, (float) Math.sin(radian) * SCALE_CONTENT); canvas.drawText(contentText, drawOutContentStart, maxTextHeight, paintOuterText); canvas.restore(); canvas.save(); canvas.clipRect(0, firstLineY - translateY, measuredWidth, (int) (itemHeight)); canvas.scale(1.0F, (float) Math.sin(radian) * 1.0F); canvas.drawText(contentText, drawCenterContentStart, maxTextHeight - centerContentOffset, paintCenterText); canvas.restore(); } else if (translateY <= secondLineY && maxTextHeight + translateY >= secondLineY) { // 条目经过第二条线 canvas.save(); canvas.clipRect(0, 0, measuredWidth, secondLineY - translateY); canvas.scale(1.0F, (float) Math.sin(radian) * 1.0F); canvas.drawText(contentText, drawCenterContentStart, maxTextHeight - centerContentOffset, paintCenterText); canvas.restore(); canvas.save(); canvas.clipRect(0, secondLineY - translateY, measuredWidth, (int) (itemHeight)); canvas.scale(1.0F, (float) Math.sin(radian) * SCALE_CONTENT); canvas.drawText(contentText, drawOutContentStart, maxTextHeight, paintOuterText); canvas.restore(); } else if (translateY >= firstLineY && maxTextHeight + translateY <= secondLineY) { // 中间条目 canvas.clipRect(0, 0, measuredWidth, maxTextHeight); //让文字居中 float Y = maxTextHeight - centerContentOffset;//因为圆弧角换算的向下取值,导致角度稍微有点偏差,加上画笔的基线会偏上,因此需要偏移量修正一下 int i = 0; for (WheelItem item : items) { if (item.getName().equals(tempStr)) { selectedIndex = i; break; } i++; } if (onlyShowCenterLabel && !TextUtils.isEmpty(label)) { contentText += label; } canvas.drawText(contentText, drawCenterContentStart, Y, paintCenterText); } else { // 其他条目 canvas.save(); canvas.clipRect(0, 0, measuredWidth, (int) (itemHeight)); canvas.scale(1.0F, (float) Math.sin(radian) * SCALE_CONTENT); canvas.drawText(contentText, drawOutContentStart, maxTextHeight, paintOuterText); canvas.restore(); } canvas.restore(); paintCenterText.setTextSize(textSize); } counter++; } } /** * 根据文字的长度 重新设置文字的大小 让其能完全显示 */ private void remeasureTextSize(String contentText) { Rect rect = new Rect(); paintCenterText.getTextBounds(contentText, 0, contentText.length(), rect); int width = rect.width(); int size = textSize; while (width > measuredWidth) { size--; //设置2条横线中间的文字大小 paintCenterText.setTextSize(size); paintCenterText.getTextBounds(contentText, 0, contentText.length(), rect); width = rect.width(); } //设置2条横线外面的文字大小 paintOuterText.setTextSize(size); } /** * 递归计算出对应的索引 */ private int getLoopMappingIndex(int index) { if (index < 0) { index = index + items.size(); index = getLoopMappingIndex(index); } else if (index > items.size() - 1) { index = index - items.size(); index = getLoopMappingIndex(index); } return index; } /** * 根据传进来的对象来获取需要显示的值 * * @param item 数据源的项 * @return 对应显示的字符串 */ private String obtainContentText(Object item) { if (item == null) { return ""; } else if (item instanceof WheelItem) { return ((WheelItem) item).getName(); } else if (item instanceof Integer) { //如果为整形则最少保留两位数. return String.format(Locale.getDefault(), "%02d", (int) item); } return item.toString(); } private void measuredCenterContentStart(String content) { Rect rect = new Rect(); paintCenterText.getTextBounds(content, 0, content.length(), rect); switch (gravity) { case Gravity.CENTER://显示内容居中 drawCenterContentStart = (int) ((measuredWidth - rect.width()) * 0.5); break; case Gravity.LEFT: drawCenterContentStart = 0; break; case Gravity.RIGHT://添加偏移量 drawCenterContentStart = measuredWidth - rect.width() - (int) centerContentOffset; break; } } private void measuredOutContentStart(String content) { Rect rect = new Rect(); paintOuterText.getTextBounds(content, 0, content.length(), rect); switch (gravity) { case Gravity.CENTER: drawOutContentStart = (int) ((measuredWidth - rect.width()) * 0.5); break; case Gravity.LEFT: drawOutContentStart = 0; break; case Gravity.RIGHT: drawOutContentStart = measuredWidth - rect.width() - (int) centerContentOffset; break; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { this.widthMeasureSpec = widthMeasureSpec; remeasure(); setMeasuredDimension(measuredWidth, measuredHeight); } @Override public boolean onTouchEvent(MotionEvent event) { boolean eventConsumed = gestureDetector.onTouchEvent(event); ViewParent parent = getParent(); switch (event.getAction()) { //按下 case MotionEvent.ACTION_DOWN: startTime = System.currentTimeMillis(); cancelFuture(); previousY = event.getRawY(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } break; //滑动中 case MotionEvent.ACTION_MOVE: float dy = previousY - event.getRawY(); previousY = event.getRawY(); totalScrollY = totalScrollY + dy; // 边界处理。 if (!isLoop) { float top = -initPosition * itemHeight; float bottom = (items.size() - 1 - initPosition) * itemHeight; if (totalScrollY - itemHeight * 0.25 < top) { top = totalScrollY - dy; } else if (totalScrollY + itemHeight * 0.25 > bottom) { bottom = totalScrollY - dy; } if (totalScrollY < top) { totalScrollY = (int) top; } else if (totalScrollY > bottom) { totalScrollY = (int) bottom; } } break; //完成滑动,手指离开屏幕 case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: default: if (!eventConsumed) {//未消费掉事件 /* * 关于弧长的计算 * * 弧长公式: L = α*R * 反余弦公式:arccos(cosα) = α * 由于之前是有顺时针偏移90度, * 所以实际弧度范围α2的值 :α2 = π/2-α (α=[0,π] α2 = [-π/2,π/2]) * 根据正弦余弦转换公式 cosα = sin(π/2-α) * 代入,得: cosα = sin(π/2-α) = sinα2 = (R - y) / R * 所以弧长 L = arccos(cosα)*R = arccos((R - y) / R)*R */ float y = event.getY(); double L = Math.acos((radius - y) / radius) * radius; //item0 有一半是在不可见区域,所以需要加上 itemHeight / 2 int circlePosition = (int) ((L + itemHeight / 2) / itemHeight); float extraOffset = (totalScrollY % itemHeight + itemHeight) % itemHeight; //已滑动的弧长值 offset = (int) ((circlePosition - visibleItemCount / 2) * itemHeight - extraOffset); if ((System.currentTimeMillis() - startTime) > 120) { // 处理拖拽事件 smoothScroll(ACTION_DRAG); } else { // 处理条目点击事件 smoothScroll(ACTION_CLICK); } } if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } break; } invalidate(); return true; } /** * 获取选项个数 */ protected int getItemCount() { return items != null ? items.size() : 0; } private int obtainTextWidth(Paint paint, String str) { int iRet = 0; if (str != null && str.length() > 0) { int len = str.length(); float[] widths = new float[len]; paint.getTextWidths(str, widths); for (int j = 0; j < len; j++) { iRet += (int) Math.ceil(widths[j]); } } return iRet; } /** * 选中项的分割线 */ public static class DividerConfig { public static final float FILL = 0f; public static final float WRAP = 1f; protected boolean visible = true; protected boolean shadowVisible = false; protected int color = DIVIDER_COLOR; protected int shadowColor = TEXT_COLOR_NORMAL; protected int shadowAlpha = 100; protected int alpha = DIVIDER_ALPHA; protected float ratio = 0.1f; protected float thick = DIVIDER_THICK; public DividerConfig() { super(); } public DividerConfig(@FloatRange(from = 0, to = 1) float ratio) { this.ratio = ratio; } /** * 线是否可见 */ public DividerConfig setVisible(boolean visible) { this.visible = visible; return this; } /** * 阴影是否可见 */ public DividerConfig setShadowVisible(boolean shadowVisible) { this.shadowVisible = shadowVisible; if (shadowVisible && color == DIVIDER_COLOR) { color = shadowColor; alpha = 255; } return this; } /** * 阴影颜色 */ public DividerConfig setShadowColor(@ColorInt int color) { shadowVisible = true; shadowColor = color; return this; } /** * 阴影透明度 */ public DividerConfig setShadowAlpha(@IntRange(from = 1, to = 255) int alpha) { this.shadowAlpha = alpha; return this; } /** * 线颜色 */ public DividerConfig setColor(@ColorInt int color) { this.color = color; return this; } /** * 线透明度 */ public DividerConfig setAlpha(@IntRange(from = 1, to = 255) int alpha) { this.alpha = alpha; return this; } /** * 线比例,范围为0-1,0表示最长,1表示最短 */ public DividerConfig setRatio(@FloatRange(from = 0, to = 1) float ratio) { this.ratio = ratio; return this; } /** * 线粗 */ public DividerConfig setThick(float thick) { this.thick = thick; return this; } @Override public String toString() { return "visible=" + visible + ",color=" + color + ",alpha=" + alpha + ",thick=" + thick; } } /** * @deprecated 使用{@link #DividerConfig}代替 */ @Deprecated public static class LineConfig extends DividerConfig { } /** * 用于兼容旧版本的纯字符串条目 */ private static class StringItem implements WheelItem { private String name; private StringItem(String name) { this.name = name; } @Override public String getName() { return name; } } public interface OnItemSelectListener { /** * 滑动选择回调 * * @param index 当前选择项的索引 */ void onSelected(int index); } /** * 兼容旧版本API * * @deprecated use {@link OnItemSelectListener} instead */ @Deprecated public interface OnWheelListener { void onSelected(boolean isUserScroll, int index, String item); } /** * @deprecated use {@link OnItemSelectListener} instead */ @Deprecated public interface OnWheelViewListener extends OnWheelListener { } private static class MessageHandler extends Handler { static final int WHAT_INVALIDATE = 1000; static final int WHAT_SMOOTH_SCROLL = 2000; static final int WHAT_ITEM_SELECTED = 3000; final WheelView view; MessageHandler(WheelView view) { this.view = view; } @Override public final void handleMessage(Message msg) { switch (msg.what) { case WHAT_INVALIDATE: view.invalidate(); break; case WHAT_SMOOTH_SCROLL: view.smoothScroll(WheelView.ACTION_FLING); break; case WHAT_ITEM_SELECTED: view.itemSelectedCallback(); break; } } } private static class SmoothScrollTimerTask extends TimerTask { int realTotalOffset = Integer.MAX_VALUE; int realOffset = 0; int offset; final WheelView view; SmoothScrollTimerTask(WheelView view, int offset) { this.view = view; this.offset = offset; } @Override public void run() { if (realTotalOffset == Integer.MAX_VALUE) { realTotalOffset = offset; } //把要滚动的范围细分成10小份,按10小份单位来重绘 realOffset = (int) ((float) realTotalOffset * 0.1F); if (realOffset == 0) { if (realTotalOffset < 0) { realOffset = -1; } else { realOffset = 1; } } if (Math.abs(realTotalOffset) <= 1) { view.cancelFuture(); view.handler.sendEmptyMessage(MessageHandler.WHAT_ITEM_SELECTED); } else { view.totalScrollY = view.totalScrollY + realOffset; //这里如果不是循环模式,则点击空白位置需要回滚,不然就会出现选到-1 item的情况 if (!view.isLoop) { float itemHeight = view.itemHeight; float top = (float) (-view.initPosition) * itemHeight; float bottom = (float) (view.getItemCount() - 1 - view.initPosition) * itemHeight; if (view.totalScrollY <= top || view.totalScrollY >= bottom) { view.totalScrollY = view.totalScrollY - realOffset; view.cancelFuture(); view.handler.sendEmptyMessage(MessageHandler.WHAT_ITEM_SELECTED); return; } } view.handler.sendEmptyMessage(MessageHandler.WHAT_INVALIDATE); realTotalOffset = realTotalOffset - realOffset; } } } private static class InertiaTimerTask extends TimerTask { float a = Integer.MAX_VALUE; final float velocityY; final WheelView view; InertiaTimerTask(WheelView view, float velocityY) { this.view = view; this.velocityY = velocityY; } @Override public final void run() { if (a == Integer.MAX_VALUE) { if (Math.abs(velocityY) > 2000F) { if (velocityY > 0.0F) { a = 2000F; } else { a = -2000F; } } else { a = velocityY; } } if (Math.abs(a) >= 0.0F && Math.abs(a) <= 20F) { view.cancelFuture(); view.handler.sendEmptyMessage(MessageHandler.WHAT_SMOOTH_SCROLL); return; } int i = (int) ((a * 10F) / 1000F); view.totalScrollY = view.totalScrollY - i; if (!view.isLoop) { float itemHeight = view.itemHeight; float top = (-view.initPosition) * itemHeight; float bottom = (view.getItemCount() - 1 - view.initPosition) * itemHeight; if (view.totalScrollY - itemHeight * 0.25 < top) { top = view.totalScrollY + i; } else if (view.totalScrollY + itemHeight * 0.25 > bottom) { bottom = view.totalScrollY + i; } if (view.totalScrollY <= top) { a = 40F; view.totalScrollY = (int) top; } else if (view.totalScrollY >= bottom) { view.totalScrollY = (int) bottom; a = -40F; } } if (a < 0.0F) { a = a + 20F; } else { a = a - 20F; } view.handler.sendEmptyMessage(MessageHandler.WHAT_INVALIDATE); } } }