Android 必知必会 - DialogFragment 实现类似 PopupWindow 效果

近期有网友根据 Android 必知必会 - DialogFragment 使用总结 做一些业务,但是目标却是用 DialogFragment 实现类似 PopupWindow 效果:

  • 只拦截自身所占空间部分的事件,其余空间的点击事件不处理
  • 可以根据某个 View 定位自身位置

虽然在功能上 PopupWindow 更符合需要,但是使用 DialogFragment 代码更简洁、更方便封装功能模块。

基础知识点

WindowManager.LayoutParams.flags

WindowManager.LayoutParams.flags 修改 Window 的表现行为

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
 /**
* Various behavioral options/flags. Default is none.
*
* @see #FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
* @see #FLAG_DIM_BEHIND
* @see #FLAG_NOT_FOCUSABLE
* @see #FLAG_NOT_TOUCHABLE
* @see #FLAG_NOT_TOUCH_MODAL
* @see #FLAG_TOUCHABLE_WHEN_WAKING
* @see #FLAG_KEEP_SCREEN_ON
* @see #FLAG_LAYOUT_IN_SCREEN
* @see #FLAG_LAYOUT_NO_LIMITS
* @see #FLAG_FULLSCREEN
* @see #FLAG_FORCE_NOT_FULLSCREEN
* @see #FLAG_SECURE
* @see #FLAG_SCALED
* @see #FLAG_IGNORE_CHEEK_PRESSES
* @see #FLAG_LAYOUT_INSET_DECOR
* @see #FLAG_ALT_FOCUSABLE_IM
* @see #FLAG_WATCH_OUTSIDE_TOUCH
* @see #FLAG_SHOW_WHEN_LOCKED
* @see #FLAG_SHOW_WALLPAPER
* @see #FLAG_TURN_SCREEN_ON
* @see #FLAG_DISMISS_KEYGUARD
* @see #FLAG_SPLIT_TOUCH
* @see #FLAG_HARDWARE_ACCELERATED
* @see #FLAG_LOCAL_FOCUS_MODE
* @see #FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
*/

以上可以看到它有很多可选项,加上可以多个相互组合,能满足很多需求,这里重点关注三个属性值:

  • FLAG_NOT_TOUCH_MODAL

    Window flag: even when this window is focusable (its FLAG_NOT_FOCUSABLE is not set), allow any pointer events outside of the window to be sent to the windows behind it.

    (API level 1)

  • FLAG_TRANSLUCENT_NAVIGATION

    Window flag: request a translucent navigation bar with minimal system-provided background protection.

    (API level 19)

  • FLAG_TRANSLUCENT_STATUS

    Window flag: request a translucent status bar with minimal system-provided background protection.

    (API level 19)

更详细的介绍请点击 文档

其中 FLAG_NOT_TOUCH_MODAL 可达到『只拦截自身所占空间部分的事件,其余空间的点击事件不处理』的需求,而 FLAG_TRANSLUCENT_NAVIGATIONFLAG_TRANSLUCENT_STATUS 主要是用来调整使用沉浸式状态栏时显示自身位置不正确的问题。

获取 View 位置的时机

如果需要让 DialogFragment 在 onCreate() 等生命周期函数内直接调用显示到某个 View 的位置处,可能无法正确获取到该 View 的坐标,具体参考 Android必知必会-获取View坐标和长宽的时机 一文。

但是,如果在界面显示给用户后,DialogFragment 的显示交给用户触发的话,就不需要在意这个问题了。

代码实现

TopFragment.java

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class TopFragment extends DialogFragment {

private static final String EXT_Y = "y value";
private static final String EXT_BAR = "isTranslucentDecor";
private int y;
private boolean isTranslucentDecor;

public static TopFragment getInstant(int y) {
return getInstant(y, false);
}

public static TopFragment getInstant(int y, boolean isTranslucentDecor) {
TopFragment fragment = new TopFragment();
Bundle ext = new Bundle();
ext.putInt(EXT_Y, y);
ext.putBoolean(EXT_BAR, isTranslucentDecor);
fragment.setArguments(ext);
return fragment;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_TITLE, R.style.dialogFrag);
Bundle args = getArguments();
if (args != null) {
y = args.getInt(EXT_Y, 0);
isTranslucentDecor = args.getBoolean(EXT_BAR, false);
} else {
y = 0;
isTranslucentDecor = false;
}
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
getDialog().setCanceledOnTouchOutside(false);
View rootView = inflater.inflate(R.layout.fragment_top, container, false);
//Do something
final Window window = getDialog().getWindow();
window.setBackgroundDrawableResource(android.R.color.transparent);
window.getDecorView().setPadding(0, 0, 0, 0);
WindowManager.LayoutParams wlp = window.getAttributes();
wlp.width = WindowManager.LayoutParams.MATCH_PARENT;
wlp.height = WindowManager.LayoutParams.WRAP_CONTENT;
wlp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
if (isTranslucentDecor) {
wlp.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
}
wlp.gravity = Gravity.TOP;//必须为 TOP,否则定位不准确
wlp.y = y;//配合 Gravity.TOP 才能准确定位
window.setAttributes(wlp);
//Debug info
rootView.findViewById(R.id.vvv).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getActivity(), "dialogFragment 响应了点击事件", Toast.LENGTH_SHORT).show();
}
});
return rootView;
}
}

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.bt_menu).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TextView title = (TextView) findViewById(R.id.tv_title);
TopFragment.getInstant(title.getBottom()).show(getSupportFragmentManager(), "tags");
}
});
}

注意:如果当前 Activity 使用了沉浸式状态栏,需要使用 TopFragment.getInstant(int y, boolean isTranslucentDecor) 方法,并且 isTranslucentDecor 传值为 true

效果图

未使用沉浸式状态栏、 isTranslucentDecor 传值为 false ,位置正确 使用沉浸式状态栏、 isTranslucentDecor 传值为 false ,位置定位差个状态栏高度 使用沉浸式状态栏、 isTranslucentDecor 传值为 true ,位置正确
s1 s2 s3

总结

总的来说,这里基本完成了要求的效果,但是定位只能指定其顶部开始的位置,不方便底部定位到某个 View 的上面,因为高度自适应的话,在页面渲染完成前是不能知道它的高度的。当然,你可以使用固定高度布局的方式来实现随意定位。

有什么建议或者问题可以随时联系我,共同探讨学习: