Android开源 - 自定义CheckBox

t6053227 3年前
   <h2>继承View还是CheckBox</h2>    <p>要实现的效果是类似</p>    <p><img alt="Android开源 - 自定义CheckBox" src="https://simg.open-open.com/show/a68832c79725180370fa5e147b19b8c5.gif"></p>    <p>考虑到关键是动画效果,所以直接继承View。不过<code>CheckBox</code>的超类<code>CompoundButton</code>实现了<code>Checkable</code>接口,这一点值得借鉴。</p>    <p>下面记录一下遇到的问题,并从源码的角度解决。</p>    <h2>问题一: 支持 wrap_content</h2>    <p>由于是直接继承自View,<code>wrap_content</code>需要进行特殊处理。<br> View measure流程的<code>MeasureSpec</code>:</p>    <pre>  <code class="language-java"> /**       * A MeasureSpec encapsulates the layout requirements passed from parent to child.       * Each MeasureSpec represents a requirement for either the width or the height.       * A MeasureSpec is comprised of a size and a mode.        * MeasureSpecs are implemented as ints to reduce object allocation. This class       * is provided to pack and unpack the <size, mode> tuple into the int.       */      public static class MeasureSpec {          private static final int MODE_SHIFT = 30;          private static final int MODE_MASK  = 0x3 << MODE_SHIFT;            /**           * Measure specification mode: The parent has not imposed any constraint           * on the child. It can be whatever size it wants.           */          public static final int UNSPECIFIED = 0 << MODE_SHIFT;            /**           * Measure specification mode: The parent has determined an exact size           * for the child. The child is going to be given those bounds regardless           * of how big it wants to be.           */          public static final int EXACTLY     = 1 << MODE_SHIFT;            /**           * Measure specification mode: The child can be as large as it wants up           * to the specified size.           */          public static final int AT_MOST     = 2 << MODE_SHIFT;            /**           * Extracts the mode from the supplied measure specification.           *           * @param measureSpec the measure specification to extract the mode from           * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},           *         {@link android.view.View.MeasureSpec#AT_MOST} or           *         {@link android.view.View.MeasureSpec#EXACTLY}           */          public static int getMode(int measureSpec) {              return (measureSpec & MODE_MASK);          }            /**           * Extracts the size from the supplied measure specification.           *           * @param measureSpec the measure specification to extract the size from           * @return the size in pixels defined in the supplied measure specification           */          public static int getSize(int measureSpec) {              return (measureSpec & ~MODE_MASK);          }      }</code></pre>    <p>从文档说明知道android为了节约内存,设计了MeasureSpec,它由mode和size两部分构成,做这么多终究是为了从父容器向子view传达长宽的要求。mode有三种模式:</p>    <ul>     <li><code>UNSPECIFIED</code>:父容器不对子view的宽高有任何限制</li>     <li><code>EXACTLY</code>:父容器已经为子view指定了确切的宽高</li>     <li><code>AT_MOST</code>:父容器指定最大的宽高,子view不能超过</li>    </ul>    <p><code>wrap_content</code>属于<code>AT_MOST</code>模式。</p>    <p>来看一下大致的measure过程:<br> 在View中首先调用<code>measure()</code>,最终调用<code>onMeasure()</code></p>    <pre>  <code class="language-java">protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                  getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));      }</code></pre>    <p><code>setMeasuredDimension</code>设置view的宽高。再来看看<code>getDefaultSize()</code></p>    <pre>  <code class="language-java">public static int getDefaultSize(int size, int measureSpec) {          int result = size;          int specMode = MeasureSpec.getMode(measureSpec);          int specSize = MeasureSpec.getSize(measureSpec);            switch (specMode) {          case MeasureSpec.UNSPECIFIED:              result = size;              break;          case MeasureSpec.AT_MOST:          case MeasureSpec.EXACTLY:              result = specSize;              break;          }          return result;      }</code></pre>    <p>由于<code>wrap_content</code>属于模式<code>AT_MOST</code>,所以宽高为specSize,也就是父容器的size,这就和<code>match_parent</code>一样了。支持wrap_content总的思路是重写onMeasure()具体点来说,模仿<code>getDefaultSize()</code>重新获取宽高。</p>    <pre>  <code class="language-java"> @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          int widthMode = MeasureSpec.getMode(widthMeasureSpec);          int widthSize = MeasureSpec.getSize(widthMeasureSpec);          int heightMode = MeasureSpec.getMode(heightMeasureSpec);          int heightSize = MeasureSpec.getSize(heightMeasureSpec);            int width = widthSize, height = heightSize;            if (widthMode == MeasureSpec.AT_MOST) {              width = dp2px(DEFAULT_SIZE);          }            if (heightMode == MeasureSpec.AT_MOST) {              height = dp2px(DEFAULT_SIZE);          }          setMeasuredDimension(width, height);      }</code></pre>    <h2>问题二:Path.addPath()和PathMeasure结合使用</h2>    <p>举例子说明问题:</p>    <pre>  <code class="language-java">    mTickPath.addPath(entryPath);      mTickPath.addPath(leftPath);      mTickPath.addPath(rightPath);      mTickMeasure = new PathMeasure(mTickPath, false);      // mTickMeasure is a PathMeasure</code></pre>    <p>尽管<code>mTickPath</code>现在是由三个path构成,但是<code>mTickMeasure</code>此时的<code>length</code>和<code>entryPath</code>长度是一样的,到这里我就很奇怪了。看一下<code>getLength()</code>的源码:</p>    <pre>  <code class="language-java">    /**       * Return the total length of the current contour, or 0 if no path is       * associated with this measure object.       */      public float getLength() {          return native_getLength(native_instance);      }</code></pre>    <p>从注释来看,获取的是<strong>当前contour</strong>的总长。</p>    <p><code>getLength</code>调用了native层的方法,到这里不得不看底层的实现了。<br> 通过阅读源代码发现,<code>Path</code>和<code>PathMeasure</code>实际分别对应底层的<code>SKPath</code>和<code>SKPathMeasure</code>。</p>    <p>查看native层的<code>getLength()</code>源码:</p>    <pre>  <code class="language-java">   SkScalar SkPathMeasure::getLength() {         if (fPath == NULL) {            return 0;         }        if (fLength < 0) {            this->buildSegments();        }        SkASSERT(fLength >= 0);        return fLength;  }</code></pre>    <p>实际上调用的<code>buildSegments()</code>来对<code>fLength</code>赋值,这里底层的设计有一个很聪明的地方——在初始化<code>SKPathMeasure</code>时对<code>fLength</code>做了特殊处理:</p>    <pre>  <code class="language-java">SkPathMeasure::SkPathMeasure(const SkPath& path, bool forceClosed) {      fPath = &path;      fLength = -1;   // signal we need to compute it      fForceClosed = forceClosed;      fFirstPtIndex = -1;       fIter.setPath(path, forceClosed);  }</code></pre>    <p>当fLength=-1时我们需要计算,也就是说当还没有执行过getLength()方法时,fLength一直是-1,一旦执行则fLength>=0,则下一次就不会执行<code>buildSegments()</code>,这样避免了重复计算.</p>    <p>截取<code>buildSegments()</code>部分代码:</p>    <pre>  <code class="language-java">void SkPathMeasure::buildSegments() {      SkPoint         pts[4];      int             ptIndex = fFirstPtIndex;      SkScalar        distance = 0;      bool            isClosed = fForceClosed;      bool            firstMoveTo = ptIndex < 0;      Segment*        seg;        /*  Note:      *  as we accumulate distance, we have to check that the result of +=      *  actually made it larger, since a very small delta might be > 0, but      *  still have no effect on distance (if distance >>> delta).      *      *  We do this check below, and in compute_quad_segs and compute_cubic_segs      */      fSegments.reset();      bool done = false;      do {          switch (fIter.next(pts)) {              case SkPath::kMove_Verb:                  ptIndex += 1;                  fPts.append(1, pts);                  if (!firstMoveTo) {                      done = true;                      break;                  }                  firstMoveTo = false;                  break;                case SkPath::kLine_Verb: {                  SkScalar d = SkPoint::Distance(pts[0], pts[1]);                  SkASSERT(d >= 0);                  SkScalar prevD = distance;                  distance += d;                  if (distance > prevD) {                      seg = fSegments.append();                      seg->fDistance = distance;                      seg->fPtIndex = ptIndex;                      seg->fType = kLine_SegType;                      seg->fTValue = kMaxTValue;                      fPts.append(1, pts + 1);                      ptIndex++;                  }              } break;                case SkPath::kQuad_Verb: {                  SkScalar prevD = distance;                  distance = this->compute_quad_segs(pts, distance, 0, kMaxTValue, ptIndex);                  if (distance > prevD) {                      fPts.append(2, pts + 1);                      ptIndex += 2;                  }              } break;                case SkPath::kConic_Verb: {                  const SkConic conic(pts, fIter.conicWeight());                  SkScalar prevD = distance;                  distance = this->compute_conic_segs(conic, distance, 0, kMaxTValue, ptIndex);                  if (distance > prevD) {                      // we store the conic weight in our next point, followed by the last 2 pts                      // thus to reconstitue a conic, you'd need to say                      // SkConic(pts[0], pts[2], pts[3], weight = pts[1].fX)                      fPts.append()->set(conic.fW, 0);                      fPts.append(2, pts + 1);                      ptIndex += 3;                  }              } break;                case SkPath::kCubic_Verb: {                  SkScalar prevD = distance;                  distance = this->compute_cubic_segs(pts, distance, 0, kMaxTValue, ptIndex);                  if (distance > prevD) {                      fPts.append(3, pts + 1);                      ptIndex += 3;                  }              } break;                case SkPath::kClose_Verb:                  isClosed = true;                  break;                case SkPath::kDone_Verb:                  done = true;                  break;          }      } while (!done);        fLength = distance;      fIsClosed = isClosed;      fFirstPtIndex = ptIndex;  </code></pre>    <p>代码较长需要慢慢思考。<code>fIter</code>是一个<code>Iter</code>类型,在<code>SKPath.h</code>中的声明:</p>    <blockquote>     <p>/<em>* Iterate through all of the segments (lines, quadratics, cubics) of<br> each contours in a path.<br> The iterator cleans up the segments along the way, removing degenerate<br> segments and adding close verbs where necessary. When the forceClose<br> argument is provided, each contour (as defined by a new starting<br> move command) will be completed with a close verb regardless of the<br> contour's contents. </em>/</p>    </blockquote>    <p>从这个声明中可以明白Iter的作用是遍历在path中的每一个contour。看一下<code>Iter.next()</code>方法:</p>    <pre>  <code class="language-java">    Verb next(SkPoint pts[4], bool doConsumeDegerates = true) {             if (doConsumeDegerates) {                 this->consumeDegenerateSegments();             }              return this->doNext(pts);      }</code></pre>    <p>返回值是一个<code>Verb</code>类型:</p>    <pre>  <code class="language-java">enum Verb {      kMove_Verb,     //!< iter.next returns 1 point      kLine_Verb,     //!< iter.next returns 2 points      kQuad_Verb,    //!< iter.next returns 3 points      kConic_Verb,    //!< iter.next returns 3 points + iter.conicWeight()      kCubic_Verb,    //!< iter.next returns 4 points      kClose_Verb,    //!< iter.next returns 1 point (contour's moveTo pt)      kDone_Verb,     //!< iter.next returns 0 points  }</code></pre>    <p>不管是什么类型的Path,它一定是由<strong>点</strong>组成,如果是直线,则两个点,贝塞尔曲线则三个点,依次类推。</p>    <p>doNext()方法的代码就不贴出来了,作用就是判断contour的类型并把相应的点的坐标取出传给<code>pts[4]</code></p>    <p>当<code>fIter.next()</code>返回<code>kDone_Verb</code>时,一次遍历结束。<br> <code>buildSegments</code>中的循环正是在做此事,而且从case <code>kLine_Verb</code>模式的<code>distance += d;</code>不难发现这个length是累加起来的。在举的例子当中,mTickPath有三个contour(mEntryPath,mLeftPath,mRightPath),我们调用mTickMeasure.getLength()时,首先会<strong>累计</strong>获取mEntryPath这个contour的长度。</p>    <p>这就不难解释为什么mTickMeasure获取的长度和mEntryPath的一样了。那么想一想,怎么让buildSegments()对下一个contour进行操作呢?关键是把<code>fLength</code>置为-1</p>    <pre>  <code class="language-java">/** Move to the next contour in the path. Return true if one exists, or false if      we're done with the path.  */  bool SkPathMeasure::nextContour() {      fLength = -1;      return this->getLength() > 0;  }</code></pre>    <p>与native层对应的API是<code>PathMeasure.nextContour()</code></p>    <p><br> 来自:http://www.jianshu.com/p/fd97dad39201</p>