我们平时在做普通页面的时候,当 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); }}复制代码
复制代码
我们期待的结果是 dialog 充满整个屏幕,并且 Hello Dialog 这几个字居中显示,但实际的结果是:
我们在根布局设置的 layout 是 match_parent
, 显示出来的结果却是 wrap_content
。
我们知道,一个 dialog 对应着一个 window, 而 window 有一个神奇的属性:isFloating
。当 isFloating
为 true 时,dialog contentView 的 宽高被重置为 wrap_content
,否者重置为 match_parent
。
让我们为 dialog 自定义主题,来改变这个值:
复制代码
在 MyDialogFragment 中应用这个主题
@Overridepublic 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 的样式能正常显示了,具体细节请参看
现在让我们更改根布局的 margin, 留出一些空间来显示遮罩:
复制代码
跑起来看看,结果是令人失望的:
layout_height
不是 200dp, 而是 match_parent
, 这是和 isFloating
这个属性密切相关的。
现在我们想到的一个解决方案是,在 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
复制代码
一切正如期待的那样,一切都变得简单,只要关注布局就可以了。不过我们可以走得更远:
当 Fragment 根布局有 layout_gravity="bottom"
属性时,自动附加 slide 动画:
状态栏花样变幻以及 Fragment 嵌套
详情请查看 。该库不仅处理了 Dialog 的问题,还处理了 Fragment 嵌套,嵌套 Fragment 懒加载,右滑返回,沉浸式状态栏,Toolbar 等一系列问题,让你可以专注于业务,而无需为导航等应用级 UI 问题操心。