Android自定义View,你必须知道的几点

来源:互联网 发布:淘宝进货网一件代发 编辑:程序博客网 时间:2024/05/29 13:48

为什么我们觉得自定义View是学习Android的一道坎? 
为什么那么多Android大神却认为自定义View又是如此的简单? 
为什么google随便定义一个View都是上千行的代码? 
以上这些问题,相信学Android的同学或多或少都有过这样的疑问。 
那么,看完此文,希望对你们的疑惑有所帮助。

回到主题,自定义View ,需要掌握的几个点是什么呢? 
我们先把自定义View细分一下,分为两种 
1) 自定义ViewGroup 
2) 自定义View

其实ViewGroup最终还是继承之View,当然它内部做了许多操作;继承之ViewGroup的View我们一般称之为容器,而今天我们不讲这方面,后续有机会再讲。 
来看看自定义View 需要掌握的几点,主要就是两点

一、重写 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法。 
二、重写 protected void onDraw(Canvas canvas) {}方法

空讲理论很难理解,我们还得用例子来说明,记得我前面来写了一篇 Android 微信6.1 tab栏图标和字体颜色渐变的实现 的博客,里面tab的每个item就是通过自定义View来实现的,那么接下来就通过此例子来说明问题。

我们可以把View理解为一张白纸,而自定义View就是在这张白纸上画上我们自己绘制的图案,可以在绘制任何图案,也可以在白纸的任何位置绘制,那么问题来了,白纸哪里来?图案哪里来?位置如何计算?

a)白纸好说,只要我们继承之View,在onDraw(Canvas canvas)中的canvas就是我们所说的白纸

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<codeclass=" hljs java">/**
 * Created by moon.zhong on 2015/2/13.
 */
publicclass CustomView extendsView {
    publicCustomView(Context context) {
        super(context);
    }
 
    publicCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
 
    publicCustomView(Context context, AttributeSet attrs, intdefStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
 
    @Override
    protectedvoid onDraw(Canvas canvas) {
        // canvas 即为白纸
        super.onDraw(canvas);
    }
}</code>

b)图案呢?这里的图案就是有图片和文字组成,这个也好说,定义一个Bitmap 成员变量,和一个String的成员变量

?
1
2
3
4
<codeclass=" hljs cs">privateBitmap mBitmap ;
privateString mName ;
mName = "这里直接赋值";
mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher) ;</code>

图片可以通过资源文件可以拿到。

