自定义View(一)

一、了解ViewRoot和DecorView

1.ViewRoot

从源码可以看出ViewRoot是ViewParent的实现类


public final class ViewRoot extends Handler implements ViewParent,

ViewRoot对应于的ViewRootImp也是ViewParent的实现类

public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks

我们知道View有三大流程(measure->layout->draw),都是通过ViewRoot完成的。内部通过performalTraversals()方法依次向下传递的。

2.View的三大流程

measure

measure决定了View的宽高,当measure完成后,我们可以通过以下代码获取view的测量宽度、测量高度,


        t.getMeasuredHeight();
        t.getMeasuredWidth();

注意:我们不可以在Activity的onCreate方法中对一个view使用上述代码获取宽度,因为在执行Activity的onCreate()方法时measure还没测量好,需要在前面调用以下代码来


    t.measure(0, 0);

这里的0是MeasureSpec,下面后介绍到。还有种获取宽度、高度的方式,如下:


    t.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
             width = t.getMeasuredWidth();
             height = t.getMeasuredHeight();
             t.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
    }) ;

其实方法有很多种,我们也可以通过view.post()将获取宽度、高度的代码放到一个任务队列的末尾、重写onWindowFocusChanged()方法等。

继续分析,View的measure是又measure()方法完成的。查看源码


     public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
                Insets insets = getOpticalInsets();
                int oWidth  = insets.left + insets.right;
                int oHeight = insets.top  + insets.bottom;
                widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
                heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
            }
            if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                    widthMeasureSpec != mOldWidthMeasureSpec ||
                    heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
    }

这是一个final方法,因此子类不可以重写,但是我们从源码中可以看到有如下代码


         // measure ourselves, this should set the measured dimension flag back
         onMeasure(widthMeasureSpec, heightMeasureSpec);

再看onMeasure()


    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

由此可以看出最后是通过setMeasuredDimension()方法进行设置宽度、高度的。再看看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;
    }

这个方法比较简单,其中specSize是测量后的大小,然后通过swich根据specMode来设置不同的值,当specMode为AT_MOST、EXACTLY时,返回specSize。而当specMode是UNSPECIFIED时,返回的就直接是getSuggestedMinimumHeight()的值。


    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }

根据背景是否为null,如果为null,则返回mMinHeight,mMinHeight默认为0。写到这,突然想到PoupuWindow的使用,不知道有没有童鞋遇到过明明代码没错但是就是不出现效果,非得设置个背景才有效,即使背景什么都没有,不知道是不是也是上面的原因,回头研究下。。。

layout

layout决定了View的四个点的坐标和位置信息,此时获取的宽度高度是真正宽度高度,并且我们可以获取left、right、top、bottom值,直接通过getWidth()、getHeight()获取宽度、高度。

源码如下:


    public void layout(int l, int t, int r, int b) {
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    }

由上面代码可以看出上述代码先是调用了下面这行代码用来确定四个顶点的位置


    boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

然后调用了onLayout()方法,而在onLayout()方法中,我们发现只是一个空实现,因此需要我们子类去重写


    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

这里我们查看一下RelativeLayout的onLayout方法,看其具体实现


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //  The layout has actually already been performed and the positions
        //  cached.  Apply the cached values to the children.
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }

相对布局的onLayout()方法的代码比较简单,首先是获取布局子view的个数,然后for遍历,只要不是gone的都是依次获取布局参数,然后调用子view的layout()方法,子view又调用setFrame(l, t, r, b),因此相对布局的每一个子view都是重叠的。

draw

draw完成了View的显示过程,只有在draw完成之后才会才屏幕上东西。

在View的draw()方法的实现我们可以看到如下:


        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas‘ layers to prepare for fading
         *      3. Draw view‘s content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

翻译过来就是:

  1. 画背景
  2. 有需要的话画layers
  3. 画内容
  4. 画字view
  5. 有需要的话画edges和报存layers
  6. 画装饰

DecorView

DecorView是顶级View,一般包括一个竖直方向的LineayLayout,主要包括标题栏和内容。标题栏一般是主题设置的,内容部分是在代码中体现的


    setContentView(R.layout.activity_main);

DecorView其实就是一个FrameLayout,View的事件都会经过它,然后再传递给子view

二、MeasureSpec

1.MeasureSpec是什么

MeasureSpec是一个32位的二进制数,高2位代表测量模式specMode,低30位代表测量大小specMode

