一、前言

Android 应用开发的世界里,setContentView 简直是每个开发者都会接触到的办法。它的作用至关重要——担任将视图(View)或布局(Layout)展现在屏幕上。虽然这看起来是一个简单直接的操作,但其背面实际上隐藏着 Android 体系中杂乱而精妙的窗口办理和视图渲染机制。最近,在与同行的交流中,我发现我简直将这一块忘得一干二净。因此,我决定从 ActivitysetContentView 办法入手,重新整理并深入探讨 Android 的窗口办理和视图展现原理,希望能够为咱们带来新的理解和启发。阿弥陀佛。

吨吨吨,喝一杯冰美式。要睡着了。

PS:由于代码中有大量的款式、动画、装备相关的代码,我会挑选性的省掉,假如你想看完好的,我运用的是“appcompact1.3.1”、SDK31。

假如您有任何疑问、对文章写的不满意、发现过错或者有更好的办法,欢迎在评论、私信或邮件中提出,十分感谢您的支持。

二、AppCompatActivity

一般来说你都会如此运用setConentView

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(mContentLayoutId);
}

当然你也能够在这么运用:

class TestActivity : AppCompatActivity(R.layout.activity_test)

咱们看看AppCompatActivitysetContentView藏着什么:

@Override
public void setContentView(@LayoutRes int layoutResID) {
  //**
  getDelegate().setContentView(layoutResID);
}

能够看到在AppCompatActivity中,运用的getDelegate,这看起来像是托付啊!点下去看看:

@NonNull
public AppCompatDelegate getDelegate() {
  if (mDelegate == null) {
    mDelegate = AppCompatDelegate.create(this, this);
   }
  return mDelegate;
}

咱们找到了全新的AppCompatDelegate!官方如是说:

此类表示一个托付,您能够运用该托付将 AppCompat 的支持扩展到任何 Activity.只能 Activity 与一个 AppCompatDelegate 实例链接,因此应保留从 create(Activity, AppCompatCallback) 回来的实例,直到 Activity 被销毁。

可是AppCompatDelegate是一个笼统类,咱们能够很轻松的找到它的实现类AppCompatDelegateImpl

咱们能够在这里看到setContentView

@Override
public void setContentView(int resId) {
  ensureSubDecor();
  ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
  contentParent.removeAllViews();
  LayoutInflater.from(mContext).inflate(resId, contentParent);
  mAppCompatWindowCallback.getWrapped().onContentChanged();
}

能够看到,咱们供给的LayoutId,最终会添加到contentParent这个View上,那么mSubDecor是从哪儿来的?看到调用办法的姓名,ensure sub decor ,子装修视图,想必在这里。咱们持续往下看:

private void ensureSubDecor() {
  // 检查子装修(sub decor)是否现已设置
  if (!mSubDecorInstalled) {
    // 创立子装修视图
    mSubDecor = createSubDecor();
    //**
   }
}

看样子藏在createSubDecor中, gogogo:

private ViewGroup createSubDecor() {
  TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
    //**省掉 获取当前上下文的主题特点,设置对应的款式
  a.recycle();
​
  // 确保窗口已装置其装修
  ensureWindow();
  mWindow.getDecorView();
​
  // 获取布局填充器
  final LayoutInflater inflater = LayoutInflater.from(mContext);
  ViewGroup subDecor = null;
​
  // 依据是否有标题和是否为起浮窗口来决定运用哪个布局
  if (!mWindowNoTitle) {
    if (mIsFloating) {
      // 假如是起浮窗口,则运用对话框标题装修
      subDecor = (ViewGroup) inflater.inflate(
          R.layout.abc_dialog_title_material, null);
      //**
     } else if (mHasActionBar) {
      // 假如有动作栏,则运用特定主题创立布局
      // 运用主题化上下文填充视图并设置为内容视图
      subDecor = (ViewGroup) LayoutInflater.from(themedContext)
           .inflate(R.layout.abc_screen_toolbar, null);
      //**
     }
   } 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);
      //**
     }
   }
  //**
  // 将窗口的内容视图设置为subDecor
  mWindow.setContentView(subDecor);
  //**
  return subDecor;
}
​

能够看到这个办法回来一个装备好的 subDecorsubDecor运用的是体系的布局,依据装备的不同,运用了不同的xml。回来给咱们用于添加LayoutId,可是它是怎么显现的仍是不清楚,可是注意到mWindow.setContentView(subDecor);

咱们点下去一看,回来到Window类:

/**
 * Convenience for
 * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
 * 将屏幕内容设置为显式视图。此视图直接放置在屏幕的视图层次结构中。它本身能够是一个杂乱的视图层次结构。
 * @param view The desired content to display.
 * @see #setContentView(View, android.view.ViewGroup.LayoutParams)
 */
