Android自定义View之刻度尺

gl7080 8年前
   <h2>背景</h2>    <p>项目中之前用的纵向滚轮用于选择身高体重之类的需求,新版设计要求用横向刻度尺控件来实现,效果图,上面的字不在刻度尺范围内,是一个<code>TextView</code>。</p>    <p><a href="/misc/goto?guid=4959673165191999736" rel="group"><img alt="刻度尺" src="https://simg.open-open.com/show/7164dd2dedc34fa1046e860af2bcfebb.png"></a></p>    <p>自定义控件对于Android开发者来说是必备技能,这篇文章就不讲自定义View的基础知识了,主要谈谈绘制逻辑。</p>    <h2>实现</h2>    <p>遵循自定义View的开发流程,<code>onMeasure()</code> –> <code>onSizeChanged()</code> –> <code>onLayout()</code> –> <code>onDraw()</code>。由于我们要自定义的是View而不是ViewGroup,所以onLayout()就不用实现了。</p>    <h3>onMeasure() 测量</h3>    <p><code>onMeasure()</code>用于测量<code>View</code>的大小,<code>View</code>的大小不仅由自身决定,同时也受父控件的影响,为了我们的控件能更好的适应各种情况,一般会自己进行测量。刻度尺<code>View</code>左右是满屏的,偷个懒宽度就不适配了,只做高度测试就好了。高度包括长刻度的高度,加上字和底部间距</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-java">@Override  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {      setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));  }    private int measureHeight(int heightMeasure) {      int measureMode = View.MeasureSpec.getMode(heightMeasure);      int measureSize = View.MeasureSpec.getSize(heightMeasure);      int result = (int) (bottomPadding + longLineHeight * 2);      switch (measureMode) {          case View.MeasureSpec.EXACTLY:              result = Math.max(result, measureSize);              break;          case View.MeasureSpec.AT_MOST:              result = Math.min(result, measureSize);              break;          default:              break;      }      height = result;      return result;  }  </code></pre> </td>      </tr>     </tbody>    </table>    <h3>onDraw() 绘制</h3>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-java">@Override  protected void onDraw(Canvas canvas) {      super.onDraw(canvas);      canvas.setDrawFilter(pfdf);      drawBg(canvas);      drawIndicator(canvas);      drawRuler(canvas);  }  </code></pre> </td>      </tr>     </tbody>    </table>    <p>绘制做了三件事情:</p>    <ol>     <li>绘制背景色</li>     <li>绘制指示器</li>     <li>绘制刻度</li>    </ol>    <p>只看下怎么画刻度,难点在于怎么确定刻度的坐标。首先高度的坐标是一样的,刻度指示器是在屏幕正中,指示某个值,那么该刻度值的x坐标就是确定的。根据这个值去画其他坐标,包括长刻度和短刻度。</p>    <p>首先要确定每个长刻度(和短刻度,如果有的话)的坐标宽度和单位,确定基础单位和基础单位宽度(如果有短刻度以短刻度为基础单位)。那么</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-java">第i个刻度x坐标 = 中间刻度x坐标 + i * 基础单位宽度  </code></pre> </td>      </tr>     </tbody>    </table>    <p>其中i的取值范围在正负屏幕可绘制多少个基础单位,第0位就是屏幕正中的刻度值。以该值为基础一次画出可以在屏幕中显示的剩余刻度,如果是长刻度单位的整数倍就画长刻度,刻度值只在长刻度下画。<br> 这样就有一个问题,正中刻度值必须是可以整除基础单位,比如,长刻度 = 1,中间两个短刻度,这样基础单位值就是0.5,currentValue = 0.3,那么下一个值就是0.8,但是这样显示并不是我们想要的,我们想要0、0.5、1、1.5这样的值。所以就是在初始化的时候格式化这些值,使得所有可显示的值都可以整除基础单位值,也就是余数为0。<br> 由于使用float计算,所以要用到float精确计算,否则取余操作会出现不等于0的误差导致画不出长刻度。</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-java">//精度支持2位小数     private float format(float vallue) {         float result = 0;         if (getBaseUnit() < 0.1) {             //0.01             result = ArithmeticUtil.round(vallue, 2);             //float精确计算 取余             if (ArithmeticUtil.remainder(result, getBaseUnit(), 2) != 0) {                 result += 0.01;                 result = format(result);             }         } else if (getBaseUnit() < 1) {             //0.1             result = ArithmeticUtil.round(vallue, 1);             if (ArithmeticUtil.remainder(result, getBaseUnit(), 1) != 0) {                 result += 0.1;                 result = format(result);             }         } else if (getBaseUnit() < 10) {             //1             result = ArithmeticUtil.round(vallue, 0);             if (ArithmeticUtil.remainder(result, getBaseUnit(), 0) != 0) {                 result += 1;                 result = format(result);             }         }         return result;     }  </code></pre> </td>      </tr>     </tbody>    </table>    <h3>处理滑动操作</h3>    <p>滑动处理比较简单,以初始化为基础,每次move操作累加x坐标,以此值绘制偏移量,停止滑动时以基础单位宽度为基准四舍五入,开始动画滑动到相应的刻度值上。<br> 主要方法</p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-java">private void drawRuler(Canvas canvas) {          if (moveX < maxRightOffset) {              moveX = maxRightOffset;          }          if (moveX > maxLeftOffset) {              moveX = maxLeftOffset;          }          int halfCount = (int) (width / 2 / getBaseUnitWidth());          float moveValue = (int) (moveX / getBaseUnitWidth()) * getBaseUnit();          currentValue = originValue - moveValue;          //剩余偏移量          offset = moveX - (int) (moveX / getBaseUnitWidth()) * getBaseUnitWidth();            for (int i = -halfCount - 1; i <= halfCount + 1; i++) {              float value = ArithmeticUtil.addWithScale(currentValue, ArithmeticUtil.mulWithScale(i, getBaseUnit(), 2), 2);              //只绘出范围内的图形              if (value >= startValue && value <= endValue) {                  //画长的刻度                  float startx = width / 2 + offset + i * getBaseUnitWidth();                  if (startx > 0 && startx < width) {                      if (microUnitCount != 0) {                          if (ArithmeticUtil.remainder(value, unit, 2) == 0) {                              drawLongLine(canvas, i, value);                          } else {                              //画短线                              drawShortLine(canvas, i);                          }                      } else {                          //画长线                          drawLongLine(canvas, i, value);                      }                  }              }          }    //通知结果          notifyValueChange();      }  </code></pre> </td>      </tr>     </tbody>    </table>    <p>关于刻度的单位,需要给出长刻度单位和中间的短刻度个数,这样中间的短刻度单位就确定了,所以理论上不管中间有几个短刻度计算都是一样的。我在里面封装了三个常用的,2、5、10三种。<br> 支持的<code>styleable</code></p>    <table>     <tbody>      <tr>       <td> <pre>  <code class="language-java"><declare-styleable name="BooheeRulerView">      <attr name="ruler_bg_color" format="color|reference"/>      <attr name="ruler_line_color" format="color|reference"/>      <attr name="ruler_text_size" format="dimension"/>      <attr name="ruler_text_color" format="color|reference"/>      <attr name="ruler_width_per_unit" format="dimension"/>  </declare-styleable>  </code></pre> </td>      </tr>     </tbody>    </table>    <p><a href="https://simg.open-open.com/show/65b843a233457eabca707745c19c2c4e.gif" rel="group"><img alt="Android自定义View之刻度尺" src="https://simg.open-open.com/show/65b843a233457eabca707745c19c2c4e.gif" width="320" height="144"></a></p>    <p><a href="https://simg.open-open.com/show/03c4fddd960d426502bab173c09ed4db.gif" rel="group"><img alt="Android自定义View之刻度尺" src="https://simg.open-open.com/show/03c4fddd960d426502bab173c09ed4db.gif" width="322" height="115"></a></p>    <p><a href="/misc/goto?guid=4959673165298727085" rel="external">代码在这里</a></p>    <h2>总结</h2>    <p>实现的效果比较单一,没有做太多的扩展,有时间再完善下。</p>    <p>via:http://w4lle.github.io/2016/05/15/Android%E8%87%AA%E5%AE%9A%E4%B9%89View%E4%B9%8B%E5%88%BB%E5%BA%A6%E5%B0%BA/</p>