Android開源 - 自定義CheckBox

t6053227 8年前發布 | 17K 次閱讀 Android Android開發 移動開發

繼承View還是CheckBox

要實現的效果是類似

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);
    }
}</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此時的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有三個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

 本文由用戶 t6053227 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!