自定义CheckBox

作者: 扎实基础cc | 来源:发表于2016-07-30 08:02 被阅读2701次

    项目地址

    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()

    相关文章

      网友评论

      • goolong:写的不错 贯穿了自定义View流程

      本文标题:自定义CheckBox

      本文链接:https://www.haomeiwen.com/subject/kezzjttx.html