坐標系
- Android中有兩種坐標系,Android坐標系和視圖坐標系
Android坐標系
- 定義:屏幕左上角頂點為Android坐標系原點,向右為X軸正方向,向下為Y軸正方向;
- MotionEvent提供的getRawX()和getRawY()獲取的坐標都是Android坐標系的坐標;
視圖坐標系
- View獲取自身寬高:getWidth(),getHeight();
- View自身坐標(View到其父控件原點的距離):getTop(),getLeft(),getRight(),getBottom()
- MotionEvent獲取焦點坐標:
getX():觸摸點到控件左邊的距離,即視圖坐標
getY():觸摸點到控件頂邊的距離,即視圖坐標
getRawX():觸摸點到屏幕左邊的距離,即絕對坐標
getRawY():觸摸點到屏幕頂邊的的距離,即絕對坐標
View的滑動
- 基本原理:觸摸事件傳到View時,系統(tǒng)記下觸摸點坐標,手指移動時系統(tǒng)記下移動后的坐標并算出偏移量,以此修改View的坐標。
實現(xiàn)View滑動的方法
1. layout()
- view繪制時會調(diào)用onLayout()來設(shè)置顯示的位置,因此可以通過修改View的left、top、right、bottom屬性來控制View的坐標。
private int lastX;
private int lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
//獲取到手指處的橫坐標和縱坐標
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//計算移動的距離
int offsetX = x - lastX;
int offsetY = y - lastY;
//調(diào)用layout方法來重新放置它的位置
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
break;
}
return true;
}
2. offsetLeftAndRight()與offsetTopAndBottom()
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
3. 改變布局參數(shù)LayoutParams
- LayoutParams保存了View的布局參數(shù)
- LinearLayout和RelativeLayout的LayoutParams都繼承自ViewGroup.MarginLayoutParams
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams layoutParams= (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
4. scollTo與scollBy
- 移動的是View的內(nèi)容,如果在ViewGroup中使用則是移動他所有的子View;
- scollBy(dx,dy)表示移動的增量為dx、dy;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
- scollBy最終也是調(diào)用scollTo
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
- scollTo(x,y)表示移動到一個具體的坐標點;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollTo(-offsetX + ((View) getParent()).getScrollX(),
-offsetY + ((View) getParent()).getScrollY());
break;
5. Scroller
- scollTo/scollBy過程是瞬發(fā)的,用戶體驗不好,可以使用Scroller來實現(xiàn)有過度效果的滑動
- Scroller本身不能實現(xiàn)View的滑動,需要配合View的computeScroll()
public class MyView extends View {
private Scroller mScroller;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
private int lastX;
private int lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
//獲取到手指處的橫坐標和縱坐標
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
mScroller.startScroll(((View) getParent()).getScrollX(),
((View) getParent()).getScrollY(), -offsetX, -offsetY, 2000);
invalidate();
break;
}
return true;
}
}
6. 動畫
private int firstX;
private int firstY;
@Override
public boolean onTouchEvent(MotionEvent event) {
//獲取到手指處的橫坐標和縱坐標
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (firstX == 0 && firstY == 0) {
firstX = x;
firstY = y;
}
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - firstX;
int offsetY = y - firstY;
ObjectAnimator.ofFloat(this, "translationX", offsetX).setDuration(0).start();
ObjectAnimator.ofFloat(this, "translationY", offsetY).setDuration(0).start();
//translationX和translationY:作為增量控制View對象從他的布局容器的左上角開始位置。
//rotation、rotationX、rotationY:這三個屬性控制View對象圍繞它的支點進行2D和3D旋轉(zhuǎn)
//PrivotX和PrivotY:控制View對象的支點位置,圍繞這個支點進行旋轉(zhuǎn)和縮放變換處理。默認該支點位置就是View對象的中心點。
//alpha:透明度,默認是1(不透明),0代表完全透明
//x和y:描述View對象在它容器中的最終位置,它是最初的做上角坐標和translationX,translationY值的累計的和
break;
default:
break;
}
return true;
}
- 如果一個屬性沒有g(shù)et,set方法,也可以通過自定義一個屬性類或則包裝類來間接地給這個屬性增加get和set方法。
private static class MyView{
private View mTarget;
private MyView(View mTarget){
this.mTarget=mTarget;
}
public int getWidth(){
return mTarget.getLayoutParams().width;
}
public void setWidth(int width){
mTarget.getLayoutParams().width=width;
mTarget.requestLayout();
}
}
MyView mMyView=new MyView(mButton);
ObjectAnimator.ofInt(mMyView,"width",500).setDuration(500).start();
- ValueAnimator:不提供任何動畫效果,它更像一個數(shù)值發(fā)生器,用來產(chǎn)生一定規(guī)律數(shù)字,通常在ValueAnimator的AnimatorUpdateListener中監(jiān)聽數(shù)值的變化,從而完成動畫的變換
ValueAnimator mValueAnimator=ValueAnimator.ofFloat(0,100);
mValueAnimator.setTarget(view);
mValueAnimator.setDuration(1000).start();
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Float mFloat=(Float)animation.getAnimatedValue();
}
});
}
View的事件分發(fā)機制
- 點擊屏幕,就產(chǎn)生了觸摸事件,這個事件被封裝成了一個類:MotionEvent。而當這個MotionEvent產(chǎn)生后,那么系統(tǒng)就會將這個MotionEvent傳遞給View的層級,MotionEvent在View的層級傳遞的過程就是點擊事件分發(fā)
ViewGroup.dispatchTouchEvent
- 總結(jié):dispatchTouchEvent負責(zé)處理事件的分發(fā),會先檢查是否遮擋,然后重置之前觸摸事件的遺留數(shù)據(jù),然后判斷是否需要攔截,需要就調(diào)用onInterceptTouchEvent,然后判斷是否取消,如果不取消不攔截,檢查子view有沒有獲得焦點的,然后遍歷子view并把事件優(yōu)先給獲得焦點的view處理,會根據(jù)child是否為空判斷是調(diào)用自己的view.dispatchTouchEvent還是child的dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
//檢查是否分發(fā)本次事件:檢查是否設(shè)置了被遮擋時不處理觸摸事件的flag && 該事件的窗口是否被其它窗口遮擋
if (onFilterTouchEventForSecurity(ev)) {
//獲取事件類型
final int action = ev.getAction();
//actionMasked能夠區(qū)分出多點觸控事件
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
//清理和重置之前觸摸事件的各種標志和TouchTarget觸摸目標鏈表
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 檢查是否攔截這個TouchEvent
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//檢查是否不允許攔截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//如果允許攔截就調(diào)用onInterceptTouchEvent
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 防止Event中途被篡改
} else {
//有FLAG_DISALLOW_INTERCEPT標志就不進行攔截
//如果子View在ACTION_DWON時處理了事件,可以通過requestDisallowInterceptTouchEvent(true)來禁止父View攔截后續(xù)事件
//可以用來解決滑動沖突問題
intercepted = false;
}
} else {
//如果不是ACTION_DOWN事件,或者沒有TouchTarget,ViewGroup就直接攔截了
intercepted = true;
}
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 標識是否需要取消
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
//檢查父View是否支持多點觸控,即將多個TouchEvent分發(fā)給子View
//可通過setMotionEventSplittingEnabled()可以修改這個值
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//判斷是否要給子View分發(fā)事件:沒有攔截和取消
if (!canceled && !intercepted) {
//檢查TouchEvent是否可以觸發(fā)View獲取焦點,
// 如果可以,查找本View中有沒有獲得焦點的子View,有就獲取它,沒有就為null
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//判斷是否ACTION_DOWN,或者支持多點觸控且ACTION_POINTER_DOWN,或者懸停啥的(鼠標)
//說明一個事件流只有一開始的DOWN事件才會去遍歷分發(fā)事件,后面的事件將不再通過遍歷分發(fā),而是直接發(fā)到觸摸目標隊列的View中去
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;
// 清理之前觸摸事件中的目標
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;//子View數(shù)量
//第一個點的Down事件newTouchTarget肯定為null
if (newTouchTarget == null && childrenCount != 0) {
final float x = isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y = isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// 將所有子View放到集合中,按照添加順序排序,但是受到Z軸影響
//如果ViewGroup的子View數(shù)量不多于一個,為null
//如果ViewGroup的所有子View的z軸都為0,為null
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
//檢查ViewGroup中的子視圖是否是按照順序繪制,其實就是不受z軸影響
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;//按照View添加順序從前往后排的
//從后往前遍歷子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
// 如果preorderedList不為空,從preorderedList中取View
// 如果preorderedList為空,從mChildren中取View
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 如果當前已經(jīng)有View獲得焦點了,后面的觸摸事件會優(yōu)先傳給它
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//檢查View是否顯示或者播放動畫以及TouchEvent點是否在View內(nèi)
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);//再次重置View
//將事件傳給子View,看子View有沒有消費,消費了執(zhí)行if中邏輯,并結(jié)束循環(huán)
//其中會根據(jù)child是否為空判斷是調(diào)用自己的dispatchTouchEvent還是child的dispatchTouchEvent
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//子View處理了本事件,那么接著會創(chuàng)建一個TouchTarget,并且關(guān)聯(lián)該子View,
//后續(xù)的觸摸事件就會通過這個TouchTarget取出子View,直接把事件分發(fā)給它
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;//標記已經(jīng)有子View消費了事件
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
// 處理多點觸控
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//如果Down事件沒有子View處理,mFirstTouchTarget會為null,
//那么把事件分發(fā)給ViewGroup自己的dispatchTransformedTouchEvent()處理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
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;
}
}
// 還原狀態(tài)
if (canceled || actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
- dispatchTransformedTouchEvent方法代碼如下
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
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;
}
...
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
// 如果沒有子view,調(diào)用自己的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
//根據(jù)滾動值計算觸摸事件的偏移位置
event.offsetLocation(offsetX, offsetY);
//讓子View處理事件
handled = child.dispatchTouchEvent(event);
//恢復(fù)TouchEvent坐標到原來位置,避免影響后面的流程
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
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;
}
View.dispatchTouchEvent
- dispatchTransformedTouchEvent中的super.dispatchTouchEvent,因為ViewGroup繼承自View,所以調(diào)用的是View的dispatchTouchEvent,OnTouchListener的優(yōu)先級要比onTouchEvent()要高,如果我們設(shè)置了OnTouchListener并且onTouch()方法返回true,則onTouchEvent()方法不會被調(diào)用,否則則會調(diào)用onTouchEvent()方法,
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//OnTouchListener不為null并且onTouch()方法返回true,
//表示事件被消費,也不會再執(zhí)行onTouchEvent(event)
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//OnTouchListener為空則調(diào)用onTouchEvent消費事件
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
ViewGroup.onInterceptTouchEvent
- 用來進行事件的攔截,在ViewGroup.dispatchTouchEvent()中調(diào)用,僅ViewGroup中有此方法,默認返回false,不進行攔截
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
View.onTouchEvent(MotionEvent ev):
- 用來處理點擊事件,在View.dispatchTouchEvent()方法中進行調(diào)用,上面view的移動就是在這個方法中實現(xiàn)的,onTouchEvent默認返回true,除非它是不可點擊的也就是CLICKABLE和LONG_CLICKABLE都為false,如果設(shè)置了OnClickListener會執(zhí)行它的onClick()方法
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;
...
//如果可點擊clickable
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
//performClickInternal中調(diào)用performClick,
//performClick中會調(diào)用mOnClickListener.onClick
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
..
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
從View體系看Activity的構(gòu)成
- Activity的onCreate中會調(diào)用setContentView加載布局文件
繼承Activity
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
- 調(diào)用了getWindow().setContentView,其中g(shù)etWindow返回的mWindow是在activity的attach方法中初始化的,實際類型是PhoneWindow,其setContentView方法如下
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
- 其中調(diào)用了installDecor,其中調(diào)用generateDecor初始化了mDecor,這個DecorView就是Activity中的根View,繼承了FrameLayout
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(-1);//return new DecorView...
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
- 其中g(shù)enerateDecor調(diào)用new DecorView創(chuàng)建了DecorView,generateLayout最終調(diào)用mDecor.onResourcesLoaded(mLayoutInflater, layoutResource),DecorView的onResourcesLoaded中通過調(diào)用LayoutInflater.inflate()解析布局賦值給mContentRoot,并在DecorView的onDraw中繪制
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
mStatusColorViewState.view, mNavigationColorViewState.view);
}
繼承AppCompatActivity
@Override
public void setContentView(@LayoutRes int layoutResID) {
initViewTreeOwners();
getDelegate().setContentView(layoutResID);
}
- 其中g(shù)etDelegate()利用設(shè)計模式中的代理模式,實現(xiàn)類是AppCompatDelegateImpl,其setContentView方法如下
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//調(diào)用LayoutInflater.inflate() 布局加載解析方法將Activity中的布局添加到父布局中
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
- 第一行調(diào)用了ensureSubDecor,代碼如下
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
//創(chuàng)建SubDecor之后獲取title并設(shè)置給對應(yīng)的view
CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
if (mDecorContentParent != null) {
mDecorContentParent.setWindowTitle(title);
} else if (peekSupportActionBar() != null) {
peekSupportActionBar().setWindowTitle(title);
} else if (mTitleView != null) {
mTitleView.setText(title);
}
}
...
}
}
private ViewGroup createSubDecor() {
...
ensureWindow();
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
...
mWindow.setContentView(subDecor);
..
return subDecor;
}
//ensureWindow調(diào)用Activity.getWindow給mWindow賦值
private void ensureWindow() {
if (mWindow == null && mHost instanceof Activity) {
attachToWindow(((Activity) mHost).getWindow());
}
if (mWindow == null) {
throw new IllegalStateException("We have not been given a Window");
}
}
- 其中ensureWindow中調(diào)用了activity.getWindow給mWindow賦值,而mWindow.getDecorView的實現(xiàn)在PhoneWindow中,最終是調(diào)用了installDecor
@Override
public final @NonNull View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
- 綜上:一個Activity包含一個window對象,這個對象是由PhoneWindow來實現(xiàn)的,PhoneWindow將DecorView做為整個應(yīng)用窗口的根View,而這個DecorView又將屏幕劃分為兩個區(qū)域一個是TitleView一個是ContentView,而我們平常做應(yīng)用所寫的布局正是展示在ContentView中的。
View的繪制
- View 繪制主要分為measure(測量),layout(布局), draw(繪制)三個階段;
measure
ViewGroup.measureChildren
- ViewGroup的measureChildren會遍歷子View調(diào)用measureChild方法;
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
//遍歷子View調(diào)用measureChild方法;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
- measureChild方法會獲取子View的LayoutParams,并調(diào)用getChildMeasureSpec獲取子元素的MeasureSpec,最后調(diào)用子View的measure()方法進行測量
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//獲取子View的LayoutParams
final LayoutParams lp = child.getLayoutParams();
//獲取子元素的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//調(diào)用子View的measure()方法進行測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
- getChildMeasureSpec會根據(jù)父View的MeasureSpec,結(jié)合子View的LayoutParams屬性,最后得到子View的MeasureSpec屬性
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;
//根據(jù)父View的specMode判斷
switch (specMode) {
case MeasureSpec.EXACTLY://精確模式,尺寸的值是多少組件的長或?qū)捑褪嵌嗌? //根據(jù)子View的size判斷
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST://最大模式,由父組件能夠給出的最大的空間決定
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED://未指定模式,當前組件可以隨便使用空間,不受限制
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//將size和mode拼接成一個int值
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
LinearLayout.onMeasure
- ViewGroup并沒有提供onMeasure()方法,而是讓其子類來各自實現(xiàn)測量的方法,究其原因就是ViewGroup有不同的布局的需要很難統(tǒng)一,我們可與來看一下其子類LinearLayout的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
- LinearLayout的onMeasure區(qū)分了水平和垂直兩個分支,我們挑其中的measureVertical看一下
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;//用來存儲LinearLayout在垂直方向的高度
...
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
...
//遍歷子View,根據(jù)子View的MeasureSpec模式分別計算每個子View的高度
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//+分隔線高度
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
//useExcessSpace表示如果高度是0并且設(shè)置了權(quán)重
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
lp.height = LayoutParams.WRAP_CONTENT;
}
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
//布局之前的測量,measureChildBeforeLayout 里面調(diào)用了 measureChildWithMargins方法,和上面的measureChild類似,
//只是getChildMeasureSpec時增加了Margin
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
//獲取子view的高度
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
//疊加到mTotalLength
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
}
...
//測量最大寬度maxWidth
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
...
}
...
//mTotalLength+父view的上下padding
mTotalLength += mPaddingTop + mPaddingBottom;
...
maxWidth += mPaddingLeft + mPaddingRight;
...
}
- 其中調(diào)用了measureChildBeforeLayout測量子View,measureChildBeforeLayout中又調(diào)用了measureChildWithMargins方法,和上面的measureChild類似,只是getChildMeasureSpec時增加了Margin的技術(shù)
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
View.measure
- viewGroup測量子view最終都會調(diào)用到child.measure,那么來看一下View.measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (forceLayout || needsLayout) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//如果沒有緩存就調(diào)用onMeasure進行測量
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
} else {
//有緩存就從緩存中取
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
...
}
...
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//緩存到數(shù)組中
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
View.onMeasure
- 上面的View.measure最終是調(diào)用了onMeasure方法進行測量的,其中又調(diào)用了setMeasuredDimension方法來設(shè)置View的寬高;
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
- 上面onMeasure中調(diào)用了getDefaultSize方法,根據(jù)measureSpec的不同返回size
public static int getDefaultSize(int size, int measureSpec) {
//從onMeasure中可看到這個size是getSuggestedMinimumWidth,getSuggestedMinimumHeight
int result = size;
//specMode是View的測量模式
int specMode = MeasureSpec.getMode(measureSpec);
//specSize是View的測量大小
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;
}
//如果View沒有設(shè)置背景則取值為mMinWidth,
//對應(yīng)android:minWidth設(shè)置的值或setMinimumWidth的值
//如果View設(shè)置了背景在取值為mMinWidth, mBackground.getMinimumWidth()的最大值,
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//mBackground是Drawable類型的, intrinsicWidth得到的是這個Drawable的固有的寬度,
//如果固有寬度大于0則返回固有寬度,否則返回0。
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
layout
View.layout
//入?yún)⑹荲iew四個點的坐標(相對于父View的)
public void layout(int l, int t, int r, int b) {
...
//setFrame()設(shè)置View的四個頂點的值,也就是mLeft 、mTop、mRight和 mBottom的值
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//調(diào)用了onLayout
onLayout(changed, l, t, r, b);
...
}
...
}
View.onLayout
- layout中調(diào)用了onLayout方法,其中并沒有具體的實現(xiàn),因為確定位置時根據(jù)不同的控件有不同的實現(xiàn),所以在View和ViewGroup中均沒有實現(xiàn)onLayout()方法。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
LinearLayout.onLayout
- View.onLayout中沒有具體實現(xiàn),那么來看一下其子類LinearLayout的實現(xiàn)吧,也是區(qū)分了水平和垂直兩個分支方法
@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);
}
}
void layoutVertical(int left, int top, int right, int bottom) {{
...
//遍歷子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
...
//setChildFrame()方法中調(diào)用子元素的layout()方法來確定自己的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
//childTop是疊加的,因為垂直方向上子元素是一個接一個排列的而不是重疊的
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
//調(diào)用子元素的layout()方法來確定自己的位置
child.layout(left, top, left + width, top + height);
}
draw流程
View.draw
- view的draw方法代碼如下,其中官方注釋寫的也很清除,總共分了7步
public void draw(Canvas canvas) {
...
/*
* 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)
* 7. If necessary, draw the default focus highlight
*/
// Step 1, draw the background, if needed
//如果設(shè)置了背景,則繪制背景
drawBackground(canvas);
...
// Step 2, save the canvas' layers
//保存canvas層
...
saveCount = canvas.getSaveCount();
int topSaveCount = -1;
int bottomSaveCount = -1;
int leftSaveCount = -1;
int rightSaveCount = -1;
int solidColor = getSolidColor();
if (solidColor == 0) {
if (drawTop) {
topSaveCount = canvas.saveUnclippedLayer(left, top, right, top + length);
}
if (drawBottom) {
bottomSaveCount = canvas.saveUnclippedLayer(left, bottom - length, right, bottom);
}
if (drawLeft) {
leftSaveCount = canvas.saveUnclippedLayer(left, top, left + length, bottom);
}
if (drawRight) {
rightSaveCount = canvas.saveUnclippedLayer(right - length, top, right, bottom);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
//調(diào)用onDraw繪制自身內(nèi)容
onDraw(canvas);
// Step 4, draw the children
//調(diào)用dispatchDraw繪制子View
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
//繪制附加效果
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
...
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
//調(diào)用onDrawForeground繪制裝飾品
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
//繪制默認的焦點高亮顯示
drawDefaultFocusHighlight(canvas);
if (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
}
View.onDraw
- draw中調(diào)用了onDraw,也是沒有具體實現(xiàn),需要子view自己去實現(xiàn)
protected void onDraw(Canvas canvas) {
}
LinearLayout.onDraw
- 同樣的我們看一下LinearLayout.onDraw方法
@Override
protected void onDraw(Canvas canvas) {
if (mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawDividersVertical(canvas);
} else {
drawDividersHorizontal(canvas);
}
}
- 我們還是選一種的垂直方法drawDividersVertical看一下
void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();
//遍歷子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
//判斷如果子view前面有分割線
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int top = child.getTop() - lp.topMargin - mDividerHeight;
//畫水平的分割線
drawHorizontalDivider(canvas, top);
}
}
}
//檢查最后位置的分割線
if (hasDividerBeforeChildAt(count)) {
final View child = getLastNonGoneChild();
int bottom = 0;
if (child == null) {
bottom = getHeight() - getPaddingBottom() - mDividerHeight;
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
bottom = child.getBottom() + lp.bottomMargin;
}
drawHorizontalDivider(canvas, bottom);
}
}
- 上面調(diào)用了drawHorizontalDivider畫分割線,其實現(xiàn)還是比較簡單的,只有兩行代碼
void drawHorizontalDivider(Canvas canvas, int top) {
mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
mDivider.draw(canvas);
}
- 其中的mDivider是Drawable 類型的,可在通過如下方法設(shè)置分割線
/*
設(shè)置顯示分割線的模式
public static final int SHOW_DIVIDER_NONE = 0; 不顯示分割線
public static final int SHOW_DIVIDER_BEGINNING = 1; 在開始處顯示分割線
public static final int SHOW_DIVIDER_MIDDLE = 2; 在子視圖之間顯示分割線
public static final int SHOW_DIVIDER_END = 4; 在結(jié)束尾部顯示分割線
*/
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
//設(shè)置分割線Drawable
linearLayout.setDividerDrawable(ResourcesCompat.getDrawable(getResources(),R.drawable.line,null));
自定義View
- 自定義View一般可以繼承View,ViewGroup,已有的系統(tǒng)控件或其他自定義控件,根據(jù)需要重寫onMeasure,onLayout,onDraw,onTouchEvent等方法;
優(yōu)點
- 少了解析xml的過程
- 自定義View 減少了ViewGroup與View之間的測量,包括父量子,子量自身,子在父中位置擺放,當子view變化時,父的某些屬性都會跟著變化;需要注意的是自定義View的onDraw()方法會被頻繁調(diào)用,此方法中盡量避免創(chuàng)建對象;
- 封裝性比較好,可以隱藏內(nèi)部實現(xiàn)
- 便于復(fù)用
幾種自定義View的實現(xiàn)方式
自定義組合控件
- 用已有的控件在xml中組合起來重新定義成一個新的控件,例如一個titleBar會在很多頁面用到,那么就可以抽出一個組合控件進行封裝,復(fù)用起來會方便很多
繼承系統(tǒng)控件
- 在系統(tǒng)控件的基礎(chǔ)上進行拓展,添加新的功能或修改顯示效果,一般在onDraw方法中處理;
繼承View
- 不只是要實現(xiàn)onDraw()方法,而且在實現(xiàn)過程中還要考慮到wrap_content屬性以及padding屬性的設(shè)置;為了方便配置自己的自定義View還會對外提供自定義的屬性,另外如果要改變觸控的邏輯,還要重寫onTouchEvent()等觸控事件的方法。
對padding屬性進行處理
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft=getPaddingLeft();
int paddingRight=getPaddingRight();
int paddingTop=getPaddingTop();
int paddingBottom=getPaddingBottom();
int width = getWidth()-paddingLeft-paddingRight;
int height = getHeight()-paddingTop-paddingBottom;
canvas.drawRect(0+paddingLeft, 0+paddingTop, width+paddingRight, height+paddingBottom, mPaint);
}
對wrap_content屬性進行處理
//在onMeasure()方法中指定一個默認的寬和高,在設(shè)置wrap_content屬性時設(shè)置此默認的寬和高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(400,400);//參數(shù)的單位是px
}else if(widthSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(400,heightSpecSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,400);
}
}
自定義屬性
// 1. 在values目錄下創(chuàng)建 attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView">
<attr name="text_color" format="color" />
</declare-styleable>
</resources>
// 2. 在自定義View構(gòu)造函數(shù)中解析自定義屬性的值:
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray mTypedArray=context.obtainStyledAttributes(attrs,R.styleable.MyView);
//提取屬性集合的text_color屬性,如果沒設(shè)置默認值為Color.RED
mColor=mTypedArray.getColor(R.styleable.MyView_text_color,Color.RED);
//獲取資源后要及時回收
mTypedArray.recycle();
initDraw();
}
// 3. 在布局文件中配置自定義屬性
<com.jinyang.jetpackdemo.activity.ui.MyView
android:layout_width="50dp"
android:background="#f00"
app:text_color="@color/purple_200"
android:layout_height="50dp"/>
繼承ViewGroup
- 相關(guān)的知識點在上面基本已經(jīng)說過了,下面直接給一個完整的例子,具體的都有注釋解釋
class HorizontalView : ViewGroup {
private var lastX = 0
private var lastY = 0
/**
* 當前子元素
*/
private var currentIndex = 0
private var childWidth = 0
private var scroller: Scroller? = null
/**
* 增加速度檢測,如果速度比較快的話,就算沒有滑動超過一半的屏幕也可以
*/
private var tracker: VelocityTracker? = null
private var lastInterceptX = 0
private var lastInterceptY = 0
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init()
}
fun init() {
scroller = Scroller(context)
tracker = VelocityTracker.obtain()
}
/**
* 重寫onMeasure處理wrap_content屬性的尺寸測量
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
//測量所有子元素(沒有考慮它的padding和子元素的margin)
measureChildren(widthMeasureSpec, heightMeasureSpec)
if (childCount == 0) {
//如果沒有子元素,就設(shè)置寬高都為0(簡化處理,正常的話應(yīng)該根據(jù)LayoutParams中的寬和高來做相應(yīng)的處理)
setMeasuredDimension(0, 0)
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//寬和高都是AT_MOST,則設(shè)置寬度所有子元素的寬度的和;高度設(shè)置為第一個元素的高度;
val childOne = getChildAt(0)
val childWidth = childOne.measuredWidth
val childHeight = childOne.measuredHeight
setMeasuredDimension(childWidth * childCount, childHeight)
} else if (widthMode == MeasureSpec.AT_MOST) {
//如果寬度是wrap_content,則寬度為所有子元素的寬度的和
val childOne = getChildAt(0)
val childWidth = childOne.measuredWidth
setMeasuredDimension(childWidth * childCount, heightSize)
} else if (heightMode == MeasureSpec.AT_MOST) {
//如果高度是wrap_content,則高度為第一個子元素的高度
val childHeight = getChildAt(0).measuredHeight
setMeasuredDimension(widthSize, childHeight)
}
}
/**
* 重寫onLayout來布局子元素
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val childCount = childCount
//左邊的距離
var left = 0
var child: View
//遍歷布局子元素
for (i in 0 until childCount) {
child = getChildAt(i)
if (child.visibility != GONE) {
//子元素不是GONE,則調(diào)用子元素的layout方法將其放置到合適的位置上
val width = child.measuredWidth
//賦值給子元素寬度變量
childWidth = width
//沒有處理自身的padding以及子元素的margin,right是left+元素的寬度
child.layout(left, 0, left + width, child.measuredHeight)
//left是一直累加的
left += width
}
}
}
/**
* 重寫onInterceptTouchEvent處理滑動沖突
*/
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
var intercept = false
val x = event.x.toInt()
val y = event.y.toInt()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
intercept = false
//如果動畫還沒有執(zhí)行完成,則打斷
if (!scroller!!.isFinished) {
scroller!!.abortAnimation()
}
}
MotionEvent.ACTION_MOVE -> {
val deltaX = x - lastInterceptX
val deltaY = y - lastInterceptY
//水平方向距離長 MOVE中返回true一次,后續(xù)的MOVE和UP都不會收到此請求
if (Math.abs(deltaX) - Math.abs(deltaY) > 0) {
intercept = true //用戶想水平滑動的,所以攔截
Log.i("wangshu", "intercept = true")
} else {
intercept = false
Log.i("wangshu", "intercept = false")
}
}
MotionEvent.ACTION_UP -> intercept = false
else -> {}
}
//因為DOWN返回false,所以onTouchEvent中無法獲取DOWN事件,這里要負責(zé)設(shè)置lastX,lastY
lastX = x
lastY = y
lastInterceptX = x
lastInterceptY = y
return intercept
}
/**
* 重寫onTouchEvent方法使用Scroller來彈性滑動到其他頁面
*/
override fun onTouchEvent(event: MotionEvent): Boolean {
tracker!!.addMovement(event)
val x = event.x.toInt()
val y = event.y.toInt()
when (event.action) {
MotionEvent.ACTION_DOWN ->//ACTION_DOWN處理再次觸摸屏幕阻止頁面繼續(xù)滑動
if (!scroller!!.isFinished) {
scroller!!.abortAnimation()
}
MotionEvent.ACTION_MOVE -> {
//跟隨手指滑動
val deltaX = x - lastX
scrollBy(-deltaX, 0)
}
MotionEvent.ACTION_UP -> {//ACTION_UP處理快速滑動到其他頁面
//相對于當前View滑動的距離,正為向左,負為向右
val distance = scrollX - currentIndex * childWidth
//必須滑動的距離要大于1/2個寬度,否則不會切換到其他頁面
if (Math.abs(distance) > childWidth / 2) {
if (distance > 0) {
currentIndex++
} else {
currentIndex--
}
} else {
//調(diào)用該方法計算1000ms內(nèi)滑動的平均速度
tracker!!.computeCurrentVelocity(1000)
val xV = tracker!!.xVelocity//獲取到水平方向上的速度
//如果速度的絕對值大于50的話,就認為是快速滑動,就執(zhí)行切換頁面
if (Math.abs(xV) > 50) {
if (xV > 0) {
//大于0切換上一個頁面
currentIndex--
} else {
//小于0切換到下一個頁面
currentIndex++
}
}
}
currentIndex = if (currentIndex < 0) 0 else Math.min(currentIndex, childCount - 1)
smoothScrollTo(currentIndex * childWidth, 0)
//重置速度計算器
tracker!!.clear()
}
else -> {}
}
lastX = x
lastY = y
return true
}
override fun computeScroll() {
super.computeScroll()
if (scroller!!.computeScrollOffset()) {
scrollTo(scroller!!.currX, scroller!!.currY)
postInvalidate()
}
}
/**
* 彈性滑動到指定位置
*/
private fun smoothScrollTo(destX: Int, destY: Int) {
scroller!!.startScroll(
scrollX, scrollY, destX - scrollX,
destY - scrollY, 1000
)
invalidate()
}
}
參考
我是今陽,如果想要進階和了解更多的干貨,歡迎關(guān)注微信公眾號 “今陽說” 接收我的最新文章