setOnApplyWindowInsetsListener never called - android

I would like to calculate the navigationBar height. I've seen this presentation : https://chris.banes.me/talks/2017/becoming-a-master-window-fitter-nyc/
So, I tried to use the method View.setOnApplyWindowInsetsListener().
But, for some reason, it's never called.
Does anyone knows why ? Any limitation there ?
I've tried to use it like this :
navBarOverlay.setOnApplyWindowInsetsListener { v, insets ->
Timber.i("BOTTOM = ${insets.systemWindowInsetBottom}")
return#setOnApplyWindowInsetsListener insets
}
Note that my root layout is a ConstraintLayout.

I faced the same issue.
If your root view is ConstraintLayout and contains android:fitsSystemWindows="true" attr, the view consumed onApplyWindowInsets callbacks.
So if you set onApplyWindowInsets on child views, they never get onApplyWindowInsets callbacks.
Or check your parent views consume the callback.

This is what I observed; in other words, your experience might be different.
[Layout for Activity]
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true" <--
tools:context=".MyAppActivity">
...
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="#dimen/fab_margin" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Notice android:fitsSystemWindows="true" in the outer most layout. As long as we have it, setOnApplyWindowInsetsListener() does get called.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
ViewCompat.setOnApplyWindowInsetsListener(fab) { view, insets ->
...
insets
}
}
Alternatively, if you are going for the "full screen", meaning you want your layout to extend to the status bar and the navigation bar, you can do something like the following.
override fun onCreate(savedInstanceState: Bundle?) {
...
WindowCompat.setDecorFitsSystemWindows(window, false) <--
ViewCompat.setOnApplyWindowInsetsListener(fab) { view, insets ->
...
insets
}
}
The same idea is applicable when you are using a Fragment, as long as the Activity (that contains the Fragment) has either fitsSystemWindows in the outer most layout or you set your Activity as full screen.

I have faced with this problem when I've used CollapsingToolbarLayout, problem is that CollapsingToolbarLayout not invoking insets listener, if you have CollapsingToolbarLayout, then right after this component all other view insets wouldn't be triggered. If so, then remove listener from CollapsingToolbarLayout by calling
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout, null)
If you don't CollapsingToolbarLayout, then some other view is blocking insets from passing from view to view.
Or you have already consumed them, I guess you didn't do it)

There is also bug with CollapsingToolbarLayout, it prevents siblings to receive insets, you can see it in issues github link. One of the solutions is to putAppbarLayout below in xml other views for them to receive insets.

I've had to (and I think I am expected to) explicitly call requestApplyInsets() at some appropriate time to make the listener get hit.
Check this article for some possible tips: https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1

My solution is to call it on navBarOverlay.rootView.

putting ViewCompat.setOnApplyWindowInsetsListener into onResume worked for me with constraintLayout.
#Override
public void onResume() {
super.onResume();
ViewCompat.setOnApplyWindowInsetsListener(requireActivity().getWindow().getDecorView(), (v, insets) -> {
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
return insets;
});
}

I faced similar issue on API 30.
For setOnApplyWindowInsetsListener() to work you have to make sure that your activity is in full-screen mode. You can use below method to do so
WindowCompat.setDecorFitsSystemWindows(activity.window, false) //this is backward compatible version
Also make sure you are not using below method anywhere to set UI flags
View.setSystemUiVisibility(int visibility)

I had this problem on android 7.1. But on android 11 it worked correctly. Just create a class:
import android.content.Context
import android.util.AttributeSet
import androidx.core.view.ViewCompat
import com.google.android.material.appbar.CollapsingToolbarLayout
class InsetsCollapsingToolbarLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : CollapsingToolbarLayout(context, attrs, defStyle) {
init {
ViewCompat.setOnApplyWindowInsetsListener(this, null)
}
}
And use everywhere InsetsCollapsingToolbarLayout instead of CollapsingToolbarLayout

In my app, it gets called once and not every time I wanted to. Therefore, in that one time it gets called, I saved the widnowInsets to a global variable to use it throughout the app.

I used the following solution using this answer:
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(android.R.id.content)
) { _: View?, insets: WindowInsetsCompat ->
navigationBarHeight = insets.systemWindowInsetBottom
insets
}

