1.Android控件架构
在Android中,控件大致分为两类:ViewGroup控件与View控件。ViewGroup作为父控件时,可以包含多个View,通过ViewGroup,整个界面形成一个树结构,上层控件负责下层控件的测量与绘制。
View数结构:
每一个Activity都包含一个Window对象,而这个对象是由PhoneWindow来实现,PhoneWindow将一个DecorView设置为整个窗口的根View,DecorView由两个部分组成:TitleView和ContentView。ContentView实际上是一个Framelayout,里面容纳的就是我们在XML中的布局文件。UI界面结构图如3.2UI界面结构图。
当设置了requestWindowFeature(Window.FEATURE_NO_TITLE)时,为什么要放在setContentView()方法前面?
这是因为,当程序在onCreate()方法中调用setContentView()方法后,ActivityManagerSerVice会回调onResume()方法,此时系统才会将整个DecorView添加到PhoneWindow中。
2.View的测量:
在需要对View进行绘制之前,需要知道View的位置和大小。
涉及的类与方法:onMeasure()进行测量、MeasureSpec。
MeasureSpec:一个32位的int值,高两位为测量模式,低30位为测量大小。
测量模式有:
EXACTLY:精确值模式。如需对layout_width、layout_height设置具体数值 或者使用match_parent时。
AT_MOST:最大值模式。对layout_width、layout_height设置为wrap_content,让空间尺寸碎内容的大小进行改变,但注意不能超过父控件允许的最大尺寸。
UNSPECIFIED:不指定大小测量模式,通常情况下在绘制自定义View时才会用到
View默认的onMeasure()方法只支持EXACTLY模式,所以如果要支持wrap_content属性,就必须重写onMeasure()方法,并在方法中给定wrap_content的大小。
重写onMeasure方法的最终工作就是把测量后的宽高值作为参数设置给setMeasuredDimension方法。
以下是用法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//在这里进行计算
setMeasuredDimension(
measureWidth(widthMeasureSpec),
measureHight(heightMeasureSpec)
);
}
private int measureWidth(int measureSpec){
int width=0;//设置返回的数值
int specMode=MeasureSpec.getMode(measureSpec);//判断是哪一种测量模式
int specSize=MeasureSpec.getSize(measureSpec);//值的大小
if (specMode==MeasureSpec.EXACTLY) {//如果是精确测量 则直接返回值
width=specSize;
}else{//指定宽度的大小
width=200;
if (specMode==MeasureSpec.AT_MOST) {//如果是最大值模式 取当中的小值 防止超出父类控件的最大值
width=Math.max(width, measureSpec);
}
}
return measureSpec;
}
private int measureHight(int measureSpec){
int higth=0;//设置返回的数值
int specMode=MeasureSpec.getMode(measureSpec);//判断是哪一种测量模式
int specSize=MeasureSpec.getSize(measureSpec);//值的大小
if (specMode==MeasureSpec.EXACTLY) {//如果是精确测量 则直接返回值
higth=specSize;
}else{//指定高度的大小
higth=200;
if (specMode==MeasureSpec.AT_MOST) {//如果是最大值模式 取当中的小值 防止超出父类控件的最大值
higth=Math.max(higth, measureSpec);
}
}
return measureSpec;
}
3.View和ViwGroup的绘制
View的onDraw()方法包含一个参数Canvas对象,使用这个Canvas对象就可以进行绘图了。
通常情况下,Canvas对象的创建需要传入参数Bitmap,这是因为传进去的Bitmap与通过这个Bitmap创建的Canvas画布是紧紧联系在一起的,这个Bitmap用来存储所有绘制在Canvas上的像素信息,当通过Canvas之后,后面调用所有的Canvas.drawXXX方法都发生在这个Bitmap上。
而ViewGroup通常不需要绘制,因为它本身没有需要绘制的东西,如果不指定ViewGroup的背景颜色,那么ViewGroup的onDraw方法都不会被调用。但是,ViewGroup会调用dispatchDraw方法来绘制其子view,其过程同样是通过遍历所有子view并调用子view的绘制方法来完成绘制工作的。
4. ViewGroup的测量与绘制
当ViewGroup的大小设置为wrap_content时,ViewGroup就需对ziView进行遍历,根据子View来设置自身的大小。
通常情况下ViewGroup不需要进行绘制,因为本身就是一个View的集合,没有绘制的东西,甚至如果不是设置背景颜色,其onDraw()方法都不会被调用。ViewGroup通过调用disPatchDraw()方法来绘制子View。自定义ViewGroup时,通常重写onLayout()方法来控制其子View的位置显示逻辑。
5.自定义View
在View中重要的回调方法:
onFinishInFlate():从XML加载组件后回调
onSizeChanged():组件大小改变时回调
onMeasure():回调该方法来进行测量
onLayout():回调该方法来确定显示位置
onTouchEvent():监听触摸事件进行回调
三种实现自定义控件方法:
1).对现有控件进行拓展
@Override
protected void onDraw(Canvas canvas) {
//在调用父类方法前,实现自己的逻辑,对TextView来说既是在绘制文本内容前
super.onDraw(canvas);
//在调用父类方法后,实现自己的逻辑,对TextView来说既是在绘制文本内容后
}
如书中绘制书中例子:
@Override
protected void onDraw(Canvas canvas) {
//绘制外层矩形
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint1);
//绘制内层矩形
canvas.drawRect(10, 10, getMeasuredWidth()-10, getMeasuredHeight()-10, mPaint2);
canvas.save();//保存画布的状态
canvas.translate(10, 0);//绘制前内容在X轴平移10个像素
super.onDraw(canvas);//调用父类完成绘制文本
canvas.restore();//取出保存的状态
}
private void init(){
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);
}
在这里需要说的是,为什么要调用canvas.sava()和canvas.restore()?
这里canvas.save();和canvas.restore();是两个相互匹配出现的,作用是用来保存画布的状态和取出保存的状态的.。
当我们对画布进行旋转,缩放,平移等操作的时候其实我们是想对特定的元素进行操作,比如图片,一个矩形等,但是当你用canvas的方法来进行这些操作的时候,其实是对整个画布进行了操作,那么之后在画布上的元素都会受到影响,所以我们在操作之前调用canvas.save()来保存画布当前的状态,当操作之后取出之前保存过的状态,这样就不会对其他的元素进行影响。
2).通过符合控件来实现自定义控件
这种方式需要需要继承一个合适的ViewGroup,再给它增添新功能,如TopBar标题栏
使用方式为:
(1)自定义属性
在res资源目录中的values目录下新建arrts.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TopBar">
<attr name="title" format="string" />
</declare-styleable>
</resources>
获取xml布局中的自定义属性且进行赋值:
TypedArray ta=context.obtainStyledAttributes(attr,R.styleable.TopBar);
String title;
title=ta.getString(R.styleable.title);
(2)组合控件,如:网布局中添加一个TextView,并让该TextView显示以上的title
定义父布局:mParentLayout 、textView;
将title显示在TextView中:textView.setText(title)
通过addView(textView,mParentLayout )
书中例子:
public class TopBar extends RelativeLayout {
// 包含topbar上的元素:左按钮、右按钮、标题
private Button mLeftButton, mRightButton;
private TextView mTitleView;
// 布局属性,用来控制组件元素在ViewGroup中的位置
private LayoutParams mLeftParams, mTitlepParams, mRightParams;
// 左按钮的属性值,即我们在attr.xml文件中定义的属性
private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
// 右按钮的属性值,即我们在attr.xml文件中定义的属性
private int mRightTextColor;
private Drawable mRightBackground;
private String mRightText;
// 标题的属性值,即我们在attr.xml文件中定义的属性
private float mTitleTextSize;
private int mTitleTextColor;
private String mTitle;
// 映射传入的接口对象
private topbarClickListener mListener;
......
}
3)重写View来实现全新的控件
创建自定义View的难点在于绘制控件和实现交互,通常需要继承View类,并重写onDraw、onMeasure等方法来实现绘制逻辑,同时通过重写onTouchEvent等触控事件方法来实现交互逻辑。在一部分需要动手自己实践。
5. 简单的 事件拦截机制分析
事件拦截机制是Android提供的一整套完善的事件传递、处理机制来处理用户从屏幕传来的操作。
触摸事件:
按钮按下:MotionEven.ACTION_DOWN
滑动: MotionEven.ACTION_MOVE
抬起:MotionEven.ACTION_UP
事件传递,书中例子 :
总经理----》MyViewGroupA 最外层ViewGroup
部长------》MyViewGroupB 中间层
干活员工---》View 底层View
事件进行传递时需要调用的方法:
作为父类的ViewGroup需要调用三个方法:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d("hjo", "MyViewGroupA dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("hjo", "MyViewGroupA onInterceptTouchEvent");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("hjo", "MyViewGroupA onTouchEvent");
return super.onTouchEvent(event);
}
作为底层View调用的方法只有两个:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d("hjo", "View dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("hjo", "View onTouchEvent");
return super.onTouchEvent(event);
}
运行后打出的log为:
MyViewGroupA dispatchTouchEvent
MyViewGroupA onInterceptTouchEvent
MyViewGroupB dispatchTouchEvent
MyViewGroupB onInterceptTouchEvent
View dispatchTouchEvent
View onTouchEvent
MyViewGroupB onTouchEvent
MyViewGroupA onTouchEvent
从上可以看出事件拦截的核心是:onInterceptTouchEvent
所以事件的传递顺序为:
总经理(MyViewGroupA)---》部长(MyViewGroupB)---》员工(View)
处理后传递顺序是:
员工(View)---》部长(MyViewGroupB)---》总经理(MyViewGroupA)
事件传递返回值的分析:true,拦截,不继续;false,不拦截,继续传递
事件处理后返回值分析:true,处理了,不用审核;false,给上级处理处理
初始状态为false。
处理过程如下图:
当部长(MyViewGroupA)拦截事件,也就是自己处理时,,此时不需要View进行处理,传递与处理如下:
事件的处理,当View无法进行事件的处理而直接返回true,事件处理如下:
本章完。