学习资料
- Android开发艺术探索
- 爱哥的自定义控件系列
在整个系列的第一篇Android自定义View学习(一)——准备中,简单学习来了测量涉及到的onMeasure()
方法,本篇进行补充学习。由于目前水平还很低,测量过程涉及到的源码没有能力去分析,只是记录一下学习怎么调用方法,很多地方都是"知其然而不知其所以然"
,不能对调用的方法本身原理进行说明。爱哥是从源码进行分析,写的非常好,可以去深入学习爱哥的博客
1. onMeasure() 测量方法
在之前的学习中,很多地方都用到了这个方法,而且几乎每次使用,代码都是不变的,套路是固定的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (wSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, hSpecSize);
} else if (hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSpecSize, 300);
}
}
偶尔做出的改动便是300
这个数值
这个方法内,主要是对于MeasureSpec.AT_MOST
这个模式,也就是针对在布局xml
文件控件的宽高写wrap_content
时的处理。无论是MeasureSpec.EXACTLY(match_parent)
还是MeasureSpec.AT_MOST
,这两种模式都是根据当前控件以及所在的父控件大小共同来确定的。
如果不进行重写这个方法,直接引用这个当前的自定义控件,无论是wrap_content
还是match_parent
都会沾满全屏,原因下面尝试分析
1.1 尝试分析原因
在一个Activity
中,在onCreate()
方法中,都会调用setContentView(@LayoutRes int layoutResID)
这样一个方法,layoutResId
就是Activity
的xml
布局文件。
例如,在MainActivity
中,setContentView(R.layout.main_activity)
。在R.layout.main_activity
中,最外层的布局往往会是一个LinearLayout
(或者RelativeLayout
)。之前认为这个最外层的布局就是一个MainActivity
的根布局,而对应的LinearLayout
(或者RelativeLayout
)便是父容器。但根据上面的图可以知道,MainActivity
的最外层布局是添加到了ContentView
中,这个ContentView
可以通过LinearLayout.getParent()
获得,返回的结果是一个id
为android:id="@android:id/content"
的FramLayout
,这个android:id="@android:id/content
才是Activity
的根布局,R.layout.main_activity
包含在其中
除了ContentView
外,手机屏幕上展示的还有ContentView
上面的TitleView
,TitleView
便是ActionBar
或者5.0
后ToolBar
。也就是说,屏幕真正可见的根View
是DecorView
。
每个Activity
都包含有一个Window
对象,通常是PhoneWindow
。当Activity
在onCreate()
方法中调用了setContentView()
方法后,ActivityManagerService
会回调onResume()
方法,系统会把整个DecorView
添加进PhoneWindow
中
上面大概叙述了一下UI界面架构图
的关系。DecorView
的大小,无论什么时候,默认都是全屏的。而DecorVew
包含两部分,TitleView
和ContentView
,除去TitleView
,屏幕可见的就是ContentView
。ContentView
包含了Activity
的xml
布局文件中最外层Layout
,自定义View
则是在这个Layout
中。而整个Activity
的绘制过程是从最外层的DecorView
开始的,由外向内。最终确定自定义View
的大小,需要根据由MeasureSpec.getMode(widthMeasureSpec)
得到的SpecMode
当做判断依据来进行确定
- AT_MOST 最大值模式,
对应LayoutParams.WARP_CONTENT
。由父容器指定了一个可用大小SpecSize
,自定义View
的大小不能超过这个SpecSize
值,而这个SpecSize
也不会超过窗口的值 - EXACTLY 精准模式
对应LayoutParams.MATCH_PARENT
或者固定大小值。父容器已经检测出View
所需大小的值SpecSize
,最终大小也就是窗口的大小或者固定值。
一开始,很不理解
warp_content
,怎么就对应的是最大值模式,而平常经常用到这个模式的TextView
或者Button
中,理解为包裹内容
。那是因为在TextView
中,都已经做了针对处理,之后使得这个模式才具备包裹内容
的特性 。这个wrap_content
模式本身并不是已经具备包裹内容
的特性
如果不对上面的onMeasure()
方法进行重写,无论是warp_content
还是match_parent
,SpecSzie
的大小都是窗口的大小,也就是自定义View
所在的布局大小,而布局的宽高一般默认是match_parent
,显示在屏幕上就是DecorView
中的ContentView
的大小。
重写了onMeasure()
方法,在AT_MOST
也就是wrap_content
时,确定了300
,也就是父容器给指定了SpecSize
为300px
不晓得整个叙述清不清楚,回头以后自己还能不能看明白。。。
虽然指定了wrap_content
的大小,但还会有问题,padding
并未处理
2. 处理Padding
先根据上面的onMeasure()
方法,创建一个针对可以处理warp_content
过的PaddingView
代码很简单,就是绘制一个Bitmap
,以Bitmap
的宽高作为wrap_content
最大值
public class PaddingView extends View {
private Paint mPaint;
public PaddingView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化
*/
private void init() {
//画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#FF4081"));
}
/**
* 绘制
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制底色 辅助用
canvas.drawColor(Color.CYAN);
//绘制Bitmap
final float left = 0;
final float top = 0;
canvas.drawBitmap(mBitmap, left, top, null);
}
/**
* 测量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidth = wSpecSize;
int resultHeight = hSpecSize;
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth();
resultHeight = mBitmap.getHeight();
} else if (wSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth();
resultHeight = hSpecSize;
} else if (hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = wSpecSize;
resultHeight = mBitmap.getHeight();
}
// 取Bitmap宽高和窗口宽高的较小的一个
resultWidth = Math.min(resultWidth, wSpecSize);
resultHeight = Math.min(resultHeight, hSpecSize);
setMeasuredDimension(resultWidth, resultHeight);
}
}
布局文件:
测试的图片有点巧合,宽度和手机屏幕差不多
简单修改代码, 在布局文件加入android:padding="50dp"
,发现并没有效果。因为测量的时候,并没有加入padding
的大小的考虑。
对onMeasure()
和onDraw()
进行简单修改:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidth = wSpecSize;
int resultHeight = hSpecSize;
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
} else if (wSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = hSpecSize;
} else if (hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = wSpecSize;
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
}
// 取Bitmap宽高和窗口宽高的较小的一个
resultWidth = Math.min(resultWidth, wSpecSize);
resultHeight = Math.min(resultHeight, hSpecSize);
setMeasuredDimension(resultWidth, resultHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制底色 辅助用
canvas.drawColor(Color.CYAN);
//绘制Bitmap
final float left = getPaddingLeft();
final float top = getPaddingTop();
canvas.drawBitmap(mBitmap, left, top, null);
}
padding
的效果是有了,可照片太大了,控件resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight()
的大小,超过了手机屏幕宽度,导致控件内容显示不全。可以考虑使用Matrix
进行优化
2.1 使用Matrix进行优化
主要是利用Matrix.setRectToRect()
方法,不了解的同学,可以去Android 自定义View学习(八)——Matrix知识学习看一下 : )
public class PaddingView extends View {
private Bitmap mBitmap;
private Matrix mMatrix ;
private RectF srcRectF;
private RectF dstRectF;
public PaddingView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化
*/
private void init() {
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
mMatrix = new Matrix();
//截取用来显示的Bitmap有效区域
srcRectF = new RectF(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//显示Bitmap的底板
dstRectF = new RectF();
}
/**
* 绘制
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制底色 辅助用
canvas.drawColor(Color.CYAN);
//绘制Bitmap
final float left = getPaddingLeft();
final float top = getPaddingTop();
//设置Bitmap的缩放模式
mMatrix.setRectToRect(srcRectF,dstRectF, Matrix.ScaleToFit.FILL);
//利用后乘平移对开始绘制位置进行改变 这里不可以使用setTranslate()
mMatrix.postTranslate(left,top);
canvas.drawBitmap(mBitmap, mMatrix, null);
}
/**
* 测量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//将最终结果宽高设置为窗口大小 这样省的判断MeasureSpec.EXACTLY模式的情况
int resultWidth = wSpecSize;
int resultHeight = hSpecSize;
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
} else if (wSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = hSpecSize;
} else if (hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = wSpecSize;
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
}
//取Bitmap宽高和窗口宽高的较小的一个
resultWidth = Math.min(resultWidth, wSpecSize);
resultHeight = Math.min(resultHeight, hSpecSize);
//确定dstRectF的范围
dstRectF.left= dstRectF.top = 0;
dstRectF.right = resultWidth-getPaddingRight()-getPaddingLeft();
dstRectF.bottom = resultHeight-getPaddingBottom()-getPaddingTop();
setMeasuredDimension(resultWidth, resultHeight);
}
}
虽然图片有些变形,这样无论多大的图片都可以填充满整个控件。有点类似ImageView
的android:scaleType="centerCrop"
这个属性,当然想要完全实现这个属性的特点,还需要对srcRectF
进行优化,截取Bitmap
的中间正方形部分来显示就可以了
Margin
外边距,封装在LayoutParams内交由父容器统一处理
3. 最后
主要是学习1.1
原因分析那里。理解UI界面架构图
,这个图看起来并不复杂,但涉及的东西却非常多。算是先了解一下View
的工作原理,View
最复杂的过程就是测量。
本人很菜,有错误,请指出
共勉 : )