I used following solution in my project and it's works like a charm.
val decorView: View = requireActivity().window.decorView
val rootView = decorView.findViewById<View>(android.R.id.content) as ViewGroup
ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
val isKeyboardVisible = isKeyboardVisible(insets)
Timber.d("isKeyboardVisible: $isKeyboardVisible")
// Do something with isKeyboardVisible
insets
}
private fun isKeyboardVisible(insets: WindowInsetsCompat): Boolean {
val systemWindow = insets.systemWindowInsets
val rootStable = insets.stableInsets
if (systemWindow.bottom > rootStable.bottom) {
// This handles the adjustResize case on < API 30, since
// systemWindow.bottom is probably going to be the IME
return true
}
return false
}
Use setWindowInsetsAnimationCallback instead of setOnApplyWindowInsetsListener in Android API > 30

One problem I had is ViewCompat.setOnApplyWindowInsetsListener was called again in some other place in the code for the same view. So make sure that is only set once.

Related

Exposed Dropdown Menu not showing items

Exposed Dropdown Menu doesn't show items after user selection and fragment transition.
Following is the basic xml declaration:
<com.google.android.material.textfield.TextInputLayout
...
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
<AutoCompleteTextView
....
android:id="#+id/dropdown"
android:dropDownHeight="300dp"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
And, the declared code on the fragment (inside onViewCreated()):
val items = listOf("Material", "Design", "Components", "Android")
val adapter = ArrayAdapter(requireContext(), R.layout.item_menu, items)
dropdown.setAdapter(adapter)
dropdown.setText(items[0], false)
As mentioned here, it was set on AutoCompleteTextView's setText method (dropdown.setText("", false)) the filter parameter as false. However, after navigating to a next fragment and coming back to it only the pre-selected text is shown on the dropdown.
Fragments are changed using navigation component (v. 2.3.2).
The fragment's view gets destroyed when using the navigation component. (maybe not always, but it will certainly happen some of the time as you experienced)
I think you might be able to make it work simply by adding a condition:
if (savedInstanceState == null) {
dropdown.setText(items[0], false)
}
So that the default is only set when not restoring the view state.
Otherwise it's just a matter saving the state as usual. Here's a documentation article about it if you're unsure what I'm talking about. It will essentially amount to adding the following code to your fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val item = savedInstanceState?.getInt("selectedPos", 0) ?: 0
dropdown.setText(items[item], false)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("selectedPos", dropdown.getListSelection())
}
If you're using the MVVM architecture, you can save the selected position using SavedStateHandle in your ViewModel, when it gets changed.
I had the same problem. I searched for issues on github page. I found this https://github.com/material-components/material-components-android/issues/2012#issuecomment-808853621 work around for now. It works.
Create an extension like below
fun AutoCompleteTextView.showDropdown(adapter: ArrayAdapter<String>?) {
if(!TextUtils.isEmpty(this.text.toString())){
adapter?.filter?.filter(null)
}
}
Then on click of dropdown
binding.quaters.setOnClickListener {
binding.quaters.showDropdown(arrayAdapter)
}
That's all it should work. This seems to be a bug which should be fixed hopefully.
This is a temprorary solution that is working for me -
https://github.com/material-components/material-components-android/issues/2012#issuecomment-868181589
Write the setup code for ExposedDropdownMenu in onResume() of a fragment,
instead of onCreateView()/onViewCreated()
override fun onResume() {
super.onResume()
val sortingArtist = resources.getStringArray(R.array.sortingArtist)
val arrayAdapterArtist = ArrayAdapter(requireContext(), R.layout.dropdown_items_artist, sortingArtist)
binding?.autoCompleteTextViewArtist?.setAdapter(arrayAdapterArtist)
binding?.autoCompleteTextViewArtist?.setText(sortingArtist[0], false)
}
For reference - https://material.io/components/menus/android#exposed-dropdown-menus

Here SDK Android resize on layout changes

