前言
网上有很多自定义View的说明的文章,首先通过以下问题,测试自己是否掌握自定义View:
1、Google提出View这个概念的目的是什么?
2、View这个概念与Activtiy、Fragment以及Drawable之间是一种什么样的关系?
3、View能够感知Activity的生命周期事件吗?为什么?
4、View的生命周期是什么?
5、当View所在的Activity进入stop状态后,View去哪了?如果我在一个后台线程中持有一个View的引用,我此时能够改变它的状态吗?为什么?
6、View能够与其他的View交叉重叠吗?重叠区域发生的点击事件交给谁去处理呢?可不可以重叠的两个View都处理?
7、View控制一个Drawable的方法途径有哪些?Drawable能不能与View通信?如果能如何通信?
8、假如View所在的ViewGroup中的子View减少了,View因此获得了更大的空间,View如何及时有效地利用这些空间,改变自己的绘制?
9、假如我要在View中动态地注册与解除广播接收器,应该在哪里完成呢?
10、假如我的手机带键盘(自带或者外接),你的自定义View应该如何响应键盘事件。
11、AnimationDrawable作为View的背景,会自动进行动画,View在其中扮演了怎样的角色?
假如以上问题你都能准确地回答出来,那你的自定义View已经学到家了。
好了,说了这么多,到底怎样才能学好自定义View?其实只需掌握三个问题,就可以轻松搞定它:
问题一:从Android系统设计者的角度,View这个概念究竟是做什么的?
问题二:Android系统中那个View类,它有哪些默认功能和行为,能干什么,不能干什么?(知己知彼,才好自定义!)
问题三:我要改变这个View的行为,外观,肯定是覆写View类中的方法,但是怎么覆写,覆写哪些方法能够改变哪些行为?
View的说明
看官方文档给出的对于View的描述
能够看出几点:1、view是用户界面的基础构建区块。2、view在屏幕上占一个矩形区域,用来绘制以及事件响应。
View.class的功能,行为
看几个问题,带着问题去思考学习:
View是怎样被显示到屏幕上的?
View在屏幕上的位置是怎样决定的?
View所占据的矩形大小是怎样决定的?
屏幕上肯定不止一个View,View之间互相知道对方吗?它们之间能协作吗?
一个UI上有很多view,有基础view,也有复合view。所有的view是怎么组合起来的呢?Google是这么解决的:用Window来展示用户界面,Window加载一个DecorView,用DecorView来包含所有的其他的view。
1、确定view的位置
我们在activity中setContentView,实际上就是将用户界面的所有的View交给了DecorView中的一个FrameLayout,这个FrameLayou代表着可以分配给用户界面使用的屏幕区域。更常见的情况是,用户界面是一个ViewGroup,里面包含了其他ViewGroup和View。
开发者在使用view的时候,向ViewGroup说明想把view放在什么位置,以LinearLayout,vertical为例,在写布局文件时,子View在LinearLayout中的出现顺序将决定它们在屏幕上的上下顺序,同时还可以借助layout_margin ,layout_gravity等配置进一步调整子View在分给自己的矩形区域中的位置。layout_*虽然是跟view的属性写在一起,但是其实并不是view的属性。这些值在Inflate时,是由ViewGroup读取,然后生成一个ViewGroup特定的LayoutParams对象,再把这个对象存入子View中的,这样,ViewGroup在为该子View安排位置时,就可以参考这个LayoutParams中的信息了。
2、确定View大小
第一步,开发者在书写布局文件时,会为一个View写上android:layout_width="***"android:layout_height="***"两个配置,这是开发者向ViewGroup表达的,我这个View需要的大小是多少。星号的取值有三种:
具体值,如50dp,很简单,不多讲
match_parent ,表示开发者向ViewGroup说,把你所有的屏幕区域都给这个View吧。
wrap_parent,表示开发者向ViewGroup说,只要给这个View够他展示自己的空间就行,至于到底给多少,你直接跟View沟通吧,看它怎么说。
第二步,ViewGroup收到了开发者对View大小的说明,然后ViewGroup会综合考虑自己的空间大小以及开发者的请求,然后生成两个MeasureSpec对象(width与height)传给View,这两个对象是ViewGroup向子View提出的要求。然后,这两个对象将会传到子View的protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中。子View再看ViewGroup的要求,于是,它从传入的两个对象中解译出如下信息:
Mode与Size一起,准确表达出了ViewGroup的要求。
Mode的取值有三种,它们代表了ViewGroup的要求条件:
1、MeasureSpec.EXACTLY,精确模式
在这种模式下,尺寸的值是多少,那么这个组件的长或宽就是多少。
2、MeasureSpec.AT_MOST,最大模式
这个也就是父组件,能够给出的最大的空间,当前组件的长或宽最大只能为这么大,当然也可以比这个小。
3、MeasureSpec.UNSPECIFIED,未指定模式
这个就是说,当前组件,可以随便用空间,不受限制。
如果子view不想遵守ViewGroup的要求怎么办?就是子view一定要设置自己考虑后的尺寸,如果不设置就相当于没有告诉ViewGroup自己想要的大小,这会导致ViewGroup无法正常工作,设置的办法就是在onMeasure方法的最后,调用setMeasuredDimension方法。
3、view的绘制
主要就是onDraw方法,主要执行几步:
绘制背景;
通过onDraw()绘制自身内容;
通过dispatchDraw()绘制子View;
绘制滚动条
4、改变view的行为,显示外观
肯定是要重载View.class中的方法,来看官方怎么说的
从上可以看出view的生命周期。View被inflated出来后,系统会回调该View的onFinishInflate方法。其他方法可以自己看文档。
5、实战
代码可见:https://github.com/feb07/CustomView
public class CustomView extends View {
private Time time;
//图片
private Drawable bgDrawable;
private Drawable hourDrawable;
private Drawable minDrawable;
//尺寸
private int bgWidth;
private int bgHeight;
private boolean mAttached;
//看名字
private float mMinutes;
private float mHour;
//用来跟踪我们的View 的尺寸的变化,
//当发生尺寸变化时,我们在绘制自己
//时要进行适当的缩放。
private boolean mChanged;
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
//初始化图片信息
if (bgDrawable == null) {
bgDrawable = context.getDrawable(R.drawable.bg);
}
if (hourDrawable == null) {
hourDrawable = context.getDrawable(R.drawable.hour);
}
if (minDrawable == null) {
minDrawable =
context.getDrawable(R.drawable.min);
}
time = new Time();
bgWidth = bgDrawable.getIntrinsicWidth();
bgHeight = bgDrawable.getIntrinsicHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
float hScale = 1.0f;
float vScale = 1.0f;
if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < bgWidth) {
hScale = (float) widthSize / (float) bgWidth;
}
if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < bgHeight) {
vScale = (float) heightSize / (float) bgHeight;
}
float scale = Math.min(hScale, vScale);
setMeasuredDimension(
resolveSizeAndState((int) (bgWidth * scale), widthMeasureSpec, 0),
resolveSizeAndState((int) (bgHeight * scale), heightMeasureSpec, 0)
);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mChanged = true;
}
private void onTimeChanged() {
time.setToNow();
int hour = time.hour;
int minute = time.minute;
int second = time.second;
/*这里我们为什么不直接把minute设置给mMinutes,而是要加上
second /60.0f呢,这个值不是应该一直为0吗?
这里又涉及到Calendar的 一个知识点,
也就是它可以是Linient模式,
此模式下,second和minute是可能超过60和24的,具体这里就不展开了,
如果不是很清楚,建议看看Google的官方文档中讲Calendar的部分*/
mMinutes = minute + second / 60.0f;
mHour = hour + mMinutes / 60.0f;
mChanged = true;
}
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//这个if判断主要是用来在时区发生变化时,更新mCalendar的时区的,这
//样,我们的自定义View在全球都可以使用了。
if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
String tz = intent.getStringExtra("time-zone");
time = new Time(TimeZone.getTimeZone(tz).getID());
}
//进行时间的更新
onTimeChanged();
//invalidate当然是用来引发重绘了。
invalidate();
}
};
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!mAttached) {
mAttached = true;
IntentFilter filter = new IntentFilter();
//这里确定我们要监听的三种系统广播
filter.addAction(Intent.ACTION_TIME_TICK);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
getContext().registerReceiver(mIntentReceiver, filter);
}
time = new Time();
onTimeChanged();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAttached) {
getContext().unregisterReceiver(mIntentReceiver);
mAttached = false;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//View尺寸变化后,我们用changed变量记录下来,
//同时,恢复mChanged为false,以便继续监听View的尺寸变化。
boolean changed = mChanged;
if (changed) {
mChanged = false;
}
int availableWidth = getRight() - getLeft();
int availableHeight = getBottom() - getTop();
int x = availableWidth / 2;
int y = availableHeight / 2;
//画背景
final Drawable drawable = bgDrawable;
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
boolean scaled = false;
/*如果可用的宽高小于表盘图片的宽高,
就要进行缩放,不过这里,我们是通过坐标系的缩放来实现的。
而且,这个缩放效果影响是全局的,
也就是下面绘制的表盘、时针、分针都会受到缩放的影响。*/
if (availableWidth < w || availableHeight < h) {
scaled = true;
float scale = Math.min((float) availableWidth / (float) w,
(float) availableHeight / (float) h);
canvas.save();
canvas.scale(scale, scale, x, y);
}
/*如果尺寸发生变化,我们要重新为表盘设置Bounds。
这里的Bounds就相当于是为Drawable在View中确定位置,
只是确定的方式更直接,直接在View中框出一个与Drawable大小
相同的矩形,
Drawable就在这个矩形里绘制自己。
这里框出的矩形,是以(x,y)为中心的,宽高等于表盘图片的宽高的一个矩形,
不用担心表盘图片太大绘制不完整,
因为我们已经提前进行了缩放了。*/
if (changed) {
drawable.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
}
drawable.draw(canvas);
canvas.save();
//接下来画时针
/*根据小时数,以点(x,y)为中心旋转坐标系。
*/
canvas.rotate(mHour / 12.0f * 360.0f, x, y);
final Drawable hourHand = hourDrawable;
//同样,根据变化重新设置时针的Bounds
if (changed) {
w = hourHand.getIntrinsicWidth();
h = hourHand.getIntrinsicHeight();
/* 仔细体会这里设置的Bounds,我们所画出的矩形,
同样是以(x,y)为中心的
矩形,时针图片放入该矩形后,时针的根部刚好在点(x,y)处,
因为我们之前做时针图片时,
已经让图片中的时针根部在图片的中心位置了,
虽然,看起来浪费了一部分图片空间(就是时针下半部分是空白的),
但却换来了建模的简单性,还是很值的。*/
hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
}
hourHand.draw(canvas);
canvas.restore();
canvas.save();
//根据分针旋转坐标系
canvas.rotate(mMinutes / 60.0f * 360.0f, x, y);
final Drawable minuteHand = minDrawable;
if (changed) {
w = minuteHand.getIntrinsicWidth();
h = minuteHand.getIntrinsicHeight();
minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
}
minuteHand.draw(canvas);
canvas.restore();
//最后,我们把缩放的坐标系复原。
if (scaled) {
canvas.restore();
}
}
}