c)计算位置 
所以最核心的也是我们认为最麻烦的地方就是计算绘制的位置,计算位置就得先测量自身的大小,也就是我们必须掌握的两点中的第一点:需要重写 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法 
先来看一下google写的TextView的onMeasure()方法是如何实现的

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
<code>@Override
protectedvoid onMeasure(intwidthMeasureSpec, intheightMeasureSpec) {
    intwidthMode = MeasureSpec.getMode(widthMeasureSpec);
    intheightMode = MeasureSpec.getMode(heightMeasureSpec);
    intwidthSize = MeasureSpec.getSize(widthMeasureSpec);
    intheightSize = MeasureSpec.getSize(heightMeasureSpec);
 
    intwidth;
    intheight;
 
    BoringLayout.Metrics boring = UNKNOWN_BORING;
    BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
 
    if(mTextDir == null) {
        mTextDir = getTextDirectionHeuristic();
    }
 
    intdes = -1;
    booleanfromexisting = false;
 
    if(widthMode == MeasureSpec.EXACTLY) {
        // Parent has told us how big to be. So be it.
        width = widthSize;
    }else{
        if(mLayout != null&& mEllipsize == null) {
            des = desired(mLayout);
        }
 
        if(des < 0) {
            boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
            if(boring != null) {
                mBoring = boring;
            }
        }else{
            fromexisting = true;
        }
 
        if(boring == null|| boring == UNKNOWN_BORING) {
            if(des < 0) {
                des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
            }
            width = des;
        }else{
            width = boring.width;
        }
 
        finalDrawables dr = mDrawables;
        if(dr != null) {
            width = Math.max(width, dr.mDrawableWidthTop);
            width = Math.max(width, dr.mDrawableWidthBottom);
        }
 
        if(mHint != null) {
            inthintDes = -1;
            inthintWidth;
 
            if(mHintLayout != null&& mEllipsize == null) {
                hintDes = desired(mHintLayout);
            }
 
            if(hintDes < 0) {
                hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
                if(hintBoring != null) {
                    mHintBoring = hintBoring;
                }
            }
 
            if(hintBoring == null|| hintBoring == UNKNOWN_BORING) {
                if(hintDes < 0) {
                    hintDes = (int) FloatMath.ceil(Layout.getDesiredWidth(mHint, mTextPaint));
                }
                hintWidth = hintDes;
            }else{
                hintWidth = hintBoring.width;
            }
 
            if(hintWidth > width) {
                width = hintWidth;
            }
        }
 
        width += getCompoundPaddingLeft() + getCompoundPaddingRight();
 
        if(mMaxWidthMode == EMS) {
            width = Math.min(width, mMaxWidth * getLineHeight());
        }else{
            width = Math.min(width, mMaxWidth);
        }
 
        if(mMinWidthMode == EMS) {
            width = Math.max(width, mMinWidth * getLineHeight());
        }else{
            width = Math.max(width, mMinWidth);
        }
 
        // Check against our minimum width
        width = Math.max(width, getSuggestedMinimumWidth());
 
        if(widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(widthSize, width);
        }
    }
 
    intwant = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
    intunpaddedWidth = want;
 
    if(mHorizontallyScrolling) want = VERY_WIDE;
 
    inthintWant = want;
    inthintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
 
    if(mLayout == null) {
        makeNewLayout(want, hintWant, boring, hintBoring,
                      width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
    }else{
        finalboolean layoutChanged = (mLayout.getWidth() != want) ||
                (hintWidth != hintWant) ||
                (mLayout.getEllipsizedWidth() !=
                        width - getCompoundPaddingLeft() - getCompoundPaddingRight());
 
        finalboolean widthChanged = (mHint == null) &&
                (mEllipsize == null) &&
                (want > mLayout.getWidth()) &&
                (mLayoutinstanceofBoringLayout || (fromexisting && des >= 0&& des <= want));
 
        finalboolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
 
        if(layoutChanged || maximumChanged) {
            if(!maximumChanged && widthChanged) {
                mLayout.increaseWidthTo(want);
            }else{
                makeNewLayout(want, hintWant, boring, hintBoring,
                        width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
            }
        }else{
            // Nothing has changed
        }
    }
 
    if(heightMode == MeasureSpec.EXACTLY) {
        // Parent has told us how big to be. So be it.
        height = heightSize;
        mDesiredHeightAtMeasure = -1;
    }else{
        intdesired = getDesiredHeight();
 
        height = desired;
        mDesiredHeightAtMeasure = desired;
 
        if(heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(desired, heightSize);
        }
    }
 
    intunpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
    if(mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
        unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
    }
 
    /*
     * We didn't let makeNewLayout() register to bring the cursor into view,
     * so do it here if there is any possibility that it is needed.
     */
    if(mMovement != null||
        mLayout.getWidth() > unpaddedWidth ||
        mLayout.getHeight() > unpaddedHeight) {
        registerForPreDraw();
    }else{
        scrollTo(0,0);
    }
 
    setMeasuredDimension(width, height);
}
</code>
?
1
2
3
4
<codeclass=" hljs parser3">
哇!好长!而且方法中还嵌套方法,如果真要算下来,代码量不会低于500行,看到这么多代码,头都大了,我想这也是我们为什么在学习Android自定义View的时候觉得如此困难的原因。大多数情况下,因为我们是自定义的View,可以说是根据我们的需求定制的View,所以很多里面的功能我们完全没必要,只需要几十行代码就能搞定。看到几十行代码就能搞定,感觉顿时信心倍增(^.^)
在重写这个方法之前,得先了解一个类 MeasureSpec ,如果不了解,没关系,下面就一起来了解一下这个类。先把代码贴出来,膜拜一下
</code>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<code>publicstatic class MeasureSpec {
    privatestatic final int MODE_SHIFT = 30;
    privatestatic final int MODE_MASK  = 0x3<< MODE_SHIFT;
    publicstatic final int UNSPECIFIED = 0<< MODE_SHIFT;
    publicstatic final int EXACTLY     = 1<< MODE_SHIFT;
    publicstatic final int AT_MOST     = 2<< MODE_SHIFT;
    publicstatic int makeMeasureSpec(intsize, intmode) {
        if(sUseBrokenMakeMeasureSpec) {
            returnsize + mode;
        }else{
            return(size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    publicstatic int getMode(intmeasureSpec) {
        return(measureSpec & MODE_MASK);
    }
    publicstatic int getSize(intmeasureSpec) {
        return(measureSpec & ~MODE_MASK);
    }
}
</code>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<codeclass=" hljs java">
这里我把里面一些我认为没必要的代码都去掉了,只留了以上几行代码,这样看起来很清晰,也非常容易理解。
我们先做个转化,把上面几个成员变量转化成二进制
 
这个就不需要转化了,这里代表的只是一个移动的位置,也就是一个单纯的数字
privatestatic final int MODE_SHIFT = 30;
0x3就是 11左移30位 ,就是补300
privatestatic final int MODE_MASK  = 11000000 0000 0000 0000 0000 0000 0000 ;
00左移30
publicstatic final int UNSPECIFIED = 00000000 0000 0000 0000 0000 0000 0000 ;
01左移30
publicstatic final int EXACTLY     = 01000000 0000 0000 0000 0000 0000 0000 ;
10左移30
publicstatic final int AT_MOST     = 10000000 0000 0000 0000 0000 0000 0000 ;
 
你就会问了,这样写有什么好处呢? 细心的人看了上面这几个方法就明白了,每个方法中都有一个 & 的操作,所以我们接下来看看这集几个方法的含义是什么,先从下往上看,先易后难
 1、      publicstatic int getSize(intmeasureSpec) {
            return(measureSpec & ~MODE_MASK);
        }
 顾名思义,通过measureSpec这个参数,获取size ,两个都是int类型,怎么通过一个int类型的数获取另一个int类型的数。我们在学习java的时候知道,一个int类型是32位,任何int类型的数都是有32位,比如一个int类型的数值3,它也是占有32位,只是高30位全部为0。google 也是利用这一点,让这个int类型的measureSpec数存了两个信息,一个就是size,保存在int类型的低30位,另一个就是mode,保存在int类型的高2位。前面我们看到了有几个成员变量,UNSPECIFIED,EXACTLY,AT_MOST
 者就是mode的三种选择,目前也只有这三种选择,所以只需要2位就能实现。
 2、      ` publicstatic int getMode(intmeasureSpec) {
                return(measureSpec & MODE_MASK);
        }`
  这也好理解,获取模式,但这些模式有啥用处呢?
  1)、EXACTLY 模式: 准确的、精确的;这种模式,是最容易理解和处理的,可以理解为大小固定,比如在定义layout_width的时候,定义为固定大小 10dp,20dp,或者match_parent(此时父控件是固定的)这时候,获取出来的mode就是EXACTLY
  2)、AT_MOST 模式: 最大的;这种模式稍微难处理些,不过也好理解,就是View的大小最大不能超过父控件,超过了,取父控件的大小,没有,则取自身大小,这种情况一般都是在layout_width设为warp_content时。
  3)、UNSPECIFIED 模式:不指定大小,这种情况,我们几乎用不上,它是什么意思呢,就是View的大小想要多大,就给多大,不受父View的限制,几个例子就好理解了,ScrollView控件就是。
 
  3、        `publicstatic int makeMeasureSpec(intsize, intmode) {
              if(sUseBrokenMakeMeasureSpec) {
                  returnsize + mode;
              }else{
                  return(size & ~MODE_MASK) | (mode & MODE_MASK);
              }
          }`
  这个方法也好理解,封装measureSpec的值,在定义一个View的大小时,我们只是固定了大小,你下次想要获取mode的时候,肯定无法拿到,所以就得自己把模式添加进去,这个方法,在自定义View中,也基本不需要用到,他所使用的场所,是在设置子View的大小的时候需要用到,所以如果是自定义ViewGroup的话,就需要用到。
 
  感觉讲了这么多,还是不知道怎么使用,接下来就来重写onMeasure()方法,写完之后,你就明白了,这里把注解下载代码里头。
</code>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<code> @Override
  protectedvoid onMeasure(intwidthMeasureSpec, intheightMeasureSpec) {
     //这里方法套路都是一样,不管三七 二十一,上来就先把mode 和 size 获取出来。
 
      intwidthMode = MeasureSpec.getMode(widthMeasureSpec);
      intheightMode = MeasureSpec.getMode(heightMeasureSpec);
      intwidthSize = MeasureSpec.getSize(widthMeasureSpec);
      intheightSize = MeasureSpec.getSize(heightMeasureSpec);
      //View 真正需要显示的大小
      intwidth = 0, height = 0;
      //这里是去测量字体大小
      measureText();
      //字体宽度加图片宽度取最大宽度,这里因为字体和图片是上下排列
      intcontentWidth = Math.max(mBoundText.width(), mIconNormal.getWidth());
     // 我们渴望得到的宽度
      intdesiredWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
      //重点来了,判断模式,这个模式哪里来的呢,就是在编写xml的时候,设置的layout_width
      switch(widthMode) {
      //如果是AT_MOST,不能超过父View的宽度
          caseMeasureSpec.AT_MOST:
              width = Math.min(widthSize, desiredWidth);
              break;
              //如果是精确的,好说,是多少,就给多少;
          caseMeasureSpec.EXACTLY:
              width = widthSize;
              break;
              //这种情况,纯属在这里打酱油的,可以不考虑
          caseMeasureSpec.UNSPECIFIED://我是路过的
              width = desiredWidth;
              break;
      }
      intcontentHeight = mBoundText.height() + mIconNormal.getHeight();
      intdesiredHeight = getPaddingTop() + getPaddingBottom() + contentHeight;
      switch(heightMode) {
          caseMeasureSpec.AT_MOST:
              height = Math.min(heightSize, desiredHeight);
              break;
          caseMeasureSpec.EXACTLY:
              height = heightSize;
              break;
          caseMeasureSpec.UNSPECIFIED:
              height = contentHeight;
              break;
      }
      //最后不要忘记了,调用父类的测量方法
      setMeasuredDimension(width, height);
  }
</code>
?
1
2
3
4
5
6
<codeclass=" hljs vbscript">
  到这里,就算View的大小就已经完成了,自定义View的计算过程和以上方法基本类似。接着就是计算需要显示的图标和字体的位置。这里希望图片和字体垂直排列,并居中显示在View当中,因为当前的View的宽高已经测量好了,接下来的计算也就非常简单了,这里就放在onDraw()方法中计算
 
 
d)绘制图标和字体
绘制图标,可以用canvas.drawBitmap(Bitmap bitmap, intleft, inttop ,Paint paint)方法,bitmap 已经有了,如果不需要对图片作特殊处理 paint 可以传入null表示原图原样的绘制在白纸上,所以就差绘制的位置 left ,top前面已经分析过了,需要把图绘制在View的中间,当然这里还需包含字体,所以可以这样计算left 和top。</code>

int left = (mViewWidth - mIconNormal.getWidth())/2 ; 
int top = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /2 ;

?
1
2
<codeclass=" hljs coffeescript">
mViewWidth --->View的宽度,mIconNormal --->图片的宽度, mBoundText.height() --->字体的高度;绘制字体,绘制字体,就比绘制图片稍微麻烦点,因为绘制字体需要用到画笔Paint ,这里定义一个画笔Paint,直接new一个出来</code>
?
1
2
3
4
5
6
7
8
<code>    mTextPaintNormal = newPaint();
    //设置字体大小
    mTextPaintNormal.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mTextSize, getResources().getDisplayMetrics()));
    //设置画笔颜色,也就是字体颜色
    mTextPaintNormal.setColor(mTextColorNormal);
    //设置抗锯齿
    mTextPaintNormal.setAntiAlias(true);
