Что именно делает fitsSystemWindows?
Я изо всех сил пытаюсь понять концепцию fitsSystemWindows
так как в зависимости от представления он делает разные вещи. Согласно официальной документации это
Логический внутренний атрибут для настройки макета представления на основе системных окон, таких как строка состояния. Если true, корректирует отступ этого представления, чтобы оставить место для системных окон.
Теперь, проверяя View.java
класс я вижу, что когда установлено true
вставки окна (строка состояния, панель навигации...) применяются к отступам вида, которые работают в соответствии с приведенной выше документацией. Это соответствующая часть кода:
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;
}
С новым дизайном материала появились новые классы, которые широко используют этот флаг, и вот тут возникает путаница. Во многих источниках fitsSystemWindows
упоминается как флаг, чтобы установить, чтобы положить представление позади системных панелей. Смотрите здесь.
Документация в ViewCompat.java
за setFitsSystemWindows
говорит:
Устанавливает, должно ли это представление учитывать декорации системного экрана, такие как строка состояния, и вставлять его содержимое; то есть управляя выполнением реализации по умолчанию {@link View#fitSystemWindows(Rect)}. Смотрите этот метод для более подробной информации.
Согласно этому, fitsSystemWindows
просто означает, что функция fitsSystemWindows()
будет выполнен? Новые классы материалов, кажется, просто используют это для рисования под строкой состояния. Если мы посмотрим на DrawerLayout.java
код, мы можем увидеть это:
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);
}
}
И мы видим ту же картину в новом CoordinatorLayout
или же AppBarLayout
,
Разве это не работает в точности наоборот как документация для fitsSystemWindows
? В последних случаях это означает рисовать за системной решеткой.
Однако, если вы хотите FrameLayout
нарисовать себя за строкой состояния, настройка fitsSystemWindows
Значение true не работает, поскольку реализация по умолчанию делает то, что было задокументировано изначально. Вы должны переопределить его и добавить те же флаги, что и другие упомянутые классы. Я что-то пропустил?
2 ответа
Системные окна - это части экрана, на которых система рисует неинтерактивный (в случае строки состояния) или интерактивный (в случае панели навигации) контент.
В большинстве случаев вашему приложению не нужно рисовать под строкой состояния или панелью навигации, но если вы это сделаете: вам нужно убедиться, что интерактивные элементы (например, кнопки) не скрыты под ними. Вот что дает стандартное поведение атрибута android: fitsSystemWindows = "true": оно устанавливает отступ представления, чтобы содержимое не перекрывало системные окна.
https://medium.com/google-developers/why-would-i-want-to-fitssystemwindows-4e26d9ce1eec
В 2015 году команда Android опубликовала хорошую статью - Зачем мне подходит SystemWindows?. Он хорошо объясняет поведение атрибута по умолчанию и то, как некоторые макеты, такие как DrawerLayout, переопределяют его.
Но это был 2015 год. Еще в 2017 году на droidcon Крис Бэйнс, который работает на Android, посоветовал не использовать
fitSystemWindows
атрибут, если в документации контейнера не указано его использование. Причина этого в том, что поведение флага по умолчанию часто не соответствует вашим ожиданиям. Это хорошо объяснено в видео.
Но что это за специальные макеты, где вам следует использовать
fitsSystemWindows
? Ну, это же
DrawerLayout
,
CoordinatorLayout
,
AppBarLayout
и
CollapsingToolbarLayout
. Эти макеты переопределяют значение по умолчанию
fitsSystemWindows
поведение и относиться к нему по-особенному, опять же, это хорошо объяснено в видео. Такая разная интерпретация атрибута иногда приводит к путанице и таким вопросам, как здесь. Фактически, в другом видео droidcon London Крис Бэйнс признает, что решение перегрузить поведение по умолчанию было ошибкой (временная метка лондонской конференции 13:10).
Хорошо, если
fitSystemWindows
не окончательное решение, что следует использовать? В другой статье от 2019 года Крис Бэйнс предлагает другое решение, основанное на API WindowInsets. Несмотря на то, что он состоит из кучи кода, пользоваться им довольно удобно. Например, если вы хотите, чтобы FAB в правом нижнем углу оставался за пределами панели навигации, вы можете легко настроить его:
<com.google.android.material.floatingactionbutton.FloatingActionButton
app:marginBottomSystemWindowInsets="@{true}"
app:marginRightSystemWindowInsets="@{true}"
... />
В решении используется настраиваемый
@BindingAdapter
s, один для заполнения, а другой для полей. Логика хорошо описана в упомянутой выше статье. Некоторые образцы Google используют это решение, например см. Приложение Owl для Android, BindingAdapters.kt. Я просто скопировал сюда код адаптера для справки:
@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
})
}
}
Также обратите внимание, что API WindowInsets будет изменен с версии 1.5.0 основной библиотеки androidx. Например
insets.systemWindowInsets
становится
insets.getInsets(Type.systemBars() or Type.ime())
. См. Документацию библиотеки и статью для получения более подробной информации.
Рекомендации:
Он не рисует за системной панелью, он как бы растягивается за панелью, чтобы подкрашивать его теми же цветами, что и у него, но содержащиеся в нем представления добавляются в строку состояния, если это имеет смысл