自定义View##
作者:桂志宏
在Android中做界面是一件较为轻松的事情,例如想要添加一个按钮,你可以用xml也可以在代码中设置相关的属性,下面是用xml添加一个按钮的脚本
<Button
android:id="@+id/buttonDemo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:layout_gravity="center_horizontal"/>
上述代码就能在界面上添加一个按钮,如下
上面添加的是Android原生态的组件,显然这种界面不太好看,对用户不友好。平时我们用各种app时,会看到很多很漂亮的界面甚至有一些炫酷的效果,而原生态的组件是做不到这种效果,要实现各式各样的精美的界面可就没那么容易了。这时就需要我们自定义View才能实现各种美观的界面,因此我们首先要先知道Android的View是如何工作的,为此,本文详细讨论View的工作原理,在讨论View之前,需要介绍一下Android的事件体系,因为View就是用来和用户交互的,当用户点击app上的某个按钮,这就是一个触摸事件,这时app需要作出相应的响应,从而不断更新界面。
事件体系
事件
当用户触摸屏幕,随即触发触摸事件序列,一个完整的事件序列一般由如下事件组成:
- ACTION_DOWN事件:用户点击屏幕触发
- ACTION_MOVE事件:用户在屏幕上滑动触发
- ACTION_UP事件:用户手指离开屏幕触发
其中一个事件序列可以有若干个ACTION_MOVE事件(或0个,即Click),但是一定会有1个ACTION_DOWN和ACTION_UP.
当触发了触摸事件序列,它是如何分发呢?
事件分发
触摸事件首先传递给当前Activity,由Activity的dispatchTouchEvent方法进行事件分发,事件分发给Window(抽象类,唯一实现为PhoneWindow),Window进一步分发事件给DecorView,而DecorView继承FrameLayout,它是所有组件的顶级View,更准确地说,DecorView是ViewGroup(DecorView一般为竖直方向的Linearlayout,包括titlebar和content,所有的控件以及View都是显示在content中),接着DecorView将事件分发给它的子View.上述过程可由下图表示
当触摸事件传递给DecorView(ViewGroup)后,它如何分发给相应的子View?
事件分发涉及如下三个重要的方法:
protected boolean dispatchTouchEvent(MotionEvent ev)
只要触摸事件分发到当前View,必定会调用该View的上述方法。该方法返回true,表示事件被该View或者其子View处理;返回false,表示该View和其所有的子View都没有处理该事件,换句话说该View及其所有的子View的onTouchEvent()方法都返回false,这时,事件会向父View传递,请求父View处理。
protected boolean onInterceptTouchEvent(MotionEvent ev)
决定当前View是否拦截触摸事件,在dispatchTouchEvent中被调用,返回true,表示拦截触摸事件,然后事件交给当前View的onTouchEvent方法处理;返回false,表示不拦截触摸事件,事件进一步分发给其子View。
protected boolean onTouchEvent(MotionEvent ev)
对当前View的触摸事件进行处理,返回true,表示事件被消耗,返回false,表示事件未被处理,因此调用其父View的onTouchEvent方法进行处理。
这里需要指出的是onTouch和onClick
当View设置了OnClickListener(),则onClick方法就会被调用,当View设置了OnTouchListener,则onTouch方法就会被调用,当View同时设置了OnClickListener和OnTouchListener,onTouch方法会在onClick方法之前被调用,如下图
我们可以从View的源码验证这一点,下面是View的dispatchTouchEvent方法的部分代码:
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
}
由源码可知,该方法会先检查onTouchListener是否实例化并进一步调用onTouch方法,接着去调用onTouchEvent方法,这里需要注意一点,如果OnTouchListener实例化并且其onTouch方法返回true,由于result为true,因此onTouchEvent方法不会被调用,从而被屏蔽,而onClick方法是在onTouchEvent方法中执行的(可以查看View的onTouchEvent方法源码),因此onClick方法也会被屏蔽。 总之,onTouch的优先级大于onTouchEvent,onTouchEvent方法的优先级又大于onClick
View和ViewGroup的工作原理
View主要有三个工程,measure,layout和draw,其中measure是测量View的宽和高,layout是将View放置在相应的位置,而draw是将相应的View绘制出来。
Measure
测量过程调用View的measure方法,如下
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
进一步,在measure中调用onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
可知,View的meausre方法是final类型,子类不能重写,自定义View只用重写OnMeasure方法.
View的onMeasure源码逻辑如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
由源码可知,onMeasure方法的参数是View的宽和高的MeasureSpec对象,其中setMeasuredDimension方法用来保存当前View测量的宽高,onMeasure方法中测量完View的宽和高,必须调用setMeasuredDimension方法,否则会抛出异常
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
其参数就是当前View的测量的宽和高。上面onMeasure方法中setMeasuredDimension方法的两个参数是getDefaultSize方法的返回值,该方法的源码如下
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
该方法的逻辑很简单,当测量模式为UNSPECIFIED,返回size,而这个size是系统设定的最小宽度或View背景最小宽度(getSuggestedMinimumWidth方法逻辑,这里就不展开其源码),当测量模式为ATMOST(wrap_content)或EXACTLY(match_parent)时,这时就返回View测量的值。
上面给出了View的测量过程,而ViewGroup(继承View)不仅需要测量自己的大小,还得测量子View的大小。由于不同ViewGoup子类有不同的布局方式(例如LinearLyaout和HorizontalScrollView等),因此其测量的方式不同,故ViewGroup中没有实现onMeasure方法,其不同子类有不同的实现方法。
由于ViewGroup不仅需要测量自身的大小,还要测量子View的大小,因此ViewGroup中提供如下方法逻辑来测量子View
/* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
上述代码逻辑很清晰,逐个测量子View的大小,其中measureChild方法的源码如下:
/ * @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上述代码的逻辑是根据父View的MeasureSpec来确定子View的MeasureSpec,利用方法getChildMeasureSpec返回相应子View的MeasureSpec,然后进入子View的measure过程(上面已详述View的测量过程),其中方法getChildMeasureSpec其源代码较长,如下:
/* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述方法的源代码的逻辑是根据当前View的MeasureSpec(参数spec)和Padding(参数padding)得到其剩余的空间,同时由当前View的子View请求的大小(高度或宽度的值,参数childDimension)来确定子View实际分配的尺寸大小以及其相应的测量模式,最后返回子View的MeasureSpec对象。 这里提一句,ViewGroup在measure时,一定记得处理padding和margin属性,因为涉及到分配给子View的空间大小,下图是padding和margin的关系
需要说明的是,上述是View和ViewGroup的measure过程的总体思想,对于具体的View或ViewGroup类,需要覆盖其中的测量方法,一般覆盖onMeasure方法,实现具体的测量细节
Layout
当测量完View的尺寸后,接下就要将View放置在相应的位置上,调用layout方法,对ViewGroup而言,调用layout是确定当前View的四个顶点,也即确定当前View在其父容器的位置,接着在layout中调用onLayout方法来确定其子View在当前View中位置,和onMeasure一样,onLayout具体实现和具体布局相关,因此不同的子View需要重写onLayout方法来确定其子View在其中的位置排布。我们以LinearLayout为例,其onLayout方法实现如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
由上述源码,onLayout首先确定线性布局是竖向还是横向的,然后调用相应的方法来放置其子View,具体的代码这里就不做展开
Draw
当将View放置在相应的位置后,接下来就是将它们画出来.具体形状的绘制这里没什么好说的,就是几何图形计算。其中几个比较重要的的接口是Canvas,Paint和Path,Canvas相当于画板,Paint相当于画笔,Path是绘制路径。关于Path的适用参考网址: http://blog.csdn.net/leejizhou/article/details/51565057
下面是一个自定义View,实现和ScrollView相似的功能。