Android开发四《View的工作原理》

一、初识ViewRoot和DecorView

Activity:
Activity并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口。Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window、以及View进行交互。

Window:
Window是视图的承载器,内部持有一个 DecorView,而这个DecorView才是 view 的根布局。Window是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局R.layout.activity_main。Window 通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互。

ViewRoot:
对应于ViewRootImpl类,他是连接WindowManager和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均是通过ViewRoot来完成的.在ActivityThread中,当Activity 对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。

DecorView:
作为顶级View,它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分,上面是标题栏,下面是内容栏.setContentView所设置的布局文件其实被加到内容栏之中的,内容栏id是contnet.其实DecorView和Content都是FrameLayout。

WindowManager
是一个接口,里面常用的方法有:添加View,更新View和删除View。主要是用来管理 Window 的。WindowManager 具体的实现类是WindowManagerImpl。最终,WindowManagerImpl 会将业务交给 WindowManagerGlobal 来处理。

WindowManagerService(WMS)
负责管理各 app 窗口的创建,更新,删除, 显示顺序。运行在 system_server 进程。

六者之间的联系
DecorView的布局
DecorView的创建

1、Activity的setContentView

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

2、PhoneWindow的setContentView:调用了getWindow().setContentView,其中getWindow返回的mWindow是在activity的attach方法中初始化的,实际类型是PhoneWindow;

@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;
}

3、调用installDecor方法

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);
    ...
}

4、调用generateDecor初始化了mDecor,这个DecorView就是Activity中的根View,继承FrameLayout

protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }

5、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);
}
Decorview添加到屏幕中

1、ActivityThread 中的 handleResumeActivity 方法

// ActivityThreadpublic void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        ......      final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);final int forwardBit = isForward
                ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

        // If the window hasn't yet been added to the window manager,
        // and this guy didn't finish itself or start another activity,
        // then go ahead and add the window.
        boolean willBeVisible = !a.mStartedActivity;
        if (!willBeVisible) {
            try {
                willBeVisible = ActivityManager.getService().willActivityBeVisible(
                        a.getActivityToken());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            ......
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }

        // Get rid of anything left hanging around.
        cleanUpPendingRemoveWindows(r, false /* force */);

        // The window is now visible if it has been added, we are not
        // simply finishing, and we are not starting another activity.
        if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
            if (r.newConfig != null) {
                performConfigurationChangedForActivity(r, r.newConfig);
                if (DEBUG_CONFIGURATION) {
                    Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig "
                            + r.activity.mCurrentConfig);
                }
                r.newConfig = null;
            }
            if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward);
            WindowManager.LayoutParams l = r.window.getAttributes();
            if ((l.softInputMode
                    & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                    != forwardBit) {
                l.softInputMode = (l.softInputMode
                        & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                        | forwardBit;
                if (r.activity.mVisibleFromClient) {
                    ViewManager wm = a.getWindowManager();
                    View decor = r.window.getDecorView();
                    wm.updateViewLayout(decor, l);
                }
            }

            r.activity.mVisibleFromServer = true;
            mNumVisibleActivities++;
            if (r.activity.mVisibleFromClient) {
          // 这里也会调用addview
                r.activity.makeVisible();
            }
        }

        r.nextIdle = mNewActivities;
        mNewActivities = r;
        if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r);
        Looper.myQueue().addIdleHandler(new Idler());
    }

上面的代码主要做了以下几件事:

    1. 获取到 DecorView,设置不可见,然后通过 wm.addView(decor, l) 将 view 添加到 WindowManager;
    1. 在某些情况下,比如此时点击了输入框调起了键盘,就会调用 wm.updateViewLayout(decor, l) 来更新 View 的布局。
    1. 这些做完以后,会调用 activity 的 makeVisible ,让视图可见。如果此时 DecorView 没有添加到 WindowManager,那么会添加。
// Activity void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

2、wm.addView 的逻辑。 WindowManager 的实现类是 WindowManagerImpl,而它则是通过 WindowManagerGlobal 代理实现 addView 的,我们看下 addView 的方法:

// WindowManagerGlobal  
 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
           // ......
    
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
           // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            } 
}

在这里,实例化了 ViewRootImpl 。同时调用 ViewRootImpl 的 setView 方法来持有了 DecorView。此外这里还保存了 DecorView ,Params,以及 ViewRootImpl 的实例。

3、ViewRootImpl 的 setView 方法

