Android开发之自定义CheckBox

要实现的效果如下

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

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

问题一: 支持 wrap_content

由于是直接继承自View,wrap_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,它由mode和size两部分构成,做这么多终究是为了从父容器向子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此时的length和entryPath长度是一样的,到这里我就很奇怪了。看一下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层的方法,到这里不得不看底层的实现了。

通过阅读源代码发现,Path和PathMeasure实际分别对应底层的SKPath和SKPathMeasure。

查看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有三个contour(mEntryPath,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 的全部内容, 来源链接: utcz.com/z/346807.html

回到顶部