造轮子:滚轮选择器实现及原理解析(一)

920 阅读1分钟

系列文章

造轮子:滚轮选择器实现及原理解析(一)

造轮子:滚轮选择器实现及原理解析(二)

造轮子:滚轮选择器实现及原理解析(三)

造轮子:滚轮选择器实现及原理解析(源码)

实现效果

上方非循环滚动,下方循环滚动 滚动下面的选择器使上方选择器联动 飞书20230720-154126.gif

需求拆解

滚轮选择器的使用场景最常见于时间选择,手机上操作出生年月时经常能看到年月日选择器。 其最典型特征:

  1. 数据在同一列展示
  2. 中间部分突出显示
  3. 有3D环绕感
  4. 支持循环滚动
  5. 拥有惯性滚动与就近吸附动画

抛开动画和展示效果不谈,首要任务在于如何把内容排列在屏幕上。

模拟顺序排列

2023-07-21T05:41:18.png 视觉上典型的LinearLayout布局,从上到下按顺序排列,甚至滚动都是类似的,不过我们这里不使用LinearLayout,当需要循环时使用LinearLayout并不方便

// onDraw()
for (int i = 0; i < size; i++) {
    drawItem(canvas, i);
}
// drawItem()
// 测量文字,绘制时居中显示
float textWidth = paint.measureText(text);
// 计算偏移量,在每行固定高度时,偏移量很好计算
float totalOffset = i* itemHeight;
canvas.save();
canvas.translate(0, -totalOffset);
// 此处用于适配文字baseline,否则会导致文字无法绘制在绝对居中位置
Paint.FontMetrics metrics = paint.getFontMetrics();
canvas.drawText(text, width / 2f - textWidth / 2f,  - (metrics.top + metrics.bottom) / 2f, paint);
canvas.restore();

2023-07-21T05:49:09.png

发现问题

  1. 在滚动时滚动距离会实时变化,没办法仅仅使用i*itemHeight计算偏移
  2. 滚动默认位置应当从正中间开始的,而非顶部
  3. 高度就那么高,我的数据过多时也看不到

改造

  1. 使用临时变量curY模拟当前滚动距离,使用该变量计算
  2. 因为是从中间开始滚动,我们绘制方式调整为从中间发散绘制(对于3D视图来说,中间处于缩放点,从中间开始计算更方便,后续会用到)
  3. 限制可见个数与绘制个数,全部绘制浪费性能不说实际也看不到

1.根据当前滚动距离得到中间位置的下标

protected int getCenterShowPosition(float y) {
    //这里进行y矫正,防止小于0或超出float上限,便于后续计算
    float newY = adjustingY(y);
    return (int) (newY / itemHeight);
}

现在我们将模型图转换: 因为我们绘制文本时,总是居中绘制,所以理论上我们只关心绘制点的中心线段在哪,后续绘制时根据itemHeight进行平移矫正一下,后续我们只关心线条的位置即可 2023-07-21T06:09:08.png

2. 改变整体绘制逻辑

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    float y = curY;
    // 获取中心位置item的下标
    int centerPosition = getCenterShowPosition(y);
    // item不可能刚好在中线,计算出它的偏移距离
    float offsetY = adjustingY(y) - itemHeight * centerPosition;
    // 限制绘制个数,为了保证上下对称,当向上滚动一段距离时,应当在下方多补充一个item绘制
    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);
    }
}

3. 改变item绘制逻辑

// 计算和中心item的差距
int count = centerPosition - position;
float totalOffset = offsetY + count * itemHeight;
canvas.save();
canvas.translate(0, -totalOffset);
Paint.FontMetrics metrics = paint.getFontMetrics();
// 绘制在中心点的位置
canvas.drawText(text, width / 2f - textWidth / 2f, height / 2f - (metrics.top + metrics.bottom) / 2f, paint);
canvas.restore();

至此,简单的滚轮选择器的基础绘制已完成,调整curY可使其位置发生改变 2023-07-21T06:25:13.png