Android 杂谈——从一个简单的弹窗说起

https://rainmonth.github.io/posts/A180417.html

摘要

本文从一个简单的弹框,引出Android弹窗的不同实现方式,并比较各种弹窗方式的异同及优缺点。同时还对Activity、Window及View三者的做了分别说明与比较。

具体实现方式

Dialog

Dialog 包含mContext,mWindowManager,mDecor等成员,可见其和Activity、WindowManager以及DecorView之间肯定有着密切联系,Dialog的显示的本质其实就是通过相关的WindowManager将一个继承自FrameLayout的DecorView添加到Dialog所属的Window中

创建并显示一个弹窗:

1
2
Dialog dialog = new Dialog(mContext);// mContext 为上下文对象
dialog.show();

这时会显示一个没有内容的弹窗

构造函数

最终都会走到这个构造方法:

方法代码(基于API19)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Dialog(Context context, int theme, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {// 根据当前的Theme值,来装饰传递过来的Context对象
if (theme == 0) {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme,
outValue, true);
theme = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, theme);
} else {
mContext = context;
}

mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
Window w = PolicyManager.makeNewWindow(mContext);
mWindow = w;
w.setCallback(this);// 设置回调
w.setWindowManager(mWindowManager, null, null);// 设置WindowManager
w.setGravity(Gravity.CENTER);// 设置显示位置
mListenersHandler = new ListenersHandler(this);// 监听设置
}

注意:

  1. Window w = PolicyManager.makeNewWindow(mContext);这句代码很关键,可以查看PolicyManager源代码并跟踪,你会发现这里采用的是工厂模式加动态加载来创建WindowLayoutInflaterWindowManager的,这里不作具体分析。

  2. 当传递的theme不为0时,你可以自定义弹出的Dialog的主题

看了这个构造函数之后,可以确定一点的就是Dialog应该拥有Window大多数的特性。

show()方法(显示Dialog)

方法代码(基于API19)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public void show() {
if (mShowing) {
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);
}

onStart();
mDecor = mWindow.getDecorView();// 初始化DecorView

//如果有ActionBar,则初始化ActionBar
if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
final ApplicationInfo info = mContext.getApplicationInfo();
mWindow.setDefaultIcon(info.icon);
mWindow.setDefaultLogo(info.logo);
mActionBar = new ActionBarImpl(this);
}

// 处理Window的布局参数
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();// 发送显示消息(消息有HandlerListener实例(继承Handler)处理)
} finally {
}
}

定制Dialog

可以作如下定制:

  • 改变大小
  • 改变现实位置
  • 改变背景
  • 自定义ContentView
改变大小

从上面代码可以看出,要定制Dialog(如改变Dialog的宽高,需要先拿到Dialog的Window实例,然后获取到其布局参数,对布局参数进行配置即可

如果遇到Dialog左右上下总有默认间隔的问题,请设置Window的backgroundDrawable为null

改变显示位置

在构造函数中有如下代码片段:

1
2
3
4
5
6
7
8
mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
Window w = PolicyManager.makeNewWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);

其中w.setGravity(Gravity.CENTER)就是设置Dialog位置的,具体设置方式同上。

改变背景

改变背景需要通过定义Dialog入手,具体如下所示:

1
2
3
4
5
6
7
8
9
<style name="TransparentDialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowIsTranslucent">false</item><!--半透明-->
<item name="android:backgroundDimEnabled">false</item><!--模糊-->
</style>

注意,如果调用Dialog.setTitle()设置标题无效,请确保Dialog的样式中的android:windowNoTitle=false,如果没有使用Dialog样式,请确保App Theme中的该值为false。

dismiss()方法(隐藏并销毁Dialog)

该方法可以在任何线程中安全的调用,之所以安全是通过Handler机制保证的,具体看源码:

1
2
3
4
5
6
7
8
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {// 如果是UI线程,直接调用
dismissDialog();
} else {// 不是,通过Handler的post方法交由mDismissAction这个Runnable对象处理
mHandler.post(mDismissAction);
}
}
1
2
3
4
5
private final Runnable mDismissAction = new Runnable() {
public void run() {
dismissDialog();
}
};

