一、分类
在《Android开发艺术探索》中,刚哥将自定义View分为了四种,个人感觉很精确:
-
继承View,创建新View
-
继承ViewGroup,创建新Layout
-
继承现有特定的View进行扩展,如继承Textview自定义字体
-
继承现有特定的ViewGroup,进行自定义组合控件
二、构造方法
构造函数是View的入口,可以用于初始化一些的内容,和获取自定义属性。
1、View的构造方法
由View的源码中可以看到,通过代码创建View会调用一个参数构造
;通过xml创建View会调用二个参数构造
,但最终会View会调用自己的四个参数的构造方法,并且在其中还调用了一个参数构造做一些初始化工作:
/**
* Simple constructor to use when creating a view from code.
*/
public View(Context context) {
mContext = context;
......
}
/**
* Constructor that is called when inflating a view from XML
*/
public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
......
}
2、自定义View构造方法
我们自定义View时,一般会写两个构造(一个参数和两个参数),而在构造方法里最终会调用super();是为了调用View父类构造中的一些代码为我们做一些初始化工作,以TextView为例,TextView用了四个构造:
/**
* 代码创建该View时会被调用的简单构造函数
* @param context 运行视图的上下文,通过它可以访问当前主题,资源等
*/
public TextView(Context context) {
this(context, null);
}
/**
* 在XML创建该View时会被调用
* @param context
* @param attrs 在xml中为该View指定的属性
*/
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,com.android.internal.R.attr.textViewStyle); // 可以在这里传入第三个参数指定样式,也可代码创建View时直接调用三参构造传入样式
}
/**
* 代码手动调用该方法才生效,给View指定样式时使用
* 如果我们没有对View设置某些属性,就使用这个样式中的属性,在xml中设置style属性不会调用此构造
* @param context
* @param attrs
* @param defStyleAttr 当前主题中的一个属性,它包含对样式资源的引用,该样式资源为视图提供默认值。只有在明确调用该构造的时候才会生效(传入0即没有默认样式)。该style需要在Context使用的theme中被引用才生效
*/
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0); // 可以在这里传入第四个参数指定样式,也可代码创建View时直接调用四参构造传入样式
}
/**
* API 21时增加,当mindSdkVersion=21时再使用
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes 指定该View的样式,仅在defStyleAttr为0或在主题中找不到时使用。可以为0则没有默认样式。
*/
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
......
TypedArray a = theme.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
......
}
可以发现一个重要的方法context.obtainStyledAttributes
,用来解析属性:
public final TypedArray obtainStyledAttributes(AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr,@StyleRes int defStyleRes) {
return getTheme().obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
}
了解一下这几个参数:
-
AttributeSet set
工具类用于解析属性集,获得属性值集合,就是我们为view设置的属性。而我们可以通过多种方式设置属性值,最终的取值是有优先级顺序的:
相同属性取值优先级:
- AttributeSet中的值(xml中指定的属性) >
- AttributeSet中的style资源(xml中使用style属性引用的资源)
- defStyleAttr指定的默认样式
- defStyleResource指定的默认样式资源
- context的Theme中的值
不同的属性:
取并集
-
int[] attrs
attrs.xml中自定义属性在R文件中生成的id数组,通过R.styleable.自定义属性名称
引用 -
int defStyleAttr
attrs.xml中自定义属性使用时引用的style样式,通过R.attr.属性名称
引用,可以用来全局定义某类view的样式。上面的注释中也说明了,这个style样式必须在context的theme中引用才生效,以TextView为例:
// attrs.xml定义属性textViewStyle
<resources>
……
<!-- Default TextView style. -->
<attr name="textViewStyle" format="reference" />
……
</resources>
// 指定全局theme
<application
……
android:theme="@style/AppTheme">
……
</application>
// theme定义
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
// 往上追溯可找到AppTheme继承于Theme.Light,而其中使用textViewStyle属性指定一个TextView默认的style
<style name="Theme.Holo.Light" parent="Theme.Light">
……
<item name="textViewStyle">@style/Widget.Holo.Light.TextView</item>
……
</style>
// textViewStyle
<style name="Widget.Holo.Light.TextView" parent="Widget.TextView" />
<style name="Widget.TextView">
<item name="textAppearance">?attr/textAppearanceSmall</item>
<item name="textSelectHandleLeft">?attr/textSelectHandleLeft</item>
<item name="textSelectHandleRight">?attr/textSelectHandleRight</item>
……
</style>
// 设置defStyleAttr为R.attr.textViewStyle
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,com.android.internal.R.attr.textViewStyle);
}
如上设置的defStyleAttr将会生效,在我们设置TextView相关属性时,未设置的属性将会从R.attr.textViewStyle引用的style中去取,继续往下看 ↓↓
// 以TextView的外观为例,TextView默认外观是属性textAppearanceSmall所对应的样式,跟踪
<item name="textAppearance">?attr/textAppearanceSmall</item>
↓↓
<item name="android:textAppearanceSmall">@style/TextAppearance.AppCompat.Small</item>
↓↓
<style name="TextAppearance.AppCompat.Small" parent="Base.TextAppearance.AppCompat.Small"/>
↓↓
// textview默认外观
<style name="Base.TextAppearance.AppCompat.Small">
<!--14sp-->
<item name="android:textSize">@dimen/abc_text_size_small_material</item>
<item name="android:textColor">?android:attr/textColorTertiary</item>
</style>
↓↓
// textColor
<item name="android:textColorTertiary">@color/abc_secondary_text_material_light</item>
↓↓
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--#24000000-->
<item android:state_enabled="false" android:color="@color/secondary_text_disabled_material_light"/>
<!--#8a000000-->
<item android:color="@color/secondary_text_default_material_light"/>
</selector>
由上可知,如果我们使用TextView未设置字体大小和颜色,那么默认字体为14sp,enable=true时,字体颜色为#8a000000 ,当然,这些是建立在我们的主题样式为Theme.AppCompat.Light.DarkActionBar
,如果设置其他样式,字体颜色会有不同。
- int defStyleRes
指定一个默认的样式资源,形式如R.style.样式资源名称
,只有defStyleAttr属性为0或指定了但没找到时才使用这个,和context的theme没关系,不需要如defStyleAttr必须在theme中引用。
三、UI加载流程
我们来分析我们为Activity设置布局是如何加载的:
1、由Activity#setContentView()开始,最终调用的是PhoneWindow#setContentView()
// MainActivity.java
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置布局资源
setContentView(R.layout.activity_main);
}
}
↓↓
// Activity.java
public void setContentView(@LayoutRes int layoutResID) {
// 调用的是window的setContentView()
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
↓↓
// 获取Window
public Window getWindow() {
return mWindow;
}
↓↓
// Window是抽象类,所以由PhoneWindow实现
mWindow = new PhoneWindow(this, window, activityConfigCallback);
↓↓
// PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// 初始化DecorView
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 {
// 将我们在Activity中设置的布局资源,加载到mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
2、跟踪PhoneWindow#installDecor()
// PhoneWindow.java中
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 创建DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
// DecorView与PhoneWindow关联
mDecor.setWindow(this);
}
if (mContentParent == null) {
// 获得mContentParent
mContentParent = generateLayout(mDecor);
}
......
}
3、跟踪PhoneWindow#generateDecor(),查看DecorView的创建
// DecorView生成
protected DecorView generateDecor(int featureId) {
......
// 调用构造方法创建DecorView
return new DecorView(context, featureId, this, getAttributes());
}
↓↓
// DecorView.java , 可以知道DecorView 是个 FrameLayout
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
......
DecorView(Context context, int featureId, PhoneWindow window,
WindowManager.LayoutParams params) {
super(context);
......
// 关联PhoneWindow
setWindow(window);
......
}
}
4、跟踪PhoneWindow#generateLayout(),查看mContentParent是什么
// PhoneWindow.java中,再看mContentParent生成
protected ViewGroup generateLayout(DecorView decor) {
TypedArray a = getWindowStyle();
......
// 根据Feature判断,获得应该加载的布局资源
layoutResource = R.layout.screen_simple;
......
mDecor.startChanging();
// 将系统布局资源加载到DecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// 获得mContentParent,系统布局资源中的其中一个ViewGroup
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
......
mDecor.finishChanging();
return contentParent;
}
↓↓
// 跳到DecorView.java中 ,看看是如何加载系统布局资源到DecorView中
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
......
mDecorCaptionView = createDecorCaptionView(inflater);
// 加载布局资源
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
// 添加到DecorView
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// 添加到DecorView
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
↓↓
// 回到PhoneWindow.java中,ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); 跟进Window.java中
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
// contentParent在DecorView中获取
return getDecorView().findViewById(id);
}
↓↓
// PhoneWindow.java中 ,看下传入的id ,最终在DecorView中找到这个id
/**
* The ID that the main layout in the XML layout file should have.
*/
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
↓↓
// 最后来看一下, 加载到DecorView中的R.layout.screen_simple
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<!--我们在Activity中设置的资源最终都放入这个id为content的FrameLayout中-->
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
5、跟踪ActivityThread#handleResumeActivity()
以上主要分析的是创建DecorView和contentView加载到DecorView上的过程,而这些都是在Activity的onCreate()中的setContentView()中进行的,那么什么时候会调用Activity的onCreate()呢?DecorView又什么时候加载到window中的呢?
先说下流程:
Activity启动流程中会在
ActivityThread#handleLaunchActivity()
中,调用performLaunchActivity()
启动Activity,在这里面会调用到Activity#onCreate()
方法,从而完成上面所述的DecorView创建动作。
后面会调用
ActivityThread#handleResumeActivity()
方法*(在android-28的源码之前,这个方法是在ActivityThread#handleLaunchActivity()中调用performLaunchActivity()之后被调用,但在android-28的源码中不是,后面研究Activity启动流程再看)*,在其中将DecorView添加到Window显示
下面我们跟踪ActivityThread#handleResumeActivity()部分源码:
// ActivityThread.java
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
......
// 关键代码,调用WindowManager的addView()方法,将decorView添加到window;因为WindowManager是抽象类,所以我们跟进到WindowManagerImpl中去看
wm.addView(decor, l);
......
}
↓↓
// WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// 关键代码
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
↓↓
// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
// 创建ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
......
// 关键方法,在其中完成View的绘制显示流程
root.setView(view, wparams, panelParentView);
}
以上就是大致的UI加载流程,有点乱,自己跟踪一遍就清楚了,最终可以清楚的了解到我们的布局资源加载到DecorView、DecorView加载到window中的整个流程,也能轻松的画出以下UI层级图:
四、UI显示流程
先补充个概念:
ViewRoot
ViewRoot对应ViewRootImpl类,它是连接DecorView和windowManager的纽带,View的三大流程都是通过它来完成的。
/**
- The top of a view hierarchy, implementing the needed protocol between View
- and the WindowManager. This is for the most part an internal implementation
- detail of {@link WindowManagerGlobal}.
- {@hide}
*/
public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
}
上一节,我们跟踪到了root.setView(view, wparams, panelParentView); 说到这个是整个加载流程中最关键的方法,在这个方法中,将完成整个视图树的绘制显示,下面我们继续跟踪:
1、跟踪ViewRootImpl#setView()方法,最终调用ViewRootImpl#requestLayout()方法
// ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// 关键代码
// 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();
// 关键代码,调用的是View中的方法,此处的view是DecorView,this是ViewRootImpl,将ViewRootImpl关联到DecorView的mParent变量,可通过getParent()获取
view.assignParent(this);
}
↓↓
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 重要代码
scheduleTraversals();
}
}
2、跟踪ViewRootImpl#requestLayout()方法,最终调用ViewRootImpl#performTraversals()
// ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 重要代码
scheduleTraversals();
}
}
↓↓
void scheduleTraversals() {
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
↓↓
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
↓↓
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
↓↓
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 关键代码
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
↓↓
private void performTraversals() {
......
// 测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
// 布局
performLayout(lp, mWidth, mHeight);
......
// 绘制
performDraw();
}
3、测量,跟踪ViewRootImpl#performMeasure()
// ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
}
↓↓
// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
↓↓
// 自定义View时用来重写
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
4、布局,跟踪ViewRootImpl#performLayout()
// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
......
final View host = mView;
......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
......
}
↓↓
// View.java
public void layout(int l, int t, int r, int b) {
......
onLayout(changed, l, t, r, b);
......
}
↓↓
// 自定义View时用来重写
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
5、绘制,跟踪ViewRootImpl#performDraw()
// ViewRootImpl.java
private void performDraw() {
......
boolean canUseAsync = draw(fullRedrawNeeded);
......
}
↓↓
private boolean draw(boolean fullRedrawNeeded) {
......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
......
}
↓↓
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
final Canvas canvas;
......
// 自定义View重写onDraw(canvas)中的canvas就是在这里被创建
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
}
↓↓
// View.java
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 子View
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance) 滚动条
*/
......
onDraw(canvas)
.....
}
↓↓
// 自定义View时用来重写
protected void onDraw(Canvas canvas) {
}
个人总结,水平有限,如果有错误,希望大家能给留言指正!如果对您有所帮助,可以帮忙点个赞!如果转载,希望可以标明文章出处!最后,非常感谢您的阅读!