Android UI-薄荷健康尺子

3,042 阅读8分钟

参考

关注我就能达到大师级水平,这话我终于敢说了 (rengwuxian.com)

让控件如此丝滑Scroller和VelocityTracker的API讲解与实战——Android高级UI

自定义 view 之薄荷 app 的卷尺效果 - 掘金 (juejin.cn

效果

源码:HenCoder-CustomView: HenCoder-三篇自定义View仿写 (gitee.com)

原的

QQ图片20220508153648.gif 仿的

QQ图片20220508153644.gif

功能点分析

  1. 根据尺子的量程 和 分度值 绘制尺子的静态效果
  2. 内容滑动,计算滑动边界
  3. 惯性滑动,智能定位
  4. 计算当前刻度值
  5. 基准线居中

实现分析

OIP-C.jfif

绘制尺子刻度

  1. 分度值:即最小刻度值,就是在测量仪器所能读出的最小值,指测量工具上相邻的两个刻度之间的最小格的数值
  2. 绘制尺子刻度,肯定要用到循环,最简单的办法,知道尺子的刻度总数,即可把尺子绘制出来。
  3. 尺子的刻度数根据 量程和分度值确定。上图尺子同样的量程 有两个分度值,尺子的刻度数完全不同
  4. 刻度数的计算:量程/分度值。 比如:
    1. 度量范围20~100,量程是80,分度值是1。 一共80个刻度
    2. 但凡事总有例外,度量范围0~100,量程100,分度值1,并不是100个刻度,而是一百零一个刻度。
    3. 0也算一个刻度,0 ~ 100 是101个数。1~100才是100个数。 程序员应该很好理解吧,毕竟从入行开始,数的起始就不是1了🐶
  5. 尺子的刻度一般都是10进制,则取余数 i % 10 == 0 表明是大刻度,其余的都是小刻度。
  6. 定义变量,刻度的长款,间隔。已经知道总刻度数,通过循环遍历即可绘制出尺子的静态样式
  7. 绘制文字比较简单 每次取余数 i % 10 == 0 表示大刻度,需要绘制文字,坐标微调即可。

尺子的滑动

  1. 滑动分两种,内容滑动和拖拽滑动。
    1. 内容滑动场景是:ScrollView ,ListView,RecyclerView,在有限的位置固定的空间内可以展示无限内容。
    2. 拖拽滑动指View内容不变,位置变化。应用场景是微信语音,视频电话的小窗口。
  2. 尺子的滑动是内容滑动
    1. 重写onTouchEvent() 。记录每次手指滑动产生的坐标,上一次坐标与当前坐标相减,计算滑动距离。
    2. 在move事件中,调用scrollBy() 传入滑动距离,内容滑动完成
  3. 惯性滑动组件介绍
    1. 仅仅使用scrollBy() 滑动无惯性,效果比较生硬,与系统滑动组件的体验相差比较多
    2. 结合 VelocityTrackerScroller 使滑动产生惯性
    3. VelocityTracker 收集手指滑动路径的坐标用作路程,传入时间,计算出速度。
    4. Scroller 滑动辅助类,并不实现View滚动。它的作用好像属性动画,计算一段时间内数字变化, 比如:一秒内从0增长到100。 开发者监听数字变化从而实现动画
  4. 惯性滑动实现
    1. 重写onTouchEvent() 调用VelocityTracker.addMovement(event) 收集手势滑动信息
    2. 在up事件,VelocityTracker.computeCurrentVelocity(1000) 计算一秒内滑动距离产生的速度
    3. 速度计算结果 ,手指左滑 速度正数 ,手指右滑 速度负数。
    4. 速度正数使坐标增加 ,负数使坐标减少。这里会引发一个问题
    5. 调用Scroller.fling()
      1. fling 参数解析
      2. startX:开始位置
      3. minX-maxX:区间 ,根据速度计算x值 的范围在 minX maxX之间
      4. velocityX 速度的影响
        1. 比如:手指右滑
        2. 期望效果 x轴正方向移动 值增加
        3. 实际效果 速度负数 Scroller.fling动画结果 x轴负方向移动 值减少
        4. 期望效果与实际效果正好相反 所以速度取相反数 效果正好
      5. fling总结
        1. startX开始位置 如 :100
        2. 受速度影响 计算结果 会从100开始增加或减少。
        3. 但不是无限增加或减少,计算结果的边界在 minX最小值,maxX最大值 之间
    6. 调用 invalidate() 触发view重绘,
    7. 重写computeScroll() ,获取Scroller动画结果 ,调用scrollTo() 实现内容滚动

滑动边界

Untitled.png

滑动边界与view的大小是两个概念

view的内容绘制在canvas上,canvas是一块无限大的画布,View有坐标系,左上角是原点(0,0),惨van无限大,坐标系也是无限大的。

View的宽高则是在无限大的canvas从原点开始圈出一块位置展示内容。

如下图,用户的可视范围只是100*100,但无限大的canvas仍然存在。假设在(200,200)的位置画了一个点,虽然用户看不见,但是它仍然存在。

上一节使用scrollBy()scrollTo() 实现内容的滑动

其内部原理是修改View的两个属性mScrollX,mScrollYmScrollX,mScrollY 表示内容在X轴Y轴的滚动距离,也可以说是确定View的展示的原点。

举例说明:

  1. 自定义View,宽高都是100
  2. 两点坐标确定一个矩形,默认原点(0,0) 由于宽高100,另一点坐标(100,100)。View展示canvas (0,0),(100,100)两点坐标圈出的部分
  3. 沿X轴移动距离100后,原点坐标(100,0),另一点坐标(200,100)。View展示canvas (100,0),(200,100)两点坐标圈出的部分

所以想要实现View内容滑动的边界,就要限制X轴坐标的取值范围,也就是mScrollX 属性的范围,从0到X。

那么如何计算滑动范围呢?

滑动范围 = 大刻度数大刻度宽 + 小刻度数小刻度宽 + 间隔数*间隔宽

基准线居中

其实这个东西吧 属于会了不难,难了不会,经验问题,不知道的可能想半天也没想出来。

先说结论:基准线x轴坐标 = view宽度/2 + mScrollX 就能达到滚动时居中效果。

分析

  1. 假设View的宽高都为100
  2. 画一条长度为10的X轴居中的线段,坐标点(50,10)
  3. 这条线段只是看起来居中,在view的可视范围(0,0),(100,100)内居中,
  4. 它并不是画在View上,而是画在canvas,view只是圈出一个范围
  5. 当内容水平滑动,x值不断改变,线段的坐标也要随着滑动不断变化,才能维持居中的效果
  6. 代表水平滑动距离的变量是mScrollX 线段坐标点为 (mScrollX +50,10)

智能定位

业务描述

当滑动到两个刻度之间,四舍五入自动定位到最近的那个刻度,比如:滑动到11.6,分度值是1,左右两个刻度分别是11,12。四舍五入滑动到12。

应用场景

  1. 惯性滑动后需要智能定位
  2. up手势之后,如果速度过小,无法出发惯性滑动,则需要智能定位

实现过程

这块挺复杂的,没办法详细说 很容易乱,我的思路不一定是最好的,当作参考

  1. 核心思路是利用等比例换算。
  2. 预先知道总滑动距离,知道当前滑动值,能够计算出滑动比例。
  3. 滑动比例 == 数值比例,通过比例计算出当前的测量值
  4. 根据分度值单位四舍五入,求出定位值,计算出定位值的X轴坐标
  5. mScrollX -定位值的X轴坐标 = 滑动距离。求出滑动距离后利用 Scroller.startScroll() 进行滑动

只绘制可视区域内容

之前几点完成之后就算是可以正常使用的组件了,原本是不打算做可视区域绘制的(懒)

但是在调试的时候发现绘制内容过多会很卡,不流畅z

比如:度量范围1~100,分度值是1,需要绘制100个刻度。分度值0.1,需要绘制1k个刻度,分度值0.01,需要绘制1w个刻度,卡顿非常明显了,简直不能用。

计算可视区域非常简单。view的可见区域 = x轴坐标范围 = 滚动距离 + view的宽度

x坐标在范围内视为可见,不在范围内视为不可见

private fun isVisibleArea(x: Int): Boolean {
    //view的可见区域 = x轴坐标范围 = 滚动距离 + view的宽度
    val offset = 20 //偏移量
    val start =scrollX-offset
    val end =scrollX+measuredWidth+ offset
    return x in start..end
}

override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
			//简化代码
     if (isVisibleArea(x)){
          drawLine(i, canvas)
          drawText(i, canvas)
     }  
}