Skip to content

Commit

Permalink
[DatePicker] Always go edge-to-edge in fullscreen mode
Browse files Browse the repository at this point in the history
Resolves #1966

PiperOrigin-RevId: 432296692
  • Loading branch information
drchen authored and afohrman committed Mar 4, 2022
1 parent 78b532c commit be6050a
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 78 deletions.
Expand Up @@ -16,34 +16,18 @@

package io.material.catalog.windowpreferences;

import static android.graphics.Color.TRANSPARENT;
import static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
import static android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
import static com.google.android.material.color.MaterialColors.isColorLight;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.view.View;
import android.view.Window;
import androidx.annotation.RequiresApi;
import androidx.core.graphics.ColorUtils;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.internal.EdgeToEdgeUtils;

/** Helper that saves the current window preferences for the Catalog. */
public class WindowPreferencesManager {

private static final String PREFERENCES_NAME = "window_preferences";
private static final String KEY_EDGE_TO_EDGE_ENABLED = "edge_to_edge_enabled";
private static final int EDGE_TO_EDGE_BAR_ALPHA = 128;

@RequiresApi(VERSION_CODES.LOLLIPOP)
private static final int EDGE_TO_EDGE_FLAGS =
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;

private final Context context;

Expand All @@ -66,67 +50,7 @@ public boolean isEdgeToEdgeEnabled() {

@SuppressWarnings("RestrictTo")
public void applyEdgeToEdgePreference(Window window) {
if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
return;
}
boolean edgeToEdgeEnabled = isEdgeToEdgeEnabled();

int statusBarColor = getStatusBarColor(isEdgeToEdgeEnabled());
int navbarColor = getNavBarColor(isEdgeToEdgeEnabled());

boolean lightBackground =
isColorLight(MaterialColors.getColor(context, android.R.attr.colorBackground, Color.BLACK));
boolean lightStatusBar = isColorLight(statusBarColor);
boolean showDarkStatusBarIcons =
lightStatusBar || (statusBarColor == TRANSPARENT && lightBackground);
boolean lightNavbar = isColorLight(navbarColor);
boolean showDarkNavbarIcons = lightNavbar || (navbarColor == TRANSPARENT && lightBackground);

View decorView = window.getDecorView();
int currentStatusBar =
showDarkStatusBarIcons && VERSION.SDK_INT >= VERSION_CODES.M
? SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
: 0;
int currentNavBar =
showDarkNavbarIcons && VERSION.SDK_INT >= VERSION_CODES.O
? SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
: 0;

window.setNavigationBarColor(navbarColor);
window.setStatusBarColor(statusBarColor);
int systemUiVisibility = (edgeToEdgeEnabled ? EDGE_TO_EDGE_FLAGS : SYSTEM_UI_FLAG_VISIBLE)
| currentStatusBar
| currentNavBar;

decorView.setSystemUiVisibility(systemUiVisibility);
}

@SuppressWarnings("RestrictTo")
@TargetApi(VERSION_CODES.LOLLIPOP)
private int getStatusBarColor(boolean isEdgeToEdgeEnabled) {
if (isEdgeToEdgeEnabled && VERSION.SDK_INT < VERSION_CODES.M) {
int opaqueStatusBarColor =
MaterialColors.getColor(context, android.R.attr.statusBarColor, Color.BLACK);
return ColorUtils.setAlphaComponent(opaqueStatusBarColor, EDGE_TO_EDGE_BAR_ALPHA);
}
if (isEdgeToEdgeEnabled) {
return TRANSPARENT;
}
return MaterialColors.getColor(context, android.R.attr.statusBarColor, Color.BLACK);
}

@SuppressWarnings("RestrictTo")
@TargetApi(VERSION_CODES.LOLLIPOP)
private int getNavBarColor(boolean isEdgeToEdgeEnabled) {
if (isEdgeToEdgeEnabled && VERSION.SDK_INT < VERSION_CODES.O_MR1) {
int opaqueNavBarColor =
MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK);
return ColorUtils.setAlphaComponent(opaqueNavBarColor, EDGE_TO_EDGE_BAR_ALPHA);
}
if (isEdgeToEdgeEnabled) {
return TRANSPARENT;
}
return MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK);
EdgeToEdgeUtils.applyEdgeToEdge(window, isEdgeToEdgeEnabled());
}

