自定义View从源码到应用

2021-08-06/2021-08-06

自定义View从源码到应用5449624.jpg

Android进阶的书看了一遍又一遍,奈何总是看了又忘,忘了又看,于是打算将自己学的总结一下,也希望我总结的内容能对它人有所帮助!

在源码中有很多英文注释,如果看不懂的可以用工具翻译一下,我就不翻译了

转载请注明原作者,谢谢!

Scroller解析

关于View的滑动有很多种实现方式,使用Scroller可以实现View的弹性滑动

关于Scroller的基本使用不再赘述

首先看看Scroller的构造方法

/**
     * Create a Scroller with the default duration and interpolator.
     */
    public Scroller(Context context) {
        this(context, null);
    }
​
    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
     * be in effect for apps targeting Honeycomb or newer.
     */
    public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }
​
    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. Specify whether or
     * not to support progressive "flywheel" behavior in flinging.
     */
    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
        mFinished = true;
        if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;
​
        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    }

通常我们都是使用第一个构造方法,那Scroller就会采用默认的插值器

一般我们在使用Scroller时,会重写computeScroll()方法,并如此实现

@Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

这是为什么呢?

带着这个疑问我们来看看 Scroller的startScroll方法

public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    }
​
 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

可以发现我们使用startScroll时并没有导致View的滑动,在这个方法中只是记录了滑动相关的值

那view是怎么滑动的呢?

我们在调用startScroll后会调用invalidate()方法(或者postInvalidate()方法,俩者的区别是invalidate要在UI线程中使用,而postInvalidate()可以不在UI线程中使用,这是因为postInvalidate底层使用了Handler)

这个方法会导致View的重绘,而View的重绘会导致View的draw方法被调用,View的draw方法被调用会导致

computeScroll被调用,而在我们重写的computeScroll方法中,只要满足mScroller.computeScrollOffset()就又会调用postInvalidate()从而形成一个循环直到mScroller.computeScrollOffset()返回false这个循环才会停止

这样我们就能实现弹性滑动,这是为什么呢,我们来看看mScroller.computeScrollOffset()方法

/**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }
​
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
        //startScroll方法中的 mMode为SCROLL_MODE
        //可以看到这里使用了插值器,关于插值器是什么可以去学习Android中的动画来了解
        //这样就能将本来要进行滑动的一大块距离变成很多段小的距离,这样我们就实现了弹性滑动
        //每一次循环可以将mCurrX mCurrY 这俩个值每一次增加一个比较小的值,然后在computeScroll中调用scrollTo(mScroller.getCurrX(), mScroller.getCurrY())就可以将要滑动的View滑动一个很小的距离,只要View还没有滑动到我们规定的位置,computeScrollOffset就会返回true,结果就是一次大滑动变成了很多次小滑动,体现的效果就是弹性滑动
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }
​
                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
                
                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);
                
                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);
​
                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }
​
                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

最后再看看View类中的computeScroll方法

发现是一个空方法

/**
     * Called by a parent to request that a child update its values for mScrollX
     * and mScrollY if necessary. This will typically be done if the child is
     * animating a scroll using a {@link android.widget.Scroller Scroller}
     * object.
     */
    public void computeScroll() {
    }

事件分发机制

我们点击屏幕的事件会被抽象为MotionEvent类,事件分发机制也就是MotionEvent的分发过程,也就是说当一个MotionEvent产生后,系统需要把这个事件传递给一个具体个View,这个传递的过程就是事件分发。

当一个点击事件产生后,他的传递过程遵循如下规则:

Activity—>Window—>View

首先我们来看Activity是怎么把点击事件传递给Window的