public abstract void setContentView(View view);

明显这是整个视图显现过程中十分核心的一步。可是它是笼统的!不过依据咱们小学二年级就学过的

The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.

是的,Window只要一个实现类-PhoneWindow

三、PhoneWindow

咱们找到PhoneWindow

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
  if (mContentParent == null) {
    // 假如内容父视图还未创立,则进行装置
    installDecor();
   } 
    //**
  if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    //**
   } else {
    // 将视图添加到内容父视图中
    mContentParent.addView(view, params);
   }
  //**
}
​

咱们看看怎么初始化mContentParent,走进installDecor()的心里。

private void installDecor() {
  mForceDecorInstall = false;
  if (mDecor == null) {
    // 创立窗口装修视图
    mDecor = generateDecor(-1);
       //**
   } else {
    mDecor.setWindow(this);
   }
​
  if (mContentParent == null) {
    // 生成并设置内容布局
    mContentParent = generateLayout(mDecor);
           //**
     } else {
      //**
     }
    // 省掉涉及到过渡办理器和动画的装备
   }
}
​

在这里咱们能够看到两个,generateDecor,可是在createSubDecor中咱们现已创立过了:

protected ViewGroup generateLayout(DecorView decor) {
  // ... 省掉了一部分特点设置代码 ...
​
  // 依据窗口特性挑选布局资源
  int layoutResource;
  int features = getLocalFeatures();
  // 依据不同的特性标志挑选不同的布局资源
  if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
    if (mIsFloating) {
      layoutResource = res.resourceId;
     } else {
      layoutResource = R.layout.screen_title_icons;
     }
   } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 
      && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
    layoutResource = R.layout.screen_progress;
   } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
    if (mIsFloating) {
      layoutResource = res.resourceId;
     } else {
      layoutResource = R.layout.screen_custom_title;
     }
   } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
    if (mIsFloating) {
      layoutResource = res.resourceId;
     } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
      layoutResource = a.getResourceId(
          R.styleable.Window_windowActionBarFullscreenDecorLayout,
          R.layout.screen_action_bar);
     } else {
      layoutResource = R.layout.screen_title;
     }
   } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
    layoutResource = R.layout.screen_simple_overlay_action_mode;
   } else {
    layoutResource = R.layout.screen_simple;
   }
  // 装修视图开始改变
  mDecor.startChanging();
  // 运用LayoutInflater加载布局资源
  mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
​
  // 获取内容父视图
  ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
  // ... 省掉了其他设置代码 ...
​
  // 装修视图完结改变
  mDecor.finishChanging();
​
  return contentParent;
}
​

在这个办法中,会依据不同的特性标志挑选不同的布局资源,可是这些布局都有一个显著的特点。他们都有一个id为content的FrameLayout。便是那个老生常谈的android.R.id.content~。

<FrameLayout
    android:id="@android:id/content"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />

四、倒着回去

至此,咱们知道了PhoneWindowsetContentView中的contentParent来自哪里:

Android一杯冰美式的时刻--去找setContentView

那咱们也是知道了AppCompatDelegate中的setContentViewcontentParent来自哪里:

Android一杯冰美式的时刻--去找setContentView

捋一下:

  1. 在活动的 onCreate 办法中调用 setContentView,传入布局资源ID或者直接传入一个视图(View)目标。
  2. AppCompatDelegate中调用Window.setContentView
  3. PhoneWindow 目标担任创立和办理顶层视图容器,DecorView。假如 DecorView 还未创立,Window 会经过调用 generateDecor 办法来创立它。DecorView中一定有一个ID为android.R.id.content的FrameLayout。
  4. AppCompatDelegateImpl将视图添加到android.R.id.content

五、Activity

为什么没有提及Activity呢?仔细的咱们应该发现了,AppCompatActivity的setContentView是一个重写办法,它完全重写了父类。

Overrides method in Activity

咱们往上看一下:

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

明显,最终Activity也是回到了PhoneWindow,至于为什么要这样呢?你猜~~~~嘻嘻。

六、LayoutInflater

不知道咱们有没有注意到PhoneWindow和AppCompatDelegate中的LayoutInflater,那么DecorView和咱们的Layout是怎么渲染到屏幕的呢?请看LayoutInflater。

Google话来说:

将布局 XML 文件实例化到其相应的 View 目标中

请见下回分解。

七、总结

啊,有点困。冰美式压不住我的睡意。刀了。下一篇咱们来说说LayoutInflater。

假如您有任何疑问、对文章写的不满意、发现过错或者有更好的办法,欢迎在评论、私信或邮件中提出,十分感谢您的支持。