欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android开发之自定义CheckBox

程序员文章站 2024-03-07 10:25:09
要实现的效果如下 考虑到关键是动画效果,所以直接继承view。不过checkbox的超类compoundbutton实现了checkable接口,这一点值得借鉴。...

要实现的效果如下

Android开发之自定义CheckBox

考虑到关键是动画效果,所以直接继承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有所帮助。