PopupWindow overlaps Navigation Drawer - android

Currently my popup is overlapping other views. setElevation(0) changes nothing. setOverlapAnchor(false) and setAttachedInDecor(true) also don't help much. Below is the code I have used. I need the popup to be located under navigation drawer
private fun showPopup(anchorView: View) {
PopupWindow(
LayoutInflater.from(activity).inflate(
R.layout.popup_layout,
null
),
100,
100,
false
)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
view?.elevation = 0f
contentView.elevation = 0f
elevation = 0f
}
isTouchable = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
isAttachedInDecor = true
}
PopupWindowCompat.setOverlapAnchor(this, false)
PopupWindowCompat.showAsDropDown(this, anchorView, 0, 0, Gravity.NO_GRAVITY)
}
}

PopupWindow is a window. Your navigation drawer is located on another window with its own view hierarchy.
Its like this:
-- activity
---- window1
------ viewhierarchy
--------NavigationDrawer
---- window2
------ popup
What you want is not possible using PopupWindow.
One possible work-around is to hide & show the popup when navigation is opened and closed. Here's the callback:
https://developer.android.com/reference/android/support/v4/widget/DrawerLayout.DrawerListener.html#ondrawerstatechanged
Or you may add a view as popup by yourself and take care of the positioning and gravity.
Last but not least, check out these libraries as they may have what you want. They work with view so you may manage it the way you want.
https://github.com/sephiroth74/android-target-tooltip
https://github.com/tomergoldst/tooltips

Related

Android 11 Full screen with Coordinator layout status bar spacing issue

I am facing an issue with the full-screen immersive mode in Android 11. My Main activity layout file look something like this,
<DrawerLayout .... android:fitsSystemWindows="true">
<CoordinatorLayout>
<AppBarLayout>
<FragmentContainerView>
I am trying to show a full-screen mode from my Fragment.
Pre Android 11, I used to call the below function from my Fragment's onCreate view
fun hideStatusBar (activity: AppCompatActivity?) {
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) }
I replaced that with,
fun hideStatusBar(activity: AppCompatActivity?) {
#Suppress("DEPRECATION")
if (isAtLeastAndroid11()) {
val controller = activity?.window?.insetsController
controller?.hide(WindowInsets.Type.statusBars())
} else {
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
}
This removes the status bar as intended but leaves a blank space at the top of the screen in the status bar area
With status bar:
Status bar removed:
I tried to measure the Window Insets inside my fragment and adjust the height of my fragment container
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if(isAtLeastAndroid11()){
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
setUiWindowInsets()
}
}
private fun setUiWindowInsets() {
ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
posTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
posBottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
rootView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
updateMargins(
top = posTop,
bottom = posBottom)
}
insets
}
}
But my ViewCompat.setOnApplyWindowInsetsListener is never called. As per this article, Coordinator Layout consumes onApplyWindowInsets callbacks and the child won't get any callbacks. rootView is the root view of my Fragment (a relative layout) which was placed in FragmentContainer in my layout hierarchy.
Comparison between Android 10 and 11
My Question:
How should I get the call to my setOnApplyWindowInsetsListener method in fragment?
How should I let my coordinator layout know to occupy full screen when status bar is hidden?
References:
Coordinator layout consumes callbacks:
setOnApplyWindowInsetsListener never called
https://medium.com/androiddevelopers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec
https://newbedev.com/fitssystemwindows-effect-gone-for-fragments-added-via-fragmenttransaction
The problem is from the App bar. even though it is full screen it will try to prevent content from colliding with status bar even if it is hidden. this is a new implementation also to prevent the phone notch from covering content.
You should remove the app bar if you want to utilize the whole screen. if really need a tooltip for actions use a bottom bar.
You may have to set fitsSystemWindows flag off (where window is Window instance):
window.setDecorFitsSystemWindows(false);
So I do like this:
private void hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowInsetsController.hide(WindowInsets.Type.statusBars());
window.setDecorFitsSystemWindows(false);
}
}
Of course, you also need to set the flag on when showing status bar:
private void showSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowInsetsController.show(WindowInsets.Type.statusBars());
window.setDecorFitsSystemWindows(true);
}
}
FYI, you may have to trigger hideSystemUI or showSystemUI in Activity.onWindowFocusChanged.
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
if (isImmersive) {
hideSystemUI();
} else {
showSystemUI();
}
}
}
(above is in Java. Please translate it into Kotlin)

PopUpWindow ignores fullscreen and also the CutoutMode

My app is a full screen app. It goes full width and does not show a status bar, uses CutoutMode and hides controls like (Home and Back). This also works well.
However, when I create a PopUpWindow, these settings don't seem to take effect. See the following image:
I would have expected that the quadrangles marked in red on the left and right would also be in the corresponding shade of gray.
My code to initialize the PopUp Window looks like this:
val inflater =
view.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater
?: return
// Inflate a custom view using layout inflater
val popUpView = inflater.inflate(R.layout.view_change_name, null)
//popUpView.animation = AnimationUtils.loadAnimation(view.context, R.anim.fade_in_animation)
// Initialize a new instance of popup window
val popupWindow = PopupWindow(
popUpView, // Custom view to show in popup window
LinearLayout.LayoutParams.MATCH_PARENT, // Width of popup window
LinearLayout.LayoutParams.MATCH_PARENT, // Window height
true
)
popupWindow.animationStyle = R.style.PopUpWindowAnimation
popupWindow.isFocusable = true
popupWindow.isOutsideTouchable = true
popupWindow.update(
0,
0,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
//Set the location of the window on the screen
popupWindow.showAtLocation(view, Gravity.BOTTOM, 0, 0)
My method to get the full screen in my activity works like this.
fun Activity.fullscreen() {
with(WindowInsetsControllerCompat(window, window.decorView)) {
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE
hide(Type.systemBars())
}
}
So my idea was to get a decorView in the PopUpWindow. But I think the PopUpWindow has not a decorView. I'm running out of ideas. Does anyone have an approach or solution?
How stupid. After I post this question I found an answer.
Use
popupWindow.isClippingEnabled = false
Documentation
#param enabled false if the window should be allowed to extend outside of the screen

Hide Status bar in a fragment or make the fragment full screen

I have a kotlin app with bottom navigation setup.
I currently have 5 fragments [ProfileFragment, SearchFragment, HomeFragment, SettingsFragment, WebViewFragment]
All of these are just blank fragments. But in my Profile Fragment, I'm showing off a panaroma widget in the top half of the page
I know about making my whole app full screen, but then, on other fragments, content will get hidden under notched displays. And by content, I mean my employer's logo, which he wants, without fail.
So, I tried another way. I made the app full screen and added padding wherever, there was content hiding under the notch. Now, there happen to be various phones, without notches. The content looked unusually padded down, because, well, there was no notch.
If I make adjustments for notched display, the non-notch displays will give issues. And vice-versa.
So, I figured, why not instead of making all activities in my app fullscreen, If I can stretch the ProfileFragment to cover the status bar, or hide the status bar, it'd be a perfect solution.
Is there a way to do either of the following?
Hide the status bar on the ProfileFragment
Stretch the fragment to the top of the screen
Make the fragment full screen, without cutting off the bottom navigation
You can try adding this code in your Activity:
// Hide the status bar.
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
// Remember that you should never show the action bar if the status bar is hidden, so hide that too if necessary.
actionBar?.hide()
More info here: https://developer.android.com/training/system-ui/status#kotlin
AndroidX (support library) has a built-in OnApplyWindowInsetsListener which helps you determine the window insets such as top (status bar) or bottom insets (ie. keyboard) in a device-compatible way.
Since the insets work for API 21+ you have to get the insets manually for below that. Here is an example in Java (v8), hope you get the hang of it:
public class MainActivity extends AppCompatActivity {
...
#Override
protected void onCreate(Bundle savedInstanceState) {
...
View mainContainer = findViewById(R.id.main_container); // You layout hierarchy root
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ViewCompat.setOnApplyWindowInsetsListener(mainContainer , (v, insets) -> {
int statusBarHeight = 0;
if (!isInFullscreenMode(getWindow())) statusBarHeight = insets.getSystemWindowInsetTop();
// Get keyboard height
int bottomInset = insets.getSystemWindowInsetBottom();
// Add status bar and bottom padding to root view
v.setPadding(0, statusBarHeight, 0, bottomInset);
return insets;
});
} else {
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
int statusBarHeight = 0;
if (resourceId > 0 && !isInFullscreenMode(getWindow())) {
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
View decorView = getWindow().getDecorView();
decorView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
Rect r = new Rect();
//r will be populated with the coordinates of your view that area still visible.
decorView.getWindowVisibleDisplayFrame(r);
//get screen height and calculate the difference with the useable area from the r
int height = decorView.getContext().getResources().getDisplayMetrics().heightPixels;
int bottomInset = height - r.bottom;
// if it could be a keyboard add the padding to the view
// if the use-able screen height differs from the total screen height we assume that it shows a keyboard now
//set the padding of the contentView for the keyboard
mainContainer.setPadding(0, statusBarHeight, 0, bottomInset);
});
}
...
}
public static boolean isInFullscreenMode(Window activityWindow) {
return (activityWindow.getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == WindowManager.LayoutParams.FLAG_FULLSCREEN;
}
}
Note that for the bottom inset to work you have to tell Android that your activity is resizable, so in your AndroidManifest.xml:
<application
...>
<activity
android:name=".MainActivity"
...
android:windowSoftInputMode="adjustResize"/>
...
</application>
If you use AppCompatActivity, you can also use:
if(getSupportActionBar() != null) {
getSupportActionBar().hide();
}
in the onCreate methode.

How to place a layout completely on top of keyboard in bottom sheet dialog fragment

I have a bottom sheet dialog fragment in an Activity. When the softkeyboard is shown I need to push the complete dialog fragment layout to the top of keyboard.
I have tried various ways like using GRAVITY.TOP , bottomMargin for the dialog increasing the height etc.
None of the above ways are working. I want to make the top of bottom sheet dialog fragment align with the parent i.e top of phone.
Since I am using Framelayout to add Bottom Sheet Dialog fragment in activity I am unable to set alignParentTopAttribute . Please let me know if there are any ways of doing this or suggestions to achieve this.
Please find below code that I have tried till now:
view?.viewTreeObserver?.addOnGlobalLayoutListener {
var r = Rect()
view?.getWindowVisibleDisplayFrame(r)
var screenHeight = view?.rootView?.height
var initialHeight = rootLayout.height
var keyPadHeight = screenHeight?.minus(r.bottom)
var layoutParams: Frame.LayoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
if (keyPadHeight!! > screenHeight?.times(0.15)!!) {// 0.15 ratio is perhaps enough to determine keypad height.
// keyboard is opened
if (!isKeyboardShowing) {
isKeyboardShowing = true
}
var window: Window? = dialog?.window
var windowManagerLayoutParams: WindowManager.LayoutParams? = window?.attributes
//Tried this
windowManagerLayoutParams.gravity = GRAVITY.TOP
window.attributes = windowManagerLayoutParams
//Tried this
layoutParams.gravity = Gravity.TOP
rootLayout.layoutParams = layoutParams
} else {
// keyboard is closed
if (isKeyboardShowing) {
isKeyboardShowing = false
}
}
}
I have also tried adjustResize and stateVisible in manifest. adjustResize will not push complete layout to the top of soft keyboard, there will be a scroll behind the soft keyboard, user will have to scroll to see the complete layout.
But I do not want to have the scroll, user should see the complete layout on top of the soft keyboard as soon as it is visible.

What exactly does fitsSystemWindows do?

