Android控件架构
Android中的每个控件都会在界面占得一块矩形的区域,控件大致被分为两类:ViewGroup控件与View控件。ViewGroup左为父控件可以包含并管理多个View,让整个界面上的控件形成了一个树形结构(控件树),上层控件负责下层子控件的测量与绘制,并传递交互事件。通常在Activity使用的findViewById()方法,就是在控件树中以树的深度优先遍历来查找对应元素。
树的顶部有一个ViewParent对象,也就是整颗树的控制核心,所有交互管理事件都由它来统一调度和分配,从而控制整个视图(如图1所示)
图1 图2
在Activity中使用setContentView()设置一个布局,如图2所示,每个Activity都包含一个Window对象,在安卓中Window通常由PhoneWindow来实现,它将一个DecorView设置为整个窗口的根View,而它作为顶层视图,封装了一些窗口操作的通用方法。可以说,DecorView将要显示的具体内容呈现在了PhoneWindow上,所有View的监听事件,都通过WindowManagerService进行接收,并通过Activity对象来回调相应的onClickListener。在显示上,它将屏幕分为两类:TitleView 和ContentView,
ContentView:它是一个ID为:content 的 FrameLayout
通过以上过程,我们可以建立起这样的一个标准视图树,如图3:
视图树的第二层装载了一个LinearLayout,作为ViewGroup,这一层的布局结构会根据对应的参数设置不同的布局,如最常用的:上面显示TitleBar,下面是Content这样的,而如果我们通过设置requestWindowFeature(Window,FEATURE_NO_TITLE)来设置全屏显示,那布局就只剩下Content了,这就解释了为什么此方法要在 setContentView()之前调用才生效的原因。
图3所绘制的很粗略,当程序在onCreate方法中调用 setContentView 后,aActivityManagerService会回调onResume,此时系统才会把整个DecorView添加到PhoneWindow中,并让其显示,从而最终完成界面的绘制
View的测量与绘制
View测量
Android在绘制View之前,必须对View进行测量,告诉系统要画一个多大的View,整个过程是在 onMeasure() 方法中进行的
直译为:测量规格,MeasureSpec到底是干什么的?
通过它帮我们测量View,通常在ViewGroup中用到,它可以根据模式调节子View的大小。确切来说,MeasureSpec在很大程度上决定了View的尺寸规格,之所以说很大程度是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程,在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec测量View的宽高,注意,这里是测量的宽高,并不一定等于View的最终宽高。
MeasureSpec是一个32位的int值,高2位为测量的模式,低30位是测量的大小,使用位运算是为了提高效率
测量的模式可以分为一下三种:
1. EXACTLY [ɪg'zæk(t)lɪ]
精确值模式,控件宽或高指定为:“xx dp”或是match_parent时使用
2. AT_MOST
最大值模式,控件宽或高指定为:wrap_content时使用
3. UNSPECIFIED ['spesifaid]
这个属性比较奇怪,它不指定其大小测量模式,View想多大就多大,通常情况下在绘制自定义View时使用
View类默认的onMeasure方法只支持EXACTLY模式,如果想让自定义View支持wrap_content属性,就必须重写该方法来指定wrap_content时的大小,(设置默认大小)
有了这些信息,我们就可以控制View最后显示的大小了。
int widthSpec = MeasureSpec.makeMeasureSpec(menuView.getLayoutParams().width, MeasureSpec.EXACTLY); menuView.measure(widthSpec, heightMeasureSpec);
查看onMeasure的源码,发现最终会调用setMeasureDimension方法将测量后的宽高值设置进去。
在重写onMeasure中调用了自定义的方法,分别对宽和高进行重新定义,模板代码如下:
setMeasuredDimension(
measuredWidth(widthMeasureSpec), measuredHeight(heightMeasureSpec)); private int measuredWidth(int widthMeasureSpec) { int result = 0; int specMode = MeasureSpec.getMode(); int specSize = MeasureSpec.getSize(); if (specMode == MeasureSpec.EXACTLY) {
//如果为精确值模式,使用指定的size即可 result = specSize; } else { result = 200; if (specMode = MeasureSpec.AT_MOST) {
//取出指定的大小与specSize中最小的一个来作为最后的测量值。 result = Math.min(result, specSize); } } return result; }
不懂得可以看看这个:http://zhujiao.iteye.com/blog/1851689
View绘制
当测量好一个View之后,我们就可以简单的重写onDraw方法,并在Canvas上绘制所需要的图形。
Canvas:要想在界面中绘制相应的图像,就必须在Canvas上绘制,它就像一个画板,使用paint就可以作画了,
Canvas canvas=new Canvas( bitmap)
之所以传入一个Bitmap,是因为需要用来存储所有绘制在Canvas上的像素信息的。这个过程称为装载画布。所以当通过这种方式创建的Canvas对象后,调用的所有Canvas.drawXXX方法都发生在这个bitmap上了
ViewGroup的测量与绘制
在前面的分析中说了,ViewGroup会去管理其子View,其中一个管理项目就是负责子View的显示大小,当ViewGroup的大小为wrap_content时,ViewGroup就需要对子View进行遍历,以便获得所有子View的大小,从而决定自己的大小,
通过遍历子View,并调用measure 方法获得每一个的测量结果
当测量结束后,就需要将子View放置到合适的位置,这个过程就是View的Layout过程,同样使用遍历来调用子View的Layout方法,指定显示位置,从而决定其布局位置
在自定义ViewGroup时,通常会去重写onLayout方法来控制子View显示的逻辑,同样,如果支持wrap_content属性,那么必须重写onMeasure方法,这点与View是相同的
ViewGroup的绘制
通常情况下不需要绘制,因为本身没有需要绘制的东西,如果不是指定了背景颜色,那连onDraw方法都不会被调用。但是,ViewGroup会使用dispatchDraw()方法来绘制其子View。
自定义View
在自定义View时,我们通常会重写onDraw来绘制显示内容,如果该View需要使用wrap_content,那么还必须重写onMeasure方法,另外,通过自定义attrs属性,还可以设置新的属性配置值
在View有一下一些比较重要的回调方法:
l onFinishInflate() : 从XML加载组件后回调
l onSizeChanged() : 组件大小改变时回调
l onMeasure() : 用于测量
l onLayout() : 回调该方法来确定显示的位置
l onTouchEvent() : 监听到触摸事件时回调
当然,创建自定义View时并不需要重写所有的方法,只需重写特定条件的回调即可
通常情况下,有以下三种方法来实现自定义的控件
l 对现有控件进行拓展
l 通过组合来实现新的控件
l 重写View来实现全新的控件
定义属性:
添加命名空间
xmlns:app="http://schemas.android.com/apk/res/com.example.shang.youkumenu" <com.example.shang.youkumenu.view.ToggleButton2 android:id="@+id/tb_switch" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" app:backgroundToggle="@mipmap/switch_background" app:slideToggle="@mipmap/slide_button_background" app:state="true" />
系统提供了TypedArray这样的数据结构来获取自定义属性集,后面引用的styleablede,就是在xml中通过declare-styleable 指定的name名,接下来通过TypedArray对象的getString、getColor等方法,就可以获取值
TypedArray typedArray = getContext().obtainStyledAttributes( attrs, R.styleable.ToggleButton2, defStyle, 0); currentState = typedArray.getBoolean(R.styleable.ToggleButton2_state, false); int backgroundToggle = typedArray.getResourceId(R.styleable.ToggleButton2_backgroundToggle, -1);
<resources> <declare-styleable name="ToggleButton2"> <attr name="state" format="boolean" /> <attr name="backgroundToggle" format="reference" /> <attr name="slideToggle" format="reference" /> </declare-styleable> </resources>
触摸事件
Android提供了一整套完善的事件传递,处理机制,来帮助开发者完成准确的事件分配与处理
在MotionEvent里封装了很多属性和方法,比如触摸点的坐标,通过getX方法和getRawX方法获得、通过不同的Action(MotionEvent.ACTION_DOWN….)来进行区分,并实现不同的逻辑。
如此看来,触摸事件还是很简单的,其实就是一个动作类型加坐标点而已,但是我们知道,Android中View的结构是树形的,View和ViewGroup是是嵌套形式存在的,可是触摸事件就一个,那就涉及到该分给谁来使用的问题了。
关于事件处理,有以下方法需要知道:
dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()
ViewGroup的级别比较高,比View多了一个方法:onInterceptTouchEvent()
它是事件拦截的核心方法,
事件传递的返回值很好理解true:拦截,不继续; false:不拦截,继续
事件处理的返回值也类似:true:处理了,不用审核了; false:给上级处理
初始情况下返回值都是false
MotionEvent:
l 常见的动作常量:
public staticfinal int ACTION_DOWN = 0;单点触摸动作
public staticfinal int ACTION_UP = 1;单点触摸离开动作
public static final int ACTION_MOVE =2;触摸点移动动作
public static final int ACTION_CANCEL = 3;触摸动作取消
public static final int ACTION_OUTSIDE = 4;触摸动作超出边界
public static final int ACTION_POINTER_DOWN = 5;多点触摸动作
public static final int ACTION_POINTER_UP = 6;多点离开动作
以下是一些非touch事件
public static final int ACTION_HOVER_MOVE = 7;
public static final int ACTION_SCROLL = 8;
public static final int ACTION_HOVER_ENTER = 9;
public static final int ACTION_HOVER_EXIT = 10;
l 通过以下常用方法进一步获取触控事件的具体信息:
event.getAction() //获取触控动作比如ACTION_DOWN
event.getPointerCount(); //获取触控点的数量,比如2则可能是两个手指同时按压屏幕
event.getPointerId(nID); //对于每个触控的点的细节,我们可以通过一个循环执行
getPointerId方法获取索引
event.getX(nID); //获取第nID个触控点的x位置
event.getY(nID); //获取第nID个点触控的y位置
event.getPressure(nID); //LCD可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的
event.getDownTime() //按下开始时间
event.getEventTime() // 事件结束时间
event.getEventTime()-event.getDownTime()); //总共按下时花费时间
ViewDragHelper
基本用法
- 在自定义ViewGroup的构造方法里调用ViewDragHelper的静态工厂方法create()创建ViewDragHelper实例
2. 实现ViewDragHelper.Callback
最重要的几个方法是tryCaptureView()、 clampViewPositionVertical()、clampViewPositionHorizontal()、 getViewHorizontalDragRange()、getViewVerticalDragRange()
l tryCaptureView()里会传递当前触摸区域下的子View实例作为参数,如果需要对当前触摸的子View进行拖拽移动就返回true,否则返回false。
l clampViewPositionVertical()决定了要拖拽的子View在垂直方向上应该移动到的位置,该方法会传递三个参数:要拖拽 的子View实例、期望的移动后位置子View的top值、移动的距离。返回值为子View在最终位置时的top值,一般直接返回第二个参数即可。
l clampViewPositionHorizontal()与clampViewPositionVertical()同理,只不过是发生在水平方向上,最终返回的是View的left值。
l getViewVerticalDragRange()要返回一个大于0的数,才会在在垂直方向上对触摸到的View进行拖动。
l getViewHorizontalDragRange()与getViewVerticalDragRange()同理,只不过是发生在水平方向上。
- 在onInterceptTouchEvent()方法里调用并返回ViewDragHelper的shouldInterceptTouchEvent()方法
- 在onTouchEvent()方法里调用ViewDragHelper()的processTouchEvent()方法。 ACTION_DOWN事件发生时,如果当前触摸点下要拖动的子View没有消费事件,此时应该在onTouchEvent()返回true,否则将收不 到后续事件,不会产生拖动。
5. 上面几个步骤已经实现了子View拖动的效果,如果还想要实现fling效果(滑动时松手后以一定速率继续自动滑动下去并逐渐停止,类似于扔东西)或者松手后自动滑动到指定位置,需要实现自定义ViewGroup的computeScroll()方法,方法实现如下:
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
postInvalidate();
}
}
6.
并在ViewDragHelper.Callback的onViewReleased()方法里调用settleCapturedViewAt()、flingCapturedView(),或在任意地方调用smoothSlideViewTo()方法。
7. 如果要实现边缘拖动的效果,需要调用ViewDragHelper的setEdgeTrackingEnabled()方法,注册想要监听 的边缘。然后实现ViewDragHelper.Callback里的onEdgeDragStarted()方法,在此手动调用 captureChildView()传递要拖动的子View。
Callback是理解ViewDragHelper的关键,因为它反映了开始移动到结束的监听过程.
ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { /** * 1.返回ture则表示可以捕获该view,你可以根据传入的第一个view参数决定哪些可以捕获 * @param child 被滑动的子View * @param pointerId 多点触摸的手指Id * @return true:可以被拖拽 */ @Override public boolean tryCaptureView(View child, int pointerId) { return true; } /** * 2.返回拖拽的范围,返回一个>0的值,决定了动画的执行时长,水平方向是否可以被滑动 * @param child 子控件 * @return mRange 范围 */ @Override public int getViewHorizontalDragRange(View child) { return mRange; } /** * 3. 可以在该方法中对child移动的边界进行控制,left , top 分别为即将移动到的位置,比如横向的情况下,我希望只在ViewGroup的内部移动,即:最小>=paddingleft,
最大& lt;=ViewGroup.getWidth()-paddingright-child.getWidth
* @param child 被拖拽的子View
* @param left 建议移动到的位置
* @param dx 与旧值的偏移量
* @return left 移动到的位置
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
/**
* 4.控件位置变化时调用 :伴随动画、状态更新、事件回调
* @param changedView 发生改变的子View
* @param left 水平方向最新的位置
* @param top ..
* @param dx 刚刚发生的偏移量
* @param dy ..
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
/**
* 5.松手之后调用的方法:结束动画
* @param releasedChild 被释放的子View
* @param xvel X轴速度
* @param yvel Y轴速度
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
};