Android进阶笔记-6. 从源码看View体系(坐标,滑动,分发,绘制)

黎腾
2023-12-01

坐标系

  • 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时,系统记下触摸点坐标,手指移动时系统记下移动后的坐标并算出偏移量,以此修改View的坐标。

实现View滑动的方法

1. layout()
  • view绘制时会调用onLayout()来设置显示的位置,因此可以通过修改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;
            //调用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. 改变布局参数LayoutParams
  • LayoutParams保存了View的布局参数
  • 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的内容,如果在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最终也是调用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过程是瞬发的,用户体验不好,可以使用Scroller来实现有过度效果的滑动
  • Scroller本身不能实现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旋转
            //PrivotX和PrivotY:控制View对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认该支点位置就是View对象的中心点。
            //alpha:透明度,默认是1(不透明),0代表完全透明
            //x和y:描述View对象在它容器中的最终位置,它是最初的做上角坐标和translationX,translationY值的累计的和
            break;
        default:
            break;
    }
    return true;
}
  • 如果一个属性没有get,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:不提供任何动画效果,它更像一个数值发生器,用来产生一定规律数字,通常在ValueAnimator的AnimatorUpdateListener中监听数值的变化,从而完成动画的变换
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的事件分发机制

  • 点击屏幕,就产生了触摸事件,这个事件被封装成了一个类:MotionEvent。而当这个MotionEvent产生后,那么系统就会将这个MotionEvent传递给View的层级,MotionEvent在View的层级传递的过程就是点击事件分发

ViewGroup.dispatchTouchEvent

  • 总结:dispatchTouchEvent负责处理事件的分发,会先检查是否遮挡,然后重置之前触摸事件的遗留数据,然后判断是否需要拦截,需要就调用onInterceptTouchEvent,然后判断是否取消,如果不取消不拦截,检查子view有没有获得焦点的,然后遍历子view并把事件优先给获得焦点的view处理,会根据child是否为空判断是调用自己的view.dispatchTouchEvent还是child的dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;
    //检查是否分发本次事件:检查是否设置了被遮挡时不处理触摸事件的flag && 该事件的窗口是否被其它窗口遮挡
    if (onFilterTouchEventForSecurity(ev)) {
        //获取事件类型
        final int action = ev.getAction();
        //actionMasked能够区分出多点触控事件
        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) {
                //如果允许拦截就调用onInterceptTouchEvent
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // 防止Event中途被篡改
            } else {
                //有FLAG_DISALLOW_INTERCEPT标志就不进行拦截
                //如果子View在ACTION_DWON时处理了事件,可以通过requestDisallowInterceptTouchEvent(true)来禁止父View拦截后续事件
                //可以用来解决滑动冲突问题
                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分发给子View
        //可通过setMotionEventSplittingEnabled()可以修改这个值
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                && !isMouseEvent;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        //判断是否要给子View分发事件:没有拦截和取消
        if (!canceled && !intercepted) {
            //检查TouchEvent是否可以触发View获取焦点,
            // 如果可以,查找本View中有没有获得焦点的子View,有就获取它,没有就为null
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            //判断是否ACTION_DOWN,或者支持多点触控且ACTION_POINTER_DOWN,或者悬停啥的(鼠标)
            //说明一个事件流只有一开始的DOWN事件才会去遍历分发事件,后面的事件将不再通过遍历分发,而是直接发到触摸目标队列的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数量
                //第一个点的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数量不多于一个,为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);
                        // 如果当前已经有View获得焦点了,后面的触摸事件会优先传给它
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }
                        //检查View是否显示或者播放动画以及TouchEvent点是否在View内
                        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有没有消费,消费了执行if中逻辑,并结束循环
                        //其中会根据child是否为空判断是调用自己的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处理了本事件,那么接着会创建一个TouchTarget,并且关联该子View,
                            //后续的触摸事件就会通过这个TouchTarget取出子View,直接把事件分发给它
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;//标记已经有子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,
        //那么把事件分发给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;
            }
        }

        // 还原状态
        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,调用自己的dispatchTouchEvent
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                //根据滚动值计算触摸事件的偏移位置
                event.offsetLocation(offsetX, offsetY);
                //让子View处理事件
                handled = child.dispatchTouchEvent(event);
                //恢复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,所以调用的是View的dispatchTouchEvent,OnTouchListener的优先级要比onTouchEvent()要高,如果我们设置了OnTouchListener并且onTouch()方法返回true,则onTouchEvent()方法不会被调用,否则则会调用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,
        //表示事件被消费,也不会再执行onTouchEvent(event)
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //OnTouchListener为空则调用onTouchEvent消费事件
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
}

ViewGroup.onInterceptTouchEvent

  • 用来进行事件的拦截,在ViewGroup.dispatchTouchEvent()中调用,仅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()方法中进行调用,上面view的移动就是在这个方法中实现的,onTouchEvent默认返回true,除非它是不可点击的也就是CLICKABLE和LONG_CLICKABLE都为false,如果设置了OnClickListener会执行它的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中调用performClick,
                        //performClick中会调用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的构成

  • Activity的onCreate中会调用setContentView加载布局文件

继承Activity

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
  • 调用了getWindow().setContentView,其中getWindow返回的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;
}
  • 其中调用了installDecor,其中调用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);
    ...
}
  • 其中generateDecor调用new DecorView创建了DecorView,generateLayout最终调用mDecor.onResourcesLoaded(mLayoutInflater, layoutResource),DecorView的onResourcesLoaded中通过调用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);
}
  • 其中getDelegate()利用设计模式中的代理模式,实现类是AppCompatDelegateImpl,其setContentView方法如下
@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    //调用LayoutInflater.inflate() 布局加载解析方法将Activity中的布局添加到父布局中
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}
  • 第一行调用了ensureSubDecor,代码如下
private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        //创建SubDecor之后获取title并设置给对应的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调用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中调用了activity.getWindow给mWindow赋值,而mWindow.getDecorView的实现在PhoneWindow中,最终是调用了installDecor
@Override
public final @NonNull View getDecorView() {
    if (mDecor == null || mForceDecorInstall) {
        installDecor();
    }
    return mDecor;
}
  • 综上:一个Activity包含一个window对象,这个对象是由PhoneWindow来实现的,PhoneWindow将DecorView做为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域一个是TitleView一个是ContentView,而我们平常做应用所写的布局正是展示在ContentView中的。

View的绘制

  • View 绘制主要分为measure(测量),layout(布局), draw(绘制)三个阶段;

measure

ViewGroup.measureChildren
  • ViewGroup的measureChildren会遍历子View调用measureChild方法;
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    //遍历子View调用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,并调用getChildMeasureSpec获取子元素的MeasureSpec,最后调用子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);
    //调用子View的measure()方法进行测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
  • getChildMeasureSpec会根据父View的MeasureSpec,结合子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;
    //根据父View的specMode判断
    switch (specMode) {
        case MeasureSpec.EXACTLY://精确模式,尺寸的值是多少组件的长或宽就是多少
            //根据子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()方法,而是让其子类来各自实现测量的方法,究其原因就是ViewGroup有不同的布局的需要很难统一,我们可与来看一下其子类LinearLayout的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}
  • LinearLayout的onMeasure区分了水平和垂直两个分支,我们挑其中的measureVertical看一下
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    mTotalLength = 0;//用来存储LinearLayout在垂直方向的高度
    ...
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    ...
    //遍历子View,根据子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并且设置了权重
        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 里面调用了 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;
    ...
}
  • 其中调用了measureChildBeforeLayout测量子View,measureChildBeforeLayout中又调用了measureChildWithMargins方法,和上面的measureChild类似,只是getChildMeasureSpec时增加了Margin的技术
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最终都会调用到child.measure,那么来看一下View.measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    if (forceLayout || needsLayout) {
        ...
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            //如果没有缓存就调用onMeasure进行测量
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            ...
        } else {
            //有缓存就从缓存中取
            long value = mMeasureCache.valueAt(cacheIndex);
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            ...
        }
        ...
    }
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
    //缓存到数组中
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
View.onMeasure
  • 上面的View.measure最终是调用了onMeasure方法进行测量的,其中又调用了setMeasuredDimension方法来设置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中调用了getDefaultSize方法,根据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没有设置背景则取值为mMinWidth,
//对应android:minWidth设置的值或setMinimumWidth的值
//如果View设置了背景在取值为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()方法如下
//入参是View四个点的坐标(相对于父View的)
public void layout(int l, int t, int r, int b) {
    ...
    //setFrame()设置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) {
        //调用了onLayout
        onLayout(changed, l, t, r, b);
        ...
    }
    ...
}
View.onLayout
  • layout中调用了onLayout方法,其中并没有具体的实现,因为确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中均没有实现onLayout()方法。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

LinearLayout.onLayout

  • View.onLayout中没有具体实现,那么来看一下其子类LinearLayout的实现吧,也是区分了水平和垂直两个分支方法
@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()方法中调用子元素的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) {
    //调用子元素的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
    //如果设置了背景,则绘制背景
    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
    //调用onDraw绘制自身内容
    onDraw(canvas);

    // Step 4, draw the children
    //调用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)
    //调用onDrawForeground绘制装饰品
    onDrawForeground(canvas);

    // Step 7, draw the default focus highlight
    //绘制默认的焦点高亮显示
    drawDefaultFocusHighlight(canvas);
    if (isShowingLayoutBounds()) {
        debugDrawFocus(canvas);
    }
}
View.onDraw
  • draw中调用了onDraw,也是没有具体实现,需要子view自己去实现
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);
    }
}
  • 上面调用了drawHorizontalDivider画分割线,其实现还是比较简单的,只有两行代码
void drawHorizontalDivider(Canvas canvas, int top) {
    mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
            getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
    mDivider.draw(canvas);
}
  • 其中的mDivider是Drawable 类型的,可在通过如下方法设置分割线
/*
  设置显示分割线的模式
  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;   在结束尾部显示分割线
*/
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
//设置分割线Drawable
linearLayout.setDividerDrawable(ResourcesCompat.getDrawable(getResources(),R.drawable.line,null));

自定义View

  • 自定义View一般可以继承View,ViewGroup,已有的系统控件或其他自定义控件,根据需要重写onMeasure,onLayout,onDraw,onTouchEvent等方法;

优点

  • 自定义view效率高于xml定义:
  1. 少了解析xml的过程
  2. 自定义View 减少了ViewGroup与View之间的测量,包括父量子,子量自身,子在父中位置摆放,当子view变化时,父的某些属性都会跟着变化;需要注意的是自定义View的onDraw()方法会被频繁调用,此方法中尽量避免创建对象;
  • 封装性比较好,可以隐藏内部实现
  • 便于复用

几种自定义View的实现方式

自定义组合控件
  • 用已有的控件在xml中组合起来重新定义成一个新的控件,例如一个titleBar会在很多页面用到,那么就可以抽出一个组合控件进行封装,复用起来会方便很多
继承系统控件
  • 在系统控件的基础上进行拓展,添加新的功能或修改显示效果,一般在onDraw方法中处理;
继承View
  • 不只是要实现onDraw()方法,而且在实现过程中还要考虑到wrap_content属性以及padding属性的设置;为了方便配置自己的自定义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()方法中指定一个默认的宽和高,在设置wrap_content属性时设置此默认的宽和高
@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);//参数的单位是px
      }else if(widthSpecMode==MeasureSpec.AT_MOST){
          setMeasuredDimension(400,heightSpecSize);
      }else if(heightSpecMode==MeasureSpec.AT_MOST){
          setMeasuredDimension(widthSpecSize,400);
      }
  }
自定义属性
// 1. 在values目录下创建 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构造函数中解析自定义属性的值:
public MyView(Context context, AttributeSet attrs) {
   super(context, attrs);
   TypedArray mTypedArray=context.obtainStyledAttributes(attrs,R.styleable.MyView);
   //提取属性集合的text_color属性,如果没设置默认值为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
  • 相关的知识点在上面基本已经说过了,下面直接给一个完整的例子,具体的都有注释解释
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) {
            //如果没有子元素,就设置宽高都为0(简化处理,正常的话应该根据LayoutParams中的宽和高来做相应的处理)
            setMeasuredDimension(0, 0)
        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            //宽和高都是AT_MOST,则设置宽度所有子元素的宽度的和;高度设置为第一个元素的高度;
            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,则调用子元素的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
                //如果动画还没有执行完成,则打断
                if (!scroller!!.isFinished) {
                    scroller!!.abortAnimation()
                }
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = x - lastInterceptX
                val deltaY = y - lastInterceptY
                //水平方向距离长  MOVE中返回true一次,后续的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事件,这里要负责设置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处理再次触摸屏幕阻止页面继续滑动
                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 {
                    //调用该方法计算1000ms内滑动的平均速度
                    tracker!!.computeCurrentVelocity(1000)
                    val xV = tracker!!.xVelocity//获取到水平方向上的速度
                    //如果速度的绝对值大于50的话,就认为是快速滑动,就执行切换页面
                    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()
    }
}

参考

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

 类似资料: