自定义CheckBox

项目地址

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);
        }
    }

从文档说明知道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() {
157    SkPoint         pts[4];
158    int             ptIndex = fFirstPtIndex;
159    SkScalar        distance = 0;
160    bool            isClosed = fForceClosed;
161    bool            firstMoveTo = ptIndex < 0;
162    Segment*        seg;
163
164    /*  Note:
165     *  as we accumulate distance, we have to check that the result of +=
166     *  actually made it larger, since a very small delta might be > 0, but
167     *  still have no effect on distance (if distance >>> delta).
168     *
169     *  We do this check below, and in compute_quad_segs and compute_cubic_segs
170     */
171    fSegments.reset();
172    bool done = false;
173    do {
174        switch (fIter.next(pts)) {
175            case SkPath::kMove_Verb:
176                ptIndex += 1;
177                fPts.append(1, pts);
178                if (!firstMoveTo) {
179                    done = true;
180                    break;
181                }
182                firstMoveTo = false;
183                break;
184
185            case SkPath::kLine_Verb: {
186                SkScalar d = SkPoint::Distance(pts[0], pts[1]);
187                SkASSERT(d >= 0);
188                SkScalar prevD = distance;
189                distance += d;
190                if (distance > prevD) {
191                    seg = fSegments.append();
192                    seg->fDistance = distance;
193                    seg->fPtIndex = ptIndex;
194                    seg->fType = kLine_SegType;
195                    seg->fTValue = kMaxTValue;
196                    fPts.append(1, pts + 1);
197                    ptIndex++;
198                }
199            } break;
200
201            case SkPath::kQuad_Verb: {
202                SkScalar prevD = distance;
203                distance = this->compute_quad_segs(pts, distance, 0, kMaxTValue, ptIndex);
204                if (distance > prevD) {
205                    fPts.append(2, pts + 1);
206                    ptIndex += 2;
207                }
208            } break;
209
210            case SkPath::kConic_Verb: {
211                const SkConic conic(pts, fIter.conicWeight());
212                SkScalar prevD = distance;
213                distance = this->compute_conic_segs(conic, distance, 0, kMaxTValue, ptIndex);
214                if (distance > prevD) {
215                    // we store the conic weight in our next point, followed by the last 2 pts
216                    // thus to reconstitue a conic, you'd need to say
217                    // SkConic(pts[0], pts[2], pts[3], weight = pts[1].fX)
218                    fPts.append()->set(conic.fW, 0);
219                    fPts.append(2, pts + 1);
220                    ptIndex += 3;
221                }
222            } break;
223
224            case SkPath::kCubic_Verb: {
225                SkScalar prevD = distance;
226                distance = this->compute_cubic_segs(pts, distance, 0, kMaxTValue, ptIndex);
227                if (distance > prevD) {
228                    fPts.append(3, pts + 1);
229                    ptIndex += 3;
230                }
231            } break;
232
233            case SkPath::kClose_Verb:
234                isClosed = true;
235                break;
236
237            case SkPath::kDone_Verb:
238                done = true;
239                break;
240        }
241    } while (!done);
242
243    fLength = distance;
244    fIsClosed = isClosed;
245    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类型:

815    enum Verb {
816        kMove_Verb,     //!< iter.next returns 1 point
817        kLine_Verb,     //!< iter.next returns 2 points
818        kQuad_Verb,    //!< iter.next returns 3 points
819        kConic_Verb,    //!< iter.next returns 3 points + iter.conicWeight()
820        kCubic_Verb,    //!< iter.next returns 4 points
821        kClose_Verb,    //!< iter.next returns 1 point (contour's moveTo pt)
822        kDone_Verb,     //!< iter.next returns 0 points
823    };

不管是什么类型的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()

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容