考虑到关键是动画效果,所以直接继承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传达长宽的要求。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(),
这样避免了重复计算.void SkPathMeasure::buildSegments() { SkPoint pts[4]; intptIndex = fFirstPtIndex; SkScalardistance = 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) ofeach contours in a path.The iterator cleans up the segments along the way, removing degeneratesegments and adding close verbs where necessary. When the forceCloseargument is provided, each contour (as defined by a new startingmove command) will be completed with a close verb regardless of thecontour"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()