Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開發之自定義CheckBox

Android開發之自定義CheckBox

編輯:關於Android編程

要實現的效果如下

考慮到關鍵是動畫效果,所以直接繼承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有所幫助。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved