美文网首页知识点手势识别Android进阶笔记
Android进阶笔记-6. 从源码看View体系(坐标,滑动,

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

作者: 今阳说 | 来源:发表于2022-01-20 16:14 被阅读0次

    坐标系

    • 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()
        }
    }
    

    参考

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

    相关文章

      网友评论

        本文标题:Android进阶笔记-6. 从源码看View体系(坐标,滑动,

        本文链接:https://www.haomeiwen.com/subject/huijhrtx.html