We are having hard times to smoothly resize a here SDK map on Android.
We want to smoothly resize the map to the bottom sheet collapse and hidden state as shown in
But as you can see it does not really resize instead its jumps to the new position while the map keeps its dimensions and does not scale.
And this is what we did:
...
<com.here.sdk.mapview.MapView
android:id="#+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="#dimen/nine_grid_unit" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/menuBottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/white"
android:clickable="true"
android:elevation="#dimen/four_grid_unit"
android:focusable="true"
app:behavior_hideable="true"
app:behavior_peekHeight="#dimen/thirtytwo_grid_unit"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<View
android:id="#+id/tap_stop"
android:layout_width="#dimen/nine_grid_unit"
android:layout_height="#dimen/one_grid_unit"
android:layout_marginTop="#dimen/one_grid_unit"
android:background="#color/grey_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<edeka.digital.app.widget.SegmentedControlView
android:id="#+id/tabSwitchSegmentedControl"
android:layout_width="#dimen/thirtyfive_grid_unit"
android:layout_height="wrap_content"
android:paddingStart="#dimen/three_grid_unit"
android:paddingEnd="#dimen/three_grid_unit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/tap_stop"
app:segmentCount="2"
app:segmentTitles="#array/segment_titles_shop_search" />
</androidx.constraintlayout.widget.ConstraintLayout>
...
And code:
val bottomBehavior = BottomSheetBehavior.from(binding.menuBottomSheet)
bottomBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
val mapView = binding.map
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
bottomSheetBehaviorObservable.onNext(newState)
when (newState) {
BottomSheetBehavior.STATE_COLLAPSED -> {
mapView.bottom = binding.menuBottomSheet.top
mapView.invalidate()
}
BottomSheetBehavior.STATE_HIDDEN -> {
mapView.bottom = binding.menuBottomSheet.top
mapView.invalidate()
}
else -> { /* void */
}
}
}
})
I would have expected some kind of resize() function or that it layouts itself if layout dimensions change.
What we really want is already implemented in HERE WeGo App. The whole maps scales (inc. here logo) if user swipes the bottom sheet:
Can anyone help us out?
The demo shown in 1 can be found here:
https://github.com/edekadigital/heremaps-demo
The best solution that I've found to achieve it is to add a new method:
private fun updateMapView(bottomSheetTop: Int) {
val mapView = binding.map
val principalY = Math.min(bottomSheetTop / 2.0, mapView.height / 2.0)
mapView.camera.principalPoint = Point2D(mapView.width / 2.0, principalY)
val logoMargin = Math.max(0, mapView.bottom - bottomSheetTop)
mapView.setWatermarkPosition(WatermarkPlacement.BOTTOM_CENTER, logoMargin.toLong())
}
and call it in onSlide and onStateChanged like this:
updateMapView(bottomSheet.top)
Note that you need to have the HERE logo at the bottom center position, otherwise it can't use an adjustable margin.
I was also trying to resize the map view, but the results were unsatisfying. Here is the code if you want to give a try:
private fun updateMapView(bottomSheetTop: Int) {
val mapView = binding.map
mapView.layoutParams.height = bottomSheetTop
mapView.requestLayout()
}
It looks like that your map view is covered by the sliding panel and is not redrawn during slide animation. It renders only when the state changes. You can try to add mapView.invalidate() in onSlide method, like this:
override fun onSlide(bottomSheet: View, slideOffset: Float) {
mapView.invalidate()
}
However, to be sure if that's the actual reason, I would need to get and build your code.
I was able to get your code, compile and reproduce the bug. I've found two options to fix that, both tested on an emulator and a real device.
Copy the code from state change handling code into onSlide method:
override fun onSlide(bottomSheet: View, slideOffset: Float) {
mapView.bottom = binding.menuBottomSheet.top
mapView.invalidate()
}
Remove map view resizing and invalidating code at all. It basically makes the whole setupBottomSheet method redundant. Map view works correctly without resizing and it's a preferable way to fix it, as it involves less code and operations.

Can you use a BindingAdapter on a ViewStub?

