妖媚的刻度尺控件,隔壁产品都馋哭了

1,835 阅读5分钟

废话不多说,有图才有阅读量

控件的刻度附带磁吸效果。同时会监控用户交互时手指的移动速度,在手指抬起后会继续保持一段时间的惯性移动,见下图

值得注意的是,控件支持渐变背景色!只要大胆配色, UI风格可以随意替换。 当然,那些屎一样的渐变配色就不要尝试了,否则你就是设计界的老八。 下面对这个控件进行逐一拆解

一、设计思路

《隔壁产品都馋哭了》系列文章的初衷就是要设计出一些和市面上不太一样的控件,给人一种耳目一新的感觉。不仅要把它设计出来,还要包括控件各个细节的实现,最后做到好看也好用。纵观市面上一些类似的刻度尺控件,配色都比较单调,如果配色不当,刻度的直角就会显得比较割眼。为尝试解决这类问题,一个自带渐变色,UI风格较为圆润的刻度尺控件就诞生了。

二、实现方式

1、UI拆解

1.1 形状构成

从设计图中可以看到,整个控件由一条条的纵向刻度条、刻度数字、顶部的刻度读数文字以及刻度读数文字下方的指示点构成。因此我们可以将整个控件拆解成如下的对应关系:

纵向刻度条:圆角矩形,分为长中短三类

刻度数字:文字

指示器:圆形

刻度读数:文字,有起始值和结束值

1.2 绘制思路

有了形状对应,整个绘制思路就逐渐清晰了,我们将刻度分为长中短三种类型,分别定义它们的高度、宽度

/**
* 长、中、短刻度的高度
*/
int maxLineHeight, midLineHeight, minLineHeight;

/**
* 刻度宽度,默认24px
*/
int lineWidth = 24;

再定义好刻度读数文字的宽高,方便在绘制时进行居中计算,同时还要声明起始刻度值和结束刻度值以及刻度间的间隔

/**
* 文字高度
*/
float textHeight;

/**
* 刻度尺的开始、结束数字
*/
int startNum = 0, endNum = 40;

/**
* 每个刻度代表的数字单位
*/
int unitNum = 1;

/**
* 刻度间隔,默认
*/
int lineSpacing = 3 * lineWidth;
最后定义好指示器的属性,由于它是一个圆形,只需要声明好半径参数即可
/**
* 指示器半径
*/
int indicatorRadius = lineWidth / 2;

形状的属性定制好了,为了方便之后的控件上色以及颜色色值计算,再定义好各个形状的颜色

/**
* 渐变色起始色
*/
@ColorInt
int startColor = Color.parseColor("#ff3415b0");


/**
* 渐变色结束色
*/
@ColorInt
int endColor = Color.parseColor("#ffcd0074");


/**
* 指示器颜色
*/
@ColorInt
int indicatorColor = startColor;

2、UI绘制

UI拆解后,UI绘制逻辑就变得非常清晰,所有的绘制关键在onDraw方法中

2.1 绘制刻度条

有了足够的属性声明,之后的关键就在坐标计算逻辑了。其中 ((endNum - startNum) / unitNum) + 1 代表了控件总共有多少个刻度,采取for循环进行绘制。注意好每次绘制时的刻度间间隔,计算逻辑为 (width / 2) - (lineWidth / 2) + (i * lineSpacing),暂且不用理会代码中的offsetStart 和movedX属性,这两个值主要作用于用户的交互逻辑。最后调用canvas的drawRoundRect进行绘制即可。

//绘制刻度
for (int i = 0; i < ((endNum - startNum) / unitNum) + 1; i++) {
    int lineHeight = minLineHeight;
    if (i % 10 == 0) {
        lineHeight = maxLineHeight;
    } else if (i % 5 == 0) {
        lineHeight = midLineHeight;
    }
    float lineLeft = offsetStart + movedX + (width / 2) - (lineWidth / 2) + (i * lineSpacing);
    float lineRight = lineLeft + lineWidth;
    RectF rectF = new RectF(lineLeft, 4 * indicatorRadius, lineRight, lineHeight);
    canvas.drawRoundRect(rectF, lineWidth / 2, lineWidth / 2, paint);
}

