1. Android控件架构
Android中每一个Activity都包含一个Window对象。
Window对象通常由PhoneWindow。
PhoneWindow将一个 DecorView作为整个应用窗口的根View。
DecorView将要显示的内容呈现在PhoneWindow上。在显示上,它将屏幕分成两部分:TitleView和ContentView。
ContentView是一个id为content的FrameLayout,其中activity_main.xml就是设置到这个FrameLayout中的。
如图所示的第二层封装了一个LinerLayout作为ViewGroup,如果用户通过requestWindowFeature(Window.FEATURE_NO_TITLE)来设置全屏显示,视图树中的布局就只有content了,这也就解释了为什么调用requestWindowFeature()方法一定要在setContentView()方法之前调用。
在代码中,当程序在onCreate()方法中调用了setContentView()方法之后,AMS会调用onResume()方法,此时系统 才会把整个DecorView添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制。
2. View的测量和绘制
View 的测量
现实生活中,绘制一个图形,我们必须要先知道绘制的大小和位置,这同样也适用于Android。
在Android中,绘制View之前,必须对View做测量,告知系统需要绘制的View的大小。这一过程在onMeasure()方法中来完成。
MeasureSpec类:用于View的测量。它是一个32位的int值,其中高2位为测量的模式,低30位为测量的大小。
EXACTLY:精确模式。当控件的大小是具体的值或者指定为match_parent时,系统使用的是EXACTLY模式。
AT_MOST:最大值模式。当控件的宽高属性指定为wrap_content时,控件的大小一般会随着控件内容或者子控件的变化而变化。此时控件的尺寸只要不超过父控件的大小即可。
UNSPECIFIED:不指定测量大小的模式。View想多大就多大,通常情况下,在绘制自定义View时才会使用。
View类默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式。此时,控件可以相应你指定的具体宽高值或者match_parent属性。而如果要让自定义View支持wrap_content属性,那么就必须重写onMeasure()方法来制定wrap_content时的大小。
View 的绘制
测量好一个View之后,就可以简单的通过重写onDraw()方法,在Canvas对象上绘制出所需要的图形。
绘制View的关键:Paint、Canvas
Canvas对象的创建需要传入一个bitmap对象,这个过程叫过装载画布,传入的bitmap对象用来存储所有绘制在Canvas上的像素信息。Canvas canvas = new Canvas(bitmap)
。当用这种方式创建了Canvas对象之后,后面调用所有的Canvas.drawXXX方法都将发生在这个bitmap对象上。
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
onDraw()方法的调用时机是在步骤3中,即绘制view的content。
3. ViewGroup的测量和绘制
ViewGroup具有管理其子View的职责。
ViewGroup的测量
如果ViewGroup的大小为wrap_content,就需要遍历所有的子View,以便获得所有子View的大小,从而来决定自己的大小。
其他模式下,ViewGroup会通过具体的指定值来设置自身大小。
ViewGroup的绘制
ViewGroup 通常情况下不需要绘制,因为其本身没有需要绘制的东西,但是ViewGroup会使用dispatchDraw()方法来绘制子View,过程是遍历所有子View并调用子View的绘制方法来完成工作。
4. 自定义控件的三种方式
- 对现有控件进行扩展
- 通过组合来实现新的控件
- 重写View来实现全新的控件
在View中通常有以下一些比较重要的回调方法:
- onFinishInflate():从XML加载组件后回调
- onSizeChanged():组件大小改变时回调
- onMeasure():回调该方法来进行测量
- onLayout():回调该方法来确定显示的位置
- onTouchEvent():监听到触摸事件时回调
下面对自定义控件实现的三种情况逐个展开来描述。
对现有控件的扩展
这是一个很重要的自定义View的方法,它可以在原生控件的基础上进行拓展,增加新的功能、修改显示的UI等。通常我们可以在onDraw()方法中对原生控件进行扩展。
@Override
protected void onDraw(Canvas canvas) {
// 在回调父类方法前,实现自己的逻辑
super.onDraw(canvas);
// 在回调父类方法后,实现自己的逻辑
}
比如说我们自定义一个TextView。在回调方法前或者回调方法后实现自己的逻辑,会有不同的效果。
可见,Android的绘制时一层层叠加的,有点类似于Photoshop中的图层。
通过组合来实现新的控件
创建复合控件可以很好地创建出具有重用功能的控件集合。这种方式通常需要一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。
通过组合来实现新的控件通常会包含以下几点内容:
- 定义属性
- 组合控件
- 暴露接口给调用者
- 实现接口回调
下面,我们就以实现一个topbar为例,来看一下通过组合来实现新控件的思路。
- 定义属性
在res/values/目录下创建一个attrs.xml的属性文件。
res/values/attrs.xml
declare-styleable标签用来声明使用自定义属性,attr用来声明具体的自定义属性。
我们可以通过下面的方式来获取XML布局文件中自定义的那些属性。
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
系统提供了TypeArray这样的数据结构来获取自定义属性集。
// 从TypedArray中取出对应的值来为要设置的属性赋值
mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
mRightText = ta.getString(R.styleable.TopBar_rightText);
mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
mTitle = ta.getString(R.styleable.TopBar_title);
// 获取完TypedArray的值后,一般要调用recyle方法来避免重新创建的时候的错误
ta.recycle();
获取完所有的属性后,需要调用TypeArray的recycle()方法来完成资源的回收。
- 组合控件
UI模板TopBar由三个控件组成,左边的点击按钮,右边的点击按钮和中间的的标题栏。通过动态添加控件,使用addView()方法将这三个控件添加到自定义的TopBar模板中,并给他们设置我们前面所获取到的具体属性值。比如,标题的颜色 、大小等。
mLeftButton = new Button(context);
mRightButton = new Button(context);
mTitleView = new TextView(context);
// 为创建的组件元素赋值
// 值就来源于我们在引用的xml文件中给对应属性的赋值
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackground(mLeftBackground);
mLeftButton.setText(mLeftText);
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackground(mRightBackground);
mRightButton.setText(mRightText);
mTitleView.setText(mTitle);
mTitleView.setTextColor(mTitleTextColor);
mTitleView.setTextSize(mTitleTextSize);
mTitleView.setGravity(Gravity.CENTER);
// 为组件元素设置相应的布局元素
mLeftParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
// 添加到ViewGroup
addView(mLeftButton, mLeftParams);
mRightParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
addView(mRightButton, mRightParams);
mTitlepParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(mTitleView, mTitlepParams);
- 暴露接口给调用者
// 按钮的点击事件,不需要具体的实现,
// 只需调用接口的方法,回调的时候,会有具体的实现
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;
}
// 接口对象,实现回调机制,在回调方法中
// 通过映射的接口对象调用接口中的方法
// 而不用去考虑如何实现,具体的实现由调用者去创建
public interface topbarClickListener {
// 左按钮点击事件
void leftClick();
// 右按钮点击事件
void rightClick();
}
- 实现接口回调
在自己的业务代码中可以实现基于接口的回调
// 为topbar注册监听事件,传入定义的接口
// 并以匿名类的方式实现接口内的方法
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();
}
});
重写View来实现全新的控件
当安卓系统的原生控件无法满足我们的需求时,我们就需要完全创建一个新的自定义View来实现需要的功能。
创建一个自定义View的难点在于:绘制控件和实现交互
通常需要继承View类,并重写它的onMeasure() 、onDraw()等方法来实现绘制逻辑,同时重写onTouchEvent()等触控事件来实现交互逻辑。当然,也可以引入自定义属性来丰富自定义View的可定制性。
5. 自定义ViewGroup
ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、响应的规则。因此,自定义ViewGroup通常需要重写onMeasure()方法来对子View进行测量,重写onLayout()方法来确定子View的位置,重写onTouchEvent()方法来增加响应事件。
相关代码如下:
@Override
protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; ++i) {
View childView = getChildAt(i);
measureChild(childView,
widthMeasureSpec, heightMeasureSpec);
}
}
....
@Override
protected void onLayout(boolean changed,
int l, int t, int r, int b) {
int childCount = getChildCount();
// 设置ViewGroup的高度
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
child.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
}
}
}
......
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
int dy = mLastY - y;
if (getScrollY() < 0) {
dy = 0;
}
if (getScrollY() > getHeight() - mScreenHeight) {
dy = 0;
}
scrollBy(0, dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
int dScrollY = checkAlignment();
if (dScrollY > 0) {
if (dScrollY < mScreenHeight / 3) {
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY);
} else {
mScroller.startScroll(
0, getScrollY(),
0, mScreenHeight - dScrollY);
}
} else {
if (-dScrollY < mScreenHeight / 3) {
mScroller.startScroll(
0, getScrollY(),
0, -dScrollY);
} else {
mScroller.startScroll(
0, getScrollY(),
0, -mScreenHeight - dScrollY);
}
}
break;
default:
break;
}
invalidate();
return true;
}
实现滑动代码
mScroller.startScroll(int startX, int startY, int dx, int dy);
invalidate();
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
// 实现自己的滚动业务, 比如:scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
......
postInvalidate();
}
}
6. 事件分发机制
触摸事件:捕获触摸屏幕后产生的事件。当点击一个按钮时,通常会产生三个事件——按钮按下,这是事件一;如果不小心滑一点,这是事件二;当手抬起,这是事件三。