private SharedPreferences getSharedPreferences() {
Expand Down
Expand Up @@ -49,9 +49,13 @@
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.core.util.Pair;
import androidx.core.view.OnApplyWindowInsetsListener;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.dialog.InsetDialogOnTouchListener;
import com.google.android.material.internal.CheckableImageButton;
import com.google.android.material.internal.EdgeToEdgeUtils;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.resources.MaterialAttributes;
import com.google.android.material.shape.MaterialShapeDrawable;
import java.lang.annotation.Retention;
Expand Down Expand Up @@ -303,6 +307,7 @@ public void onStart() {
if (fullscreen) {
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
window.setBackgroundDrawable(background);
enableEdgeToEdge(window);
} else {
window.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
int inset =
Expand Down Expand Up @@ -351,6 +356,32 @@ public final S getSelection() {
return getDateSelector().getSelection();
}

private void enableEdgeToEdge(Window window) {
final View headerLayout = requireView().findViewById(R.id.fullscreen_header);
EdgeToEdgeUtils.applyEdgeToEdge(
window, true, ViewUtils.getBackgroundColor(headerLayout), null);
final int originalPaddingTop = headerLayout.getPaddingTop();
final int originalHeaderHeight = headerLayout.getLayoutParams().height;
ViewCompat.setOnApplyWindowInsetsListener(
headerLayout,
new OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
int topInset = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
if (originalHeaderHeight >= 0) {
headerLayout.getLayoutParams().height = originalHeaderHeight + topInset;
headerLayout.setLayoutParams(headerLayout.getLayoutParams());
}
headerLayout.setPadding(
headerLayout.getPaddingLeft(),
originalPaddingTop + topInset,
headerLayout.getPaddingRight(),
headerLayout.getPaddingBottom());
return insets;
}
});
}

private void updateHeader() {
String headerText = getHeaderText();
headerSelectionText.setContentDescription(
Expand Down
Expand Up @@ -21,6 +21,7 @@
android:layout_height="@dimen/mtrl_calendar_header_height_fullscreen">

<LinearLayout
android:id="@+id/fullscreen_header"
style="?attr/materialCalendarHeaderLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/mtrl_calendar_header_height_fullscreen"
Expand Down
Expand Up @@ -48,6 +48,7 @@
</style>

<style name="ThemeOverlay.MaterialComponents.MaterialCalendar.Fullscreen" parent="ThemeOverlay.MaterialComponents.MaterialCalendar">
<item name="android:windowIsFloating">false</item>
<item name="materialCalendarStyle">@style/Widget.MaterialComponents.MaterialCalendar.Fullscreen</item>
<item name="materialCalendarHeaderSelection">@style/Widget.MaterialComponents.MaterialCalendar.HeaderSelection.Fullscreen</item>
</style>
Expand Down Expand Up @@ -80,6 +81,7 @@
</style>

<style name="ThemeOverlay.Material3.MaterialCalendar.Fullscreen">
<item name="android:windowIsFloating">false</item>
<item name="android:windowElevation" tools:ignore="NewApi">@dimen/m3_datepicker_elevation</item>
<item name="materialCalendarStyle">@style/Widget.Material3.MaterialCalendar.Fullscreen</item>
<item name="materialCalendarHeaderSelection">@style/Widget.Material3.MaterialCalendar.HeaderSelection.Fullscreen</item>
Expand Down
152 changes: 152 additions & 0 deletions lib/java/com/google/android/material/internal/EdgeToEdgeUtils.java
@@ -0,0 +1,152 @@
/*
* Copyright (C) 2022 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 com.google.android.material.internal;

import static android.graphics.Color.TRANSPARENT;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static com.google.android.material.color.MaterialColors.isColorLight;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Color;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.view.Window;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.google.android.material.color.MaterialColors;

/**
* A util class that helps apply edge-to-edge mode to activity/dialog windows.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public class EdgeToEdgeUtils {
private static final int EDGE_TO_EDGE_BAR_ALPHA = 128;

private EdgeToEdgeUtils() {}

/**
* Applies or removes edge-to-edge mode to the provided {@link Window}. When edge-to-edge mode is
* applied, the activities, or the non-floating dialogs, that host the provided window will be
* drawn over the system bar area by default and the system bar colors will be adjusted according
* to the background color you provide.
*/
public static void applyEdgeToEdge(@NonNull Window window, boolean edgeToEdgeEnabled) {
applyEdgeToEdge(window, edgeToEdgeEnabled, null, null);
}

/**
* Applies or removes edge-to-edge mode to the provided {@link Window}. When edge-to-edge mode is
* applied, the activities, or the non-floating dialogs, that host the provided window will be
* drawn over the system bar area by default and the system bar colors will be adjusted according
* to the background color you provide.
*
* @param statusBarOverlapBackgroundColor The reference background color to decide the text/icon
* colors on status bars. {@code null} to use the default color from
* {@code ?android:attr/colorBackground}.
* @param navigationBarOverlapBackgroundColor The reference background color to decide the icon
* colors on navigation bars.{@code null} to use the default color from
* {@code ?android:attr/colorBackground}.
*/
public static void applyEdgeToEdge(
@NonNull Window window,
boolean edgeToEdgeEnabled,
@Nullable @ColorInt Integer statusBarOverlapBackgroundColor,
@Nullable @ColorInt Integer navigationBarOverlapBackgroundColor) {
if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
return;
}

// If the overlapping background color is unknown or TRANSPARENT, use the default one.
boolean useDefaultBackgroundColorForStatusBar =
statusBarOverlapBackgroundColor == null || statusBarOverlapBackgroundColor == 0;
boolean useDefaultBackgroundColorForNavigationBar =
navigationBarOverlapBackgroundColor == null || navigationBarOverlapBackgroundColor == 0;
if (useDefaultBackgroundColorForStatusBar || useDefaultBackgroundColorForNavigationBar) {
int defaultBackgroundColor =
MaterialColors.getColor(window.getContext(), android.R.attr.colorBackground, Color.BLACK);
if (useDefaultBackgroundColorForStatusBar) {
statusBarOverlapBackgroundColor = defaultBackgroundColor;
}
if (useDefaultBackgroundColorForNavigationBar) {
navigationBarOverlapBackgroundColor = defaultBackgroundColor;
}
}

WindowCompat.setDecorFitsSystemWindows(window, !edgeToEdgeEnabled);

int statusBarColor = getStatusBarColor(window.getContext(), edgeToEdgeEnabled);
int navigationBarColor = getNavigationBarColor(window.getContext(), edgeToEdgeEnabled);

window.setStatusBarColor(statusBarColor);
window.setNavigationBarColor(navigationBarColor);

boolean isLightStatusBar =
isUsingLightSystemBar(statusBarColor, isColorLight(statusBarOverlapBackgroundColor));
boolean isLightNavigationBar =
isUsingLightSystemBar(
navigationBarColor, isColorLight(navigationBarOverlapBackgroundColor));

WindowInsetsControllerCompat insetsController =
WindowCompat.getInsetsController(window, window.getDecorView());
if (insetsController != null) {
insetsController.setAppearanceLightStatusBars(isLightStatusBar);
insetsController.setAppearanceLightNavigationBars(isLightNavigationBar);
}
}

@TargetApi(VERSION_CODES.LOLLIPOP)
private static int getStatusBarColor(Context context, boolean isEdgeToEdgeEnabled) {
if (isEdgeToEdgeEnabled && VERSION.SDK_INT < VERSION_CODES.M) {
// Light status bars are only supported on M+. So we need to use a translucent black status
// bar instead to ensure the text/icon contrast of it.
int opaqueStatusBarColor =
MaterialColors.getColor(context, android.R.attr.statusBarColor, Color.BLACK);
return ColorUtils.setAlphaComponent(opaqueStatusBarColor, EDGE_TO_EDGE_BAR_ALPHA);
}
if (isEdgeToEdgeEnabled) {
return TRANSPARENT;
}
return MaterialColors.getColor(context, android.R.attr.statusBarColor, Color.BLACK);
}

@TargetApi(VERSION_CODES.LOLLIPOP)
private static int getNavigationBarColor(Context context, boolean isEdgeToEdgeEnabled) {
// Light navigation bars are only supported on O_MR1+. So we need to use a translucent black
// navigation bar instead to ensure the text/icon contrast of it.
if (isEdgeToEdgeEnabled && VERSION.SDK_INT < VERSION_CODES.O_MR1) {
int opaqueNavBarColor =
MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK);
return ColorUtils.setAlphaComponent(opaqueNavBarColor, EDGE_TO_EDGE_BAR_ALPHA);
}
if (isEdgeToEdgeEnabled) {
return TRANSPARENT;
}
return MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK);
}

private static boolean isUsingLightSystemBar(int systemBarColor, boolean isLightBackground) {
return isColorLight(systemBarColor) || (systemBarColor == TRANSPARENT && isLightBackground);
}
}
11 changes: 11 additions & 0 deletions lib/java/com/google/android/material/internal/ViewUtils.java
Expand Up @@ -24,6 +24,7 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
Expand Down Expand Up @@ -339,4 +340,14 @@ public static void removeOnGlobalLayoutListener(
viewTreeObserver.removeGlobalOnLayoutListener(victim);
}
}

/**
* Returns the provided view's background color, if it has ColorDrawable as its background, or
* {@code null} if the background has a different drawable type.
*/
@Nullable
public static Integer getBackgroundColor(@NonNull View view) {
return view.getBackground() instanceof ColorDrawable
? ((ColorDrawable) view.getBackground()).getColor() : null;
}
}

0 comments on commit be6050a

Please sign in to comment.