2.2 绘制刻度文字

绘制文字需要考虑文字与刻度线居中,计算逻辑如下

//绘制刻度文字
    if (i % 10 == 0) {
        textPaint.setColor(ColorUtils.getColor(startColor, endColor, (float) i / (float) ((endNum - startNum) / unitNum)));
        canvas.drawText(i + "", lineLeft + lineWidth / 2 - textPaint.measureText("" + i) / 2, lineHeight + 20 + textHeight, textPaint);
}

其中lineLeft + lineWidth / 2 - textPaint.measureText("" + i) / 2表示文字绘制的最左边的坐标,lineLeft为刻度线绘制区域的左边坐标,可以看到,这里调用了textPaint.measureText("" + i)进行宽度的动态计算

2.3 绘制指示器

指示器比较简单,绘制一个圆形就完事儿了,其中width为整个控件的宽度,通过int indicatorX = width / 2的计算,就可以将指示器绘制在控件的中线位置

//draw indicator
int indicatorX = width / 2;
int indicatorY = indicatorRadius;
canvas.drawCircle(indicatorX, indicatorY, indicatorRadius, paint);

3、渐变色背景

获取渐变背景色并设置给对应的刻度条。通过之前的属性声明,有startColor和endColor,此步骤的关键在于计算好每个刻度对应的颜色值,封装一个ColorUtils工具方法进行计算。一个色彩渐变对应成代码无非就是一堆int值的变换计算,因此,可以将颜色分解成红、蓝、绿的int值,通过ratio参数获取渐变颜色带区间的某一颜色值

/**
* 计算渐变颜色中间色值
*
* @param startColor 起始颜色
* @param endColor   结束颜色
* @param ratio      百分比,取值范围【0~1】
* @return 颜色值
*/
public static int getColor(int startColor, int endColor, float ratio) {
    int redStart = Color.red(startColor);
    int blueStart = Color.blue(startColor);
    int greenStart = Color.green(startColor);
    int redEnd = Color.red(endColor);
    int blueEnd = Color.blue(endColor);
    int greenEnd = Color.green(endColor);


    int red = (int) (redStart + ((redEnd - redStart) * radio + 0.5));
    int greed = (int) (greenStart + ((greenEnd - greenStart) * radio + 0.5));
    int blue = (int) (blueStart + ((blueEnd - blueStart) * radio + 0.5));
    return Color.argb(255, red, greed, blue);
}

有了颜色,就可以赋值给对应的文字、刻度条、指示器了

//设置刻度条颜色,比例由(float) i / (float) ((endNum - startNum) / unitNum))计算得出
paint.setColor(ColorUtils.getColor(startColor, endColor, (float) i / (float) ((endNum - startNum) / unitNum)));

//设置刻度文字颜色
textPaint.setColor(ColorUtils.getColor(startColor, endColor, (float) i / (float) ((endNum - startNum) / unitNum)));

//设置指示器颜色
indicatorColor = ColorUtils.getColor(startColor, endColor, Math.abs((float) (offsetStart + movedX) / (float) (lineSpacing * ((endNum - startNum) / unitNum))));

4、实现交互逻辑

通过前面3步,控件的表皮已经开发完毕。“皮”有了,接下来就是“心”了。交互逻辑肯定主要在onTouchEvent中进行处理。需要特别注意的是,要严格控制好边界结算,拖动位置不能超过刻度尺的范围,否则最后的效果肯定拉跨。offsetStart代表用户开始操作前,当前刻度尺刻度读数相对于起始刻度的偏移量,moveX代表用户手指滑动时移动的距离

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            scroller.forceFinished(true);
            downX = event.getX();
            movedX = 0;
            break;
        case MotionEvent.ACTION_MOVE:
            movedX = event.getX() - downX;
            Log.i(TAG, "offsetStart==>" + offsetStart);
            Log.i(TAG, "movedX==>" + movedX);
            Log.i(TAG, "offsetStart + movedX==>" + (offsetStart + movedX));
            //边界控制
            if (offsetStart + movedX > 0) {
                movedX = 0;
                offsetStart = 0;
            } else if (offsetStart + movedX < -((endNum - startNum) / unitNum) * lineSpacing) {
                offsetStart = -((endNum - startNum) / unitNum) * lineSpacing;
                movedX = 0;
            }
            if (listener != null) {
                Log.i(TAG, "getSelectedNum()==>" + getSelectedNum());
                listener.onNumSelect(getSelectedNum());
            }
            postInvalidate();
            break;
        case MotionEvent.ACTION_UP:
            //todo:手指松开时需要磁吸效果&边界判断
            //todo:计算当前手指放开时的滑动速率
            postInvalidate();
            break;
    }
    return true;
}