/**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

从Activity中的dispatchTouchEvent方法中好像什么也看不出,我们来看看getWindow().superDispatchTouchEvent(ev),getWindow拿到的是一个mWindow对象

private Window mWindow;
​
public Window getWindow() {
    return mWindow;
}

那么mWindow是什么呢?

可以再Activity中的attach方法中发现

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) 
{
    ...省略几行代码
​
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    
    ...省略很多代码
}

可知mWindow是一个PhoneWindow对象,那我们来看看PhoneWindow的superDispatchTouchEvent方法

// This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以发现getWindow().superDispatchTouchEvent(ev)其实就是mDecor.superDispatchTouchEvent(event)

那么mDecor是什么呢?注释说了,它是最高等级的View

当一个点击事件产生后,他的传递过程遵循如下规则:

Activity—>Window—>View

那么,分析至此,点击事件已经从Activity分发到了View

从以上的分析我们可以知道当点击事件发生后,事件会首先传递到当前的Activity,这回调用Activity的dispatchTouchEvent方法。当然,具体的事件处理工作都是交由Activity的PhoneWindow来完成的。然后PhoneWindow再将事件处理工作交给DecorView,再有DecorView将事件处理交给根ViewGroup。所以下面我们从ViewGroup的dispatchTouchEvent开始分析。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
            ......

        // Handle an initial down.
         //这个方法会在Actiondown事件到来时将FLAG_DISALLOW_INTERCEPT标志位重置,关于这个标志位有何用,我们后面分析
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Throw away all previous state when starting a new touch gesture.
            // The framework may have dropped the up or cancel event for the previous gesture
            // due to an app switch, ANR, or some other state change.
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // Check for interception.

    /**
      这段代码是用来判断ViewGroup是否拦截这个事件的
    可以看出,ViewGroup会在俩种情况下判断是否要拦截这个事件,注意是判断,不一定会拦截,如果不满足这俩种条件,那么
    intercepted = true ViewGroup就会直接拦截事件
    那么是哪俩种条件呢?
    1.actionMasked == MotionEvent.ACTION_DOWN  2.mFirstTouchTarget != null
    那么mFirstTouchTarget是个什么东西呢?这个从后面的代码可以看出如果点击事件成功传递给了ViewGroup的子View,那么
    mFirstTouchTarget就会赋值这个处理事件的子View
    从这个if语句的判断条件我们可知如果没有子View来处理这个点击事件,那么mFirstTouchTarget == null那么一个点击事件的一序列事件(如actionup,actionmove)等都会交由ViewGroup处理
    但是这里还有一种特殊情况。那就是FLAG_DISALLOW_INTERCEPT这个标志位,这个标志位是通过requestDisallowInteerceptTouchEvent来设置的,一般用于子View中,设置后,ViewGroup将无法拦截处理ACTION_DOWN之外的事件,为什么还能拦截ACTION_DOWN事件呢?通过之前的分析可知这时这个标志位会被重置
    
    此外,我们查看源码发现onInterceptTouchEvent(ev)默认返回false,这说明ViewGroup默认不拦截事件,如果想要ViewGroup拦截事件则要重写onInterceptTouchEvent方法
    
    */
    	final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }

         ...
             
        if (!canceled && !intercepted) {
                       ...
    /**
    这里是遍历ViewGroup的每一个子View,!child.canReceivePointerEvents()
    || !isTransformedTouchPointInView(x, y, child, null)这个条件是判断点击事件是否位于当前所判断的View的范围内和这个View是否正在播放动画,如果View满足这俩个判断条件中的一种就会执行continue语句开始判断下一个子View直到找到一个符合条件的子View
     */
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;
                        }

                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // Child is already receiving touch within its bounds.
                            // Give it the new pointer in addition to the ones it is handling.
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        
                        /**
                        再来分析dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)这个方法
                        对于这个方法,如果传入的child为null,那么返回super.dispatchTouchEvent(event)
                        否则返回child.dispatchTouchevent(event),这样,点击事件就由父View传递到了子View
                        */
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                // childIndex points into presorted list, find original index
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            //如果点击事件成功分发给了子View,就会通过这行代码给mFirstTouchTarget赋值,对应了前面的分析
                            //具体实现可以查看addTouchTarget方法
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        // The accessibility focus didn't handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                          ...
        }


/**
mFirstTouchTarget==null有俩种条件,一种是没有在ViewGroup中找到的合适的子View,还有一个情况就是找到了合适的子View,但是
child.dispatchTouchevent(event)返回了false,从前面的分析我们可以直到这回导致mFirstTouchTarget不被赋值
*/
        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
           ...
        }

          ...
    return handled;
}

接下来,我们来看看View的事件分发相关的源码

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    
         ......

             /**
             从下面的代码我们可以看出首先会判断有没有OnTouchListener,如果OnTouchListener的onTouch方法返回true,那么result=true,onTouchEvent就不会被调用,从而我们可以知道OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理事件
             */
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

         ......

    return result;
}

再看看onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
       
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
         ......

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                ......
            }

            return true;
        }

        return false;
    }

//从上面的代码可以看出,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么onTouchEvent()就会返回true,CLICKABLE和LONG_CLICKABLE可以通过View的相关set方法来设置。对于一些控件像Button CLICKABLE属性不用设置便具有接着在ACTION_UP事件中会调用performClicK方法
public boolean performClick() {
    // We still need to call this method to handle the cases where performClick() was called
    // externally, instead of through performClickInternal()
    notifyAutofillManagerOnClick();

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    //可以看出,如果View设置了点击事件,那么它的onClick方法就会执行
    //则
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

综合以上分析,我们可以知道点击事件由上而下的传递规则,当点击事件产生后会由Activity处理,传递给PhoneWindow,再传递给DecorView,最后传递给顶层的ViewGroup。对于根ViewGroup,点击事件首先传递给它的dispatchTouchEvent,如果该ViewGroup的onIntercept方法放回true,则表示他要拦截这个事件,这个事件就会交由他的onTouchEvent处理,反则这个事件会传递给它的子View处理,如此传递下去,这个事件就会传递给最底层的View,如果最底层的View也不处理这个事件,那么这个事件又会一层层的往上传。

关于为什么如果最底层的View不处理这个事件,那么这个事件为什么又会一层层的往上传我之前一直没能理解,搞了好久才懂,因此额外多说几句关于这个的

对于 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))这个方法如果传入的child为null,那么返回super.dispatchTouchEvent(event),否则返回child.dispatchTouchevent(event),这样,点击事件就由父View传递到了子View,但是如果child.dispatchTouchevent(event)返回了false,那么mFirstTouchTarget就不会被赋值,那么mFirstTouchTarget == null就会执行

handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);

通过对dispatchTransformedTouchEvent的分析我们可以知道此时就将点击事件又由子View往上传了!

滑动冲突处理

滑动冲突处理一般有俩种方法,一种是外部拦截法,还有一种是内部拦截法

外部拦截法

所谓的“外部拦截法“中的外部是指出现滑动冲突的这两个布局的外层。我们知道,一个事件序列是由Parent View先获取到的,如果Parent View不拦截事件那么才会交由子View去处理。既然是外层先获知事件,那外层View根据自身情况来决定是否要拦截事件不就行了吗?因此外部拦截法的实现是非常简单的,大概思路如下:

public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN: {
        intercepted = false;
        break;
      }
      case MotionEvent.ACTION_MOVE: {
        if (needIntercept) { // 这里根据需求判断是否需要拦截
          intercepted = true;
        } else {
          intercepted = false;
        }
        break;
      }
      case MotionEvent.ACTION_UP: {
        intercepted = false;
        break;
      }
      default:
        break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
  }

内部拦截法

所谓的”内部拦截法“指的是对内部的View做文章,让内部View决定是不是拦截事件。但是现在就有问题了,你怎么知道外部的View是不是要拦截事件啊??如果外部View把事件拦截了,内部的View岂不是连西北风都喝不到了?

别着急,Google官方当然有考虑到这种情况。在ViewGroup中有一个叫requestDisallowInterceptTouchEvent的方法(关于这个方法的原理已经在前面分析过了),这个方法接受一个boolean值,意思是是否要禁止ViewGroup拦截当前事件。如果是true的话那么该ViewGroup则无法对事件进行拦截。有了这个方法那我们就可以让内部View大显神通了。来看下内部拦截法的代码:

public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                // 禁止parent拦截down事件
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (disallowParentInterceptTouchEvent) { // 根据需求条件来决定是否让Parent View拦截事件。
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

解决ViewPager2的滑动冲突问题

关于ViewPager2的基本使用方法可以看这篇文章

https://zhpanvip.gitee.io/2019/12/14/24.Know%20about%20ViewPager2/

关于如何解决ViewPager2的滑动冲突问题,请看我写的这篇文章:

(暂时还没有写。。。)

View的绘制

首先介绍ViewRoot,ViewRoot对应于ViewRootImpl类,它是连接WindowManger和DecorView得纽带,view的三大流程都是通过ViewRoot来完成的,当Activity被创建时,会将DecorView添加到Window中,同时创建ViewRootImpl类,然后将DecorView与ViewRootImpl建立关联。

View的绘制流程从ViewRoot的performTraversals方法开始,performTraversals依次调用performMeasure performLayout 和

performDraw三个方法,这三个方法分别完成View的measure,layout,draw。其中在performMeasure中viewRoot会调用DecorView的measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有子View进行Measure过程,对于performLayout 和 performDraw也是差不多的道理。如此就完成了整个View树的遍历。

理解MeasureSpec

首先来看看MeasureSpen的源码

/**
 * A MeasureSpec encapsulates the layout requirements passed from parent to child.
 * Each MeasureSpec represents a requirement for either the width or the height.
 * A MeasureSpec is comprised of a size and a mode. There are three possible
 * modes:
 * <dl>
 * <dt>UNSPECIFIED</dt>
 * <dd>
 * The parent has not imposed any constraint on the child. It can be whatever size
 * it wants.
 * </dd>
 *
 * <dt>EXACTLY</dt>
 * <dd>
 * The parent has determined an exact size for the child. The child is going to be
 * given those bounds regardless of how big it wants to be.
 * </dd>
 *
 * <dt>AT_MOST</dt>
 * <dd>
 * The child can be as large as it wants up to the specified size.
 * </dd>
 * </dl>
 *
 * MeasureSpecs are implemented as ints to reduce object allocation. This class
 * is provided to pack and unpack the &lt;size, mode&gt; tuple into the int.
 */
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /** @hide */
    @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {}

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    /**
     * Creates a measure specification based on the supplied size and mode.
     *
     * The mode must always be one of the following:
     * <ul>
     *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
     *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
     *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
     * </ul>
     *
     * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
     * implementation was such that the order of arguments did not matter
     * and overflow in either value could impact the resulting MeasureSpec.
     * {@link android.widget.RelativeLayout} was affected by this bug.
     * Apps targeting API levels greater than 17 will get the fixed, more strict
     * behavior.</p>
     *
     * @param size the size of the measure specification
     * @param mode the mode of the measure specification
     * @return the measure specification based on size and mode
     */
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    /**
     * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
     * will automatically get a size of 0. Older apps expect this.
     *
     * @hide internal use only for compatibility with system widgets and older apps
     */
    @UnsupportedAppUsage
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

    /**
     * Extracts the mode from the supplied measure specification.
     *
     * @param measureSpec the measure specification to extract the mode from
     * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
     *         {@link android.view.View.MeasureSpec#AT_MOST} or
     *         {@link android.view.View.MeasureSpec#EXACTLY}
     */
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    /**
     * Extracts the size from the supplied measure specification.
     *
     * @param measureSpec the measure specification to extract the size from
     * @return the size in pixels defined in the supplied measure specification
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

    static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        if (mode == UNSPECIFIED) {
            // No need to adjust size for UNSPECIFIED mode.
            return makeMeasureSpec(size, UNSPECIFIED);
        }
        size += delta;
        if (size < 0) {
            Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                    ") spec: " + toString(measureSpec) + " delta: " + delta);
            size = 0;
        }
        return makeMeasureSpec(size, mode);
    }

    /**
     * Returns a String representation of the specified measure
     * specification.
     *
     * @param measureSpec the measure specification to convert to a String
     * @return a String with the following format: "MeasureSpec: MODE SIZE"
     */
    public static String toString(int measureSpec) {
        int mode = getMode(measureSpec);
        int size = getSize(measureSpec);

        StringBuilder sb = new StringBuilder("MeasureSpec: ");

        if (mode == UNSPECIFIED)
            sb.append("UNSPECIFIED ");
        else if (mode == EXACTLY)
            sb.append("EXACTLY ");
        else if (mode == AT_MOST)
            sb.append("AT_MOST ");
        else
            sb.append(mode).append(" ");

        sb.append(size);
        return sb.toString();
    }
}