/**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ......
               
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.

                requestLayout();
                ......
            }
        }
    }

这里先将 mView 保存了 DecorView 的实例,然后调用 requestLayout() 方法,以完成应用程序用户界面的初次布局。

public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

因为是 UI 绘制,所以一定要确保是在主线程进行的,checkThread 主要是做一个校验。接着调用 scheduleTraversals 开始计划绘制了。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

这里主要关注两点:

mTraversalBarrier : Handler 的同步屏障。它的作用是可以拦截 Looper 对同步消息的获取和分发,加入同步屏障之后,Looper 只会获取和处理异步消息,如果没有异步消息那么就会进入阻塞状态。也就是说,对 View 绘制渲染的处理操作可以优先处理(设置为异步消息)。

mChoreographer: 编舞者。统一动画、输入和绘制时机。也是这章需要重点分析的内容。

mTraversalRunnable :TraversalRunnable 的实例,是一个Runnable,最终肯定会调用其 run 方法:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal,如其名,开始绘制了,该方法内部最终会调用 performTraversals 进行绘制。

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }
4.png

参考: Android View 绘制流程
问题:

  1. 首次 View 的绘制流程是在什么时候触发的?
  2. ViewRootImpl 创建的时机?
  3. ViewRootImpl 和 DecorView 的关系是什么?
  4. DecorView 的布局是什么样的?
  5. DecorView 的创建时机?
  6. setContentView 的流程
  7. Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?
  8. PhoneWindow 的创建时机?
  9. 如何触发重新绘制?

二、理解MeasureSpec

1、MeasureSpec:测量规格,决定了一个View的尺寸规格;

父容器影响View的MeasureSpec的创建过程.系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽高.

2、MeasureSpec代表一个32位int值;

高两位代表SpecMode,指测量模式;
低30位代表SpecSize,指在某种测量模式下的规格大小;

SpecMode和SpecSize可以打包为一个MeasureSpec,一个MeasureSpec也可以解包的形式来得出其原始的SpecMode和SpecSize;

3、SpecMode测量模式:
  1. UNSPECIFIED= 0 << MODE_SHIFT:即: 00000000 00000000 00000000 00000000父容器不对子View有任何限制,子View要多大给多大,有系统内部调用,我们不需要研究,例如ScrollView

  2. EXACTLY =1<< MODE_SHIFT:即: 01000000 00000000 00000000 00000000父容器已经测量出子View所需要的大小,即measureSpec中封装的specsize,对应于LayoutParams中的match_parent和设置的固定值

  3. AT_MOST=2 << MODE_SHIFT:即: 10000000 00000000 00000000 00000000父窗口限定了一个最大值给子View即SpecSize,对应于LayoutParams中的wrap_content
    size & ~MODE_MASK是获得SpecSize,mode & MODE_MASK是获得SpecMode,之后再或运算即可得到MeasureSpec。

    /**
     * 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
    */
      * MeasureSpec封装从父对象传递给孩子的布局要求。
      * 每个MeasureSpec表示宽度或高度的要求。
      * MeasureSpec由尺寸和模式组成。 有三种模式:
    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        public static int makeMeasureSpec(int size,int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK)
            }
        }
}
4、MeasureSpec和LayoutParams的对应关系

子View的LayoutParams需要和父容器的MeasureSpec一起才能决定子View的MeasureSpec,从而进一步决定View的宽高.


对应关系
  1. 当View采用固定宽/高时(即设置固定的dp/px),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循我们设置的值。
    当View的宽/高是match_parents时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式那么View也是最大模式并且其大小不会超过父容器的剩余空间
  2. 当View的宽/高是wrap_content时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。
  3. 只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以确定出子元素的MeasureSpec,进一步便可以确定出测量后的大小。

三、View的工作流程

View的工作流程主要指测量measure,布局layout,绘制draw这三大流程,其中measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,而draw将View绘制到屏幕上.

绘制流程

要知道,当用户点击屏幕产生一个触摸行为,这个触摸行为则是通过底层硬件来传递捕获,然后交给ViewRootImpl,接着将事件传递给DecorView,而DecorView再交给PhoneWindow,PhoneWindow再交给Activity,然后接下来就是我们常见的View事件分发了。

硬件 -> ViewRootImpl -> DecorView -> PhoneWindow -> Activity

1、measure过程

measure过程主要分为两类:
1、ViewGroup的measure过程
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);
}

ViewGroup并没有提供onMeasure()方法,而是让其子类来各自实现测量的方法,究其原因就是ViewGroup有不同的布局的需要很难统一,具体的可以查看各个ViewGroup源码实现.

