Disable HideBottomViewOnScrollBehaviour - android

I currently have a bottom navigation bar inside a Coordinator layout, which I added a HideBottomViewOnScrollBehaviour to. Some screens require the navigation bar to be hidden, which I can achieve by calling the slideUp / slideDown methods from the behaviour object of BottomNavigationBar layout params.
The issue is, even if i'm hiding it programatically, you can reveal it by simply scrolling up again.
I didn't find any solutions, i was thinking there will be something like disabling the behaviour and enabling it on certain screens, but that's not a thing.
Any solutions?
Thanks!

Here are methods to disable/enable scrolling behaviour:
fun enableLayoutBehaviour() {
val params = navView?.layoutParams as CoordinatorLayout.LayoutParams
if (params.behavior == null) {
params.behavior = HideBottomViewOnScrollBehavior<View>()
}
navView?.let {
(params.behavior as HideBottomViewOnScrollBehavior).slideUp(it)
}
}
fun disableLayoutBehaviour() {
val params = navView?.layoutParams as CoordinatorLayout.LayoutParams
navView?.let {
(params.behavior as HideBottomViewOnScrollBehavior).slideDown(it)
}
params.behavior = null
}
You can also just replace NestedScrollView with regular ScrollView on dedicated tabs to disable bottom bar from scrolling.

Related

How to scroll a sync'd up MotionLayout+AppBarLayout programmatically?

Consider the following Coordinator+AppBar+MotionLayout and its MotionScene from Google's demo. This creates a MotionLayout that sync's its Transition progress when the user is scrolling.
Video Preview: https://i.imgur.com/1MnPB8R.mp4
However, I would like to do this in programmatically in Kotlin. Here are my failed attempts.
val motion = findViewById<MotionLayout>(R.id.constraintToolbar)
motion.transitionToState(R.id.end)
The above would cause the MotionLayout to animate, but would immediately blink back to the start state once finished. The AppBarLayout also does not change height.
val appbar = findViewById<AppBarLayout>(R.id.app_bar)
val scrollable = findViewById<NestedScrollView>(R.id.scrollable)
val s = if(motion.progress==0F) appbar.totalScrollRange else 0
scrollable.smoothScrollTo(0, s)
The above scrolls the NestedScrollView, but the AppBarLayout and MotionLayout does not receive the scroll events.
val s = if(motion.progress==0F)-appbar.totalScrollRange else 0
val appbar = findViewById<AppBarLayout>(R.id.app_bar)
appbar.scrollTo(0,s)
appbar.offsetTopAndBottom(s)
The above shifts the AppBarLayout, breaking the layout completely.
How do you trigger this scroll programmatically?
If there's only 2 states, you should try setExpanded()
val e = motion.progress != 0F
appbar.setExpanded(e, true)

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)

Android Support BottomSheetBehavior can't be dynamic?

I'm using Bottom Sheet from Android support library like this:
XML:
<LinearLayout
android:id="#+id/bottomSheetLinearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/fourth_white"
android:orientation="vertical"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior" />
I add child views to LinearLayout:
bottomSheet.addView(actionButtonView);
After I've finished adding child views, I initialize BottomSheetBehavior and expand it:
BottomSheetBehavior sheetBehavior = BottomSheetBehavior.from(bottomSheet);
sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
This doesn't work. Nothing shows. Even if I preset the LinearLayout height inside XML, it's just all white.
If I add all the child views inside LinearLayout in XML, then everything works fine. It just doesn't work when I try to dynamically add views programatically.
Anyone had any similar issues?
Troubles with dynamic content on BottomSheetBehavior related to implementation of it's expanded size calculation. BottomSheetBehavior calculates expanded size in onLayoutChild method. But when you change content of sheet layout process launches asynchronous. Even if you call RequestLayout or something similar. So consequence of calls is like this:
BottomSheetBehavior have old expanded size (in your case I think it is zero)
You add content to BottomSheet. Expanded size is still old.
You call SetState to EXPANDED. BottomSheetBehavior still remember old expanded size and launches animation to that size. State changed to STATE_SETTLING!
onLayoutChild called and BottomSheetBehavior calculates new expanded size. But animation is already in progress and state is STATE_SETTLING so BottomSheetBehavior do not change its size
Animation finished. Size of BottomSheet is old. State changed to EXPANDED but BottomSheetBehavior "forgot" that expanded size was changed during animation.
It is surely the bug of BottomSheetBehaviour implementation.
In my project I found such workaround:
private void showPanel(final View panelContent) {
if (panelBehavior.getState()!=BottomSheetBehavior.STATE_EXPANDED) {
panelBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(final View bottomSheet, int newState) {
if (newState==BottomSheetBehavior.STATE_EXPANDED) {
panelBehavior.setBottomSheetCallback(null);
contentView.removeAllViews();
contentView.addView(panelContent);
panelView.setVisibility(View.VISIBLE);
}
}
#Override
public void onSlide(View bottomSheet, float slideOffset) {
}
});
panelBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
return;
}
contentView.removeAllViews();
contentView.addView(panelContent);
panelView.setVisibility(View.VISIBLE);
}
private void hidePanel() {
panelBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
panelView.setVisibility(View.GONE);
contentView.removeAllViews();
}
So when you need to show BottomSheet with new content call ShowPanel. When you need to completely hide BottomSheet call hidePanel (if you need to hide it in your project. If not you could remove setVisibility from methods).
The idea of workaround is to never change content of BottomSheet when BottomSheetBehavior is not in expanded state. If state is not expanded just change it to expanded, wait until animation finished and only then change content.
Try to post runnable to view's message queue:
bottomSheet.post(new Runnable() {
#Override
public void run() {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
Or with retrolambda:
bottomSheet.post(() -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED));

How to lock CollapsingToolbarLayout from Android support library

I am using Activity class with (usually) one fragment as a content. In the Activity I use CollapsingToolbarLayout as some kind of header for some information and everything works fine. But in some cases (when some fragments are attached) I don't want to show that info, I don't want CollapsingToolbarLayout to open on scroll.
What I want to achieve is to lock CollapsingToolbarLayout, prevent it from opening from the fragment. I am collapsing it programmatically with appBarLayout.setExpanded(false, true);
I came up with a different method as setting the nested scrolling flag only works when dragging the NestedScrollView. The appbar can still be expanded by swiping on the bar itself.
I set this up as a static function in "Utils" class. Obviously the flags you set upon unlocking will depend on which ones are relevant for your use case.
This function assumes you are are starting with an expanded toolbar
public static void LockToolbar(boolean locked, final AppBarLayout appbar, final CollapsingToolbarLayout toolbar) {
if (locked) {
// We want to lock so add the listener and collapse the toolbar
appbar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (toolbar.getHeight() + verticalOffset < 2 * ViewCompat.getMinimumHeight(toolbar)) {
// Now fully expanded again so remove the listener
appbar.removeOnOffsetChangedListener(this);
} else {
// Fully collapsed so set the flags to lock the toolbar
AppBarLayout.LayoutParams lp = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
lp.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED);
}
}
});
appbar.setExpanded(false, true);
} else {
// Unlock by restoring the flags and then expand
AppBarLayout.LayoutParams lp = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
lp.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED);
appbar.setExpanded(true, true);
}
}
Well, I managed to solve it myself. The trick is to disable nested scrolling behaviour with ViewCompat.setNestedScrollingEnabled(recyclerView, expanded);
As I am using one fragment in the activity as a content view and putting it on the backstack I simply check when backstack has changed and which fragment is visibile. Note that I NestedScrollView in every fragment to trigger collapsible toolbar. This is my code:
getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
#Override
public void onBackStackChanged() {
NestedScrollView nestedScrollView = (NestedScrollView)findViewById(R.id.nested_scroll_view);
int size = getSupportFragmentManager().getBackStackEntryCount();
if (size >= 1 && nestedScrollView != null) {
if (getSupportFragmentManager().getBackStackEntryAt(size - 1).getName().equals("SpotDetailsFragment")) {
Log.d(LOG_TAG, "Enabling collapsible toolbar.");
ViewCompat.setNestedScrollingEnabled(nestedScrollView, true);
} else {
Log.d(LOG_TAG, "Disabling collapsible toolbar.");
ViewCompat.setNestedScrollingEnabled(nestedScrollView, false);
}
}
}
});
This thread helped me a lot, where another possible solution is presented:
Need to disable expand on CollapsingToolbarLayout for certain fragments

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