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就是我们所说的白纸
<code
class
=
" hljs java"
>
/**
* Created by moon.zhong on 2015/2/13.
*/
public
class
CustomView
extends
View {
public
CustomView(Context context) {
super
(context);
}
public
CustomView(Context context, AttributeSet attrs) {
super
(context, attrs);
}
public
CustomView(Context context, AttributeSet attrs,
int
defStyleAttr) {
super
(context, attrs, defStyleAttr);
}
@Override
protected
void
onDraw(Canvas canvas) {
// canvas 即为白纸
super
.onDraw(canvas);
}
}</code>
b)图案呢?这里的图案就是有图片和文字组成,这个也好说,定义一个Bitmap 成员变量,和一个String的成员变量
<code
class
=
" hljs cs"
>
private
Bitmap mBitmap ;
private
String mName ;
mName =
"这里直接赋值"
;
mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher) ;</code>
图片可以通过资源文件可以拿到。
c)计算位置
所以最核心的也是我们认为最麻烦的地方就是计算绘制的位置,计算位置就得先测量自身的大小,也就是我们必须掌握的两点中的第一点:需要重写 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}方法
先来看一下google写的TextView的onMeasure()方法是如何实现的
<code>
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
int
widthMode = MeasureSpec.getMode(widthMeasureSpec);
int
heightMode = MeasureSpec.getMode(heightMeasureSpec);
int
widthSize = MeasureSpec.getSize(widthMeasureSpec);
int
heightSize = MeasureSpec.getSize(heightMeasureSpec);
int
width;
int
height;
BoringLayout.Metrics boring = UNKNOWN_BORING;
BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
if
(mTextDir ==
null
) {
mTextDir = getTextDirectionHeuristic();
}
int
des = -
1
;
boolean
fromexisting =
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;
}
final
Drawables dr = mDrawables;
if
(dr !=
null
) {
width = Math.max(width, dr.mDrawableWidthTop);
width = Math.max(width, dr.mDrawableWidthBottom);
}
if
(mHint !=
null
) {
int
hintDes = -
1
;
int
hintWidth;
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);
}
}
int
want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
int
unpaddedWidth = want;
if
(mHorizontallyScrolling) want = VERY_WIDE;
int
hintWant = want;
int
hintWidth = (mHintLayout ==
null
) ? hintWant : mHintLayout.getWidth();
if
(mLayout ==
null
) {
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(),
false
);
}
else
{
final
boolean
layoutChanged = (mLayout.getWidth() != want) ||
(hintWidth != hintWant) ||
(mLayout.getEllipsizedWidth() !=
width - getCompoundPaddingLeft() - getCompoundPaddingRight());
final
boolean
widthChanged = (mHint ==
null
) &&
(mEllipsize ==
null
) &&
(want > mLayout.getWidth()) &&
(mLayout
instanceof
BoringLayout || (fromexisting && des >=
0
&& des <= want));
final
boolean
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
{
int
desired = getDesiredHeight();
height = desired;
mDesiredHeightAtMeasure = desired;
if
(heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
}
int
unpaddedHeight = 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>
<code
class
=
" hljs parser3"
>
哇!好长!而且方法中还嵌套方法,如果真要算下来,代码量不会低于
500
行,看到这么多代码,头都大了,我想这也是我们为什么在学习Android自定义View的时候觉得如此困难的原因。大多数情况下,因为我们是自定义的View,可以说是根据我们的需求定制的View,所以很多里面的功能我们完全没必要,只需要几十行代码就能搞定。看到几十行代码就能搞定,感觉顿时信心倍增(^.^)
在重写这个方法之前,得先了解一个类 MeasureSpec ,如果不了解,没关系,下面就一起来了解一下这个类。先把代码贴出来,膜拜一下
</code>
<code>
public
static
class
MeasureSpec {
private
static
final
int
MODE_SHIFT =
30
;
private
static
final
int
MODE_MASK =
0x3
<< MODE_SHIFT;
public
static
final
int
UNSPECIFIED =
0
<< MODE_SHIFT;
public
static
final
int
EXACTLY =
1
<< MODE_SHIFT;
public
static
final
int
AT_MOST =
2
<< MODE_SHIFT;
public
static
int
makeMeasureSpec(
int
size,
int
mode) {
if
(sUseBrokenMakeMeasureSpec) {
return
size + mode;
}
else
{
return
(size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public
static
int
getMode(
int
measureSpec) {
return
(measureSpec & MODE_MASK);
}
public
static
int
getSize(
int
measureSpec) {
return
(measureSpec & ~MODE_MASK);
}
}
</code>
<code
class
=
" hljs java"
>
这里我把里面一些我认为没必要的代码都去掉了,只留了以上几行代码,这样看起来很清晰,也非常容易理解。
我们先做个转化,把上面几个成员变量转化成二进制
这个就不需要转化了,这里代表的只是一个移动的位置,也就是一个单纯的数字
private
static
final
int
MODE_SHIFT =
30
;
0x3
就是
11
左移
30
位 ,就是补
30
个
0
;
private
static
final
int
MODE_MASK =
1100
0000
0000
0000
0000
0000
0000
0000
;
00
左移
30
位
public
static
final
int
UNSPECIFIED =
0000
0000
0000
0000
0000
0000
0000
0000
;
01
左移
30
位
public
static
final
int
EXACTLY =
0100
0000
0000
0000
0000
0000
0000
0000
;
10
左移
30
位
public
static
final
int
AT_MOST =
1000
0000
0000
0000
0000
0000
0000
0000
;
你就会问了,这样写有什么好处呢? 细心的人看了上面这几个方法就明白了,每个方法中都有一个 & 的操作,所以我们接下来看看这集几个方法的含义是什么,先从下往上看,先易后难
1
、
public
static
int
getSize(
int
measureSpec) {
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
、 `
public
static
int
getMode(
int
measureSpec) {
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
、 `
public
static
int
makeMeasureSpec(
int
size,
int
mode) {
if
(sUseBrokenMakeMeasureSpec) {
return
size + mode;
}
else
{
return
(size & ~MODE_MASK) | (mode & MODE_MASK);
}
}`
这个方法也好理解,封装measureSpec的值,在定义一个View的大小时,我们只是固定了大小,你下次想要获取mode的时候,肯定无法拿到,所以就得自己把模式添加进去,这个方法,在自定义View中,也基本不需要用到,他所使用的场所,是在设置子View的大小的时候需要用到,所以如果是自定义ViewGroup的话,就需要用到。
感觉讲了这么多,还是不知道怎么使用,接下来就来重写onMeasure()方法,写完之后,你就明白了,这里把注解下载代码里头。
</code>
<code>
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
//这里方法套路都是一样,不管三七 二十一,上来就先把mode 和 size 获取出来。
int
widthMode = MeasureSpec.getMode(widthMeasureSpec);
int
heightMode = MeasureSpec.getMode(heightMeasureSpec);
int
widthSize = MeasureSpec.getSize(widthMeasureSpec);
int
heightSize = MeasureSpec.getSize(heightMeasureSpec);
//View 真正需要显示的大小
int
width =
0
, height =
0
;
//这里是去测量字体大小
measureText();
//字体宽度加图片宽度取最大宽度,这里因为字体和图片是上下排列
int
contentWidth = Math.max(mBoundText.width(), mIconNormal.getWidth());
// 我们渴望得到的宽度
int
desiredWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
//重点来了,判断模式,这个模式哪里来的呢,就是在编写xml的时候,设置的layout_width
switch
(widthMode) {
//如果是AT_MOST,不能超过父View的宽度
case
MeasureSpec.AT_MOST:
width = Math.min(widthSize, desiredWidth);
break
;
//如果是精确的,好说,是多少,就给多少;
case
MeasureSpec.EXACTLY:
width = widthSize;
break
;
//这种情况,纯属在这里打酱油的,可以不考虑
case
MeasureSpec.UNSPECIFIED:
//我是路过的
width = desiredWidth;
break
;
}
int
contentHeight = mBoundText.height() + mIconNormal.getHeight();
int
desiredHeight = getPaddingTop() + getPaddingBottom() + contentHeight;
switch
(heightMode) {
case
MeasureSpec.AT_MOST:
height = Math.min(heightSize, desiredHeight);
break
;
case
MeasureSpec.EXACTLY:
height = heightSize;
break
;
case
MeasureSpec.UNSPECIFIED:
height = contentHeight;
break
;
}
//最后不要忘记了,调用父类的测量方法
setMeasuredDimension(width, height);
}
</code>
<code
class
=
" hljs vbscript"
>
到这里,就算View的大小就已经完成了,自定义View的计算过程和以上方法基本类似。接着就是计算需要显示的图标和字体的位置。这里希望图片和字体垂直排列,并居中显示在View当中,因为当前的View的宽高已经测量好了,接下来的计算也就非常简单了,这里就放在onDraw()方法中计算
d)绘制图标和字体
绘制图标,可以用canvas.drawBitmap(Bitmap bitmap,
int
left,
int
top ,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 ;
<code
class
=
" hljs coffeescript"
>
mViewWidth --->View的宽度,mIconNormal --->图片的宽度, mBoundText.height() --->字体的高度;绘制字体,绘制字体,就比绘制图片稍微麻烦点,因为绘制字体需要用到画笔Paint ,这里定义一个画笔Paint,直接
new
一个出来</code>
<code> mTextPaintNormal =
new
Paint();
//设置字体大小
mTextPaintNormal.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mTextSize, getResources().getDisplayMetrics()));
//设置画笔颜色,也就是字体颜色
mTextPaintNormal.setColor(mTextColorNormal);
//设置抗锯齿
mTextPaintNormal.setAntiAlias(
true
);
</code>
<code
class
=
" hljs avrasm"
> 这里也是调用Canvas的方法 canvas.drawText(mTextValue,x,y, mTextPaintNormal);mTextValue需要绘制的字体内容, mTextPaintNormal画笔,x,y需要绘制的位置</code>
<code>
float
x = (mViewWidth - mBoundText.width())/
2
.0f ;
float
y = (mViewHeight + mIconNormal.getHeight() + mBoundText.height()) /
2
.0F ;
整体来说代码还是相当少的。下面把onDraw的代码也贴出来
@Override
protected
void
onDraw(Canvas canvas) {
drawBitmap(canvas) ;
drawText(canvas) ;
}
private
void
drawBitmap(Canvas canvas) {
int
left = (mViewWidth - mIconNormal.getWidth())/
2
;
int
top = (mViewHeight - mIconNormal.getHeight() - mBoundText.height()) /
2
;
canvas.drawBitmap(mIconNormal, left, top ,
null
);
}
private
void
drawText(Canvas canvas) {
float
x = (mViewWidth - mBoundText.width())/
2
.0f ;
float
y = (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等方法,
- Android自定义View,你必须知道的几点
- Android自定义View,你必须知道的几点 入门
- Android自定义View,你必须知道的几点
- Android自定义View,你必须知道的几点
- Android 自定义View 需要知道的几点
- Android自定义view 必须知道的 Android View绘制流程
- DEDECMS安装你必须知道的几点
- 使用C风格字符串你必须知道的几点
- 机器学习你必须知道的几点知识
- 作为程序员你必须知道的几点
- php文件上传你必须知道的几点
- 关于dhcp服务你必须知道的几点
- Android 你不知道的自定义View(一)
- Android自定义View你所要知道的(一):坐标系
- android Application 必须知道的7点
- 自定义view你需要知道的
- 高考填报志愿必须知道的几点知识
- 学习Java类必须知道的几点
- C#高亮关键字
- 封装概念
- 如何用SED批量 查找、添加、删除、替换配置文件里面的选项
- 构造方法
- 关于String的解析
- Android自定义View,你必须知道的几点
- C++拷贝构造函数详解
- 挂掉的行程
- C++ 的构造/析构/赋值/拷贝函数比较
- 看图知“财经热点”,和讯“词云新闻”上线内测!
- map 泛型
- leetcode 280: Wiggle Sort
- swift语言基础:UIView视图,CGRect,UIColor
- Cocos2d-x学习(一) Cocos2d-x2.2.6安装前准备