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; }
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()来保存画布当前的状态,当操作之后取出之前保存过的状态,这样就不会对其他的元素进行影响。具体详见:http://blog.csdn.net/oney139/article/details/8143281
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>
TypedArray ta=context.obtainStyledAttributes(attr,R.styleable.TopBar); String title; title=ta.getString(R.styleable.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); }
@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,事件处理如下:
本章完。