Android進階筆記-6. 從源碼看View體系(坐標,滑動,分發(fā),繪制)

坐標系

  • 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()
  • 和layout()方法效果差不多,使用也差不多
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. 動畫
  • 以屬性動畫ObjectAnimator為例
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);
            }
        }
        ...
    }
}
  • 其中createSubDecor代碼如下
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
  • 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);
    }
}
  • 我們來看一下layoutVertical方法
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)點

  • 自定義view效率高于xml定義:
  1. 少了解析xml的過程
  2. 自定義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)注微信公眾號 “今陽說” 接收我的最新文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容