更多关于安卓源码分析文章,请看:Android源码分析专栏
最近工作需要,将AlertDialog的源码拷贝一份并替换为公司所要求的界面,这样就拥有了一个完全属于公司自己的弹窗。在这个过程中,也研究了AlertDialog的源码,觉得也有不少的收获,于是分享一下,如果可以帮助同行更好熟悉AlertDialog那就更wonderful了。以下为安卓版本为5.1。
首先必须明确一点,AlertDialog是Dialog的子类,他扩展了Dialog的功能,例如可以选择积极,消极,中性按钮,增加标题、消息等。
首先是构造方法。由于构造方法修饰符都是protected或者默认,所以我们在程序使用中不能直接new一个AlertDialog。而使用过AlertDialog的朋友一定知道要使用Builder类去构建一个AlertDialog。那构造方法会在哪里调用呢?是的,你们都能猜到是在Builder中,而Buidler和AlertDialog类是什么关系呢?后面会一一帮你们解释~~
protected AlertDialog(Context context) {
this(context, resolveDialogTheme(context, 0), true);
}
/**
* Construct an AlertDialog that uses an explicit theme. The actual style
* that an AlertDialog uses is a private implementation, however you can
* here supply either the name of an attribute in the theme from which
* to get the dialog's style (such as {@link android.R.attr#alertDialogTheme}
* or one of the constants {@link #THEME_TRADITIONAL},
* {@link #THEME_HOLO_DARK}, or {@link #THEME_HOLO_LIGHT}.
*/
protected AlertDialog(Context context, int theme) {
this(context, theme, true);
}
AlertDialog(Context context, int theme, boolean createThemeContextWrapper) {
super(context, resolveDialogTheme(context, theme), createThemeContextWrapper);
mWindow.alwaysReadCloseOnTouchAttr();
mAlert = new AlertController(getContext(), this, getWindow());
}
protected AlertDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
super(context, resolveDialogTheme(context, 0));
mWindow.alwaysReadCloseOnTouchAttr();
setCancelable(cancelable);
setOnCancelListener(cancelListener);
mAlert = new AlertController(context, this, getWindow());
}
构造方法主要就是传入上下文Context(一般就是弹窗所在的Activity)、Cancel监听器、样式(这个传入父类Dialog处理,默认为0,则使用系统指定的样式)、createContextThemeWrapper是传给父类Dialog的,这个涉及到Dialog中的ContextThemeWrapper(Context一个包装类),这里可以暂时不管。
可以看到这里new出了一个类:AlertController。这个类将是重点类,因为AlertDialog核心代码都在这个类中执行,可以看作其核心控制类。要知道这个类,首先先看下Builder,它是AlertDialog的内部类,作用就是去构建一个AlertDialog,客户端程序可以很方便地传入自己想要的多个参数去构建一个开发者需要的AlertDialog。
首先看下Builder的部分主要代码:
public static class Builder {
private final AlertController.AlertParams P;
private int mTheme;
/**
* Constructor using a context for this builder and the {@link AlertDialog} it creates.
*/
public Builder(Context context) {
this(context, resolveDialogTheme(context, 0));
}
/**
* Constructor using a context and theme for this builder and
* the {@link AlertDialog} it creates. The actual theme
* that an AlertDialog uses is a private implementation, however you can
* here supply either the name of an attribute in the theme from which
* to get the dialog's style (such as {@link android.R.attr#alertDialogTheme}
* or one of the constants
* {@link AlertDialog#THEME_TRADITIONAL AlertDialog.THEME_TRADITIONAL},
* {@link AlertDialog#THEME_HOLO_DARK AlertDialog.THEME_HOLO_DARK}, or
* {@link AlertDialog#THEME_HOLO_LIGHT AlertDialog.THEME_HOLO_LIGHT}.
*/
public Builder(Context context, int theme) {
P = new AlertController.AlertParams(new ContextThemeWrapper(
context, resolveDialogTheme(context, theme)));
mTheme = theme;
}
/**
* Returns a {@link Context} with the appropriate theme for dialogs created by this Builder.
* Applications should use this Context for obtaining LayoutInflaters for inflating views
* that will be used in the resulting dialogs, as it will cause views to be inflated with
* the correct theme.
*
* @return A Context for built Dialogs.
*/
public Context getContext() {
return P.mContext;
}
/**
* Set the title using the given resource id.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setTitle(int titleId) {
P.mTitle = P.mContext.getText(titleId);
return this;
}
/**
* Set the title displayed in the {@link Dialog}.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setTitle(CharSequence title) {
P.mTitle = title;
return this;
}
/**
* Set the title using the custom view {@code customTitleView}. The
* methods {@link #setTitle(int)} and {@link #setIcon(int)} should be
* sufficient for most titles, but this is provided if the title needs
* more customization. Using this will replace the title and icon set
* via the other methods.
*
* @param customTitleView The custom view to use as the title.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setCustomTitle(View customTitleView) {
P.mCustomTitleView = customTitleView;
return this;
}
/**
* Set the message to display using the given resource id.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setMessage(int messageId) {
P.mMessage = P.mContext.getText(messageId);
return this;
}
/**
* Set the message to display.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setMessage(CharSequence message) {
P.mMessage = message;
return this;
}
这部分就是平时使用AlertDialog设置弹窗参数的主要方法。可以看到Builder中有一个成员变量AlertController.AlertParams P,这个AlertController.AlertParams起始是AlertController的一个静态内部类,主要作用就是保存AlertDialog的一些设置参数。可以看到Buidler中设置的参数都是赋值给了它对应的成员变量。注意到Buidler的所有set方法都是返回Builder,这也是我们使用Builder可以使用构建链连续传参数的原因。每次set参数都是传给了AlertController.AlertParams进行保存。
我们在构建链的最后,肯定会调用Builder的create()去创建一个AlertDialog对象,这时候才调用AlertDialog的构造方法,create()代码如下:
/**
* Creates a {@link AlertDialog} with the arguments supplied to this builder. It does not
* {@link Dialog#show()} the dialog. This allows the user to do any extra processing
* before displaying the dialog. Use {@link #show()} if you don't have any other processing
* to do and want this to be created and displayed.
*/
public AlertDialog create() {
final AlertDialog dialog = new AlertDialog(P.mContext, mTheme, false);
P.apply(dialog.mAlert);
dialog.setCancelable(P.mCancelable);
if (P.mCancelable) {
dialog.setCanceledOnTouchOutside(true);
}
dialog.setOnCancelListener(P.mOnCancelListener);
dialog.setOnDismissListener(P.mOnDismissListener);
if (P.mOnKeyListener != null) {
dialog.setOnKeyListener(P.mOnKeyListener);
}
return dialog;
}
这里主要是创建一个新的AlertDialog对象并添加一些设置的监听器。其中 P.apply(dialog.mAlert);是将之前保存在AlertController.AlertParams的参数值赋给当前AlertDialog对象的AlertController成员变量对应的成员变量(AlertController.AlertParams和AlertController不要混淆,就是原来是将各个变量保存在AlertController.AlertParams中的,调用apply之后从AlertController.AlertParams取出来保存到AlertController对应的成员变量中)。
当然,这只是创建AlertDialog,还没show出来。可以直接调用刚创建的AlertDialog的show()方法(实际为调用父类Dialog的show()),也可以在构建完Builder之后直接调用Builder的show方法:
/**
* Creates a {@link AlertDialog} with the arguments supplied to this builder and
* {@link Dialog#show()}'s the dialog.
*/
public AlertDialog show() {
AlertDialog dialog = create();
dialog.show();
return dialog;
}
其实都一样,这样AlertDIalog就可以愉快地show出来了^_^。
等等,那些构建的参数是如何作用在AlertDialog中的,那个核心控制类AlertController还没分析呢。写博客累了,好吧,这个AlertDialog最重要的类,我还是得花时间好好讲下吧。
我们从show()方法入手,看看究竟我们的弹窗是怎么被show出来的。show方法调用的还是父类Dialog的show()。代码如下:
/**
* Start the dialog and display it on screen. The window is placed in the
* application layer and opaque. Note that you should not override this
* method to do initialization when the dialog is shown, instead implement
* that in {@link #onStart}.
*/
public void show() {
//如果弹窗已经show出来了
if (mShowing) {
//如果顶级view存在则设置窗口window,并显示顶级View
if (mDecor != null) {
if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
mDecor.setVisibility(View.VISIBLE);
}
return;
}
mCanceled = false;
//如果窗口还未被创建
if (!mCreated) {
dispatchOnCreate(null);
}
//获得Windowd的顶级View,并将其添加到对应的WindowManager中,然后发送show的消息
onStart();
mDecor = mWindow.getDecorView();
if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
final ApplicationInfo info = mContext.getApplicationInfo();
mWindow.setDefaultIcon(info.icon);
mWindow.setDefaultLogo(info.logo);
mActionBar = new WindowDecorActionBar(this);
}
WindowManager.LayoutParams l = mWindow.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
nl.copyFrom(l);
nl.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
l = nl;
}
try {
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
} finally {
}
}
首先判断下弹窗是否已经被显示出来过,因为可能调用hide暂时隐藏起来,如果已经被show过就重新设置顶级View—DecorView可见(DecorView是所有窗口的根View,常用的Activity的setcontentView()实际上是将布局添加到DecorVeiw的子View,即id为content的ViewGroup中,所以这个方法名很贴切,关于这一部分知识请看郭霖老师写的 Android LayoutInflater原理分析,带你一步步深入了解View(一))
接下来,如果Dialog还未被创建,则 dispatchOnCreate()方法,之所以为红色字体是因为它是重点。它内部确定了Dialog的显示布局界面,一旦界面创建之后,剩下的工作就是将DecorView和WindowManager关联,有兴趣的同学可以看下柯元旦老师写的《Android内核剖析》,讲的很详细。
dispatchOnCreate()方法代码:
// internal method to make sure mcreated is set properly without requiring
// users to call through to super in onCreate
void dispatchOnCreate(Bundle savedInstanceState) {
if (!mCreated) {
onCreate(savedInstanceState);
mCreated = true;
}
}
很简单,只是调用了onCreate(),而onCreate()在Dialog中是空方法,具体在AlertDialog中实现了。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAlert.installContent();
}
是的,mAlert就是之前那个好像很神秘的AlertController。到此该揭开它的神秘面纱了吧~~
点这个类进去看,结果进不去,额~其实这个类是internal包中的类,是不能从android studio从AelrtDialog直接跳进去看源码的。
没关系,如果sdk有下载源码的话,直接文件夹搜就可以找到这个类了,找到installContent()方法:
public void installContent() {
/* We use a custom title so never request a window title */
mWindow.requestFeature(Window.FEATURE_NO_TITLE);
int contentView = selectContentView();
mWindow.setContentView(contentView);
setupView();
setupDecor();
}
这个方法负责完成Dialog显示界面的工作。第一行是确定窗口的样式,第二行是确定窗口的总体布局,进入selectContentView()
方法:
private int selectContentView() {
if (mButtonPanelSideLayout == 0) {
return mAlertDialogLayout;
}
if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) {
return mButtonPanelSideLayout;
}
// TODO: use layout hint side for long messages/lists
return mAlertDialogLayout;
}
就是一个布局的选择,默认返回mAlertDialogLayout,那这个布局什么样子的呢?它的初始化在AlertController的构造方法中:
public AlertController(Context context, DialogInterface di, Window window) {
mContext = context;
mDialogInterface = di;
mWindow = window;
mHandler = new ButtonHandler(di);
//获得AlertDialog相关的属性集
TypedArray a = context.obtainStyledAttributes(null,
com.android.internal.R.styleable.AlertDialog,
com.android.internal.R.attr.alertDialogStyle, 0);
//获取不同布局在安卓系统中对应的id
mAlertDialogLayout = a.getResourceId(com.android.internal.R.styleable.AlertDialog_layout,
com.android.internal.R.layout.alert_dialog);
mButtonPanelSideLayout = a.getResourceId(
com.android.internal.R.styleable.AlertDialog_buttonPanelSideLayout, 0);
mListLayout = a.getResourceId(
com.android.internal.R.styleable.AlertDialog_listLayout,
com.android.internal.R.layout.select_dialog);
mMultiChoiceItemLayout = a.getResourceId(
com.android.internal.R.styleable.AlertDialog_multiChoiceItemLayout,
com.android.internal.R.layout.select_dialog_multichoice);
mSingleChoiceItemLayout = a.getResourceId(
com.android.internal.R.styleable.AlertDialog_singleChoiceItemLayout,
com.android.internal.R.layout.select_dialog_singlechoice);
mListItemLayout = a.getResourceId(
com.android.internal.R.styleable.AlertDialog_listItemLayout,
com.android.internal.R.layout.select_dialog_item);
a.recycle();
}
构造方法主要进行了初始化工作,从AlertDialog获得Context和Window对象,并创建了ButtonHandler作为点击按钮的一个消息
处理类。接下来就是对AletDialog需要用到的各种布局的初始化(得到其在安卓系统中的id),有总体布局mAlertDialogLayout,以及其他按钮和列表的
布局,这些从名字就可以看出来。
大家一定很想知道mAlertDialogLayout对应的布局什么样子吧:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/parentPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="9dip"
android:paddingBottom="3dip"
android:paddingStart="3dip"
android:paddingEnd="1dip">
<LinearLayout android:id="@+id/topPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="54dip"
android:orientation="vertical">
<LinearLayout android:id="@+id/title_template"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="6dip"
android:layout_marginBottom="9dip"
android:layout_marginStart="10dip"
android:layout_marginEnd="10dip">
<ImageView android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingTop="6dip"
android:paddingEnd="10dip"
android:src="@drawable/ic_dialog_info" />
<com.android.internal.widget.DialogTitle android:id="@+id/alertTitle"
style="?android:attr/textAppearanceLarge"
android:singleLine="true"
android:ellipsize="end"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart" />
</LinearLayout>
<ImageView android:id="@+id/titleDivider"
android:layout_width="match_parent"
android:layout_height="1dip"
android:visibility="gone"
android:scaleType="fitXY"
android:gravity="fill_horizontal"
android:src="@android:drawable/divider_horizontal_dark" />
<!-- If the client uses a customTitle, it will be added here. -->
</LinearLayout>
<LinearLayout android:id="@+id/contentPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<ScrollView android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="2dip"
android:paddingBottom="12dip"
android:paddingStart="14dip"
android:paddingEnd="10dip"
android:overScrollMode="ifContentScrolls">
<TextView android:id="@+id/message"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dip" />
</ScrollView>
</LinearLayout>
<FrameLayout android:id="@+id/customPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<FrameLayout android:id="@+android:id/custom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dip"
android:paddingBottom="5dip" />
</FrameLayout>
<LinearLayout android:id="@+id/buttonPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="54dip"
android:orientation="vertical" >
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="4dip"
android:paddingStart="2dip"
android:paddingEnd="2dip"
android:measureWithLargestChild="true">
<LinearLayout android:id="@+id/leftSpacer"
android:layout_weight="0.25"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<Button android:id="@+id/button1"
android:layout_width="0dip"
android:layout_gravity="start"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"
android:maxLines="2"
android:layout_height="wrap_content" />
<Button android:id="@+id/button3"
android:layout_width="0dip"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"
android:maxLines="2"
android:layout_height="wrap_content" />
<Button android:id="@+id/button2"
android:layout_width="0dip"
android:layout_gravity="end"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"
android:maxLines="2"
android:layout_height="wrap_content" />
<LinearLayout android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_weight="0.25"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
看起来很长,拆解一下就容易理解了。
顶级ViewGroup是一个叫parentPanel的LinearLayout,他包裹着四个部分的ViewGroup,分别是topPanel、contentPanel、customPanel、buttonPanel,
其中topPanel又包含着title_template,即标题的布局。后面三者分别是Dialog的主要内容布局、开发者自定义的内容布局、按钮的布局。
回到installContent,接下来就执行 mWindow.setContentView(contentView);将这个布局添加到对应的Window中。
接下来执行setUpView(),该方法具体指定了Dialog的界面布局:
<pre name="code" class="java">private void setupView() {
final ViewGroup contentPanel = (ViewGroup) mWindow.findViewById(R.id.contentPanel);
setupContent(contentPanel);
final boolean hasButtons = setupButtons();
final ViewGroup topPanel = (ViewGroup) mWindow.findViewById(R.id.topPanel);
final TypedArray a = mContext.obtainStyledAttributes(
null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
final boolean hasTitle = setupTitle(topPanel);
final View buttonPanel = mWindow.findViewById(R.id.buttonPanel);
if (!hasButtons) {
buttonPanel.setVisibility(View.GONE);
final View spacer = mWindow.findViewById(R.id.textSpacerNoButtons);
if (spacer != null) {
spacer.setVisibility(View.VISIBLE);
}
mWindow.setCloseOnTouchOutsideIfNotSet(true);
}
final FrameLayout customPanel = (FrameLayout) mWindow.findViewById(R.id.customPanel);
final View customView;
if (mView != null) {
customView = mView;
} else if (mViewLayoutResId != 0) {
final LayoutInflater inflater = LayoutInflater.from(mContext);
customView = inflater.inflate(mViewLayoutResId, customPanel, false);
} else {
customView = null;
}
final boolean hasCustomView = customView != null;
if (!hasCustomView || !canTextInput(customView)) {
mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
}
if (hasCustomView) {
final FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom);
custom.addView(customView, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
if (mViewSpacingSpecified) {
custom.setPadding(
mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom);
}
if (mListView != null) {
((LinearLayout.LayoutParams) customPanel.getLayoutParams()).weight = 0;
}
} else {
customPanel.setVisibility(View.GONE);
}
// Only display the divider if we have a title and a custom view or a
// message.
if (hasTitle) {
final View divider;
if (mMessage != null || customView != null || mListView != null) {
divider = mWindow.findViewById(R.id.titleDivider);
} else {
divider = mWindow.findViewById(R.id.titleDividerTop);
}
if (divider != null) {
divider.setVisibility(View.VISIBLE);
}
}
setBackground(a, topPanel, contentPanel, customPanel, buttonPanel, hasTitle, hasCustomView,
hasButtons);
a.recycle();
}
代码的逻辑基本就是如果有标题,就将标题添加到title的布局中,代码有占篇幅,但是很好理解。重要的代码是setupContent、
setupButtons、setupTitle、setBackground几个方法,分别是根绝内容、按钮、标题去设置各自的界面(先判断是否存在这些元素)
比如应用并没有在构建的过程中添加title,则将title布局隐藏掉。message、button、列表(createListView()方法中实现)。自定义界面则是有的话就添加
到custom的layout中。这里篇幅有限,大家可以看源码具体了解。最后设置背景。注意到createListView()中使用到了一个类RecycleListView,其实这
是AlertController中一个继承ListView的内部类。
看下createListView()方法中的第一行: final RecycleListView listView = (RecycleListView)mInflater.inflate(dialog.mListLayout, null);
dialog.mListLayout在installContent()中已经初始化,它指向的是安卓系统中的一个布局:
<view class="com.android.internal.app.AlertController$RecycleListView"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+android:id/select_dialog_listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="5px"
android:cacheColorHint="@null"
android:divider="?android:attr/listDividerAlertDialog"
android:scrollbars="vertical"
android:overScrollMode="ifContentScrolls"
android:textAlignment="viewStart" />
RecycleListView就是当你AlertDialog中设置有列表,不管是单选还是多选列表的情况都使用的列表类。
回到installContent(),最后一个方法setUpDecor()就是对窗口顶级View的一些初始化设置工作。
就这样,一个AlertDialog就这样创建并show了出来,当然这里讲得仅是属于AlertDialog的源码,关于它窗口的更本质的东西还得
看父类Dialog的源码,这里才可以看到弹窗的本质的东西。
简单总结一下:
1.使用AlertDialog内部类Builder设置AlertDialog的参数保存到AlertController.AlertParams中。
2.AlertDialog调用Builder.create()方法new 一个AlertDialog,并将之前保存的参数取出来传入AlertDialog的AlertController。
3.AlertDialog调用show()(Dialog.show()),会调用AlertDialog的AlertController的installContent()方法,根据之前传递的参数设置弹窗的界面。
4.show()最后将弹窗显示出来。
如果大家想自定义一个完全属于自己的AlertDialog,过程如下:
1.创建一个类继承AlertDialog(由于AlertDialog很多成员变量的类是包访问权限,所以如果单纯将AlertDialog整个代码拷贝过来不能使用其中很多类)。
2.创建一个自己的AlertController,把AlertController代码全拷过来,把构造方法中的默认布局以及列表布局这些都替换成自己的。
(就是mAlertDialogLayout 赋值为自己布局的id)
3.让自己的AlertDialog类持有自己的AlertController类引用,引用的命名与AlertDialog中AlertController成员变量一样(即替换掉)