View的生命周期
View分类
分为继承View与ViewGroup两种。
View的流程
构造函数
绘制初始化,进行各种计算,设定默认值或做任何我们需要的事情。
为了使我们的View更易于使用和配置,Android提供了很有用的AttributeSet接口。它很容易实现,通过静态参数来设置View,对于以后新特性加入或者新屏幕拓展性支持也更好。
- 首先,创建一个新的文件attrs.xml,自定义View属性都可以放在该文件中。
<resources>
<declare-styleable name="MyView">
<attr name="exampleString" format="string"/>
<attr name="exampleDimension" format="dimension"/>
<attr name="exampleColor" format="color"/>
<attr name="exampleDrawable" format="color|reference"/>
</declare-styleable>
</resources>
- 引入到xml中
<FrameLayout
<--引入命名空间-->
xmlns:app="http://schemas.android.com/apk/res-auto">
<xxxx.MyView
app:exampleColor="#33b5e5"
app:exampleDimension="24sp"
app:exampleDrawable="@android:drawable/ic_menu_add"
app:exampleString="Hello, MyView"/>
</FrameLayout>
- 在代码中调用
final TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.MyView, defStyle, 0);
- 提取属性
例如:
mExampleString = a.getString( R.styleable.MyView_exampleString);
mExampleColor = a.getColor(R.styleable.MyView_exampleColor,mExampleColor);
// Use getDimensionPixelSize or getDimensionPixelOffset when dealing with
// values that should fall on pixel boundaries.
mExampleDimension =a.getDimension(R.styleable.MyView_exampleDimension,mExampleDimension);
属性的前缀是:MyView_,不再使用时,调用recycle()方法回收资源。
- 构造函数的几种形式:
public MyView(Context context) {
super(context);
init(null, 0);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
- 在代码中直接new一个CustomView实例的时候,会调用第一个构造函数
- 在xml布局文件中调用CustomView的时候,调用第二个构造函数
- 在xml布局文件中调用CustomView指定style的时候,调用第三个构造函数
private void init(AttributeSet attrs, int defStyle) {
// Load attributes
final TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.MyView, defStyle, 0);
mExampleString = a.getString(
R.styleable.MyView_exampleString);
mExampleColor = a.getColor(
R.styleable.MyView_exampleColor,
mExampleColor);
// Use getDimensionPixelSize or getDimensionPixelOffset when dealing with
// values that should fall on pixel boundaries.
mExampleDimension = a.getDimension(
R.styleable.MyView_exampleDimension,
mExampleDimension);
if (a.hasValue(R.styleable.MyView_exampleDrawable)) {
mExampleDrawable = a.getDrawable(
R.styleable.MyView_exampleDrawable);
mExampleDrawable.setCallback(this);
}
a.recycle();
// Set up a default TextPaint object
mTextPaint = new TextPaint();
mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextAlign(Paint.Align.LEFT);
// Update TextPaint and text measurements from attributes
invalidateTextPaintAndMeasurements();
}
可以看出,构造函数的职责主要是:
1. 提取xml中的属性
2. 初始化画笔或其他属性
onAttachedToWindow
父View调用addView(View)后,这个View将被依附到一个窗口。在这个阶段,我们的View会知道它被包围的其他view
onMeasure
该阶段决定了自己的大小,当你重写此方法,需要记得的是,在结束时调用setMeasuredDimension(int width, int height)方法。
可以通过xml或者动态设置了具体的大小。
- 获取你的View MeasureSpec大小和模式,MeasureSpec由32位 int值组成,高2位表示的是测量模式(specMode),后面的30位代表的是测量的大小(specSize)。
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);
}
MeasureSpec.getSize()会解析MeasureSpec值得到父容器width或者height
MeasureSpec.getMode()会得到三个int类型的值分别为:
- MeasureSpec.EXACTLY : 适配Match_Parent , 或指定大小的时候
- MeasureSpec.AT_MOST : 匹配wrap_content
- MeasureSpec.UNSPECIFIED :未定义
//根据不同模式选择大小
private int measureHanlder(int measureSpec, int defaultSize) {
int result = defaultSize;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//适配Match_Parent , 或指定大小的时候
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
}
//适配wrap_content
else if (specMode == MeasureSpec.AT_MOST) {
//默认大小
result = Math.min(defaultSize, specSize);
}
return result;
}
■ 假设:
android:layout_width="300dp"
android:layout_height="300dp"
注:density值表示每英寸有多少个显示点,与分辨率是两个概念。
■ 则实际大小与设备有关:
- drawable-mdpi: 屏幕密度Density为160的手机设备(在此设备上,1dp = 1px)
- drawable-hdpi: 屏幕密度Density为240的手机设备
- drawable-xhdpi: 屏幕密度Density为320的手机设备
- drawable-xxhdpi: 屏幕密度Density为480的手机设备
转化系数分别为:1 , 1.5 , 2 , 3
■ 可以通过代码识别屏幕:
DisplayMetrics dm = new DisplayMetrics();
dm = getResources().getDisplayMetrics();
float density = dm.density;
// 屏幕密度(像素比例:0.75/1.0/1.5/2.0)
int densityDPI = dm.densityDpi;
// 屏幕密度(每寸像素:120/160/240/320)
float xdpi = dm.xdpi;
float ydpi = dm.ydpi;
int screenWidth = dm.widthPixels; // 屏幕宽(像素,如:480px)
int screenHeight = dm.heightPixels; // 屏幕高(像素,如:800px)
Log.i(TAG, "onCreate: x" + screenWidth + " y " + screenHeight + "density:" + density);
onLayout
因为我们现在讨论的是View,没有子View需要排列,所以这一步其实我们不需要做额外的工作。对ViewGroup类,onLayout方法中,我们需要将所有子View的大小宽高设置好
onDraw
在这里,使用Canvas和Paint对象你将可以画任何你需要的东西,onDraw的参数Canvas,它一般用于绘制不同形状,而Paint对象定义形状颜色。简单地说,Canvas用于绘制对象,而Paint用于造型。
使自定义View,要始终牢记onDraw会花费大量的时间。当布局有一些变化,滚动、快速滑动都会导致重新绘制。避免在onDraw中进行对象分配的操作,对象应该只创建一次并在将来重用。
关于画笔与画布API的使用,另起文章介绍
View更新
从View的生命周期图可以得知,可以重绘View自身有两种方法。invalidate()和requestLayout()方法会帮助你在运行时动态改变View状态
- invalidate()方法,用来简单重绘View。例如更新其文本,色彩或触摸交互性。View将只调用onDraw()方法再次更新其状态。
- requestLayout()方法,你可以看到其将会从`onMeasure()开始更新View。这意味着你的View更新后,它改变它的大小,你需要再次测量它,并依赖于新的大小来重新绘制。
- forceLayout()方法,标识View在下一次重绘,需要重新调用layout过程。