注意:dismiss()方法和hide()方法都可以隐藏Dialog,但二者有本质上的区别:调用dismiss会销毁Dialog,释放资源,而调用hide不会,Activity销毁之前,如果页面存在Dialog,一定要调用dismiss()方法,不然会有惊喜哦!

Dialog里面的消息处理

Dialog的显示、消失、取消等消息的处理也是通过Handler机制来完成的,具体是这个Handler实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static final class ListenersHandler extends Handler {
private WeakReference<DialogInterface> mDialog;

public ListenersHandler(Dialog dialog) {
mDialog = new WeakReference<DialogInterface>(dialog);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DISMISS:
((OnDismissListener) msg.obj).onDismiss(mDialog.get());
break;
case CANCEL:
((OnCancelListener) msg.obj).onCancel(mDialog.get());
break;
case SHOW:
((OnShowListener) msg.obj).onShow(mDialog.get());
break;
}
}
}

持有一个Dialog对象的弱引用(避免内存泄漏),然后处理发送过来的消息。

特殊的子类AlertDialog

Dialog有一个特殊的子类AlertDialog,它采用建造者模式实现(这种代码的书写方式很舒服)。创建一个AlertDialog很简单:

1
2
3
4
5
6
7
8
9
10
private void createAlertDialog() {
Dialog alertDialog = new AlertDialog.Builder(this)
.setTitle("标题")
.setMessage("AlertDialog测试")
.setNegativeButton("取消", null)
.setPositiveButton("确定", null)
.setCancelable(true)
.create();
alertDialog.show();
}

注意:AlertDialog不提供主题相关设置,它默认采用应用的主题。

Dialog就先分析到此,后面如果实际开发中遇到新问题会慢慢补充。

本质是将一个FrameLayout通过WindowManager添加到Window上显示出来。特性:

  • 可以显示任意的View
  • 悬浮在当前的Activity之上

重要函数

构造函数

值得关注的构造函数有两个:

  1. public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes),代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    mContext = context;
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    // 属性获取
    final TypedArray a = context.obtainStyledAttributes(
    attrs, R.styleable.PopupWindow, defStyleAttr, defStyleRes);
    final Drawable bg = a.getDrawable(R.styleable.PopupWindow_popupBackground);
    mElevation = a.getDimension(R.styleable.PopupWindow_popupElevation, 0);
    mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false);

    // Preserve default behavior from Gingerbread. If the animation is
    // undefined or explicitly specifies the Gingerbread animation style,
    // use a sentinel value.
    if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupAnimationStyle)) {
    final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, 0);
    if (animStyle == R.style.Animation_PopupWindow) {
    mAnimationStyle = ANIMATION_STYLE_DEFAULT;
    } else {
    mAnimationStyle = animStyle;
    }
    } else {
    mAnimationStyle = ANIMATION_STYLE_DEFAULT;
    }

    // 动画设置
    final Transition enterTransition = getTransition(a.getResourceId(
    R.styleable.PopupWindow_popupEnterTransition, 0));
    final Transition exitTransition;
    if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupExitTransition)) {
    exitTransition = getTransition(a.getResourceId(
    R.styleable.PopupWindow_popupExitTransition, 0));
    } else {
    exitTransition = enterTransition == null ? null : enterTransition.clone();
    }

    a.recycle();

    setEnterTransition(enterTransition);
    setExitTransition(exitTransition);
    setBackgroundDrawable(bg);// 设置背景
    }
  2. public PopupWindow(View contentView, int width, int height, boolean focusable),代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public PopupWindow(View contentView, int width, int height, boolean focusable) {
    if (contentView != null) {
    mContext = contentView.getContext();
    mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }

    setContentView(contentView);
    setWidth(width);
    setHeight(height);
    setFocusable(focusable);
    }

第一个构造函数支持通过xml文件来配置PopupWindow的属性(如mElevation、mOverlapAnchor和进入退出动画等),调用构造函数后,还需要调用setContentView才能为PopupWindow填充内容(否则没有内容显示出来的)