MeasureSpec是一个32位int值,高2位表示SpecMode,低30位表示SpenSize,MeasureSpec将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,并且MeasureSpen提供了打包和解包的操作。

View的MeasureSpec是怎么产生的

View的MeasureSpec与其LayoutParams及它的父View有关,为什么View的MeasureSpec与其LayoutParams及它的父View有关呢?带着这个问题我们去查看源码

首先从View的onMeasure方法开始

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension方法会设置View的宽高,我们来看getDefaultSize是怎么实现的

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    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;
}

可以看到其实就是把onMeasure中提供的Spec参数进行解包后根据不同的规则得到对应的值。

同时我们可以发现

case MeasureSpec.AT_MOST:

** case MeasureSpec.EXACTLY:**

都是让result = specSize;

这也说明直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的大小,否则使用warp_content等同于使用match_parent.

通过以上的分析我们可以知道 View的宽高和onMeasure中传入的int widthMeasureSpec, int heightMeasureSpec俩个参数有关,而这俩个参数是View的父View传入的,我们来看看父View关于这个的源码

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的MeasureSpec与其LayoutParams及它的父View有关这个问题,可以看到getChildMeasureSpec这个方法传入的是父View的MeasureSpec和子View的LayoutParams.

接下来咱们再看看子View的MeasureSpec具体是怎么生成的,打开getChildMeasureSpec的源码

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;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

可以将上面的代码总结成一张表:

如何在Activity中获取View的宽高

1.通过 OnWindowFocusChanged方法

2.通过View.post(runnable) 方法

通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了

3.通过ViewTreeObserver的OnGlobalLayoutListener接口

当View树的状态发生改变时,OnGlobalLayout方法将被回调

4.view.measure(...,...)

手动对View来measure来获得View的宽高

ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子View的measure方法,各个子元素在递归去执行这个过程,ViewGroup没有onMeasure方法,但是它提供了一个叫measureChildren的方法:

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

ViewGroup没有定义其具体的测量过程这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现。

ViewGroup的layout过程

Layout过程就是ViewGroup将其子View根据测量值来将其摆放在自己的空间中

layout方法确定view自身的位置而onLayout方法则会确定ViewGroup所有子View的位置

View的layout和onLayout方法

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        super.layout(l, t, r, b);
    } else {
        // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}
@Override
protected abstract void onLayout(boolean changed,
        int l, int t, int r, int b);

draw过程


标题:自定义View从源码到应用
作者:OkAndGreat
地址:http://zhongtai521.wang/articles/2021/08/06/1628242427608.html

评论
发表评论
       
       
取消