</code>
?
1
<codeclass=" hljs avrasm">        这里也是调用Canvas的方法 canvas.drawText(mTextValue,x,y, mTextPaintNormal);mTextValue需要绘制的字体内容, mTextPaintNormal画笔,x,y需要绘制的位置</code>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<code>   floatx = (mViewWidth - mBoundText.width())/2.0f ;
    floaty = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /2.0F ;
 
    整体来说代码还是相当少的。下面把onDraw的代码也贴出来
    @Override
    protectedvoid onDraw(Canvas canvas) {
        drawBitmap(canvas) ;
        drawText(canvas) ;
    }
    privatevoid drawBitmap(Canvas canvas) {
        intleft = (mViewWidth - mIconNormal.getWidth())/2;
        inttop = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /2;
        canvas.drawBitmap(mIconNormal, left, top ,null);
    }
    privatevoid drawText(Canvas canvas) {
        floatx = (mViewWidth - mBoundText.width())/2.0f ;
        floaty = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /2.0F ;
        canvas.drawText(mTextValue,x,y, mTextPaintNormal);
    }
</code>

“` 
总结: 
onMeasure() 方法只要了解了 MeasureSpec 类就不是什么问题,而MeasureSpec 也很简单,onDraw() 方法就需要了解Canvas 类的绘制方法,并且通过简单的Api查询,就基本能实现我们所需的要求。对于自定义View,如果你会重写 测量 和 onDraw 方法,那么就具备了此技能,而如果需要了解更深,自定义有个性,更绚丽的View,就还得深入了解Canvas 、Paint等方法,

0 0
原创粉丝点击