整个类比较简单,源码如下


    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;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * <ul>
         *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
         *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
         *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
         * </ul>
         *
         * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec‘s
         * implementation was such that the order of arguments did not matter
         * and overflow in either value could impact the resulting MeasureSpec.
         * {@link android.widget.RelativeLayout} was affected by this bug.
         * Apps targeting API levels greater than 17 will get the fixed, more strict
         * behavior.</p>
         *
         * @param size the size of the measure specification
         * @param mode the mode of the measure specification
         * @return the measure specification based on size and mode
         */
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

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

        static int adjust(int measureSpec, int delta) {
            return makeMeasureSpec(getSize(measureSpec + delta), getMode(measureSpec));
        }

        /**
         * Returns a String representation of the specified measure
         * specification.
         *
         * @param measureSpec the measure specification to convert to a String
         * @return a String with the following format: "MeasureSpec: MODE SIZE"
         */
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }

其中比较重要的是specMode。

2.specMode

specMode有三个值分别是:

  1. AT_MOST:这个值表示父容器指定了可用大小,view的大小不可以大于该指定大小,这个对应于LayoutParams的wrap_content。这个模式处理起来稍麻烦点,因为需要我们自行测量。
  2. EXACTLY:父容器已经知道子view 的精确大小,这时候的view 的大小就是specSize。
  3. UNSPECIFIED:父容器不对view有任何限制,这个模式用的比较少。

3.LayoutParams

上面提到到AT_MOST的时候该模式对应于LayoutParams的wrap_content,那么说明LayoutParams也会影响view的测量的。在对View进行测量的时候,系统会将LayoutParams在父容器的约束下转换成MeasureSpec.

总的来说:对于普通View的MeasureSpec由父容器的MeasureSpec和LayoutParams决定,而对应顶级View来说,因为没有父容器,故由窗口的大小和自身LayoutParams决定。而一旦MeasureSpec确定,就可以对其测量了。这里提到MesureSpec是又父容器的MeasureSpec和LayoutParams决定的,就从源码看看。


        protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这是ViewGroup的measureChild方法,该方法调用了getChildMeasureSpec()方法,并且将父容器的MeasureSpec传递过去然后再返回子view的MeasureSpec。


        public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can‘t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can‘t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

从源码可以看出,首先通过父容器的MeasureSpec来switch判断,例如,如果父容器的MeasureSpec是EXACTLY,而自view的LayoutParams是WRAP_CONTENT,那么


             resultSize = size;
             resultMode = MeasureSpec.AT_MOST;

三、实例

理论说的比较多,上代码,


    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        tools:context=".MainActivity" >

         <com.lw.viewdemo.MyView
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:background="#000"
             />

    </LinearLayout>

注意:这里的父布局的宽度、高度我使用的是wrap_content,而MyView的宽度、高度使用的都是是match_parent。


    public class CircleView extends View {

        private int mColor = Color.BLUE ;
        private Paint mPaint = new Paint(Paint.DEV_KERN_TEXT_FLAG) ;
        public CircleView(Context context) {
            super(context);
            init();
        }

        public CircleView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
        private void init() {
            mPaint.setColor(mColor);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
             if (widthSpecMode == MeasureSpec.AT_MOST
                        && heightSpecMode == MeasureSpec.AT_MOST) {
                    setMeasuredDimension(200, 200);
                } else if (widthSpecMode == MeasureSpec.AT_MOST) {
                    setMeasuredDimension(200, heightSpecSize);
                } else if (heightSpecMode == MeasureSpec.AT_MOST) {
                    setMeasuredDimension(widthSpecSize, 200);
                }
        }

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int paddingLeft = getPaddingLeft();
            int paddingRight = getPaddingRight();
            int paddingBottom = getPaddingBottom();
            int paddingTop = getPaddingTop();
            int width = getWidth();
            width = width - paddingLeft - paddingRight ;
            int height = getHeight() ;
            height = height - paddingTop - paddingBottom;
            int radius = Math.min(width/2, height/2);
            canvas.drawCircle(paddingLeft + width / 2 , paddingTop + height / 2 , radius, mPaint);
        }

    }

在onMeasure()方法中判断子view的测量模式,如果模式为AT_MOST的话,我们需要自己手动为其指定高度和宽度,而根据前面我们分析getChildMeasureSpec()方法中可以知道,我们这里对应的是滴一个else if语句块。因此此时onDraw()画出来的是200dp。其实从源码可以看出,子父容器为AT_MOST的条件下,子view是WRAP_CONTENT和是MATCH_PARENT没什么区别。当然,若是父容器是EXACTLY的话,两者是有区别的。然后就是setMeasuredDimension()方法调用,这个方法之前在分析源码的时候看到过了,默认会调用这个方法的,作用就是设置宽度高度。


    // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can‘t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

由于代码比较简单,就不上传了,需要的留言。。。

OK,这篇简单的分析了自定义view的基本知识和一个小demo,下篇将会继续学习自定义View。

