13.View工作原理
用户通过触摸屏或键盘灯输入设备产生输入消息,该消息首先被消息处理前端转换为更明确的消息值。接下来,窗口管理系统WMS根据所有窗口的状态判断用户正在与哪个窗口进行交互,然后把该消息发送给当前窗口。因为窗口是由WMS创建的,因此WMS知道所有客户窗口的信息,包括窗口的大小、位置等。当消息到达时,如果是按键消息,则直接发送给当前窗口,如果是触摸消息,则WMS会根据消息的位置坐标去匹配所有的窗口,判断该坐标落到了哪个窗口区域中,然后把该消息发送给相应的窗口。最后,消息顺利到达目标窗口,至于目标窗口内部如何处理该消息则完全是窗口的“内政”。
View系统获得消息后,会按照默认的逻辑来派发消息,主要就是把该消息派发给所有的子视图,以便相应的子视图能够获得消息并执行不同的任务。
重绘界面的过程大致分为三步:
- 测量measure,即测量出所有视图的实际尺寸大小。并不是所有导致界面重绘的操作都需要重新计算窗口的大小,在View的内部逻辑中,使用了一个内部变量保存相应的状态,当用户的某个操作导致改变了视图大小时,会设置该变量,而View的内部逻辑会根据该变量,决定是否需要重新测量。
- 布局layout,即应该把视图放在屏幕的什么位置上。视图必须要有一个位置,该位置坐标相对于其父视图。不同视图的位置可以重叠,View系统本身并不限定子视图的位置。
- 绘制draw,把视图绘制到屏幕上,当系统获知了窗口中所有视图的大小和位置后,就可以完全确定屏幕上应该显示哪些视图了。在绘制时,系统内部为每一个窗口创建了一个Canvas对象,并把这个Canvas对象传递给从根视图到所有子视图。View系统将Canvas传递给子视图时,都先将对该画布进行一次裁剪clip,从而在子视图看来,总是从画布的(0,0)坐标开始绘制。
View的dispatchPointerEvent方法
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//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;
}
}
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
View可以通过setOnTouchListener来设置一个Event的监听者,这样当事件来临时,View会主动调用这个listener来告知对方。从优先级上看,这种方式较下面的onTouchEvent先行处理。
如果用户没有指定OnTouchListener,或者flags中指明被disabled,又或者onTouch的返回值为false,那么系统会将event传递给onTouchEvent。
View在disabled的情况下,同样会消耗这个事件,只是不做出任何回应而已。
在DOWN事件的处理中,setPressed用于指示View对象是否要进入pressed状态。同时,View开始监测它会不会演变成长按事件。可以通过ViewConfiguration.getLongPressTimeout()来取得长按事件的产生标准,然后postDelayed一个CheckForLongPress的Runnable,在timeout后(在timeout之前如果有ACTION_UP或者ACTION_CANCEL产生,会通过removeLongPressCallback移除这个Runnable)系统就需要进行长按事件的处理。如果设置了setOnLongClickListener设置了响应的函数,那么就会回调这些函数。
ACTION_UP是手势操作的结束点,除了改变View的一系列状态外,它的另一个重要操作就是判断是否会产生Click,即setOnClick所要响应的事件。当然,并不是任何ACTION_UP都对应一个click,比如前面如果已经产生了长按事件,或者当前不是pressed状态等情况都会阻碍click的形成。
ACTION_CANCEL比较特殊,它并不由用户主动产生,而是系统在谨慎判断后得出的结果。这个事件说明当前的手势已经被废弃,后续不会再有任何与该手势相关联的事件产生。开发人员可以把它看成手势的结束标志,类似于ACTION_UP,并做好清理工作。
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
一旦收到ACTION_DOWN,程序就会清理以前的状态。
如果是DOWN事件,或者mFirstTouchTarget不为空,又分为两种子情况
- disallowintercept为false,ViewGroup允许拦截,此时就可以进一步调用intercepted = onInterceptTouchEvent来由后者判断是否要真正执行拦截了。disallowintercept为true,说明ViewGroup不允许拦截,intercepted = false
- 如果这不是DOWN事件,而且在之前的判断中mFirstTouchTarget为空,那么intercepted为true,表示ViewGroup选择继续拦截事件。
当onInterceptTouchEvent返回false,说明当前的ViewGroup并没有截留这个事件,所以它需要继续往下传输,直到有人接收并处理它;同时,后续的事件还会源源不断地调用onInterceptTouchEvent进行判断。而当onInterceptTouchEvent返回true时,说明当前的ViewGroup需要截留这个事件以供内部处理。此时意味着这个事件以及后续的事件直到ACTION_UP前都会直接投递到ViewGroup的onTouchEvent中,而不会往下传递。
在一般情况下,导致View树重新遍历的原因主要有三个,一个是视图本身内部状态变化引起重绘,第二个是View树内部添加或者删除了View,最后一个是View本身的大小及可见性发生变化。
在View视图中定义了多种和界面效果有关的状态,比如拥有焦点focus,按下pressed等,不同的状态一般会显示不同的界面效果,有多种操作会引起这些状态的改变。Android中应用程序时按照消息机制执行的,每次处理一个消息。如果该消息引起状态改变,则代码中仅做一些状态标识,然后发送一个异步消息,而不是立即重绘。然后在下一次消息处理中,根据保存的状态数据,绘制不同的界面效果。
能引起View树重新遍历的操作总的来讲可以分为三类。一类是导致视图大小发生变化,第二类是导致ViewGroup重新为子视图分配位置,第三类是视图显示情况发生变化需要重绘。这三种情况最后都直接或间接调用到三个函数,分别为invalidate(),requestLayout()及requestFocus(),而这三个函数最终都会调用到ViewRootImpl的scheduleTraversals(),该函数然后发起一个异步消息,消息处理中调用performTraversals()开始对整个View树进行重新遍历。
能导致调用invalidate()的包含三种情况:当应用程序改变视图显示属性时,调用setVisibility();当改变视图selected状态时,调用setSelected();当改变视图enable状态时,调用setEnable()函数。
导致调用requestLayout()函数的情况包含两种:当应用程序改变视图显示属性时,调用setVisibility(),由于显示或者不显示将影响其他兄弟视图的位置,因此会调用到requestLayout();第二种是应用程序直接或间接调用该函数,间接调用是指应用程序调用了View类的其他函数,从而间接调用到requestLayout().
requestFocus()一般由程序直接调用,间接调用是指当用户按“上/下”,“左/右”键时,相关的处理逻辑会间接调用到该函数。
应用程序可以调用ViewGroup的setAddStatesFromChildren()函数将该ViewGroup的背景图设置为和子视图的背景图同步。
绘制流程中,首先绘制最底层的根视图,然后再绘制其包含的子视图。子视图或者是一个ViewGroup,或者是一个View,如果是ViewGroup,则继续再绘制ViewGroup内部的子视图,绘制过程一般并不会对所有视图进行重绘,而仅绘制那些“需要重绘”的视图。那么,什么是“需要重绘”的视图呢?View类内部变量mPrivateFlags中包含了一个标志位DRAWN,当该视图需要绘制时,就给mPrivateFlags中添加该标识位。函数invalidate()的作用就是要根据所有视图中的该标志位计算具体哪个区域需要重绘,这个区域将用一个矩形标识,并最终将这个矩形存放到ViewRootImpl类中的mDirty变量中,之后的重绘过程将重绘所有包含在该mDirty区中的视图。
invalidate()大致做了两件事情,其一是给所有需要重绘的视图添加了一个DIRTY或者DIRTY_OPAQUE标记;其二是通过矩阵计算,找到真正需要重绘的矩形区,并将其保存在了ViewRootImpl类中的mDirty变量中。有了这两个信息,View树重绘就能够决定通知哪些View进行重绘,并告诉它们应该重绘什么区域。
和invalidate()的调用有点类似,requestFocus()也是不能独立完成的,当一个视图想要获取焦点时,必须请求它的父视图完成该操作,为什么呢?因为父视图知道当前哪个视图正在拥有焦点,如果要进行焦点切换,则必须先告诉原先的视图放弃焦点,而这些操作所需要的信息是在父视图中保存的,所以requestFocus()也必须由父视图完成。
invalidate()最终会调到ViewRootImpl中的invalidateChildInParent,requestFocus会调到ViewRootImpl中的requestChildFocus.
requestLayout首先给mPrivateFlags添加FORCE_LAYOUT标识,然后调用mParent.requestLayout()函数。对于一个具体的View对象而言,其父视图要么是一个ViewGroup实例,要么是一个ViewRootImpl实例,而前者并没有对该函数重载。也就是说,ViewGroup会按照基类View中的该函数进行处理,如果有多层视图嵌套,这就会产生一个递归调用,并最终调用到ViewRootImpl类的requestLayout()函数。
fitSystemWindow()函数的作用是告诉窗口中所有子视图根据该Inset调整自己的布局,实际上就是用inset大小改变视图的mPaddingXXX的值。
host.measure()会调用到View类的measure()函数,该函数然后回调onMeasure()。在一般情况下,host对象是一个ViewGroup实例,该ViewGroup会重载onMeasure(),当然如果host没有重载onMeasure(),则会执行View类中默认的onMeasure()。在一般情况下,程序员需要在重载的onMeasure()函数中逐一对所包含的子视图执行measure()操作,为了简化程序设计,ViewGroup类内部提供了measureChildWithMargin()函数,该函数内部会进行一定的参数调整,然后再次调用子视图的measure()函数,这就又调用onMeasure()。如果子视图是一个ViewGroup实例,则应该继续调用measureChildWithMargin()对下一层的子视图进行measure操作;如果子视图就是一个具体的View实例,则在重载的onMeasure()函数内部就不需要再次调用measureChildWithMargins(),从而一次measure()过程结束。
LinearLayout中的layout_height扮演了两个角色,一个是和LinearLayout的父视图一起对LinearLayout本身进行measure操作;另一个角色是作为TextView的measureSpec,并和TextView中的layout_height一起对TextView进行measure操作。
ViewGroup.getChildMeasureSpec(int spec, int padding, int childDimension)中spec是父视图提供的spec,padding代表的是在父视图提供的spec尺寸中,已经使用了多少,该值包含了padding填充,margin空余,以及父视图所提供的已经使用的高度heightUsed:mPaddingTop+mPaddingBottom+lp.topMargin+lp.bottomMargin+heightUsed,参数childDimension即为子视图期望获得的高度,其值来源于lp.height,这个值正是xml文件中android:layout_height对应的值,可能是一个具体的值,也可能是MATCH_PARENT或者WRAP_CONTENT。
最初调用layout()函数是从ViewRootImpl类的performTraversals(),host是一个View对象,View类中layout()函数的类型是final,其子类不能重载该函数,以保证View系统中layout过程不变。在layout()函数中,首先调用setFrame()函数给当前视图设置参数中指定的位置,然后回调onLayout()函数。ViewGroup类中重载了onLayout()函数,并且将其函数类型设置成了一个abstract类型,因此,所有的ViewGroup实例必须实现onLayout()。View系统希望程序员能够在onLayout()函数中对该视图所包含的子视图进行layout操作,当该视图是ViewGroup时,程序员一般会在onLayout()函数中调用子视图child的layout()方法,从而完成对子视图的位置分配。当然,如果子视图也是一个ViewGroup实例,就又会调用相应的onLayout()函数。
绘制过程主要是把View对象绘制到屏幕上,并且如果该View是一个ViewGroup,则需要递归绘制该ViewGroup中所包含的所有子视图。
总的来说,绘制元素包含4个:
- View背景
- 视图本身的内容
- 渐变边框Fading Edge,渐变边框的作用是为了让视图的边框看起来更有层次感,其本质是一个Shader对象。
- 滚动条Scroll Bar,与PC上的滚动条不同,Android中的滚动条仅仅是显示滚动的状态,而不能被用户直接下拉。
绘制完界面内容后,如果该视图内部还包含子视图,则调用dispatchDraw()函数,ViewGroup重载了该函数。因此,实际调用的是ViewGroup中的dispatchDraw()函数,dispatchDraw()内部会在一个for循环内,循环调用drawChild()分别绘制每一个子视图,而drawChild()内部又会调用draw()函数完成子视图的内部绘制工作。当然,如果子视图本身也是一个ViewGroup,就会递归执行以上流程。
draw fading edge时scrollabilityCache.setFadeColor(solidColor);可以重载View的getSolidColor来设置渐变色。
dispatchDraw()中drawChild()的核心过程是为子视图分配合适的Canvas剪切区,剪切区的大小取决于child的布局大小,剪切区的位置取决于child的内部滚动值及child内部的当前动画。
滚动条包含垂直滚动条和水平滚动条,滚动条本身是一个Drawable对象,View类内部使用ScrollBarDrawable类表示滚动条,View可以同时绘制水平滚动条和垂直滚动条。
在ScrollBarDrawable类中,包含三个基本尺寸,分别是range,offset,extent
- range:代表该滚动条从头到尾滚动中所跨越的范围有多大
- offset:代表滚动条当前的偏移量
- extent:代表该滚动条在屏幕上的实际高度
有了以上三个尺寸后,ScrollBarDrawable内部就可以计算出滚动条的高度及滚动条的位置。
除了以上尺寸外,ScrollBarDrawable类内部还包含两个Drawable对象,一个标识滚动条的背景,另一个标识滚动条自身。这两个Drawable对象分别是track和thumble,水平方向和垂直方向分别有两个该Drawable对象。
在View的draw()函数中,会判断当前视图是否包含动画,如果包含,就根据动画的参数对当前View做一定的图形变换,比如缩放,平移等,然后将变换后的图像绘制到屏幕上。绘制完后,再发起一个重绘的消息,就这样连续绘制,直到动画参数指示动画结束。
从效果的角度看,View系统中包含的动画可以分为三类,分别是窗口动画,视图动画,布局动画。
- 窗口动画:指窗口对应的动画。窗口可以是一个Activity对应的窗口,也可以是一个对话框对应的窗口,还可以是应用程序调用WindowManager类的addView()函数添加的任意窗口。窗口动画一般定义了窗口在显示,消失时的动画
- 视图动画:指View对象在显示及消失时对应的动画,它影响的事视图自身的动画效果。
- 布局动画:指ViewGroup对象包含的动画。该动画定义了ViewGroup中子视图第一次显示时的动画,它影响的是ViewGroup中子视图的整体动画效果,其本质过程是根据布局动画为子视图分别设置不同的动画,从而使得整体上看来像是一个布局动画的效果。所以说,布局动画仅仅是一个概念。
View系统中动画的设计思路如下:首先要有yi'g额动画主体,实际上就是一个View对象,然后可以为该View对象指定一个动画。动画使用一个Animation类来表示,当View要开始动画时,从Animation类中获取动画的参数,并根据这些参数对View进行图形变换,然后将变换后的图形绘制到屏幕上。Animation类中会保存动画的起始时间,并且在动画开始后,Animation会在不同的时间返回不同的动画参数,从而使得View在随后的时间中会变换出不同的图像。View系统会连续从Animation中取出动画参数,并将变换后的图像绘制到屏幕上,直到动画结束。而对用户来讲,这个过程感觉上就是View在变换,也就是“动画”。
动画设计中还包含一个重要数据类Transformation,正如其名称所指,该数据类保存了Animation中的“动画参数”。Transformation类用一下变量进行保存:
- Matrix mMatrix:该矩阵变量中保存了旋转,缩放,移动,扭曲相关的变换参数。
- float alpha:该变量保存了颜色阿尔法通道的值。
ViewGroup的drawChild()函数内部会使用动画参数对canvas进行一定的变换,即判断transformToApply是否为空,如果不为空,则用该对象中的Matrix对canvas进行变换。
动画执行过程中的消息处理逻辑有以下特点:
- 动画被变换后尽管显示的大小发生了变换,但其在消息处理过程中所占据的窗口大小却并没有改变,因此,在动画的运行过程中,该视图依然会获取用户消息。
- 由于动画过程是逐个绘制子视图,因此,后面子视图的动画绘制区域可能覆盖或部分覆盖前面子视图动画绘制区域。
ViewGroup中dispatchDraw()中布局动画。dispatchDraw()函数首先会获得该ViewGroup中包含的布局动画参数,然后根据该参数逐个设置子视图的动画参数,最后调用drawChild()函数将子视图绘制到屏幕上。
LayoutParams中LayoutAnimationController.AnimationParameters layoutAnimationParameters