所见即所得 Dialog
我们平时在做普通页面的时候,当 app 运行起来时,所看到的界面,往往就是我们预览 xml 布局文件所看到的那样,即所见即所得。可是如果这些布局文件是放在 dialog 里展示的,情况就不一样了,往往要煞费苦心,才能得到我们想要的效果。
本文分享如何定义一个 BaseDialogFragment 来实现所见即所得的效果。文末还附有处理 dialog 中嵌套 Fragment,status bar 相关问题实践方案。
首先我们创建一个 DialogFragment
public class MyDialogFragment extends DialogFragment {
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_dialog, container, false);
}
}
<?xml version="1.0" encoding="utf-8"?>
<!--fragment_dialog.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Hello Dialog"
android:textSize="32dp" />
</LinearLayout>
我们期待的结果是 dialog 充满整个屏幕,并且 Hello Dialog 这几个字居中显示,但实际的结果是:
我们在根布局设置的 layout 是 match_parent
, 显示出来的结果却是 wrap_content
。
我们知道,一个 dialog 对应着一个 window, 而 window 有一个神奇的属性:isFloating
。当 isFloating
为 true 时,dialog contentView 的 宽高被重置为 wrap_content
,否者重置为 match_parent
。
让我们为 dialog 自定义主题,来改变这个值:
<!-- styles.xml -->
<resources>
<style name="FullScreenDialog" parent="Theme.AppCompat.Dialog">
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">false</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
</resources>
在 MyDialogFragment 中应用这个主题
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NORMAL, R.style.FullScreenDialog);
}
跑起来看看:
果然实现全屏了,但是有两个问题,第一,状态栏变黑色了,第二,'Hello Dialog' 不见了。
第一个问题我们延后解决,先让我们来解决第二个问题。
目前,支持库中存在一个错误,导致样式无法正常显示。 可以通过使用 Activity 的 inflater 来解决这个问题,更改 onCreateView 方法:
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return getActivity().getLayoutInflater().inflate(R.layout.fragment_dialog, container, false);
}
现在,Dialog 的样式能正常显示了,具体细节请参看 stackoverflow 这篇文章open in new window
现在让我们更改根布局的 margin, 留出一些空间来显示遮罩:
<?xml version="1.0" encoding="utf-8"?>
<!--fragment_dialog.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginLeft="32dp"
android:layout_marginRight="32dp"
android:layout_gravity="center"
android:background="#FFFFFF"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Hello Dialog"
android:textSize="32dp" />
</LinearLayout>
跑起来看看,结果是令人失望的:
layout_height
不是 200dp, 而是 match_parent
, 这是和 isFloating
这个属性密切相关的。
现在我们想到的一个解决方案是,在 LinearLayout 外再套一层 FrameLayout
<?xml version="1.0" encoding="utf-8"?>
<!--fragment_dialog.xml-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center"
android:layout_marginLeft="32dp"
android:layout_marginRight="32dp"
android:background="#FFFFFF"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Hello Dialog"
android:textSize="32dp" />
</LinearLayout>
</FrameLayout>
现在,我们得到了预期效果:
但是点击遮罩,dialog 并没有消失,因为这个 dialog 实际上是全屏的,并没有 outside 可以点击。
现在开始封装我们的 BaseDialogFragment, 来解决以下问题:
- 不需要在正常的布局外再套一层 FrameLayout
- 点击遮罩,Dialog 可以消失
- 解决黑色状态栏的问题
定义 DialogFrameLayout,用来处理点击遮罩的问题
public class DialogFrameLayout extends FrameLayout {
interface OnTouchOutsideListener {
void onTouchOutside();
}
GestureDetector gestureDetector = null;
OnTouchOutsideListener onTouchOutsideListener;
public void setOnTouchOutsideListener(OnTouchOutsideListener onTouchOutsideListener) {
this.onTouchOutsideListener = onTouchOutsideListener;
}
public DialogFrameLayout(@NonNull Context context) {
super(context);
commonInit(context);
}
private void commonInit(@NonNull Context context) {
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Rect rect = new Rect();
getHitRect(rect);
int count = getChildCount();
for (int i = count - 1; i > -1; i--) {
View child = getChildAt(i);
Rect outRect = new Rect();
child.getHitRect(outRect);
if (outRect.contains((int) e.getX(), (int) e.getY())) {
return false;
}
}
if (onTouchOutsideListener != null) {
onTouchOutsideListener.onTouchOutside();
}
return true;
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
}
定义 DialogLayoutInflater, 让我们可以不再需要额外的 FrameLayout
public class DialogLayoutInflater extends LayoutInflater {
private LayoutInflater layoutInflater;
private DialogFrameLayout.OnTouchOutsideListener listener;
public DialogLayoutInflater(Context context, LayoutInflater layoutInflater, DialogFrameLayout.OnTouchOutsideListener listener) {
super(context);
this.layoutInflater = layoutInflater;
this.listener = listener;
}
@Override
public LayoutInflater cloneInContext(Context context) {
return layoutInflater.cloneInContext(context);
}
@Override
public View inflate(int resource, @Nullable ViewGroup root, boolean attachToRoot) {
DialogFrameLayout dialogFrameLayout = new DialogFrameLayout(getContext());
dialogFrameLayout.setOnTouchOutsideListener(listener);
dialogFrameLayout.setLayoutParams(new ViewGroup.LayoutParams(-1, -1));
layoutInflater.inflate(resource, dialogFrameLayout, true);
return dialogFrameLayout;
}
}
编写 BaseDialogFragment, 把一切连接起来:
public class BaseDialogFragment extends DialogFragment {
@NonNull
@Override
public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NORMAL, R.style.FullScreenDialog);
super.onGetLayoutInflater(savedInstanceState);
// 换成 Activity 的 inflater, 解决 fragment 样式 bug
LayoutInflater layoutInflater = getActivity().getLayoutInflater();
if (!getDialog().getWindow().isFloating()) {
setupDialog();
layoutInflater = new DialogLayoutInflater(requireContext(), layoutInflater,
new DialogFrameLayout.OnTouchOutsideListener() {
@Override
public void onTouchOutside() {
if (isCancelable()) {
dismiss();
}
}
});
}
return layoutInflater;
}
protected void setupDialog() {
Window window = getDialog().getWindow();
// 解决黑色状态栏的问题
AppUtils.setStatusBarTranslucent(window, true);
AppUtils.setStatusBarColor(window, Color.TRANSPARENT, false);
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialogInterface, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
if (isCancelable()) {
dismiss();
}
return true;
}
return false;
}
});
}
}
就这样,一个 BaseDialogFragment 封装好了,MyDialogFragment 继承 BaseDialogFragment, 即可实现所见即所得。
public class MyDialogFragment extends BaseDialogFragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
// 注意,这里不再需要 getActivity().getLayoutInflater(), 因为 BaseDialogFragment 已经返回了正确的 inflater
return inflater.inflate(R.layout.fragment_dialog, container, false);
}
}
布局文件也不再需要在外面再套个 FrameLayout
<?xml version="1.0" encoding="utf-8"?>
<!--fragment_dialog.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center"
android:layout_marginLeft="32dp"
android:layout_marginRight="32dp"
android:background="#FFFFFF"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Hello Dialog"
android:textSize="32dp" />
</LinearLayout>
一切正如期待的那样,一切都变得简单,只要关注布局就可以了。不过我们可以走得更远:
当 Fragment 根布局有 layout_gravity="bottom"
属性时,自动附加 slide 动画:
状态栏花样变幻以及 Fragment 嵌套
详情请查看 AndroidNavigationopen in new window。该库不仅处理了 Dialog 的问题,还处理了 Fragment 嵌套,嵌套 Fragment 懒加载,右滑返回,沉浸式状态栏,Toolbar 等一系列问题,让你可以专注于业务,而无需为导航等应用级 UI 问题操心。