《Android群英传》学习笔记之Android控件架构与自定义控件详解
一、Android控件架构:
-
控件大致分为两类:ViewGroup控件与View控件。View是绘制在屏幕上的用户能与之交互的一个对象。而ViewGroup则是一个用于存放其他View(和ViewGroup)对象的布局容器。其中,View是所有UI组件的基类,而 ViewGroup是容纳这些组件的容器,其本身也是从View派生出来的。
- (1)View:View对象是Android平台中用户界面体现的基础单位,它是用来创建交互性的UI组件(如:按钮文本框等等)的widgets的父类。它们提供了诸如文本输入框和按钮之类的UI对象的完整实。
- (2)ViewGroup:ViewGroup继承自View,是一种特殊的View,它可以装其他的Views(或其他的ViewGroup)。ViewGroup是布局(layouts)和views containers的父类。它的直接子类有: FrameLayout、GridLayout、LinearLayout等等。
- 下面列出View及ViewGroup的所有子类
- View派生出的直接子类有
ImageView,ProgressBar,TextView,ViewGroup,AnalogClock,KeyboardView,ViewStub,SurfaceView; - View派生出的间接子类有
Button,CheckBox,AbsoluteLayout,AdapterView,AdapterViewAnimator,AdapterViewFlipper,AppWidgetHostView,AutoCompleteTextView,CalendarView,CheckedTextView,Chronometer,AbsListView,AbsSeekBar,AbsSpinner,CompoundButton; - ViewGroup派生出的直接子类有
AbsoluteLayout,FrameLayout,LinearLayout,RelativeLayout,AdapterView,FragmentBreadCrumbs,SlidingDrawer; - ViewGroup派生出的间接子类有
ListView,GridView,AbsListView,AbsSpinner,AdapterViewAnimator,AdapterViewFlipper,AppWidgetHostView,CalendarView,DatePicker,DialerFilter,ExpandableListView,Gallery,GestureOverlayView,HorizontalScrollView,ImageSwitcher;
- View派生出的直接子类有
-
View树结构 (eg:通常在Activty中使用的findViewById()方法就是在控件树中以树的深度优先遍历来查找对应元素)
-
UI界面架构图(DecorView被设置为整个应用窗口的根View)
-
标准视图树
-
在代码中,当程序在onCreate()方法中调用setContentView()方法后,ActivityManagerService会回调OnResume()方法,此时系统会把整个DecorView添加到PhoneWindow中,并让其显示出来
二、View/ViewGroup的测量与绘制
1、View的测量
- 在onMeasure() 方法中测量,借助MeasureSpec类来帮助测量View
- 测量模式:
- EXACTLY(精确值模式):指定数值(在onMeasure()方法中,如果不重写方法,只能使用这种测量模式)
- AT_MOST(最大值模式):例如wrap_content,只要不超过父控件允许的最大尺寸即可
- UNSPECIFIED(不指定大小):通常在绘制自定义View时使用
- 当View需要使用wrap_content属性时,重写onMeasure()方法,最终要做的工作就是把测量后的宽高作为参数设置给setMeasuredDimension(宽,高) 方法
- 自定义测量值
- 1.从MeasureSpec对象中提取出具体的测量模式和大小
int specMode = MeasureSpec.getMode(measureSpec) int specSize = MeasureSpec.getSize(measureSpec)
- 2.通过测量的模式给出不同的测量值
- 1.从MeasureSpec对象中提取出具体的测量模式和大小
2、View的绘制
- 系统2D绘图API需使用Canvas对象绘制,Canvas像画板,使用Paint就可以在上面作画。
- 通常需要继承View并重写的它的onDraw() 方法进行绘图
- 要在创建Canvas对象的时候同时传入一个bitmap对象。因为传进去的bitmap与通过这个bitmap创建的Canvas画布是紧紧联系在一起的,这个过程称之为装载画布。这个bitmap用来存储所有绘制在Canvas上的像素信息。
Canvas canvas = new Canvas(bitmap); //如果在onDraw()方法中,可将bitmap1再装载到其他Canvas对象中,刷新后会重新装载画布 canvas.drawBitmap(bitmap1,0,0,null);
3、ViewGroup的测量:
- ViewGroup在测量时通过遍历所有子View,从而调用子View的Measure方法来获得每一个子View的测量结果,前面所说的对View的测量,就是在这里进行的。
4、ViewGroup的绘制:
- ViewGroup如果不是被指定了背景色,通常不需要绘制。但是ViewGroup会使用dispatchDraw() 方法遍历所有的子View,并通过调用子View来完成绘制工作。
三、自定义View
通常情况下,有以下三种方法来实现自定义控件
- 对现有控件进行拓展
- 通过组合来实现新的控件
- 重写View来实现全新的控件
1、对现有控件进行拓展
- 一般来说,在onDraw() 方法中进行对原生控件的拓展
@Override protected void onDraw(Canvas canvas) { //在回调父类方法前,实现自己的逻辑,对TextView来说,即是在绘制文本内容前 super.onDraw(); //在回调父类方法后,实现自己的逻辑,对TextView来说,即是在绘制文本内容后 }
- 程序调用super.onDraw(canvas) 方法来实现原生控件的功能。
- 通过canvas、paint等对象实现逻辑
- 一般在构造方法中完成对象的初始化工作,如初始化画笔
mPaint1 = new Paint(); mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light)); mPaint1.setStyle(Paint.Style.FILL);
- 可通过getPaint() 方法获取到当前绘制原生控件的Paint对象,然后对这个对象设置一些原生控件没有的属性
2、创建复合控件
- 这种方式通常需要继承一个合适的ViewGroup,再给他添加指定功能的控件,从而组合成新的复合控件
Ⅰ、定义属性
①确定属性:在res资源目录的values目录下创建一个attrs.xml的属性定义文件
<resources>
<declare-styleable name="TopBar">
<attr name = "title" format = "string" />
<attr name = "titeTextSize" format = "dimension" />
<attr name = "titleTextColor" format = "color" />
<attr name = "rightBackground" format = "reference|color"
......
</declare-styleable>
</resources>
- 通过 标签声明了使用自定义属性,并通过name属性来确定引用的名称
- 通过 标签来声明具体的自定义属性,并通过format属性来指定属性的类型
- 需要注意的是,有的属性可以是颜色属性,也可以是引用属性。比如按钮的背景,可以指定为具体的颜色,也可以把它指定为一张具体的图片,所以使用“|”来分隔不同的属性
②获取属性
- 系统提供了TypedArray这样的数据结构来获取自定义属性集,然后通过TypedArray对象的getString()、getColor() 等方法,就可以获取这些定义的属性值。当获取完所有属性值后,需要调用TypedArray的recycle方法来完成资源的回收。
//通过这个方法,将在attrs.xml中定义的declare-styleable的所有属性的值存储到TypedArray中。
//第一个参数为xml名,第二个参数为style的name
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TopBar);
//从TypedArray中取出对应的值来为要设置的属性赋值
mLeftTextColor = ta.getColor(R.styleable.TopBar_leftText,0);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftText);
//获取玩TypedArray的值后,一般要调用recyle的方法来避免重新创建时的错误
ta.recycle();
Ⅱ、组合控件
- 通过动态添加控件的方式,使用addView() 的方法将重置属性后的系统控件放入自定义的模板中
//为组件元素设置相应的布局元素
mLeftParams = new LayoutParams(LayoutParamas.WRAP_CONTENT, LayoutParamas.MATCH_PARENT);
mLeftParamas.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
//添加到ViewGroup
addView(mLeftButton, mLeftParams);
- 实现自定义控件的点击事件
①定义接口
//接口对象,实现回调机制,在回调方法中,通过映射的接口对象调用接口中的方法,而不用去考虑如何具体实现
public interface topbarClickListener {
//左按点击事件
void leftClick();
//右按点击事件
void rightClick();
}
②暴露接口给调用者(使用者)
//按钮的点击事件,不需要具体的实现
//秩序调用接口的方法,回调的时候,会有具体的实现
mRightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.rightClick();
}
});
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.leftClick();
}
});
//暴露一个方法给调用者来注册接口回调
//通过接口来获得回调者对接口方法的实现
public void setOnTopbarClickListener(topbarClickListener mListener) {
this.mListener = mListener;
}
③实现接口回调(定义具体实现者)
在调用者的代码中,调用者需要实现这样一个接口,并完成接口中的方法,确定具体实现逻辑,并使用第二步中暴露的方法,将接口的对象传递进去,从而完成回调。
mTopbar.setOnTopbarClickListener(
new TopBar.topbarClickListener(){
@Override
public void rightClick(){
Toast.makeText(TopBarTest.this, "right", Toast.LENGTH_SHORT).show();
}
@Override
public void leftClick(){
Toast.makeText(TopBarTest.this, "left", Toast.LENGTH_SHORT).show();
}
}
)
Ⅲ、引用UI模板
- 在引用前,需要指定引用第三方控件的命名空间。
//假设将引入的第三方控件的名字空间取名为custom xmlns:custom="http://schemas.android.com/apk/res-auto"
- 使用自定义View与系统原生的View 的最大区别就是在申明控件时,需要指定完整的包名,而在引用自定义的属性时,需要使用自定义的xmlns名字。
- 通过include引用布局文件模板
<include layout="@layout/topbar" />
3、重写View来实现全新的控件
- 创建一个自定义View,难点在于绘制控件和实现交互
- 通常需要继承View类,并重写它的onDraw()、onMeasure()等方法来实现绘制逻辑。
- 同时通过onTouchEvent()等触控事件来实现交互逻辑。
四、自定义ViewGroup
- ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、响应的规则。
- 通常需要重写onMeasure() 方法对子View进行测量
- 重写onLayout() 方法来确定子View的位置
- 重写onTouchEvent() 方法来增加响应事件
五、事件拦截机制分析
- Android为触摸事件封装了一个类:MotionEvent
-
触摸事件先按视图树的顺序从上到下顺序对事件进行分发、拦截,再按逆序进行事件处理或审核。
- 事件传递的返回值:True,拦截,不继续;false,不拦截,继续流程。
- 事件处理的返回值:True,处理了,不用审核了;false,给上级处理
- ViewGroup与View的方法:
- ViewGroup
//事件分发 public boolean dispatchTouchEvent(MotionEvent ev) //事件拦截 public boolean onInterceptTouchEvent(MotionEvent ev) //事件审核 public boolean onTouchEvent(MotionEvent ev)
- View(位于最底层,无事件拦截的方法)
//事件分发 public boolean dispatchTouchEvent(MotionEvent ev) //事件处理 public boolean onTouchEvent(MotionEvent ev)
- ViewGroup