第二个构造函数,直接填充PopupWindow内容,设置其宽、高及是否可获取焦点等(更常用)

showAtLocation()

需要指定一个View作为Parent View用来获取WindowToken(实现IBinder接口的类的实例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void showAtLocation(IBinder token, int gravity, int x, int y) {
// 如果已经处于显示状态或内容为空,直接返回
if (isShowing() || mContentView == null) {
return;
}

TransitionManager.endTransitions(mDecorView);
// 从之前的锚点断开,移除相关的监听
detachFromAnchor();

mIsShowing = true;
mIsDropdown = false;
mGravity = gravity;

final WindowManager.LayoutParams p = createPopupLayoutParams(token);
// 显示之前的准备,将PopupWindow的内容嵌入到ViewGroup(即FrameLayout)中
preparePopup(p);

p.x = x;
p.y = y;
// 正式开始显示
invokePopup(p);
}
showAsDropDown()

需指定一个锚点View来作为PopupWindow的锚点View。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
// 如果已经处于显示状态或内容为空,直接返回
if (isShowing() || !hasContentView()) {
return;
}

TransitionManager.endTransitions(mDecorView);
// 将PopupWindow和锚点关联起来
attachToAnchor(anchor, xoff, yoff, gravity);

mIsShowing = true;
mIsDropdown = true;

final WindowManager.LayoutParams p =
createPopupLayoutParams(anchor.getApplicationWindowToken());
preparePopup(p);
// 判断是否因为PopupWindow过大而需要覆盖锚点View(先判断是否允许滚动,如果允许,则通过滚动来自适应;否则通过覆盖锚点View,函数实现比较复杂,这里不做具体分析)
final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
p.width, p.height, gravity, mAllowScrollingAnchorParent);
updateAboveAnchor(aboveAnchor);
p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;

invokePopup(p);
}
attachToAnchor()

将PopupWindow和锚点View关联起来。

preparePopup()

包装显示的内容。

invokePopup()

将包装好的内容添加到Window的具体实施方法

update()

从意思上看,就是更新PopupWindow信息的,哪些信息改变了需要调用update以及时更新PopupWindow呢,具体有如下这些:

  • setClippingEnabled(boolean)
  • setFocusable(boolean)
  • setIgnoreCheekPress()
  • setInputMethodMode(int)
  • setTouchable(boolean)
  • setAnimationStyle(int)

注意:在弹出的PopupWindow中再显示一个PopupWindow,如在一个PopupWindow中使用自定义键盘,如果键盘开启了Preview,则点击预览的时候会crash。

Activity(采用Dialog style)

采用Activity来实现弹框操作起来比较简单,只要将Dialog style样式应用到对应的Activity即可。

具体的样式设置一般如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style name="Activity.Dialog" parent="Theme.AppCompat.Dialog.Alert">
<!--无ActionBar、无标题-->
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<!--边框-->
<!--<item name="android:windowFrame">@android:color/transparent</item>-->
<item name="android:windowFrame">@null</item>
<!--是否浮现在activity之上-->
<item name="android:windowIsFloating">true</item>
<!--半透明-->
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item><!--无标题-->
<item name="android:windowBackground">@android:color/transparent</item><!--背景透明-->
<item name="android:backgroundDimAmount">0.3</item>
<item name="android:windowAnimationStyle">@null</item>
</style>

当然具体可更具自己项目需要进行定制了。

各种实现方式的优缺点

Dialog

优点:

  • 实现简单,尤其是AlertDialog显示更是方便

缺点:

  • 定制起来麻烦,通常需要重写部分方法
  • DropDown效果需要自己实现

优点:

  • 实现起来简单
  • 定制容易
  • 可自由控制显示位置

缺点:

  • 管理起来没有Dialog方便

Activity(采用Dialog style)

优点:

  • 具有Activity的生命周期

缺点:

  • 同样存在Activity可能存在的问题

总结

在本质上,弹窗都是通过WindowManager向Window添加View的过程。