Skip to content

Commit a6cf098

Browse files
Material Design Teamleticiarossi
authored andcommitted
[M3][Color] Added ColorContrast API
PiperOrigin-RevId: 527645245
1 parent c031144 commit a6cf098

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* Copyright (C) 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.material.color;
18+
19+
import android.app.Activity;
20+
import android.app.Application;
21+
import android.app.Application.ActivityLifecycleCallbacks;
22+
import android.app.UiModeManager;
23+
import android.app.UiModeManager.ContrastChangeListener;
24+
import android.content.Context;
25+
import android.os.Build.VERSION_CODES;
26+
import android.os.Bundle;
27+
import android.view.ContextThemeWrapper;
28+
import androidx.annotation.ChecksSdkIntAtLeast;
29+
import androidx.annotation.NonNull;
30+
import androidx.annotation.Nullable;
31+
import androidx.annotation.RequiresApi;
32+
import androidx.core.content.ContextCompat;
33+
import androidx.core.os.BuildCompat;
34+
import java.util.LinkedHashSet;
35+
import java.util.Set;
36+
37+
/**
38+
* Utility for applying contrast colors to application/activities.
39+
*
40+
* <p>Please note that if you are already using dynamic colors, contrast will be applied
41+
* automatically on Android U+. This is only needed if you have a branded or custom theme and want
42+
* to support contrast.
43+
*/
44+
public class ColorContrast {
45+
46+
private static final float MEDIUM_CONTRAST_THRESHOLD = 1 / 3f;
47+
private static final float HIGH_CONTRAST_THRESHOLD = 2 / 3f;
48+
49+
private ColorContrast() {}
50+
51+
/**
52+
* Applies contrast to all activities by registering a {@link ActivityLifecycleCallbacks} to your
53+
* application.
54+
*
55+
* <p>A normal usage of this method should happen only once in {@link Application#onCreate()} or
56+
* any methods that run before any of your activities are created. For example:
57+
*
58+
* <pre>
59+
* public class YourApplication extends Application {
60+
* &#64;Override
61+
* public void onCreate() {
62+
* super.onCreate();
63+
* ColorContrast.applyToActivitiesIfAvailable(this);
64+
* }
65+
* }
66+
* </pre>
67+
*
68+
* <p>This method will try to apply a theme overlay in every activity's {@link
69+
* ActivityLifecycleCallbacks#onActivityPreCreated(Activity, Bundle)} callback.
70+
*
71+
* @param application The target application.
72+
* @param colorContrastOptions The color contrast options object that specifies the theme overlay
73+
* resource IDs for medium and high contrast mode.
74+
*/
75+
public static void applyToActivitiesIfAvailable(
76+
@NonNull Application application, @NonNull ColorContrastOptions colorContrastOptions) {
77+
if (!isContrastAvailable()) {
78+
return;
79+
}
80+
81+
application.registerActivityLifecycleCallbacks(
82+
new ColorContrastActivityLifecycleCallbacks(colorContrastOptions));
83+
}
84+
85+
/**
86+
* Applies contrast to the given activity.
87+
*
88+
* <p>Note that this method does not guarantee the consistency of contrast throughout the app. If
89+
* you want contrast to be updated automatically when a different contrast level is selected in
90+
* the system, please use #applyToActivitiesIfAvailable(Application, ColorContrastOptions).
91+
*
92+
* @param activity The target activity.
93+
* @param colorContrastOptions The color contrast options object that specifies the theme overlay
94+
* resource IDs for medium and high contrast mode.
95+
*/
96+
public static void applyToActivityIfAvailable(
97+
@NonNull Activity activity, @NonNull ColorContrastOptions colorContrastOptions) {
98+
if (!isContrastAvailable()) {
99+
return;
100+
}
101+
102+
int themeOverlayResourcesId = getContrastThemeOverlayResourceId(activity, colorContrastOptions);
103+
if (themeOverlayResourcesId != 0) {
104+
ThemeUtils.applyThemeOverlay(activity, themeOverlayResourcesId);
105+
}
106+
}
107+
108+
/**
109+
* Wraps the given context with the theme overlay where color resources are updated. The returned
110+
* context can be used to create views with contrast support.
111+
*
112+
* <p>Note that this method does not guarantee the consistency of contrast throughout the app. If
113+
* you want contrast to be updated automatically when a different contrast level is selected in
114+
* the system, please use #applyToActivitiesIfAvailable(Application, ColorContrastOptions).
115+
*
116+
* @param context The target context.
117+
* @param colorContrastOptions The color contrast options object that specifies the theme overlay
118+
* resource IDs for medium and high contrast mode.
119+
*/
120+
@NonNull
121+
public static Context wrapContextIfAvailable(
122+
@NonNull Context context, @NonNull ColorContrastOptions colorContrastOptions) {
123+
if (!isContrastAvailable()) {
124+
return context;
125+
}
126+
127+
int themeOverlayResourcesId = getContrastThemeOverlayResourceId(context, colorContrastOptions);
128+
if (themeOverlayResourcesId == 0) {
129+
return context;
130+
}
131+
return new ContextThemeWrapper(context, themeOverlayResourcesId);
132+
}
133+
134+
/** Returns {@code true} if contrast control is available on the current SDK level. */
135+
@ChecksSdkIntAtLeast(api = VERSION_CODES.UPSIDE_DOWN_CAKE)
136+
public static boolean isContrastAvailable() {
137+
return BuildCompat.isAtLeastU();
138+
}
139+
140+
private static int getContrastThemeOverlayResourceId(
141+
Context context, ColorContrastOptions colorContrastOptions) {
142+
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
143+
if (!isContrastAvailable() || uiModeManager == null) {
144+
return 0;
145+
}
146+
147+
float currentContrast = uiModeManager.getContrast();
148+
int mediumContrastThemeOverlay = colorContrastOptions.getMediumContrastThemeOverlay();
149+
int highContrastThemeOverlay = colorContrastOptions.getHighContrastThemeOverlay();
150+
if (currentContrast >= HIGH_CONTRAST_THRESHOLD) {
151+
// Falls back to mediumContrastThemeOverlay if highContrastThemeOverlay is not set in
152+
// ColorContrastOptions. If mediumContrastThemeOverlay is not set, default 0 will be returned.
153+
return highContrastThemeOverlay == 0 ? mediumContrastThemeOverlay : highContrastThemeOverlay;
154+
} else if (currentContrast >= MEDIUM_CONTRAST_THRESHOLD) {
155+
// Falls back to highContrastThemeOverlay if mediumContrastThemeOverlay is not set in
156+
// ColorContrastOptions. If highContrastThemeOverlay is not set, default 0 will be returned.
157+
return mediumContrastThemeOverlay == 0
158+
? highContrastThemeOverlay
159+
: mediumContrastThemeOverlay;
160+
}
161+
return 0;
162+
}
163+
164+
@RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
165+
private static class ColorContrastActivityLifecycleCallbacks
166+
implements ActivityLifecycleCallbacks {
167+
168+
private final Set<Activity> activitiesInStack = new LinkedHashSet<>();
169+
private final ColorContrastOptions colorContrastOptions;
170+
171+
@Nullable private ContrastChangeListener contrastChangeListener;
172+
173+
ColorContrastActivityLifecycleCallbacks(ColorContrastOptions colorContrastOptions) {
174+
this.colorContrastOptions = colorContrastOptions;
175+
}
176+
177+
@Override
178+
public void onActivityPreCreated(
179+
@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
180+
UiModeManager uiModeManager =
181+
(UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
182+
if (uiModeManager != null && activitiesInStack.isEmpty() && contrastChangeListener == null) {
183+
contrastChangeListener =
184+
new ContrastChangeListener() {
185+
@Override
186+
public void onContrastChanged(float contrastLevel) {
187+
for (Activity activityInStack : activitiesInStack) {
188+
activityInStack.recreate();
189+
}
190+
}
191+
};
192+
// Register UiContrastChangeListener on the application level.
193+
uiModeManager.addContrastChangeListener(
194+
ContextCompat.getMainExecutor(activity.getApplicationContext()),
195+
contrastChangeListener);
196+
}
197+
198+
activitiesInStack.add(activity);
199+
if (uiModeManager != null) {
200+
applyToActivityIfAvailable(activity, colorContrastOptions);
201+
}
202+
}
203+
204+
@Override
205+
public void onActivityCreated(
206+
@NonNull Activity activity, @Nullable Bundle savedInstanceState) {}
207+
208+
@Override
209+
public void onActivityStarted(@NonNull Activity activity) {}
210+
211+
@Override
212+
public void onActivityResumed(@NonNull Activity activity) {}
213+
214+
@Override
215+
public void onActivityPaused(@NonNull Activity activity) {}
216+
217+
@Override
218+
public void onActivityStopped(@NonNull Activity activity) {}
219+
220+
@Override
221+
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}
222+
223+
@Override
224+
public void onActivityDestroyed(@NonNull Activity activity) {
225+
// Always remove the activity from the stack to avoid memory leak.
226+
activitiesInStack.remove(activity);
227+
228+
UiModeManager uiModeManager =
229+
(UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
230+
if (uiModeManager != null && contrastChangeListener != null && activitiesInStack.isEmpty()) {
231+
uiModeManager.removeContrastChangeListener(contrastChangeListener);
232+
contrastChangeListener = null;
233+
}
234+
}
235+
}
236+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright (C) 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.material.color;
18+
19+
import com.google.android.material.R;
20+
21+
import androidx.annotation.NonNull;
22+
import androidx.annotation.StyleRes;
23+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
24+
25+
/**
26+
* Wrapper class for specifying color contrast options when applying contrast to branded and custom
27+
* themes. Clients have the options to provide theme overlay resource ids for medium and high
28+
* contrast mode.
29+
*
30+
* <p>An example of the provided theme overlay resource ids could be one of the following:
31+
*
32+
* <ul>
33+
* <li>contrast in light mode: R.style.ThemeOverlay_XxxContrast_Light
34+
* <li>contrast in dark mode: R.style.ThemeOverlay_XxxContrast_Dark
35+
* <li>contrast in both light and dark mode: R.style.ThemeOverlay_XxxContrast_DayNight
36+
* </ul>
37+
*/
38+
public class ColorContrastOptions {
39+
40+
@StyleRes private final int mediumContrastThemeOverlayResourceId;
41+
42+
@StyleRes private final int highContrastThemeOverlayResourceId;
43+
44+
private ColorContrastOptions(Builder builder) {
45+
this.mediumContrastThemeOverlayResourceId = builder.mediumContrastThemeOverlayResourceId;
46+
this.highContrastThemeOverlayResourceId = builder.highContrastThemeOverlayResourceId;
47+
}
48+
49+
/** Returns the resource id of the medium contrast theme overlay. */
50+
@StyleRes
51+
public int getMediumContrastThemeOverlay() {
52+
return mediumContrastThemeOverlayResourceId;
53+
}
54+
55+
/** Returns the resource id of the high contrast theme overlay. */
56+
@StyleRes
57+
public int getHighContrastThemeOverlay() {
58+
return highContrastThemeOverlayResourceId;
59+
}
60+
61+
/** Builder class for specifying options when applying contrast. */
62+
public static class Builder {
63+
64+
@StyleRes private int mediumContrastThemeOverlayResourceId;
65+
66+
@StyleRes private int highContrastThemeOverlayResourceId;
67+
68+
/** Sets the resource id of the medium contrast theme overlay. */
69+
@NonNull
70+
@CanIgnoreReturnValue
71+
public Builder setMediumContrastThemeOverlay(
72+
@StyleRes int mediumContrastThemeOverlayResourceId) {
73+
this.mediumContrastThemeOverlayResourceId = mediumContrastThemeOverlayResourceId;
74+
return this;
75+
}
76+
77+
/** Sets the resource id of the high contrast theme overlay. */
78+
@NonNull
79+
@CanIgnoreReturnValue
80+
public Builder setHighContrastThemeOverlay(@StyleRes int highContrastThemeOverlayResourceId) {
81+
this.highContrastThemeOverlayResourceId = highContrastThemeOverlayResourceId;
82+
return this;
83+
}
84+
85+
@NonNull
86+
public ColorContrastOptions build() {
87+
return new ColorContrastOptions(this);
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)