2、View的measure过程
viewGroup测量子view最终都会调用到child.measure,那么来看一下View.measure方法

View. 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;
}

注意:四种办法获取View的宽高

  1. Activity/View#onWindowFocusChanged()
  2. View.post(runnable)投递到消息队列的尾部,然后等待Looper调用此Runnable.
  3. ViewTreeObserver#addonGlobalLayoutListener()
  4. View.measure();
2、layout过程

Layout的作用是ViewGroup用来确定子元素的位置.
layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置.

layout分为两类:
1、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);
        ...
    }
    ...
}

layout中调用了onLayout方法,其中并没有具体的实现,因为确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中均没有实现onLayout()方法。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

2、ViewGroup的layout
View和ViewGroup中均没有实现onLayout()方法,所以具体的子类会重写onLayout方法,
具体的实现根据子类功能不同而不同.

3、draw过程

View的绘制过程遵循如下几步:

  1. 绘制背景,background.draw()
  2. 绘制自己,onDraw()
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

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) {
}

注意:
setWillNotDraw方法:如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化.默认情况:View没有开启这个标记位,ViewGroup默认开启这个标记位;

四、自定义View

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

自定义View三个构造方法使用

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

自定义View三个构造方法使用:
1、一个参数:java代码创建视图的时候被调用;
2、两个参数:这个是在xml创建但是没有指定style的时候被调用 ;
3、三个参数:给View设置了基本的style,就使用这个style中的属性。

1、自定义View的分类:
  1. 继承View重写onDraw()方法
    不只是要实现onDraw()方法,而且在实现过程中还要考虑到wrap_content属性以及padding属性的设置;为了方便配置自己的自定义View还会对外提供自定义的属性,另外如果要改变触控的逻辑,还要重写onTouchEvent()等触控事件的方法。
  2. 继承系统控件(比如TextView)
    在系统控件的基础上进行拓展,添加新的功能或修改显示效果,一般在onDraw方法中处理。
  3. 自定义组合控件
    用已有的控件在xml中组合起来重新定义成一个新的控件,例如一个titleBar会在很多页面用到,那么就可以抽出一个组合控件进行封装,复用起来会方便很多。
2、自定义View须知:
  1. 让View支持wrap_content,否则会充满父布局剩余空间;
  2. 让View支持Padding,否则padding会失效
  3. 不要在View中使用Handler,View内部本身提供了post系列的方法,可以替代;
  4. View中线程或者动画要及时停止,参考View.onDetachedFromWindow;
  5. View如果可以滑动,处理好滑动冲突;
3、自定义View思想:

掌握自定义基本功,比如View的绘制原理,弹性滑动,滑动冲突等;
熟练掌握基本功以后,面对新的自定义View,能够对其分类并选择合适的实现思路.

五、LayoutInflater介绍

1、LayoutInflater获取方式

//1、getLayoutInflater()方式
// Activity中使用:PhoneWindow创建调用LayoutInflater.from()
LayoutInflater layoutInflater = getLayoutInflater();
// Fragment中使用:FragmentHostCallback创建调用LayoutInflater.from()
LayoutInflater layoutInflater = getLayoutInflater(savedInstanceState);

//2、getSystemService()方式
LayoutInflater inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

//3、LayoutInflater.from()方式
LayoutInflater inflater = LayoutInflater.from(this);

//4、View.inflate()方式:View中创建调用LayoutInflater.from()
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
    LayoutInflater factory = LayoutInflater.from(context);
    return factory.inflate(resource, root);
}

总结:所有方式最终都是调用LayoutInflater.from(this);静态方法

2、LayoutInflater静态方法

public static LayoutInflater from(@UiContext Context context) {
        LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

3、LayoutInflater.inflate()方法重载

//第一种
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
//第二种
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                  + Integer.toHexString(resource) + ")");
        }

        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
//第三种
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }
//第四种
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                advanceToRootNode(parser);
                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(
                        getParserStateDescription(inflaterContext, attrs)
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

总结:最终都是调用第四种方法;最常用第二种方法;

参数详解:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

resource:布局资源id;

root:resource生成view对象要填入的父布局。
为null,则返回的view就是布局资源,并且它的根节点的宽高属性会失效;
不为null,需要参照第三个参数;

attachToRoot:是否将resource生成view对象填入root,以root作为最终返回view的根布局。
false,root不为null,则提供root的LayoutParams约束resource生成的view;true,root不为null,以root作为最终返回view的根布局;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容