当前位置: 移动技术网 > 移动技术>移动开发>Android > Android开发之自定义CheckBox

Android开发之自定义CheckBox

2019年07月24日  | 移动技术网移动技术  | 我要评论

要实现的效果如下

考虑到关键是动画效果,所以直接继承view。不过checkbox的超类compoundbutton实现了checkable接口,这一点值得借鉴。

下面记录一下遇到的问题,并从源码的角度解决。

问题一: 支持 wrap_content

由于是直接继承自viewwrap_content需要进行特殊处理。

view measure流程的measurespec:

 /**
  * 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);
  }
 }

从文档说明知道android为了节约内存,设计了measurespec,它由modesize两部分构成,做这么多终究是为了从父容器向子view传达长宽的要求。

mode有三种模式:

      1、unspecified:父容器不对子view的宽高有任何限制

      2、exactly:父容器已经为子view指定了确切的宽高

      3、at_most:父容器指定最大的宽高,子view不能超过

wrap_content属于at_most模式。

来看一下大致的measure过程:

在view中首先调用measure(),最终调用onmeasure()

protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
  setmeasureddimension(getdefaultsize(getsuggestedminimumwidth(), widthmeasurespec),
    getdefaultsize(getsuggestedminimumheight(), heightmeasurespec));
 }

setmeasureddimension设置view的宽高。再来看看getdefaultsize()

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;
 }

由于wrap_content属于模式at_most,所以宽高为specsize,也就是父容器的size,这就和match_parent一样了。支持wrap_content总的思路是重写onmeasure()具体点来说,模仿getdefaultsize()重新获取宽高。

 @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);
 }

问题二:path.addpath()和pathmeasure结合使用

举例子说明问题:

 mtickpath.addpath(entrypath);
 mtickpath.addpath(leftpath);
 mtickpath.addpath(rightpath);
 mtickmeasure = new pathmeasure(mtickpath, false);
 // mtickmeasure is a pathmeasure

尽管mtickpath现在是由三个path构成,但是mtickmeasure此时的lengthentrypath长度是一样的,到这里我就很奇怪了。看一下getlength()的源码:

 /**
  * 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);
 }

从注释来看,获取的是当前contour的总长。

getlength调用了native层的方法,到这里不得不看底层的实现了。

通过阅读源代码发现,pathpathmeasure实际分别对应底层的skpathskpathmeasure

查看native层的getlength()源码:

 skscalar skpathmeasure::getlength() {
  if (fpath == null) {
   return 0;
  }
  if (flength < 0) {
   this->buildsegments();
  }
  skassert(flength >= 0);
  return flength;
}

实际上调用的buildsegments()来对flength赋值,这里底层的设计有一个很聪明的地方——在初始化skpathmeasure时对flength做了特殊处理:

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);
}

flength=-1时我们需要计算,也就是说当还没有执行过getlength()方法时,flength一直是-1,一旦执行则flength>=0,则下一次就不会执行buildsegments(),这样避免了重复计算.

截取buildsegments()部分代码:

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;

代码较长需要慢慢思考。fiter是一个iter类型,在skpath.h中的声明:

/* iterate through all of the segments (lines, quadratics, cubics) of
each contours in a path.
the iterator cleans up the segments along the way, removing degenerate
segments and adding close verbs where necessary. when the forceclose
argument is provided, each contour (as defined by a new starting
move command) will be completed with a close verb regardless of the
contour's contents. /

从这个声明中可以明白iter的作用是遍历在path中的每一个contour。看一下iter.next()方法:

 verb next(skpoint pts[4], bool doconsumedegerates = true) {
   if (doconsumedegerates) {
    this->consumedegeneratesegments();
   }
   return this->donext(pts);
 }

返回值是一个verb类型:

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
}

不管是什么类型的path,它一定是由点组成,如果是直线,则两个点,贝塞尔曲线则三个点,依次类推。

donext()方法的代码就不贴出来了,作用就是判断contour的类型并把相应的点的坐标取出传给pts[4]

fiter.next()返回kdone_verb时,一次遍历结束。

buildsegments中的循环正是在做此事,而且从case kline_verb模式的distance += d;不难发现这个length是累加起来的。在举的例子当中,mtickpath有三个contourmentrypath,mleftpath,mrightpath),我们调用mtickmeasure.getlength()时,首先会累计获取mentrypath这个contour的长度。

这就不难解释为什么mtickmeasure获取的长度和mentrypath的一样了。那么想一想,怎么让buildsegments()对下一个contour进行操作呢?关键是把flength置为-1

/** 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;
}

native层对应的api是pathmeasure.nextcontour()

总结

以上就是android开发之自定义checkbox的全部内容,希望本文对大家开发android有所帮助。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网