Android開源 - 自定義CheckBox
繼承View還是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);
}
}</code></pre>
從文檔說明知道android為了節約內存,設計了MeasureSpec,它由mode和size兩部分構成,做這么多終究是為了從父容器向子view傳達長寬的要求。mode有三種模式:
UNSPECIFIED
:父容器不對子view的寬高有任何限制
EXACTLY
:父容器已經為子view指定了確切的寬高
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()
來自:http://www.jianshu.com/p/fd97dad39201