5、实现磁吸效果

在实现交互逻辑的过程中有个小细节,就是要考虑当用户手指滑动控件完成后,当前滑动的距离并不在刻度上怎么办?所以还需要实现一个磁吸效果,通过对计算结果的int强转进行模拟,offsetStart为float类型

if (offsetStart + movedX <= 0 && offsetStart + movedX >= -((endNum - startNum) / unitNum) * lineSpacing) {
                //手指松开时需要磁吸效果
                offsetStart = offsetStart + movedX;
                movedX = 0;
                offsetStart = ((int) (offsetStart / lineSpacing)) * lineSpacing;
}

6、实现惯性

为实现惯性,需要用到两个重要的辅助类,一个是Scroller,另一个是VelocityTracker。

6.1 Scroller简介

Scroller顾名思义为滚动者,和《上古卷轴》没有任何关系。负责计算滚动逻辑,当用户手指抬起后,如果之前的滑动速度过大,则可以直接调用fling进行滚动过程的计算,非常方便。它有几个核心方法:

getCurX():获取当前滚动位置的X轴的坐标值

getCurY():获取当前滚动位置的Y轴的坐标值

fling():给定一个速度以及速度上下限,自动计算滚动的惯性过程

startScroll():按照给定参数开始滚动

computeScrollOffset():判断滚动是否已结束

需要注意的是,使用Scroller需要重写View的computeScroll方法

6.2 VelocityTracker简介

VelocityTracker负责获取用户手指滑动的速度,核心方法如下;

addMovement():添加移动事件以便监控用户的手指滑动速度

getXVelocity():获取X方向的移动速度

6.3 Scroller、VelocityTracker相结合

//创建VelocityTracker
velocityTracker = VelocityTracker.obtain();

//添加事件监控
@Override
public boolean onTouchEvent(MotionEvent event) {
    velocityTracker.addMovement(event);
}

//计算速度并获取
velocityTracker.computeCurrentVelocity(500);
float velocityX = velocityTracker.getXVelocity();

//惯性滚动计算
scroller.fling(0, 0, (int) velocityX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);

//滚动过程
@Override
public void computeScroll() {
    super.computeScroll();
    if (scroller.computeScrollOffset()) {
        if (scroller.getCurrX() == scroller.getFinalX()) {
            //todo:磁吸效果和边界控制
        } else {
            //继续惯性滑动
            movedX = scroller.getCurrX() - scroller.getStartY();
            //todo:滑动结束:边界控制
        }
    } else {
        //滑动结束后归位
        if (offsetStart + movedX >= 0) {
            offsetStart = 0;
            movedX = 0;
        }
    }
    if (listener != null) {
        listener.onNumSelect(getSelectedNum());
    }
    postInvalidate();
}

7、其它细节

增加当前读数回调方法暴露给外部调用者、暴露可以获取指示器颜色的方法方便调用者可以配置自己的UI风格等等。我们再来重温下最后的实现效果。

三、后记

《隔壁产品都馋哭了》系列文章的初衷就是要设计出一些和市面上不太一样的控件,给人一种耳目一新的感觉。无论什么UI控件开发,只要逐步分析拆解,将大问题分解为小问题,将大需求分解为小结构,层层递进,最终水到渠成。

控件放在了gitee上,地址在

我是地址

既然看到这里了,是时候亮出我的公众号了,啊,我还没有公众号。需要此控件的扫一扫加我vx:pengyeah888