系列文章
造轮子:滚轮选择器实现及原理解析(一)
造轮子:滚轮选择器实现及原理解析(二)
造轮子:滚轮选择器实现及原理解析(三)
造轮子:滚轮选择器实现及原理解析(源码)
VerticalPicker
public class VerticalPicker extends View {
/**
* 3个状态,常规,惯性滚动,滚动
*/
public static final int SCROLL_STATE_NORMAL = 0;
public static final int SCROLL_STATE_FLING = 1;
public static final int SCROLL_STATE_SCROLLING = 2;
protected Context context;
/**
* 轮盘数据源
*/
protected String[] data = new String[]{};
protected Paint paint;
/**
* 当前滚动距离
*/
protected float curY;
/**
* 每个item默认状态下的高度,item不在中心时该高度会被缩放
*/
protected int itemHeight;
/**
* 单边最大显示个数(包含中心item)
*/
protected int showCount;
/**
* 边缘item最小缩放值
*/
protected float minScale;
/**
* 边缘item最小透明度
*/
protected float minAlpha;
/**
* 是否循环显示
*/
protected boolean isLoop;
protected int textColorId;
protected int textSize;
protected int width;
protected int height;
protected float lastY;
protected int scrollState;
/**
* 当前选中position
*/
protected int curPosition;
protected VelocityTracker velocityTracker = VelocityTracker.obtain();
protected Scroller scroller;
protected PickerChangeListener pickerChangeListener;
private float startY;
private long startTime;
private final ValueAnimatorManager manager;
public VerticalPicker(Context context) {
this(context, null);
}
public VerticalPicker(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VerticalPicker(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public VerticalPicker(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
init(attrs, defStyleAttr, defStyleRes);
manager = new PickerAnimManager((key, newValue) -> {
if (TextUtils.equals(key, KEY_PICKER_ADSORB_ANIM)) {
curY = (float) newValue;
invalidate();
} else if (TextUtils.equals(key, KEY_PICKER_SCROLL_ANIM)) {
curY = adjustingY((float) newValue);
invalidate();
}
});
}
protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 字重500,Medium效果
paint.setFakeBoldText(true);
scroller = new Scroller(context, new DecelerateInterpolator());
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Picker, defStyleAttr, defStyleRes);
textSize = typedArray.getDimensionPixelOffset(R.styleable.Picker_android_textSize, 0);
itemHeight = typedArray.getDimensionPixelOffset(R.styleable.Picker_pickerItemWidth, 0);
minScale = typedArray.getFloat(R.styleable.Picker_pickerScaleMin, 0);
minAlpha = typedArray.getFloat(R.styleable.Picker_pickerAlphaMin, 0);
showCount = typedArray.getInt(R.styleable.Picker_pickerShowCount, 1);
isLoop = typedArray.getBoolean(R.styleable.Picker_pickerLoop, true);
paint.setColor(typedArray.getColor(R.styleable.Picker_android_textColor, Color.WHITE));
paint.setTextSize(textSize);
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float y = curY;
int centerPosition = getCenterShowPosition(y);
float offsetY = adjustingY(y) - itemHeight * centerPosition;
int max = centerPosition + showCount;
// 处于正中心时使两侧显示相同个数item,非中心时下方增加1个
if (offsetY > 0f) {
max += 1;
}
for (int i = centerPosition - showCount + 1; i < max; i++) {
drawItem(canvas, i, centerPosition, offsetY);
}
}
/**
* 获取中心点position,显示在正中心到中心上方itemHeight距离的item会被视为中心
*
* @param y 当前Y滚动距离
* @return 中心点的position
*/
protected int getCenterShowPosition(float y) {
float newY = adjustingY(y);
return (int) (newY / itemHeight);
}
/**
* @param y 滚动距离Y
* @return 调整后的Y,其范围在0 ~ itemHeight * count 之间,方便计算
*/
protected float adjustingY(float y) {
float newY = y;
if (isLoop) {
while (newY < 0 || newY > getTotalY()) {
if (newY < 0) {
newY += getTotalY();
} else {
newY -= getTotalY();
}
}
} else {
// 非循环时不可滚动超出边界
newY = Math.min((data.length - 1) * itemHeight, Math.max(0, newY));
}
return newY;
}
/**
* 调整下标,防止越界
*
* @param position 下标
* @return 调整后的下标
*/
protected int adjustingPosition(int position) {
int newPosition = position;
while (newPosition < 0 || newPosition > data.length - 1) {
if (newPosition < 0) {
newPosition += data.length;
} else {
newPosition -= data.length;
}
}
return newPosition;
}
/**
* 获取理论上的Y整体高度偏移,作为计算参考。
*
* @return 整体Y偏移量
*/
protected float getTotalY() {
return data.length * itemHeight;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
width = right - left;
height = bottom - top;
}
/**
* 绘制每个item
*
* @param canvas 画布
* @param position 当前position,可能为负数
* @param centerPosition 中心item的position
* @param offsetY 中心item的偏移值
*/
protected void drawItem(Canvas canvas, int position, int centerPosition, float offsetY) {
// 不循环显示时忽略超出边界的部分
if (!isLoop && (position < 0 || position >= data.length)) {
return;
}
String text = data[adjustingPosition(position)];
float textWidth = paint.measureText(text);
int count = centerPosition - position;
float totalOffset = offsetY + count * itemHeight;
float percent = 1f - Math.abs(totalOffset) / (itemHeight * showCount);
canvas.save();
// 先缩放后位移,可以使不同item的间距不同,距离中心越近间距越大
canvas.scale(minScale + (1 - minScale) * percent, minScale + (1 - minScale) * percent, width / 2f, height / 2f);
canvas.translate(0, -totalOffset);
paint.setAlpha((int) (255 * (minAlpha + (1 - minAlpha) * percent)));
Paint.FontMetrics metrics = paint.getFontMetrics();
canvas.drawText(text, width / 2f - textWidth / 2f, height / 2f - (metrics.top + metrics.bottom) / 2f, paint);
canvas.restore();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
// 触摸时取消滚动和吸附
stopScroll();
stopAdsorbAnim();
// 速度监听
velocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 重置状态
scrollState = SCROLL_STATE_NORMAL;
lastY = event.getY();
startY = event.getY();
startTime = System.currentTimeMillis();
break;
case MotionEvent.ACTION_MOVE:
curY -= event.getY() - lastY;
// 调整y使其不超出边界
curY = adjustingY(curY);
lastY = event.getY();
invalidate();
break;
case MotionEvent.ACTION_UP:
if (Math.abs(startY - event.getY()) < 10 && System.currentTimeMillis() - startTime < 100) {
setCenterPosition(getNewPosition(curPosition, startY > height / 2f));
} else {
curY -= event.getY() - lastY;
// 调整y使其不超出边界
curY = adjustingY(curY);
lastY = event.getY();
// 计算速度,根据速度决定惯性滚动还是吸附
velocityTracker.computeCurrentVelocity(1000, 2000);
checkTouch((int) velocityTracker.getXVelocity(), (int) velocityTracker.getYVelocity());
}
break;
default:
break;
}
return true;
}
private int getNewPosition(int position, boolean positionUp) {
int newPosition = positionUp ? position + 1 : position - 1;
if (isLoop) {
return adjustingPosition(newPosition);
}
return Math.max(0, Math.min(data.length, newPosition));
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(event);
}
/**
* 开始惯性滚动
*
* @param xSpeed X轴速度,可为负数
* @param ySpeed Y轴速度,可为负数
*/
private void startFling(int xSpeed, int ySpeed) {
scrollState = SCROLL_STATE_FLING;
scroller.fling(0, (int) lastY, xSpeed, ySpeed, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
// 当开始吸附时舍弃计算值
if (!manager.isAnimRunning(KEY_PICKER_ADSORB_ANIM) && scrollState == SCROLL_STATE_FLING && scroller.computeScrollOffset()) {
// 速度低于一定值时执行吸附操作
if (Math.abs(scroller.getCurrVelocity()) > 100) {
curY -= scroller.getCurrY() - lastY;
curY = adjustingY(curY);
lastY = scroller.getCurrY();
// 不循环时限制滚动到边界,且相同值时不多次触发回调
if (!isLoop) {
int newPosition = -1;
if (curY == 0) {
newPosition = 0;
} else if (curY == itemHeight * (data.length - 1)) {
newPosition = data.length - 1;
}
if (pickerChangeListener != null && newPosition >= 0 && curPosition != newPosition) {
curPosition = newPosition;
pickerChangeListener.onPickerSelectedChanged(this, curPosition, data[curPosition], true);
}
}
invalidate();
} else {
startAdsorbAnim();
}
}
}
/**
* 开始吸附动画
*/
public void startAdsorbAnim() {
float y = curY;
int centerPosition = getCenterShowPosition(y);
float offsetY = adjustingY(y) - itemHeight * centerPosition;
int newPosition;
// 超出一半时吸附到下一个item
if (offsetY >= itemHeight / 2f) {
manager.playAnim(KEY_PICKER_ADSORB_ANIM, curY, curY + itemHeight - offsetY);
newPosition = adjustingPosition(centerPosition + 1);
} else {
manager.playAnim(KEY_PICKER_ADSORB_ANIM, curY, curY - offsetY);
newPosition = adjustingPosition(centerPosition);
}
if (pickerChangeListener != null && curPosition != newPosition) {
curPosition = newPosition;
pickerChangeListener.onPickerSelectedChanged(this, curPosition, data[curPosition], true);
}
}
/**
* 停止吸附
*/
public void stopAdsorbAnim() {
manager.stopAnim(KEY_PICKER_ADSORB_ANIM);
}
/**
* 开始滚动动画,用于手动指定中心position时使用
*
* @param endY 滚动的结束值
*/
protected void startScroll(float endY) {
float y = adjustingY(curY);
scrollState = SCROLL_STATE_SCROLLING;
int min = 0;
if (isLoop) {
// 循环时就近选择最近的滚动方式,上下各延长getTotalY距离
int normal = (int) (endY - y);
int less = (int) (endY - y - getTotalY());
int more = (int) (endY - y + getTotalY());
if (Math.abs(normal) < Math.abs(less)) {
min = normal;
} else {
min = less;
}
if (Math.abs(more) < Math.abs(min)) {
min = more;
}
} else {
min = (int) (endY - y);
}
manager.playAnim(KEY_PICKER_SCROLL_ANIM, y, min + y);
}
/**
* 停止滚动
*/
private void stopScroll() {
manager.stopAnim(KEY_PICKER_SCROLL_ANIM);
}
/**
* 根据当前速度决定是否要进行惯性滚动
*
* @param speedX X轴速度
* @param speedY Y轴速度
*/
protected void checkTouch(int speedX, int speedY) {
if (Math.abs(speedY) > 100) {
startFling(speedX, speedY);
} else {
startAdsorbAnim();
}
}
/**
* 设置中心显示的item的下标
*
* @param position 目标position
*/
public void setCenterPosition(int position) {
setCenterPosition(position, true);
}
/**
* 设置中心显示的item的下标
*
* @param position 目标position
* @param smooth 是否播放动画
*/
public void setCenterPosition(int position, boolean smooth) {
stopAdsorbAnim();
stopScroll();
scrollState = SCROLL_STATE_NORMAL;
if (!smooth) {
curY = itemHeight * position;
invalidate();
} else {
startScroll(itemHeight * position);
}
curPosition = position;
if (pickerChangeListener != null) {
pickerChangeListener.onPickerSelectedChanged(this, curPosition, data[curPosition], false);
}
}
/**
* 重新设置数据
*
* @param data 新数据
*/
public void setData(String[] data) {
this.data = data;
invalidate();
}
/**
* 设置行高
*
* @param itemHeight 行高
*/
public void setItemHeight(int itemHeight) {
this.itemHeight = itemHeight;
invalidate();
}
/**
* 设置单边显示个数(包含中心item)
*
* @param showCount 显示个数
*/
public void setShowCount(int showCount) {
this.showCount = showCount;
invalidate();
}
/**
* 设置边缘最小缩放
*
* @param minScale 最小缩放
*/
public void setMinScale(float minScale) {
this.minScale = minScale;
invalidate();
}
/**
* 设置边缘最小透明度
*
* @param minAlpha 最小透明度
*/
public void setMinAlpha(float minAlpha) {
this.minAlpha = minAlpha;
invalidate();
}
/**
* 设置文字颜色id
*
* @param textColorId 文字颜色id
*/
public void setTextColorId(int textColorId) {
this.textColorId = textColorId;
if (textColorId > 0) {
paint.setColor(context.getColor(textColorId));
}
invalidate();
}
/**
* 设置中心文字颜色
*
* @param color 颜色值int
*/
public void setTextColor(int color) {
this.textColorId = 0;
paint.setColor(color);
invalidate();
}
/**
* 设置中心文字大小
*
* @param textSize 文字大小
*/
public void setTextSize(int textSize) {
this.textSize = textSize;
invalidate();
}
public void updateTheme() {
if (textColorId > 0) {
paint.setColor(context.getColor(textColorId));
}
invalidate();
}
public void setPickerChangeListener(PickerChangeListener pickerChangeListener) {
this.pickerChangeListener = pickerChangeListener;
}
/**
* 获取当前选中item在数组里的下标
*
* @return 当前选中下标
*/
public int getCurPosition() {
return curPosition;
}
}
PickerAnimManager
public class PickerAnimManager extends ValueAnimatorManager {
public static final String KEY_PICKER_ADSORB_ANIM = "key_picker_adsorb_anim";
public static final String KEY_PICKER_SCROLL_ANIM = "key_picker_scroll_anim";
public PickerAnimManager() {
}
public PickerAnimManager(AnimListener animListener) {
super(animListener);
}
@Override
protected ValueAnimator buildAnimator(String key) {
ValueAnimator animator = null;
if (TextUtils.equals(key, KEY_PICKER_ADSORB_ANIM)) {
animator = ValueAnimator.ofFloat(0, 0).setDuration(200);
} else if (TextUtils.equals(key, KEY_PICKER_SCROLL_ANIM)) {
animator = ValueAnimator.ofFloat(0, 0).setDuration(300);
}
return animator;
}
}
PickerChangeListener
public interface PickerChangeListener {
/**
* onPickerSelectedChanged
* @param view 当前View本体
* @param position 中心item的下标
* @param data 数据
* @param fromUser 是否用户操作
*/
void onPickerSelectedChanged(View view, int position, String data, boolean fromUser);
}
ValueAnimatorManager
public abstract class ValueAnimatorManager {
private AnimListener animListener;
private final ArraySet<String> autoCancelAnimKeyList;
private final HashMap<String, ValueAnimator> animatorHashMap;
public ValueAnimatorManager() {
autoCancelAnimKeyList = new ArraySet<>();
animatorHashMap = new HashMap<>();
}
public ValueAnimatorManager(AnimListener animListener) {
this.animListener = animListener;
autoCancelAnimKeyList = new ArraySet<>();
animatorHashMap = new HashMap<>();
}
protected abstract ValueAnimator buildAnimator(String key);
public void playAnim(String key, float... args) {
playAnim(key, null, args);
}
public void playAnim(String key, Animator.AnimatorListener listener, float... args) {
if (!checkExist(key)) {
return;
}
ValueAnimator animator = animatorHashMap.get(key);
animator.removeAllListeners();
if (listener != null) {
animator.addListener(listener);
}
animator.setFloatValues(args);
animator.start();
}
public void playAnim(String key, int... args) {
playAnim(key, null, args);
}
public void playAnim(String key, Animator.AnimatorListener listener, int... args) {
if (!checkExist(key)) {
return;
}
ValueAnimator animator = animatorHashMap.get(key);
animator.removeAllListeners();
if (listener != null) {
animator.addListener(listener);
}
animator.setIntValues(args);
animator.start();
}
public void playAnimDelayed(String key, long delayed, float... args) {
if (!checkExist(key)) {
return;
}
ValueAnimator animator = animatorHashMap.get(key);
animator.setFloatValues(args);
animator.setStartDelay(delayed);
}
public void stopAnim(String... keys) {
for (String key : keys) {
if (!checkExist(key)) {
continue;
}
animatorHashMap.get(key).cancel();
}
}
public void stopAllAnim() {
for (ValueAnimator animator : animatorHashMap.values()) {
animator.cancel();
}
}
protected boolean checkExist(String key) {
ValueAnimator animator = null;
if (animatorHashMap.containsKey(key)) {
animator = animatorHashMap.get(key);
} else {
animator = buildAnimator(key);
if (animator == null) {
return false;
}
animator.addUpdateListener(animation -> {
if (animListener != null) {
animListener.onAnimUpdate(key, animation.getAnimatedValue());
}
});
animatorHashMap.put(key, animator);
}
return animator != null;
}
public boolean isAnimRunning(String key) {
if (!checkExist(key)) {
return false;
}
return animatorHashMap.get(key).isRunning();
}
public void setAnimListener(AnimListener animListener) {
this.animListener = animListener;
}
public void setDuration(String key, long duration) {
if (!checkExist(key)) {
return;
}
ValueAnimator animator = animatorHashMap.get(key);
animator.setDuration(duration);
}
public interface AnimListener {
void onAnimUpdate(String key, Object newValue);
}
}
attr
<declare-styleable name="Picker">
<!-- 中央文字颜色,会被动态调整透明度 -->
<attr name="android:textColor"/>
<!-- 中央item文字大小,会被动态缩放 -->
<attr name="android:textSize"/>
<!-- 每个item的宽度,随着滚动距离会出现缩放 -->
<attr name="pickerItemWidth" format="dimension"/>
<!-- 最边缘的item的缩放极值 -->
<attr name="pickerScaleMin" format="float" />
<!-- 最边缘的item的透明度极值 -->
<attr name="pickerAlphaMin" format="float" />
<!-- picker的前景,用于设置修饰图,如横线等 -->
<attr name="pickerForeground" format="reference" />
<!-- 最大显示item个数(包含中央item),最少为1,且最好不要大于总个数的一半 -->
<attr name="pickerShowCount" format="integer" />
<!-- 是否循环展示 -->
<attr name="pickerLoop" format="boolean" />
</declare-styleable>
本文由 bt 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。