View绘制源码浅析(一)布局的加载

Posted zhuliyuan丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了View绘制源码浅析(一)布局的加载相关的知识,希望对你有一定的参考价值。

前言

说到View的绘制大部分人都能说上一两句,但细节实在太多如果没系统的去看很难吃透。近期我专门抽了一周多的时间读了绘制相关的源码,这里准备用三篇博客做一个系统的讲述,目录如下。

  1. View绘制源码浅析(一)布局的加载
  2. View绘制源码浅析(二)布局的测量、布局、绘制
  3. View绘制源码浅析(三)requestLayoutinvalidatepostInvalidate三者的区别

本文的源码基于API27。

疑问

布局加载最重要的就是setContentView()方法了,只需要我们传入一个布局id即可完成布局的加载,但实际上这里是有几个疑问的。

  1. 如何根据xml创建View的。
  2. 如何读取xml中View相关属性的。
  3. 创建的View添加到了哪。

接下来我们带着这些问题再去看源码,避免迷失方向。

setContentView()

我们先从setContentView()这个布局加载的入口开始,看看究竟如何加载布局的。

//MainActivity.java
public class MainActivity extends AppCompatActivity //继承appCompatActivity
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);//布局加载的入口
    


//AppCompatActivity.java
@Override
public void setContentView(@LayoutRes int layoutResID) 
    getDelegate().setContentView(layoutResID);//拿到Activity的委托对象调用setContentView()


//AppCompatActivity.java
@NonNull
public AppCompatDelegate getDelegate() //获取Activity委托对象
    if (mDelegate == null) 
        mDelegate = AppCompatDelegate.create(this, this);
    
    return mDelegate;


//AppCompatDelegate.java
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) //创建Activity委托对象
    return create(activity, activity.getWindow(), callback);//这里将activity.getWindow()传入。


//AppCompatDelegate.java
private static AppCompatDelegate create(Context context, Window window,
                                        AppCompatCallback callback) //根据不同的版本创建不同的Activity委托对象
    if (Build.VERSION.SDK_INT >= 24) 
        return new AppCompatDelegateImplN(context, window, callback);
     else if (Build.VERSION.SDK_INT >= 23) 
        return new AppCompatDelegateImplV23(context, window, callback);
     else 
        return new AppCompatDelegateImplV14(context, window, callback);
    


//AppCompatDelegateImplV9.java 
//最终是调到v9的setContentView方法
@Override
public void setContentView(int resId) 
    ensureSubDecor();//确保SubDecor相关布局初始化完成
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);//找到id为content的view
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);//通过LayoutInflater直接把我们的布局添加到id为content的布局上
    mOriginalWindowCallback.onContentChanged();

由于继承的是appCompatActivity这个兼容的Activity所以是根据不同的api版本创建不同的AppCompatDelegate实现类以兼容老逻辑。setContentView()最终是调到了AppCompatDelegateImplV9setContentView(),接下来具体实现分为两步。

  1. 通过ensureSubDecor()方法确保SubDecor相关布局初始化完成。
  2. 找到SubDecor中id为content的布局,将我们自己的布局inflater到content上。

这里说明下,SubDecor不是DecorView,只是一个变量名为subDecorViewGroup不过这里充当DecorView的角色,不要混淆了。

这里先说第一步ensureSubDecor()

//AppCompatDelegateImplV9.java
private void ensureSubDecor() //确保SubDecor的创建
    if (!mSubDecorInstalled) //如果没有创建SubDecor
        mSubDecor = createSubDecor();//创建SubDecor
        ...
    


