ViewGroup可以包含多个View,形成控件树。上层控件负责下层子控件的测量与绘制。findViewById是在控件树中进行深度优先搜索。ViewGroup实现了ViewParent接口,Viewparent定义了一个控件作为父控件的职责,负责子布局与父布局的交互,例如requestLayout。
UI界面架构图
每个Activity都有一个Window对象,一般由PhoneWindow实现,各种监听事件都通过WindowManagerService接收,PhoneWindow将DecorView作为整个应用窗口的根View。DecorView分为两部分:TitleView和ContentView。
为什么requestWindowFeature方法一定要在setContentView之前才能生效?
因为一般的视图树会默认基本分为两部分,上面TitleBar下面Content,在requestWindowFeature(Window.Feature_NO_TITLE)后,DecorView只剩Content。而在setContentView后,WindowManagerService会回调onResume,把整个DecorView添加到PhoneWindow,让其显示。若在setContentView之后再设置requestWindowFeature,因为DecorView已经被添加了,所以再改变也没有用了。
即使我们不看Android的View源码,对于一个手机上的视图,我们也应该想到,它应该是先确定大小,再确定它的位置,最后进行绘制。
核心类:MeasureSpec
核心方法:MeasureSpec.getMode、MeasureSpec.getSize
MeasureSpec是一个32位的int值,高2位是测量模式,低30位是测量大小。
测量模式有三种
1. EXACTLY:精确值模式,属性设置为具体数值或match_parent时,使用此模式
2. AT_MOST:最大值模式,属性设置为wrap_content时,使用此模式
3. UNSPECIFIED:不指定大小测量模式,通常在绘制自定义View时才会用到
View类默认的onMeasure方法只支持EXACTLY模式,想让自定义View支持wrap_content属性,必须重写onMeasure方法来指定wrap_content时的大小
重写onMeasure后,最终的的工作就是把测量后的宽高值作为参数设置给setMeasuredDimension方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//计算宽和高
//模板代码...
widthMeasureSpec = measureWidth(widthMeasureSpec);
heightMeasureSpec = measureHeight(heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
}
计算宽度值的模板代码(计算高度同理)
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
//未定义模式时的大小
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
重写View的onDraw(Canvas canvas),在画布上绘图即可
通常情况下,为啥Canvas对象的创建要传入参数Bitmap?(好吧,通常情况?我为啥不知道)
穿进去的Bitmap与通过Bitmap创建的Canvas是紧紧联系在一起的,这个Bitmap用来存储所有绘制在Canvas上的像素信息,当使用Bitmap创建Canvas后,后面调用的所有Canvas.drawXXX方法都发生在这个Bitmap上。
还是不太明白,绘制内容都放在bitmap上而不绘制在画布上,貌似有利于重用bitmap在多个画布上?
ViewGroup在测量时遍历所有的子View,调用子View的measure方法来返回每一个子view的大小。
测量完毕后,就把子View放到合适的位置,就是Layout的过程。
除非要指定ViewGroup的背景,否则ViewGroup的onDraw不会被调用,但是ViewGroup会使用dispatchDraw来遍历子View,调用他们的绘制方法。
有三种自定义View的方式
对现有控件进行拓展的代码结构:
@Override
protected void onDraw(Canvas canvas) {
//在回调父类方法之前实现自己的逻辑,对TextView来说就是在绘制文本之前
super.onDraw(canvas);
//在回调父类方法之后实现自己的逻辑,对TextView来说就是在绘制文本之后
}
书中对TextView进行拓展的例子
private void initView() {
mPaint1 = new Paint();
mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
mPaint1.setStyle(Paint.Style.FILL);
mPaint2 = new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
// 绘制外层矩形
canvas.drawRect(
0,
0,
getMeasuredWidth(),
getMeasuredHeight(),
mPaint1);
// 绘制内层矩形
canvas.drawRect(
10,
10,
getMeasuredWidth() - 10,
getMeasuredHeight() - 10,
mPaint2);
canvas.save();
// 绘制文字前平移10像素
canvas.translate(10, 0);
// 父类完成的方法,即绘制文本
super.onDraw(canvas);
canvas.restore();
}
一般需要继承一个合适的ViewGroup
在res资源目录的value目录下创建一个attrs.xml,一般形如:
<resources>
<declare-styleable name="TitleBar">
<attr name="title" format="string" />
<attr name="titleTextColor" format="color" />
<attr name="titleTextSize" format="dimension" />
declare-styleable>
resources>
自定义属性的使用
//获取自定义属性的对象 TypedArray
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTitle);
//getString 获取String类型的值
mTitle = ta.getString(R.styleable.MyTitle_title);
//getColor 获取Color类型的值
mTitleColor = ta.getColor(R.styleable.MyTitle_titleTextColors, 0);
//getDimension 获取尺寸类型的值
mTitleSize = ta.getDimension(R.styleable.MyTitle_titleTextSize, 0);
//一般到最后,调用recyle避免重新创建时的错误
ta.recycle();
//引入命名空间,此命名空间为custom
xmlns:custom="http://schemas.android.com/apk/res-auto"
//自定义属性的定义
custom:title="标题"
custom:titleTextColor="#123412"
custom:titleTextSize="15sp"
创建自定义View的难点在于绘制控件和实现交互,通常需要继承View类,并重写onDraw、onMeasure等方法来实现绘制逻辑,同时通过重写onTouchEvent等触控事方法来实现交互逻辑。
一般需要重写onMeasure方法来对子View进行测量,onLayout来确定子View的位置,onTouchEvent来增加响应时间。
核心类:MotionEvent
核心方法:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
dispatchTouchEvent方法一般不管。ViewGroup比View多重写了一个onInterceptTouchEvent方法,也就是事件拦截机制的核心方法。
onInterceptTouchEvent方法返回值:true,拦截,false,不拦截
onTouchEvent方法返回值:true,代表已自行处理,不往上传递,未处理(其实可以偷偷处理,就告诉你没处理false),继续往上传递