时间: 2024-10-27 06:05:54

自定义View(一)的相关文章

自定义View 篇三 《手动打造ViewPage》

有了之前自定义View的理论基础,有了ViewPage.事件分发机制.滑动冲突.Scroller使用等相关知识的铺垫,今天纯手动打造一款ViewPage. 1.完成基本的显示: 在MainActivity中: public class MainActivity extends AppCompatActivity { private MyViewPage mViewPage; int[] imageIds = new int[]{ R.drawable.pic_0, R.drawable.pic_

九点(九宫格)式手势解锁自定义view

周末闲着没事,写了个手势解锁的view,实现起来也蛮快的,半天多一点时间就完事.把源码和资源贴出来,给大家分享,希望对大家有用. 效果,就跟手机上的九点手势解锁一样,上个图吧: 过程嘛感觉确实没啥好讲的了,涉及的知识以前的博客都说过了,无非就是canva,paint,touch事件这些,画画圆圈画画线条,剩下的就是细节处理逻辑了.都在代码里,所以这里就主要是贴资源吧. 这个自定义view就一个类,源码如下: package com.cc.library.view; import android.

iOS开发——笔记篇&amp;关于字典plist读取/字典转模型/自定义View/MVC/Xib的使用/MJExtension使用总结

关于字典plist读取/字典转模型/自定义View/MVC/Xib的使用/MJExtension使用总结 一:Plist读取 1 /******************************************************************************/ 2 一:简单plist读取 3 4 1:定义一个数组用来保存读取出来的plist数据 5 @property (nonatomic, strong) NSArray *shops; 6 7 2:使用懒加载的方

自定义View之实现日出日落太阳动效

以前也很羡慕网上大神随手写写就是一个很漂亮的自定义控件,所以我下决心也要学着去写,刚好最近复习了Android View的绘制流程知识,看来看去就是那些个知识点,没点产出总感觉很迷.现在个人呢用的是华为荣耀8手机,碰巧在看自带的天气APP时,滑到最下面看到那个动效图:日出时间和日落时间上边是一个半圆,白天任意的时刻(在日出和日落时间之间)都有对应一个太阳从日出时刻沿着半圆弧做动画特效,个人第一感觉就是:就拿这个来练练手啦!于是拿着笔和纸,画了模型图,甚至求什么sin.cos函数,有点过分了哈,还

Android 自定义View

最近在看鸿洋大神的博客,在看到自定义部分View部分时,想到之前案子中经常会要用到"图片 + 文字"这类控件,如下图所示: 之前的做法是在布局文件中,将一个Image & TextView组件放在LinearLayout/RelativeLayout中.今天就尝试了通过自定义View的方式来实现"图片 + 文字"组件. 首先在/res/value/目录下新建attrs.xml文件,在该文件中为CustomTextView定义以下几个attr.分别为 1.文字

Android自定义View探索(一)—生命周期

Activity代码: public class FiveActivity extends AppCompatActivity { private MyView myView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.e("log", "Activity生命周期:onCreate"); setConte

Android自定义view之仿支付宝芝麻信用仪表盘

自定义view练习 仿支付宝芝麻信用的仪表盘 对比图: 首先是自定义一些属性,可自己再添加,挺基础的,上代码 <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="RoundIndicatorView"> <!--最大数值--> <attr name="maxNum" form

自定义view—折线图

学习导航 第一节:http://blog.csdn.net/bobo8945510/article/details/53197727 -自定义View-自定义属性及引用 第二节:http://blog.csdn.net/bobo8945510/article/details/53203233 自定义view02-图形绘制 第三节:http://blog.csdn.net/bobo8945510/article/details/53213938 自定义View-绘图基础之Path 第四节:http

Android笔记自定义View之制作表盘界面

前言 最近我跟自定义View杠上了,甚至说有点上瘾到走火入魔了.身为菜鸟的我自然要查阅大量的资料,学习大神们的代码,这不,前两天正好在郭神在微信公众号里推送一片自定义控件的文章--一步步实现精美的钟表界面.正适合我这种菜鸟来学习,闲着没事,我就差不多依葫芦画瓢也写了一个自定义表盘View,现在纯粹最为笔记记录下来.先展示下效果图: 下面进入正题 自定义表盘属性 老规矩,先在attrs文件里添加表盘自定义属性 <declare-styleable name="WatchView"&

Android 创建自定义 View 的属性 (attrs) 时需要注意的问题

自定义 View 的属性并不难,可以参照官方的文档 https://developer.android.com/training/custom-views/create-view.html 但是需要注意一个问题,否则可能浪费很多时间. <resources> <declare-styleable name="AppsControllerBlock"> <attr name="letterCase" format="enum&q