private ViewGroup createSubDecor() 
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);//拿到AppCompat相关的主题属性
	//根据主题中的属性执行对应的Feature方法
    if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) 
        requestWindowFeature(Window.FEATURE_NO_TITLE);//我们比较熟悉的FEATURE_NO_TITLE Feature
     else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) 
        // Don't allow an action bar if there is no title.
        requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
    
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) 
        requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
    
    if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) 
        requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
    
    mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
    a.recycle();

    mWindow.getDecorView();//确保DecorView的创建

    final LayoutInflater inflater = LayoutInflater.from(mContext);//用来填充SubDecor的inflater
    ViewGroup subDecor = null;//subDecor布局

	//接下来是根据主题属性初始化不同的subDecor布局
    if (!mWindowNoTitle) 
        if (mIsFloating) 
            // If we're floating, inflate the dialog title decor
            subDecor = (ViewGroup) inflater.inflate(
                R.layout.abc_dialog_title_material, null);

            // Floating windows can never have an action bar, reset the flags
            mHasActionBar = mOverlayActionBar = false;
         else if (mHasActionBar) 
            /**
                 * This needs some explanation. As we can not use the android:theme attribute
                 * pre-L, we emulate it by manually creating a LayoutInflater using a
                 * ContextThemeWrapper pointing to actionBarTheme.
                 */
            TypedValue outValue = new TypedValue();
            mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);

            Context themedContext;
            if (outValue.resourceId != 0) 
                themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
             else 
                themedContext = mContext;
            

            // Now inflate the view using the themed context and set it as the content view
            subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                .inflate(R.layout.abc_screen_toolbar, null);

            mDecorContentParent = (DecorContentParent) subDecor
                .findViewById(R.id.decor_content_parent);
            mDecorContentParent.setWindowCallback(getWindowCallback());

            /**
                 * Propagate features to DecorContentParent
                 */
            if (mOverlayActionBar) 
                mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
            
            if (mFeatureProgress) 
                mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
            
            if (mFeatureIndeterminateProgress) 
                mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
            
        
     else 
        if (mOverlayActionMode) 
            subDecor = (ViewGroup) inflater.inflate(
                R.layout.abc_screen_simple_overlay_action_mode, null);
         else 
            subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
        

        if (Build.VERSION.SDK_INT >= 21) 
            // If we're running on L or above, we can rely on ViewCompat's
            // setOnApplyWindowInsetsListener
            ViewCompat.setOnApplyWindowInsetsListener(subDecor,
                                                      new OnApplyWindowInsetsListener() 
                                                          @Override
                                                          public WindowInsetsCompat onApplyWindowInsets(View v,
                                                                                                        WindowInsetsCompat insets) 
                                                              final int top = insets.getSystemWindowInsetTop();
                                                              final int newTop = updateStatusGuard(top);

                                                              if (top != newTop) 
                                                                  insets = insets.replaceSystemWindowInsets(
                                                                      insets.getSystemWindowInsetLeft(),
                                                                      newTop,
                                                                      insets.getSystemWindowInsetRight(),
                                                                      insets.getSystemWindowInsetBottom());
                                                              

                                                              // Now apply the insets on our view
                                                              return ViewCompat.onApplyWindowInsets(v, insets);
                                                          
                                                      );
         else 
            // Else, we need to use our own FitWindowsViewGroup handling
            ((FitWindowsViewGroup) subDecor).setOnFitSystemWindowsListener(
                new FitWindowsViewGroup.OnFitSystemWindowsListener() 
                    @Override
                    public void onFitSystemWindows(Rect insets) 
                        insets.top = updateStatusGuard(insets.top);
                    
                );
        
    

    if (subDecor == null) //检查SubDecor是否创建
        throw new IllegalArgumentException(
            "AppCompat does not support the current theme features:  "
            + "windowActionBar: " + mHasActionBar
            + ", windowActionBarOverlay: "+ mOverlayActionBar
            + ", android:windowIsFloating: " + mIsFloating
            + ", windowActionModeOverlay: " + mOverlayActionMode
            + ", windowNoTitle: " + mWindowNoTitle
            + " ");
    

    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
        R.id.action_bar_activity_content);//找到subDecor中id为action_bar_activity_content的布局

    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);//找到PhoneWindow中id为content的布局
    if (windowContentView != null) 
        while (windowContentView.getChildCount() > 0) //将windowContentView的子布局全部添加到subDecor中id为action_bar_activity_content的布局上
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        

        windowContentView.setId(View.NO_ID);//清除PhoneWindow中id为content的布局id
        contentView.setId(android.R.id.content);//给subDecor中id为action_bar_activity_content的布局设置上新的id为content以假乱真

        if (windowContentView instanceof FrameLayout) 
            ((FrameLayout) windowContentView).setForeground(null);
        
    

    // Now set the Window's content view with the decor
    mWindow.setContentView(subDecor);//将subDecor设置到window上
    return subDecor;

这里先拿到主题中的属性,然后根据主题中的属性设置对应的Feature,并根据条件创建对应的subDecor。接下来拿到PhoneWindow中id为content的View,把它的子布局全部添加到subDecor中id为action_bar_activity_content的布局上,然后将windowContentView的id移除,给subDecor中id为action_bar_activity_content的布局设置上新的id为content以假乱真充当ContentView的角色,最后将SubDecor通过mWindow.setContentView(subDecor)设置到window上。

那么经过ensureSubDecor()方法后我们就完成了DecorViewSubDecor的初始化并通过mWindow.setContentView(subDecor)SubDecor添加到了DecorView上。完成了SubDecorDecorView的关联。

在回到我们之前的setContentView()

//AppCompatDelegateImplV9.java 
//最终是调到v9的setContentView方法
@Override
public void setContentView(int resId) 
    ensureSubDecor();//确保SubDecor相关布局初始化完成
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);//找到id为content的view
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);//通过LayoutInflater直接把我们的布局添加到id为content的布局上
    mOriginalWindowCallback.onContentChanged();

完成SubDecor初始化后,我们通过mSubDecor.findViewById(android.R.id.content)找到contentParent,然后直接LayoutInflater.from(mContext).inflate(resId, contentParent)将布局添加到了contentParent上完成了布局的添加。

那么对于第一和第二个问题则必须在LayoutInflater.inflate()中寻找答案了,而第三个问题我们已经可以回答了

创建的View添加到了哪?

答:添加到了id为android.R.id.content的view上。

LayoutInflater.inflate()

接下来我们看下inflate()是如何将我们的布局添加到id为content的view上的。

    //LayoutInflater.java
    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 XmlResourceParser parser = res.getLayout(resource);//根据资源id创建解析器
        try 
            return inflate(parser, root, attachToRoot);
         finally 
            parser.close();
        
    

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 
        synchronized (mConstructorArgs) 
           	...
            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try 
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) //拿到第一个节点
                    // Empty
                

                if (type != XmlPullParser.START_TAG) //如果不是开始标签报错
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                

                final String name = parser.getName();//拿到节点名字
                if (TAG_MERGE.equals(name)) //merge单独处理
                    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);//创建xml中根布局
                    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);//根据父布局创建xml中根布局的lp,因为自己的lp是跟父布局有关的。
                        if (!attachToRoot) //当满足root不为null并且attachToRoot为false则直接将lp设置到temp上
                            // 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);//inflate temp的子布局,具体实现就是递归的把view添加到temp上

                    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不为null 并且 attachToRoot为true则直接add到root上
                        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(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
             finally 
    

以上是关于View绘制源码浅析(一)布局的加载的主要内容,如果未能解决你的问题,请参考以下文章

View的绘制流程源码分析

Android面试收集录12 View测量布局及绘制原理

[Android FrameWork 6.0源码学习] View的重绘过程之Layout

面试一问:关于 View测量布局及绘制原理

android基础-viewgroup的测量,布局,绘制

View绘制流程二:测量布局绘制