I want to create an "inflateWhen" BindingAdapter and attach it to a viewstub to have it inflate when a boolean value is true. However, the BindingAdapter keeps trying to operate on the root view of the viewstub, causing it to fail to compile. Is there any way to do this as a bindingadapter rather than having to do it programmatically in the activity?
Here's what I have so far:
#BindingAdapter("inflateWhen")
fun inflateWhen(viewstub: ViewStub, inflate: Boolean) {
if (inflate) {
viewstub.inflate()
} else {
viewstub.visibility = View.GONE
}
}
This is what I have, but when attached to a viewstub like
<ViewStub
android:id="#+id/activity_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:inflateWhen="#{viewmodel.userid != 0}" />
it fails to compile. The error is:
ActivityMyAccountSectionedBindingImpl.java:1087: error: cannot find symbol
if (this.pageFooter.isInflated()) this.pageFooter.getBinding().setVariable(BR.inflateWhen, viewmodelRatingInt0);
Looks like it's trying to apply the binding to the inflated view, but that's not what I want here.
08.10.2020 Update:
I have written an article on Medium where I provide an example of how to switch between layouts on the fly depending on the screen state using ViewStub and DataBinding:
https://medium.com/#mxdiland/viewstub-databinding-handle-screen-states-easily-2f1c01098b87
Old accepted answer:
I also faced the problem to write #BindingAdapter for the ViewStub to control layout inflation using databinding instead of direct referencing to the ViewStub and calling inflate()
Along the way, I did some investigation and studied the following things:
ViewStub must have android:id attribute to avoid build errors like java.lang.IllegalStateException: target.id must not be null;
any custom attribute declared for ViewStub in an XML, databinding tries to set as a variable to the layout which will be inflated instead of the stub;
... that's why any binding adapter is written for ViewStub will never be used by databinding
there is only one but pretty tricky #BindingAdapter which works: androidx.databinding.adapters.ViewStubBindingAdapter and allows setting ViewStub.OnInflateListener trough XML attribute android:onInflate
the ViewStubBindingAdapter's first argument is ViewStubProxy not View or ViewStub!;
any different adapter written similarly does not work - databinding tries to set variable to the future layout instead of using the adapter
BUT it is allowed to override existing androidx.databinding.adapters.ViewStubBindingAdapter and implement some desired logic.
Because this adapter is one and the only option to interact with ViewStub using databinding I decided to override the adapter and use not for its intended purpose
The idea is to provide specific ViewStub.OnInflateListener which will be the listener itself and at the same time will be a signal that ViewStub.inflate() should be called:
class ViewStubInflationProvoker(
listener: ViewStub.OnInflateListener = ViewStub.OnInflateListener { _, _ -> }
) : ViewStub.OnInflateListener by listener {
companion object {
#JvmStatic
fun provideIf(clause: Boolean): ViewStubInflationProvoker? {
return if (clause) {
ViewStubInflationProvoker()
} else {
null
}
}
}
}
and overriding binding adapter:
#BindingAdapter("android:onInflate")
fun setOnInflateListener(
viewStubProxy: ViewStubProxy,
listener: ViewStub.OnInflateListener?
) {
viewStubProxy.setOnInflateListener(listener)
if (viewStubProxy.isInflated) {
viewStubProxy.root.visibility = View.GONE.takeIf { listener == null } ?: View.VISIBLE
return
}
if (listener is ViewStubInflationProvoker) {
viewStubProxy.viewStub?.inflate()
}
}
and XML part
...
<ViewStub
android:id="#+id/no_data_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="#layout/no_data"
android:onInflate="#{ViewStubInflationProvoker.provideIf(viewModel.state == State.Empty.INSTANCE)}"
app:viewModel="#{viewModel.noDataViewModel}"
/>
...
So now the inflation will happen only when the state is State.Empty and databinding will set viewModel variable to the inflated #layout/no_data layout.
Not really graceful but working solution.

Why is setAlpha() not working in RecyclerViews?

