摘要
本文从一个简单的弹框,引出Android弹窗的不同实现方式,并比较各种弹窗方式的异同及优缺点。同时还对Activity、Window及View三者的做了分别说明与比较。
具体实现方式
Dialog
Dialog 包含mContext,mWindowManager,mDecor等成员,可见其和Activity、WindowManager以及DecorView之间肯定有着密切联系,Dialog的显示的本质其实就是通过相关的WindowManager将一个继承自FrameLayout的DecorView添加到Dialog所属的Window中
创建并显示一个弹窗:
1 | Dialog dialog = new Dialog(mContext);// mContext 为上下文对象 |
这时会显示一个没有内容的弹窗
构造函数
最终都会走到这个构造方法:
方法代码(基于API19)
1 | Dialog(Context context, int theme, boolean createContextThemeWrapper) { |
注意:
Window w = PolicyManager.makeNewWindow(mContext);
这句代码很关键,可以查看PolicyManager源代码并跟踪,你会发现这里采用的是工厂模式加动态加载来创建Window
、LayoutInflater
和WindowManager
的,这里不作具体分析。当传递的theme不为0时,你可以自定义弹出的Dialog的主题
看了这个构造函数之后,可以确定一点的就是Dialog应该拥有Window大多数的特性。
show()方法(显示Dialog)
方法代码(基于API19)
1 | public void show() { |
定制Dialog
可以作如下定制:
- 改变大小
- 改变现实位置
- 改变背景
- 自定义ContentView
改变大小
从上面代码可以看出,要定制Dialog(如改变Dialog的宽高,需要先拿到Dialog的Window实例,然后获取到其布局参数,对布局参数进行配置即可
如果遇到Dialog左右上下总有默认间隔的问题,请设置Window的backgroundDrawable为null
改变显示位置
在构造函数中有如下代码片段:
1 | mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); |
其中w.setGravity(Gravity.CENTER)
就是设置Dialog位置的,具体设置方式同上。
改变背景
改变背景需要通过定义Dialog入手,具体如下所示:
1 | <style name="TransparentDialog"> |
注意,如果调用Dialog.setTitle()设置标题无效,请确保Dialog的样式中的
android:windowNoTitle=false
,如果没有使用Dialog样式,请确保App Theme中的该值为false。
dismiss()方法(隐藏并销毁Dialog)
该方法可以在任何线程中安全的调用,之所以安全是通过Handler机制保证的,具体看源码:
1 |
|
1 | private final Runnable mDismissAction = new Runnable() { |
注意:dismiss()方法和hide()方法都可以隐藏Dialog,但二者有本质上的区别:调用dismiss会销毁Dialog,释放资源,而调用hide不会,Activity销毁之前,如果页面存在Dialog,一定要调用dismiss()方法,不然会有惊喜哦!
Dialog里面的消息处理
Dialog的显示、消失、取消等消息的处理也是通过Handler机制来完成的,具体是这个Handler实例:
1 | private static final class ListenersHandler extends Handler { |
持有一个Dialog对象的弱引用(避免内存泄漏),然后处理发送过来的消息。
特殊的子类AlertDialog
Dialog有一个特殊的子类AlertDialog,它采用建造者模式实现(这种代码的书写方式很舒服)。创建一个AlertDialog很简单:
1 | private void createAlertDialog() { |
注意:AlertDialog不提供主题相关设置,它默认采用应用的主题。
Dialog就先分析到此,后面如果实际开发中遇到新问题会慢慢补充。
PopupWindow
本质是将一个FrameLayout通过WindowManager添加到Window上显示出来。特性:
- 可以显示任意的View
- 悬浮在当前的Activity之上
重要函数
构造函数
值得关注的构造函数有两个:
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
41public 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);// 设置背景
}public PopupWindow(View contentView, int width, int height, boolean focusable)
,代码如下:1
2
3
4
5
6
7
8
9
10
11public 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 | public void showAtLocation(IBinder token, int gravity, int x, int y) { |
showAsDropDown()
需指定一个锚点View来作为PopupWindow的锚点View。
1 | public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { |
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 | <style name="Activity.Dialog" parent="Theme.AppCompat.Dialog.Alert"> |
当然具体可更具自己项目需要进行定制了。
各种实现方式的优缺点
Dialog
优点:
- 实现简单,尤其是AlertDialog显示更是方便
缺点:
- 定制起来麻烦,通常需要重写部分方法
- DropDown效果需要自己实现
PopupWindow
优点:
- 实现起来简单
- 定制容易
- 可自由控制显示位置
缺点:
- 管理起来没有Dialog方便
Activity(采用Dialog style)
优点:
- 具有Activity的生命周期
缺点:
- 同样存在Activity可能存在的问题
总结
在本质上,弹窗都是通过WindowManager向Window添加View的过程。