I'm struggling to understand the concept of fitsSystemWindows as depending on the view it does different things. According to the official documentation it's a
Boolean internal attribute to adjust view layout based on system windows such as the status bar. If true, adjusts the padding of this view to leave space for the system windows.
Now, checking the View.java class I can see that when set to true, the window insets (status bar, navigation bar...) are applied to the view paddings, which works according to the documentation quoted above. This is the relevant part of the code:
private boolean fitSystemWindowsInt(Rect insets) {
if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
mUserPaddingStart = UNDEFINED_PADDING;
mUserPaddingEnd = UNDEFINED_PADDING;
Rect localInsets = sThreadLocal.get();
if (localInsets == null) {
localInsets = new Rect();
sThreadLocal.set(localInsets);
}
boolean res = computeFitSystemWindows(insets, localInsets);
mUserPaddingLeftInitial = localInsets.left;
mUserPaddingRightInitial = localInsets.right;
internalSetPadding(localInsets.left, localInsets.top,
localInsets.right, localInsets.bottom);
return res;
}
return false;
}
With the new Material design there are new classes which make extensive use of this flag and this is where the confusion comes. In many sources fitsSystemWindows is mentioned as the flag to set to lay the view behind the system bars. See here.
The documentation in ViewCompat.java for setFitsSystemWindows says:
Sets whether or not this view should account for system screen decorations
such as the status bar and inset its content; that is, controlling whether
the default implementation of {#link View#fitSystemWindows(Rect)} will be
executed. See that method for more details.
According to this, fitsSystemWindows simply means that the function fitsSystemWindows() will be executed? The new Material classes seem to just use this for drawing under the status bar. If we look at DrawerLayout.java's code, we can see this:
if (ViewCompat.getFitsSystemWindows(this)) {
IMPL.configureApplyInsets(this);
mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);
}
...
public static void configureApplyInsets(View drawerLayout) {
if (drawerLayout instanceof DrawerLayoutImpl) {
drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener());
drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
And we see the same pattern in the new CoordinatorLayout or AppBarLayout.
Doesn't this work in the exact opposite way as the documentation for fitsSystemWindows? In the last cases, it means draw behind the system bars.
However, if you want a FrameLayout to draw itself behind the status bar, setting fitsSystemWindows to true does not do the trick as the default implementation does what's documented initially. You have to override it and add the same flags as the other mentioned classes. Am I missing something?
System windows are the parts of the screen where the system is drawing
either non-interactive (in the case of the status bar) or interactive
(in the case of the navigation bar) content.
Most of the time, your app won’t need to draw under the status bar or
the navigation bar, but if you do: you need to make sure interactive
elements (like buttons) aren’t hidden underneath them. That’s what the
default behavior of the android:fitsSystemWindows=“true” attribute
gives you: it sets the padding of the View to ensure the contents
don’t overlay the system windows.
https://medium.com/google-developers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec
it does not draw behind the system bar
it kind of stretches behind the bar to tint it with the same colors it has but the views it contains is padded inside the status bar
if that makes sense
In short, if you're trying to figure out whether to use fitsSystemWindows or not, there's Insetter library by Chris Banes (a developer from the Android team) which offers a better alternative to fitsSystemWindows. For more details let's see the explanation below.
There's a good article published by Android team in 2015 - Why would I want to fitsSystemWindows?. It well explains the default behavior of the attribute and how some layouts like DrawerLayout overrides it.
But, it was 2015. Back in 2017 at droidcon Chris Banes, who works on Android, advised not to use fitSystemWindows attribute unless a container documentation says to use it. And the reason for this is that the default behavior of the flag often doesn't meet your expectations. It's well explained in the video.
But what are these special layouts where you should use fitsSystemWindows? Well, it's DrawerLayout, CoordinatorLayout, AppBarLayout and CollapsingToolbarLayout. These layouts override the default fitsSystemWindows behavior and treat it in a special way, again it's well explained in the video. Such different interpretation of the attribute sometimes leads to a confusion and questions like here. Actually, in another video of droidcon London Chris Banes admits that the decision to overload the default behavior was a mistake (13:10 timestamp of the London conf).
Ok, if fitSystemWindows isn't the ultimate solution, what should be used? In another article from 2019 Chris Banes suggests another solution, a few custom layout attributes based on WindowInsets API. For example, if you want a bottom-right FAB to margin from the navigation bar, you can easily configure it:
<com.google.android.material.floatingactionbutton.FloatingActionButton
app:marginBottomSystemWindowInsets="#{true}"
app:marginRightSystemWindowInsets="#{true}"
... />
The solution uses custom #BindingAdapters, one for paddings and another for margins. The logic is well described in the article I've mentioned above. Some google samples use the solution, for example see Owl android material app, BindingAdapters.kt. I just copy the adapter code here for a reference:
#BindingAdapter(
"paddingLeftSystemWindowInsets",
"paddingTopSystemWindowInsets",
"paddingRightSystemWindowInsets",
"paddingBottomSystemWindowInsets",
requireAll = false
)
fun View.applySystemWindowInsetsPadding(
previousApplyLeft: Boolean,
previousApplyTop: Boolean,
previousApplyRight: Boolean,
previousApplyBottom: Boolean,
applyLeft: Boolean,
applyTop: Boolean,
applyRight: Boolean,
applyBottom: Boolean
) {
if (previousApplyLeft == applyLeft &&
previousApplyTop == applyTop &&
previousApplyRight == applyRight &&
previousApplyBottom == applyBottom
) {
return
}
doOnApplyWindowInsets { view, insets, padding, _ ->
val left = if (applyLeft) insets.systemWindowInsetLeft else 0
val top = if (applyTop) insets.systemWindowInsetTop else 0
val right = if (applyRight) insets.systemWindowInsetRight else 0
val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0
view.setPadding(
padding.left + left,
padding.top + top,
padding.right + right,
padding.bottom + bottom
)
}
}
#BindingAdapter(
"marginLeftSystemWindowInsets",
"marginTopSystemWindowInsets",
"marginRightSystemWindowInsets",
"marginBottomSystemWindowInsets",
requireAll = false
)
fun View.applySystemWindowInsetsMargin(
previousApplyLeft: Boolean,
previousApplyTop: Boolean,
previousApplyRight: Boolean,
previousApplyBottom: Boolean,
applyLeft: Boolean,
applyTop: Boolean,
applyRight: Boolean,
applyBottom: Boolean
) {
if (previousApplyLeft == applyLeft &&
previousApplyTop == applyTop &&
previousApplyRight == applyRight &&
previousApplyBottom == applyBottom
) {
return
}
doOnApplyWindowInsets { view, insets, _, margin ->
val left = if (applyLeft) insets.systemWindowInsetLeft else 0
val top = if (applyTop) insets.systemWindowInsetTop else 0
val right = if (applyRight) insets.systemWindowInsetRight else 0
val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = margin.left + left
topMargin = margin.top + top
rightMargin = margin.right + right
bottomMargin = margin.bottom + bottom
}
}
}
fun View.doOnApplyWindowInsets(
block: (View, WindowInsets, InitialPadding, InitialMargin) -> Unit
) {
// Create a snapshot of the view's padding & margin states
val initialPadding = recordInitialPaddingForView(this)
val initialMargin = recordInitialMarginForView(this)
// Set an actual OnApplyWindowInsetsListener which proxies to the given
// lambda, also passing in the original padding & margin states
setOnApplyWindowInsetsListener { v, insets ->
block(v, insets, initialPadding, initialMargin)
// Always return the insets, so that children can also use them
insets
}
// request some insets
requestApplyInsetsWhenAttached()
}
class InitialPadding(val left: Int, val top: Int, val right: Int, val bottom: Int)
class InitialMargin(val left: Int, val top: Int, val right: Int, val bottom: Int)
private fun recordInitialPaddingForView(view: View) = InitialPadding(
view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom
)
private fun recordInitialMarginForView(view: View): InitialMargin {
val lp = view.layoutParams as? ViewGroup.MarginLayoutParams
?: throw IllegalArgumentException("Invalid view layout params")
return InitialMargin(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin)
}
fun View.requestApplyInsetsWhenAttached() {
if (isAttachedToWindow) {
// We're already attached, just request as normal
requestApplyInsets()
} else {
// We're not attached to the hierarchy, add a listener to
// request when we are
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
v.requestApplyInsets()
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}
As you can see the realization isn't trivial. As I mentioned before, you're welcome to use Insetter library by Chris Banes which offers the same functionality, see insetter-dbx.
Also note that WindowInsets API is going to change since version 1.5.0 of androidx core library. For example insets.systemWindowInsets becomes insets.getInsets(Type.systemBars() or Type.ime()). See the library documentation and the article for more details.
References:
Why would I want to fitsSystemWindows?
WindowInsets — listeners to layouts
Animating your keyboard (part 1)
Becoming a master window fitter (droidcon London 2017)
Becoming a master window fitter (droidcon NYC 2017)

Categories

Resources