I am trying to change the transparency of item-views in a RecyclerView according to certain user inputs.
if (quantity>0) {
holder.itemView.setAlpha((float) 1);
} else {
holder.itemView.setAlpha((float) 0.65);
}
Changing alpha from 0.65 to 1 works fine when quantity > 0. But the reverse is not working on the other case. When debugging, it clearly shows going through the line holder.itemView.setAlpha((float) 0.65); However, alpha is not reduced. Any clue about what's going on?
recycler's ItemAnimator changes alpha during update item process
you can try to add
((SimpleItemAnimator) myRecyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
I had this same problem. Instead of changing the alpha of the itemView, give an name to your root layout and change its alpha instead, as the recyclerview animations handle the itemView alpha animation making it not work.
Remove item animator
In Java:
mRecyclerView.setItemAnimator(null);
Or in Kotlin:
recycler_view.itemAnimator = null
Consider this is the HolderView class
class MyViewHolder(val viewHolder: View) : RecyclerView.ViewHolder(view)
And consider this is how your Adapter class looks like from inside
// ...
override fun onBindViewHolder(holder: MyViewHolder, i: Int) {
holder.viewHolder.alpha = 0.65f
}
Sometimes if your code is holder.viewHolder.alpha = 0.65f it doesn't work always!
Rather than that, you may use alpha of the main container just like that
<RelativeLayout
android:id="#+id/viewMain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
... Your other components goes here
</RelativeLayout>
Now, from your adapter use this instead, it should work in all cases
// ...
override fun onBindViewHolder(holder: MyViewHolder, i: Int) {
holder.viewMain.alpha = 0.65f
}

Toolbar animateLayoutChanges strange behavior

I set animateLayoutChanges=true in my code, to animate the options item I have in my fragments.
When I open the PreferenceFragment I enable the up navigation icon, then I disable it when I close it, but it behaves in a strange way, leaving a sort of padding to the left of the title, where the NavigationIcon was.
Here's a gif showing what's happening:
Do you guy's have any idea on why is this happening?
I searched far and wide on the internet, but found nothing.
Is there any workaround to animate these items in the same way?
Thanks.
Remark: I know this thread in more than a year long but a lot of people still stumble on this problem.
It took me good few hours to finally dig into this problem and solution is actually relatively easy.
The Scenario
When you call ActionBar.setDisplayHomeAsUpEnabled() or Toolbar.setNavigationIcon(), Toolbar will dynamically add or remove navigation view depending on specified value
public void setNavigationIcon(#Nullable Drawable icon) {
if (icon != null) {
ensureNavButtonView();
if (!isChildOrHidden(mNavButtonView)) {
addSystemView(mNavButtonView, true);
}
} else if (mNavButtonView != null && isChildOrHidden(mNavButtonView)) {
removeView(mNavButtonView);
mHiddenViews.remove(mNavButtonView);
}
if (mNavButtonView != null) {
mNavButtonView.setImageDrawable(icon);
}
}
And in onLayout it will check whether navigation button is present and lay it out.
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// more code before
if (shouldLayout(mNavButtonView)) {
if (isRtl) {
right = layoutChildRight(mNavButtonView, right, collapsingMargins,
alignmentHeight);
} else {
left = layoutChildLeft(mNavButtonView, left, collapsingMargins,
alignmentHeight);
}
}
// more code after. also mTitleTextView is laid out here
}
Toolbar also increments left/right position after laying out each view. These values used as an offset for each subsequent view.
To check whether view should be laid out it uses helper function:
private boolean shouldLayout(View view) {
return view != null && view.getParent() == this && view.getVisibility() != GONE;
}
The Problem
When setting layoutTransition through the code or animateLayoutChanges in the xml we change behaviour a little bit.
Lets start with what happening when we remove child from parent (ViewGroup code):
private void removeFromArray(int index) {
final View[] children = mChildren;
if (!(mTransitioningViews != null && mTransitioningViews.contains(children[index]))) {
children[index].mParent = null;
}
// more code after
}
Did you mention that child's parent is not nulled when we have transition set? This is important!
Also please recall what shouldLayout from above does - it checks for only three cases:
view is not null
view's parent is toolbar
view is not GONE
And here is the root of the problem - navigation button is removed from the toolbar but it doesn't event know about this.
The Solution
In order to fix this problem we need to force toolbar into thinking that view shouldn't be laid out. Ideally it would be nice to override shouldLayout method but we don't have such luxury... So the only way we can accomplish this is by changing visibility on the navigation button.
class FixedToolbar(context: Context, attrs: AttributeSet?) : MaterialToolbar(context, attrs) {
companion object {
private val navButtonViewField = Toolbar::class.java.getDeclaredField("mNavButtonView")
.also { it.isAccessible = true }
}
override fun setNavigationIcon(icon: Drawable?) {
super.setNavigationIcon(icon)
(navButtonViewField.get(this) as? View)?.isGone = (icon == null)
}
}
Now toolbar will know that navigation button must not be laid out and all animations will go as intended.
I found a workaround to get the transition to work.
After calling supportActionBar?.setDisplayHomeAsUpEnabled(false) to hide the back arrow button, call the TransitionManager.beginDelayedTransition() method to trigger the transition manually.
// Hide the back arrow button
supportActionBar?.setDisplayHomeAsUpEnabled(false)
// Add a transition listener to the toolbar to set the toolbar title later
mToolbar.layoutTransition.addTransitionListener(object : LayoutTransition.TransitionListener {
override fun startTransition(transition: LayoutTransition?, container: ViewGroup?, view: View?, transitionType: Int) {}
override fun endTransition(transition: LayoutTransition?, container: ViewGroup?, view: View?, transitionType: Int) {
setToolbarTitle("Agenda")
mToolbar.layoutTransition.removeTransitionListener(this)
}
})
TransitionManager.beginDelayedTransition(mToolbar)
BEFORE
AFTER
Even with this questions more than a year back, this issue is still happening with the version androidx.navigation:navigation-ui-ktx:2.4.2
According to #MatrixDev, yes, this is still the final solution is:
class FixedToolbar(context: Context, attrs: AttributeSet?) : MaterialToolbar(context, attrs) {
companion object {
private val navButtonViewField = Toolbar::class.java.getDeclaredField("mNavButtonView")
.also { it.isAccessible = true }
}
override fun setNavigationIcon(icon: Drawable?) {
super.setNavigationIcon(icon)
(navButtonViewField.get(this) as? View)?.isGone = (icon == null)
}
}

Categories

Resources