I'm using BottomSheetDialogFragment from the support library and it warns me that the function setupDialog() should only be used within the library group. But this function is the place where I initialize my dialog:
#Override
public void setupDialog(final Dialog dialog, int style) {
super.setupDialog(dialog, style);
FragmentArgs.inject(this);
dialog.setOnShowListener(dialogINterface -> {
if(dialog.getWindow() != null) {
dialog.getWindow().setLayout(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
});
BottomSheetStatisticsExportBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(getContext()),
R.layout.bottom_sheet_statistics_export,
null,
false
);
View contentView = binding.getRoot();
dialog.setContentView(contentView);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) ((View) contentView.getParent()).getLayoutParams();
CoordinatorLayout.Behavior behavior = params.getBehavior();
if( behavior != null && behavior instanceof BottomSheetBehavior )
((BottomSheetBehavior) behavior).setBottomSheetCallback(bottomSheetBehaviorCallback);
for (Export export : exports)
binding.flexbox.addView(new ExportItemView(getContext(), export));
}
The warning is because I'm using the super method. But what should I do instead? Should I move my code inside another function (onCreateDialog(), onResume()...?), should I remove the call to the super?
Anyone knows?
Should I move my code inside another function (onCreateDialog(),
onResume()...?)
Yes. As it demonstrates in the DialogFragment documentation (which BottomSheetDialogFragment extends), you should use onCreateView() to set up your dialog.
The View you return from this method will be set as the content view for the dialog provided by onCreateDialog(). And the getDialog() method can be used from within onCreateView() to make any adjustments to the aforementioned Dialog.
onCreateDialog() would be used to replace the default Dialog. Which in your case you probably don't want to do; considering that is the method BottomSheetDialogFragment uses to replace the default Dialog with a BottomSheetDialog (in fact, it is the only method BottomSheetDialogFragment overrides).
Here are the BottomSheetDialog and BottomSheetDialogFragment classes I created to replace the support library versions (see comments for more information).
Related
Question:
How to use the latest WindowInset API to adjust space betweeen my dialog and softkeyboard?
I have a BottomSheetDialog with some EditText. The default animation will show the soft keyboard right below my EditText which will cover my save button. After doing some research, I added this line into my BottomSheetDialog fragment
getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
And it worked (as the picture is shown down below)!
This is what I wanted
But apparently SOFT_INPUT_ADJUST_RESIZE is deprecated.
* #deprecated Call {#link Window#setDecorFitsSystemWindows(boolean)} with {#code false} and
* install an {#link OnApplyWindowInsetsListener} on your root content view that fits insets
* of type {#link Type#ime()}.
And I couldn't figure out how to use the new OnApplyWindowInsetsListener to achieve the same effect.
Here is my current BottomSheetDialog fragment:
public class BottomSheetDialog extends BottomSheetDialogFragment {
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
// Adding this line works, but it's deprecated in API 30
// getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
getDialog().getWindow().setDecorFitsSystemWindows(false);
view = inflater.inflate(R.layout.fragment_bottom_dialog_cash, container, false);
view.setOnApplyWindowInsetsListener((v, insets) -> {
Log.d("dialog", "onCreateView: ");
Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
v.setPadding(0,0,0,imeInsets.bottom);
return insets;
});
return view;
}
I use an onclicklistener in another fragment to show this dialog. Code in Another fragment
#Override
public void onItemClick(int position) {
BottomSheetDialog dialog = new BottomSheetDialog();
dialog.show(getParentFragmentManager(), "BottomSheetDialog");
}
In fact, the log indicates that the listener is never triggered when the soft keyboard pop up.
FYI, I'm following this video and this blog.
After more research, I find out that if I use viewBinding and replace view with bind.getRoot(), then everything works fine. I'm not sure why (maybe I should use in onViewCreated instead of onCreateView ?) but the code should be helpful for people having the same issue.
// NOTE: you have to set this in the activity instead of fragment.
getWindow().setDecorFitsSystemWindows(false);
// Only work with API30 or above!
bind.getRoot().setOnApplyWindowInsetsListener((v, insets) -> {
imeHeight = insets.getInsets(WindowInsets.Type.ime()).bottom;
bind.getRoot().setPadding(0, 0, 0, imeHeight);
return insets;
});
One thing to be noticed is that setDecorFitsSystemWindows(false) means the app (you) are responsible for all the system windows includes the status bar and navigation bar.
I also find the tutorials linked down below are very useful, please check if you wanna know more about windowInsets and new animation callback.
New WindowInsets APIs for checking the keyboard (IME) visibility and size
Window Insets and Keyboard Animations Tutorial for Android 11
Compat Version
WindowCompat.setDecorFitsSystemWindows(window, false)
ViewCompat.setOnApplyWindowInsetsListener(rootView()) { _, insets ->
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
rootView().setPadding(0, 0, 0, imeHeight)
insets
}
Or use Insetter library
WindowCompat.setDecorFitsSystemWindows(window, false)
rootView().applyInsetter {
type(ime = true) {
padding()
}
}
The only thing that seemed to do the trick for me was setting the style of the BottomSheetDialog to use the following:
<style name="BottomSheetDialogTheme" parent="Theme.MaterialComponents.Light.BottomSheetDialog">
<item name="android:windowIsFloating">false</item>
<item name="android:windowSoftInputMode">adjustResize|stateVisible</item>
</style>
Is it possible to disable dragging for a BottomSheetDialogFragment, containing scrollable views such as a ViewPager or a NestedScrollView, such that it can't be dragged neither up nor down but still be dismissed by touching outside and that the children can be dragged anyways?
I've looked at all the answers here but I am not pleased since most don't take into account scrollable children or work by forcing the expanded state. The closest is this answer but nonetheless allows dragging the sheet up.
Is there any solution or at least guidance at what should I modify of the original source code?
If you debug your application and use Layout Inspector tool, you will see that BottomSheetDialogFragment uses CoordinatorLayout. Dimmed background is a simple view with an OnClickListener that closes the dialog, and sheet movement is driven by CoordinatorLayout.Behavior.
This can be overriden by modifying created dialog:
Java:
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Dialog d = super.onCreateDialog(savedInstanceState);
// view hierarchy is inflated after dialog is shown
d.setOnShowListener(new DialogInterface.OnShowListener() {
#Override
public void onShow(DialogInterface dialogInterface) {
//this disables outside touch
d.getWindow().findViewById(R.id.touch_outside).setOnClickListener(null);
//this prevents dragging behavior
View content = d.getWindow().findViewById(R.id.design_bottom_sheet);
((CoordinatorLayout.LayoutParams) content.getLayoutParams()).setBehavior(null);
}
});
return d;
}
Kotlin:
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val d = super.onCreateDialog(savedInstanceState)
//view hierarchy is inflated after dialog is shown
d.setOnShowListener {
//this disables outside touch
d.window.findViewById<View>(R.id.touch_outside).setOnClickListener(null)
//this prevents dragging behavior
(d.window.findViewById<View>(R.id.design_bottom_sheet).layoutParams as CoordinatorLayout.LayoutParams).behavior = null
}
return d
}
This does use internal IDs of design library, but unless for some reason they're changed this should be stable.
I noticed I was doing this in my code:
ResultButton = new Button( theActivity );
ButtonUtils.setButtonValues( ... );
((ViewGroup) (theActivity).findViewById( android.R.id.content )).addView( ResultButton );
ResultButton.setOnClickListener( new OnClickListener()
{
#Override
public void onClick(View arg0) {
doStuff();
((ViewGroup) (getActivity()).findViewById( android.R.id.content )).removeView( ResultButton );
}
});
That seems clearly wrong, to remove itself inside its OnClickListener. But what is the right way to deal with these things. Since there is no main method in an Android program I cannot just set a flag and then have it deal with it later.
You never really remove things? You just set them to invisible?
Inside your onClick implementation, the argument that you didn't rename corresponds to the view that triggered the event, you can call the parent of said view and ask it to remove the said child view.
ViewGroup parentView = (ViewGroup) view.getParent();
parentView.removeView(view);
To do this, rename arg0 to view, and you should be fine
Another option, as you mention is to just toggle its visibility, calling the setVisibility method, to either View.GONE or View.INVISIBLE depending on wether you want to to keep taking the screen space it was taking when visible or be completely gone, but since you asked to remove the view, the first option should suffice.
I was looking through the WindowManager's API but I couldn't find a way to make the window disappear after a certain period of time. My desired functionality is to initially make the window pop up, wait until timeout and then disappear/delete itself.
ViewManager.removeView(View v)
try it:
new Handler().postDelayed(new Runnable(){
public void run() {
yourParentView.removeView(childView);
}
}, TIME);
Try adding and using this method:
/**
* Simple method that will take any view class and remove it from it's parent
* #param viewToRemove the view you want to remove from its parent
*/
private void removeViewFromItsParent(View viewToRemove){
if (viewToRemove == null || viewToRemove.getParent() == null){
Log.w("tag", "view or parent is null, no-operation");
return;
}
ViewGroup viewGroupParent = (ViewGroup) viewToRemove.getParent();
viewGroupParent.removeView(viewToRemove);
}
Something like: removeViewFromItsParent(view);
More info here about ViewGroup including its subclasses and what methods you can use (there are a few remove calls that do slightly different things) : http://developer.android.com/reference/android/view/ViewGroup.html
I am using a third-party library and sometimes it pops up a dialog. Before I finish the current activity, I want to check whether there is a dialog popped up in the current context.
Is there any API for this?
You can check it running over the active fragments of that activity and checking if one of them is DialogFragment, meaning that there's a active dialog on the screen:
public static boolean hasOpenedDialogs(FragmentActivity activity) {
List<Fragment> fragments = activity.getSupportFragmentManager().getFragments();
if (fragments != null) {
for (Fragment fragment : fragments) {
if (fragment instanceof DialogFragment) {
return true;
}
}
}
return false;
}
I faced a similar problem, and did not want to modify all locations where dialogs were being created and shown. My solution was to look at whether the view I was showing had window focus via the hasWindowFocus() method. This will not work in all situations, but worked in my particular case (this was for an internal recording app used under fairly restricted circumstances).
This solution was not thoroughly tested for robustness but I figured I would post in in case it helped somebody.
This uses reflection and hidden APIs to get the currently active view roots. If an alert dialog shows this will return an additional view root. But careful as even a toast popup will return an additional view root.
I've confirmed compatibility from Android 4.1 to Android 6.0 but of course this may not work in earlier or later Android versions.
I've not checked the behavior for multi-window modes.
#SuppressWarnings("unchecked")
public static List<ViewParent> getViewRoots() {
List<ViewParent> viewRoots = new ArrayList<>();
try {
Object windowManager;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
windowManager = Class.forName("android.view.WindowManagerGlobal")
.getMethod("getInstance").invoke(null);
} else {
Field f = Class.forName("android.view.WindowManagerImpl")
.getDeclaredField("sWindowManager");
f.setAccessible(true);
windowManager = f.get(null);
}
Field rootsField = windowManager.getClass().getDeclaredField("mRoots");
rootsField.setAccessible(true);
Field stoppedField = Class.forName("android.view.ViewRootImpl")
.getDeclaredField("mStopped");
stoppedField.setAccessible(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
List<ViewParent> viewParents = (List<ViewParent>) rootsField.get(windowManager);
// Filter out inactive view roots
for (ViewParent viewParent : viewParents) {
boolean stopped = (boolean) stoppedField.get(viewParent);
if (!stopped) {
viewRoots.add(viewParent);
}
}
} else {
ViewParent[] viewParents = (ViewParent[]) rootsField.get(windowManager);
// Filter out inactive view roots
for (ViewParent viewParent : viewParents) {
boolean stopped = (boolean) stoppedField.get(viewParent);
if (!stopped) {
viewRoots.add(viewParent);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return viewRoots;
}
AFAIK - there is no public API for this.
Recommended way is to have a reference to the dialog, and check for isShowing() and call dismiss() if necessary, but since you're using a third party library, this may not be an options for you.
Your best bet is to check the documentation for the library you use. If that doesn't help, you're out of luck.
Hint: Activity switches to 'paused' state if a dialog pops up. You may be able to 'abuse' this behavior ;)
You can override activity method onWindowFocusChanged(boolean hasFocus) and track the state of your activity.
Normally, if some alert dialog is shown above your activity, the activity does not get onPause() and onResume() events. But it loses focus on alert dialog shown and gains it when it dismisses.
For anyone reading this and wondering how to detect a Dialog above fragment or activity, my problem was that inside my base fragment I wanted to detect if I'm displaying a Dialog on top of my fragment. The dialog itself was displayed from my activity and I didn't want to reach it there, so the solution I came up with (Thanks to all answers related to this kind of question) was to get the view (or you can get the view.rootView) of my fragment and check whether any of its children have the focus or not. If none of its children have no focus it means that there is something (hopefully a Dialog) being displayed above my fragment.
// Code inside my base fragment:
val dialogIsDisplayed = (view as ViewGroup).children.any { it.hasWindowFocus() }
Solution in kotlin
Inside Fragment
val hasWindowFocus = activity?.hasWindowFocus()
In Activity
val hasWindowFocus = hasWindowFocus()
If true, there is no Dialog in the foreground
if FALSE , there is a view/dialog in the foreground and has focus.
I am assuming, you are dealing with third party library and you don't have access to dialog object.
You can get the root view from the activity,
Then you can use tree traversal algorithm to see if you can reach any of the child view. You should not reach any of your child view if alert box is displayed.
When alert view is displayed ( check with Ui Automator ), the only element present in UI tree are from DialogBox / DialogActivity. You can use this trick to see if dialog is displayed on the screen. Though it sounds expensive, it could be optimized.
If you are using Kotlin just:
supportFragmentManager.fragments.any { it is DialogFragment }