WindowInsetsApplier.java
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.viewpager2.widget;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.view.OnApplyWindowInsetsListener;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.recyclerview.widget.RecyclerView;
/**
* An {@link OnApplyWindowInsetsListener} that applies {@link WindowInsetsCompat WindowInsets} to
* all children of a {@link ViewPager2}, making sure they all receive the same insets regardless
* of whether any of them consumed any insets.
*
* <p>To prevent the ViewPager2 itself from dispatching the insets incorrectly, this listener will
* consume all insets it applies. As a consequence, siblings of ViewPager2, or siblings of its
* parents, to whom the WindowInsets haven't yet been dispatched, won't receive them at all. If
* you require those views to receive the WindowInsets, do not set this listener on ViewPager2
* and do not consume insets in any of the pages.
*
* <p>Call {@link #install(ViewPager2)} to install this listener in ViewPager2.
*
* <p>When running on API 30 or higher and the targetSdkVersion is set to API 30 or higher, the
* fix is not needed and {@link #install(ViewPager2)} will do nothing. None of the above described
* effects will happen.
*/
public final class WindowInsetsApplier implements OnApplyWindowInsetsListener {
private WindowInsetsApplier() {
// private constructor, only we get to instantiate this fix to ensure type safety.
}
/**
* Installs a {@link WindowInsetsApplier} into the given ViewPager2, but only when window
* insets dispatching hasn't been fixed in the current run configuration. It will return
* whether or not the WindowInsetsApplier was installed.
*
* <p>Window insets dispatching is fixed on Android SDK R, but the targetSdk of the app also
* needs to be set to R or higher. If both these conditions hold, the WindowInsetsApplier
* won't be installed. If either we're running on SDK < R, or the targetSdk of the app is set
* to < R, then the WindowInsetsApplier will be installed.
*
* @param viewPager The ViewPager2 to install the WindowInsetsApplier into
* @return Whether or not the WindowInsetsApplier was installed
*/
public static boolean install(@NonNull ViewPager2 viewPager) {
// From R onwards, insets dispatching has been fixed in the framework, but only if the
// targetSdk is R or later. If we're running on a fixed version (SDK >= R && target >= R),
// don't install our fix.
ApplicationInfo appInfo = viewPager.getContext().getApplicationInfo();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& appInfo.targetSdkVersion >= Build.VERSION_CODES.R) {
return false;
}
// We're not running on a fixed version. Apply the fix.
ViewCompat.setOnApplyWindowInsetsListener(viewPager, new WindowInsetsApplier());
return true;
}
@NonNull
@Override
public WindowInsetsCompat onApplyWindowInsets(@NonNull View v,
@NonNull WindowInsetsCompat insets) {
ViewPager2 viewPager = (ViewPager2) v;
// First let the ViewPager2 itself try and consume them...
final WindowInsetsCompat applied = ViewCompat.onApplyWindowInsets(viewPager, insets);
if (applied.isConsumed()) {
// If the ViewPager2 consumed all insets, return now
return applied;
}
// Now we'll manually dispatch the insets to our children. Since ViewPager2
// children are always full-height, we do not want to use the standard
// ViewGroup dispatchApplyWindowInsets since if child 0 consumes them, the
// rest of the children will not receive any insets. To workaround this we
// manually dispatch the applied insets, not allowing children to consume
// them from each other, making a copy for every invocation
final RecyclerView rv = viewPager.mRecyclerView;
for (int i = 0, count = rv.getChildCount(); i < count; i++) {
// We don't care about b/168984101 here, as we're not using the return value
ViewCompat.dispatchApplyWindowInsets(rv.getChildAt(i), new WindowInsetsCompat(applied));
}
// Now return a new WindowInsets where we consume all insets to prevent the
// platform from dispatching the insets to ViewPager2's children, because the platform's
// dispatch is broken (it will leak insets consumed by one child to other children).
// There is a trade off here: by consuming all insets, we fix insets dispatching for our
// children, but we break it for siblings. By not consuming all insets, it would work for
// siblings but we break it for children.
return consumeAllInsets(applied);
}
@SuppressWarnings("deprecation") // consumeSystemWindowInsets, consumeStableInsets
private WindowInsetsCompat consumeAllInsets(@NonNull WindowInsetsCompat insets) {
if (Build.VERSION.SDK_INT >= 21) {
if (WindowInsetsCompat.CONSUMED.toWindowInsets() != null) {
return WindowInsetsCompat.CONSUMED;
}
// On API < 29, WindowInsetsCompat.CONSUMED can fail initialization because it uses
// reflection to create the CONSUMED WindowInsets.
// When that happens, fall back to consuming everything in the given insets. The given
// insets is guaranteed to hold non-null WindowInsets because those were created by the
// platform. We only have to consume insets that were in API < 29.
return insets.consumeSystemWindowInsets().consumeStableInsets();
}
return insets;
}
}