-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
2 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
236 changes: 236 additions & 0 deletions
236
lib/java/com/google/android/material/color/ColorContrast.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
/* | ||
* Copyright (C) 2023 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.color; | ||
|
||
import android.app.Activity; | ||
import android.app.Application; | ||
import android.app.Application.ActivityLifecycleCallbacks; | ||
import android.app.UiModeManager; | ||
import android.app.UiModeManager.ContrastChangeListener; | ||
import android.content.Context; | ||
import android.os.Build.VERSION_CODES; | ||
import android.os.Bundle; | ||
import android.view.ContextThemeWrapper; | ||
import androidx.annotation.ChecksSdkIntAtLeast; | ||
import androidx.annotation.NonNull; | ||
import androidx.annotation.Nullable; | ||
import androidx.annotation.RequiresApi; | ||
import androidx.core.content.ContextCompat; | ||
import androidx.core.os.BuildCompat; | ||
import java.util.LinkedHashSet; | ||
import java.util.Set; | ||
|
||
/** | ||
* Utility for applying contrast colors to application/activities. | ||
* | ||
* <p>Please note that if you are already using dynamic colors, contrast will be applied | ||
* automatically on Android U+. This is only needed if you have a branded or custom theme and want | ||
* to support contrast. | ||
*/ | ||
public class ColorContrast { | ||
|
||
private static final float MEDIUM_CONTRAST_THRESHOLD = 1 / 3f; | ||
private static final float HIGH_CONTRAST_THRESHOLD = 2 / 3f; | ||
|
||
private ColorContrast() {} | ||
|
||
/** | ||
* Applies contrast to all activities by registering a {@link ActivityLifecycleCallbacks} to your | ||
* application. | ||
* | ||
* <p>A normal usage of this method should happen only once in {@link Application#onCreate()} or | ||
* any methods that run before any of your activities are created. For example: | ||
* | ||
* <pre> | ||
* public class YourApplication extends Application { | ||
* @Override | ||
* public void onCreate() { | ||
* super.onCreate(); | ||
* ColorContrast.applyToActivitiesIfAvailable(this); | ||
* } | ||
* } | ||
* </pre> | ||
* | ||
* <p>This method will try to apply a theme overlay in every activity's {@link | ||
* ActivityLifecycleCallbacks#onActivityPreCreated(Activity, Bundle)} callback. | ||
* | ||
* @param application The target application. | ||
* @param colorContrastOptions The color contrast options object that specifies the theme overlay | ||
* resource IDs for medium and high contrast mode. | ||
*/ | ||
public static void applyToActivitiesIfAvailable( | ||
@NonNull Application application, @NonNull ColorContrastOptions colorContrastOptions) { | ||
if (!isContrastAvailable()) { | ||
return; | ||
} | ||
|
||
application.registerActivityLifecycleCallbacks( | ||
new ColorContrastActivityLifecycleCallbacks(colorContrastOptions)); | ||
} | ||
|
||
/** | ||
* Applies contrast to the given activity. | ||
* | ||
* <p>Note that this method does not guarantee the consistency of contrast throughout the app. If | ||
* you want contrast to be updated automatically when a different contrast level is selected in | ||
* the system, please use #applyToActivitiesIfAvailable(Application, ColorContrastOptions). | ||
* | ||
* @param activity The target activity. | ||
* @param colorContrastOptions The color contrast options object that specifies the theme overlay | ||
* resource IDs for medium and high contrast mode. | ||
*/ | ||
public static void applyToActivityIfAvailable( | ||
@NonNull Activity activity, @NonNull ColorContrastOptions colorContrastOptions) { | ||
if (!isContrastAvailable()) { | ||
return; | ||
} | ||
|
||
int themeOverlayResourcesId = getContrastThemeOverlayResourceId(activity, colorContrastOptions); | ||
if (themeOverlayResourcesId != 0) { | ||
ThemeUtils.applyThemeOverlay(activity, themeOverlayResourcesId); | ||
} | ||
} | ||
|
||
/** | ||
* Wraps the given context with the theme overlay where color resources are updated. The returned | ||
* context can be used to create views with contrast support. | ||
* | ||
* <p>Note that this method does not guarantee the consistency of contrast throughout the app. If | ||
* you want contrast to be updated automatically when a different contrast level is selected in | ||
* the system, please use #applyToActivitiesIfAvailable(Application, ColorContrastOptions). | ||
* | ||
* @param context The target context. | ||
* @param colorContrastOptions The color contrast options object that specifies the theme overlay | ||
* resource IDs for medium and high contrast mode. | ||
*/ | ||
@NonNull | ||
public static Context wrapContextIfAvailable( | ||
@NonNull Context context, @NonNull ColorContrastOptions colorContrastOptions) { | ||
if (!isContrastAvailable()) { | ||
return context; | ||
} | ||
|
||
int themeOverlayResourcesId = getContrastThemeOverlayResourceId(context, colorContrastOptions); | ||
if (themeOverlayResourcesId == 0) { | ||
return context; | ||
} | ||
return new ContextThemeWrapper(context, themeOverlayResourcesId); | ||
} | ||
|
||
/** Returns {@code true} if contrast control is available on the current SDK level. */ | ||
@ChecksSdkIntAtLeast(api = VERSION_CODES.UPSIDE_DOWN_CAKE) | ||
public static boolean isContrastAvailable() { | ||
return BuildCompat.isAtLeastU(); | ||
} | ||
|
||
private static int getContrastThemeOverlayResourceId( | ||
Context context, ColorContrastOptions colorContrastOptions) { | ||
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); | ||
if (!isContrastAvailable() || uiModeManager == null) { | ||
return 0; | ||
} | ||
|
||
float currentContrast = uiModeManager.getContrast(); | ||
int mediumContrastThemeOverlay = colorContrastOptions.getMediumContrastThemeOverlay(); | ||
int highContrastThemeOverlay = colorContrastOptions.getHighContrastThemeOverlay(); | ||
if (currentContrast >= HIGH_CONTRAST_THRESHOLD) { | ||
// Falls back to mediumContrastThemeOverlay if highContrastThemeOverlay is not set in | ||
// ColorContrastOptions. If mediumContrastThemeOverlay is not set, default 0 will be returned. | ||
return highContrastThemeOverlay == 0 ? mediumContrastThemeOverlay : highContrastThemeOverlay; | ||
} else if (currentContrast >= MEDIUM_CONTRAST_THRESHOLD) { | ||
// Falls back to highContrastThemeOverlay if mediumContrastThemeOverlay is not set in | ||
// ColorContrastOptions. If highContrastThemeOverlay is not set, default 0 will be returned. | ||
return mediumContrastThemeOverlay == 0 | ||
? highContrastThemeOverlay | ||
: mediumContrastThemeOverlay; | ||
} | ||
return 0; | ||
} | ||
|
||
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) | ||
private static class ColorContrastActivityLifecycleCallbacks | ||
implements ActivityLifecycleCallbacks { | ||
|
||
private final Set<Activity> activitiesInStack = new LinkedHashSet<>(); | ||
private final ColorContrastOptions colorContrastOptions; | ||
|
||
@Nullable private ContrastChangeListener contrastChangeListener; | ||
|
||
ColorContrastActivityLifecycleCallbacks(ColorContrastOptions colorContrastOptions) { | ||
this.colorContrastOptions = colorContrastOptions; | ||
} | ||
|
||
@Override | ||
public void onActivityPreCreated( | ||
@NonNull Activity activity, @Nullable Bundle savedInstanceState) { | ||
UiModeManager uiModeManager = | ||
(UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE); | ||
if (uiModeManager != null && activitiesInStack.isEmpty() && contrastChangeListener == null) { | ||
contrastChangeListener = | ||
new ContrastChangeListener() { | ||
@Override | ||
public void onContrastChanged(float contrastLevel) { | ||
for (Activity activityInStack : activitiesInStack) { | ||
activityInStack.recreate(); | ||
} | ||
} | ||
}; | ||
// Register UiContrastChangeListener on the application level. | ||
uiModeManager.addContrastChangeListener( | ||
ContextCompat.getMainExecutor(activity.getApplicationContext()), | ||
contrastChangeListener); | ||
} | ||
|
||
activitiesInStack.add(activity); | ||
if (uiModeManager != null) { | ||
applyToActivityIfAvailable(activity, colorContrastOptions); | ||
} | ||
} | ||
|
||
@Override | ||
public void onActivityCreated( | ||
@NonNull Activity activity, @Nullable Bundle savedInstanceState) {} | ||
|
||
@Override | ||
public void onActivityStarted(@NonNull Activity activity) {} | ||
|
||
@Override | ||
public void onActivityResumed(@NonNull Activity activity) {} | ||
|
||
@Override | ||
public void onActivityPaused(@NonNull Activity activity) {} | ||
|
||
@Override | ||
public void onActivityStopped(@NonNull Activity activity) {} | ||
|
||
@Override | ||
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} | ||
|
||
@Override | ||
public void onActivityDestroyed(@NonNull Activity activity) { | ||
// Always remove the activity from the stack to avoid memory leak. | ||
activitiesInStack.remove(activity); | ||
|
||
UiModeManager uiModeManager = | ||
(UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE); | ||
if (uiModeManager != null && contrastChangeListener != null && activitiesInStack.isEmpty()) { | ||
uiModeManager.removeContrastChangeListener(contrastChangeListener); | ||
contrastChangeListener = null; | ||
} | ||
} | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
lib/java/com/google/android/material/color/ColorContrastOptions.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/* | ||
* Copyright (C) 2023 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.color; | ||
|
||
import com.google.android.material.R; | ||
|
||
import androidx.annotation.NonNull; | ||
import androidx.annotation.StyleRes; | ||
import com.google.errorprone.annotations.CanIgnoreReturnValue; | ||
|
||
/** | ||
* Wrapper class for specifying color contrast options when applying contrast to branded and custom | ||
* themes. Clients have the options to provide theme overlay resource ids for medium and high | ||
* contrast mode. | ||
* | ||
* <p>An example of the provided theme overlay resource ids could be one of the following: | ||
* | ||
* <ul> | ||
* <li>contrast in light mode: R.style.ThemeOverlay_XxxContrast_Light | ||
* <li>contrast in dark mode: R.style.ThemeOverlay_XxxContrast_Dark | ||
* <li>contrast in both light and dark mode: R.style.ThemeOverlay_XxxContrast_DayNight | ||
* </ul> | ||
*/ | ||
public class ColorContrastOptions { | ||
|
||
@StyleRes private final int mediumContrastThemeOverlayResourceId; | ||
|
||
@StyleRes private final int highContrastThemeOverlayResourceId; | ||
|
||
private ColorContrastOptions(Builder builder) { | ||
this.mediumContrastThemeOverlayResourceId = builder.mediumContrastThemeOverlayResourceId; | ||
this.highContrastThemeOverlayResourceId = builder.highContrastThemeOverlayResourceId; | ||
} | ||
|
||
/** Returns the resource id of the medium contrast theme overlay. */ | ||
@StyleRes | ||
public int getMediumContrastThemeOverlay() { | ||
return mediumContrastThemeOverlayResourceId; | ||
} | ||
|
||
/** Returns the resource id of the high contrast theme overlay. */ | ||
@StyleRes | ||
public int getHighContrastThemeOverlay() { | ||
return highContrastThemeOverlayResourceId; | ||
} | ||
|
||
/** Builder class for specifying options when applying contrast. */ | ||
public static class Builder { | ||
|
||
@StyleRes private int mediumContrastThemeOverlayResourceId; | ||
|
||
@StyleRes private int highContrastThemeOverlayResourceId; | ||
|
||
/** Sets the resource id of the medium contrast theme overlay. */ | ||
@NonNull | ||
@CanIgnoreReturnValue | ||
public Builder setMediumContrastThemeOverlay( | ||
@StyleRes int mediumContrastThemeOverlayResourceId) { | ||
this.mediumContrastThemeOverlayResourceId = mediumContrastThemeOverlayResourceId; | ||
return this; | ||
} | ||
|
||
/** Sets the resource id of the high contrast theme overlay. */ | ||
@NonNull | ||
@CanIgnoreReturnValue | ||
public Builder setHighContrastThemeOverlay(@StyleRes int highContrastThemeOverlayResourceId) { | ||
this.highContrastThemeOverlayResourceId = highContrastThemeOverlayResourceId; | ||
return this; | ||
} | ||
|
||
@NonNull | ||
public ColorContrastOptions build() { | ||
return new ColorContrastOptions(this); | ||
} | ||
} | ||
} |