Skip to content

Commit 869d943

Browse files
Material Design Teamhunterstich
authored andcommitted
[M3][Color] Add support for color resources harmonization in XML
PiperOrigin-RevId: 430757816
1 parent 43114c4 commit 869d943

File tree

9 files changed

+559
-45
lines changed

9 files changed

+559
-45
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (C) 2022 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.content.Context;
20+
import android.content.res.loader.ResourcesLoader;
21+
import android.content.res.loader.ResourcesProvider;
22+
import android.os.Build.VERSION_CODES;
23+
import android.os.ParcelFileDescriptor;
24+
import android.system.Os;
25+
import android.util.Log;
26+
import androidx.annotation.NonNull;
27+
import androidx.annotation.Nullable;
28+
import androidx.annotation.RequiresApi;
29+
import java.io.FileDescriptor;
30+
import java.io.FileOutputStream;
31+
import java.io.OutputStream;
32+
import java.util.Map;
33+
34+
/** This class creates a Resources Table at runtime and helps replace color Resources on the fly. */
35+
@RequiresApi(VERSION_CODES.R)
36+
final class ColorResourcesLoaderCreator {
37+
38+
private ColorResourcesLoaderCreator() {}
39+
40+
private static final String TAG = ColorResourcesLoaderCreator.class.getSimpleName();
41+
42+
@Nullable
43+
static ResourcesLoader create(
44+
@NonNull Context context, @NonNull Map<Integer, Integer> colorMapping) {
45+
try {
46+
byte[] contentBytes = ColorResourcesTableCreator.create(context, colorMapping);
47+
Log.i(TAG, "Table created, length: " + contentBytes.length);
48+
if (contentBytes.length == 0) {
49+
return null;
50+
}
51+
FileDescriptor arscFile = null;
52+
try {
53+
arscFile = Os.memfd_create("temp.arsc", /* flags= */ 0);
54+
// Note: This must not be closed through the OutputStream.
55+
try (OutputStream pipeWriter = new FileOutputStream(arscFile)) {
56+
pipeWriter.write(contentBytes);
57+
58+
try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(arscFile)) {
59+
ResourcesLoader colorsLoader = new ResourcesLoader();
60+
colorsLoader.addProvider(
61+
ResourcesProvider.loadFromTable(pfd, /* assetsProvider= */ null));
62+
return colorsLoader;
63+
}
64+
}
65+
} finally {
66+
if (arscFile != null) {
67+
Os.close(arscFile);
68+
}
69+
}
70+
} catch (Exception e) {
71+
Log.e(TAG, "Failed to create the ColorResourcesTableCreator.", e);
72+
}
73+
return null;
74+
}
75+
}

lib/java/com/google/android/material/color/ColorResourcesTableCreator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ static byte[] create(Context context, Map<Integer, Integer> colorMapping) throws
7474
context.getResources().getResourceName(entry.getKey()),
7575
entry.getValue());
7676
if (colorResource.typeId != TYPE_ID_COLOR) {
77-
throw new IllegalArgumentException("Expected color resource not found.");
77+
throw new IllegalArgumentException("Non color resource found: " + colorResource.name);
7878
}
7979
PackageInfo packageInfo;
8080
if (colorResource.packageId == ANDROID_PACKAGE_ID) {

lib/java/com/google/android/material/color/DynamicColors.java

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,12 @@
2222
import android.app.Application;
2323
import android.app.Application.ActivityLifecycleCallbacks;
2424
import android.content.Context;
25-
import android.content.res.Resources.Theme;
2625
import android.content.res.TypedArray;
2726
import android.os.Build;
2827
import android.os.Build.VERSION;
2928
import android.os.Build.VERSION_CODES;
3029
import android.os.Bundle;
3130
import android.view.ContextThemeWrapper;
32-
import android.view.View;
33-
import android.view.Window;
3431
import androidx.annotation.ChecksSdkIntAtLeast;
3532
import androidx.annotation.NonNull;
3633
import androidx.annotation.Nullable;
@@ -40,9 +37,7 @@
4037
import java.util.HashMap;
4138
import java.util.Map;
4239

43-
/**
44-
* Utility for applying dynamic colors to application/activities.
45-
*/
40+
/** Utility for applying dynamic colors to application/activities. */
4641
public class DynamicColors {
4742
private static final int[] DYNAMIC_COLOR_THEME_OVERLAY_ATTRIBUTE =
4843
new int[] { R.attr.dynamicColorThemeOverlay };
@@ -127,12 +122,11 @@ public static void applyToActivitiesIfAvailable(@NonNull Application application
127122
}
128123

129124
/**
130-
* Applies dynamic colors to all activities with the given theme overlay by registering a
131-
* {@link ActivityLifecycleCallbacks} to your application.
125+
* Applies dynamic colors to all activities with the given theme overlay by registering a {@link
126+
* ActivityLifecycleCallbacks} to your application.
132127
*
133128
* @see #applyToActivitiesIfAvailable(Application, int, Precondition) for more detailed info and
134-
* examples.
135-
*
129+
* examples.
136130
* @param application The target application.
137131
* @param theme The resource ID of the theme overlay that provides dynamic color definition.
138132
*/
@@ -161,8 +155,9 @@ public static void applyToActivitiesIfAvailable(
161155
* Applies dynamic colors to all activities with the given theme overlay according to the given
162156
* precondition by registering a {@link ActivityLifecycleCallbacks} to your application.
163157
*
164-
* A normal usage of this method should happen only once in {@link Application#onCreate()} or any
165-
* methods that run before any of your activities are created. For example:
158+
* <p>A normal usage of this method should happen only once in {@link Application#onCreate()} or
159+
* any methods that run before any of your activities are created. For example:
160+
*
166161
* <pre>
167162
* public class YourApplication extends Application {
168163
* &#64;Override
@@ -172,9 +167,10 @@ public static void applyToActivitiesIfAvailable(
172167
* }
173168
* }
174169
* </pre>
175-
* This method will try to apply the given dynamic color theme overlay in every activity's
176-
* {@link ActivityLifecycleCallbacks#onActivityPreCreated(Activity, Bundle)} callback. Therefore,
177-
* if you are applying any other theme overlays after that, you will need to be careful about not
170+
*
171+
* This method will try to apply the given dynamic color theme overlay in every activity's {@link
172+
* ActivityLifecycleCallbacks#onActivityPreCreated(Activity, Bundle)} callback. Therefore, if you
173+
* are applying any other theme overlays after that, you will need to be careful about not
178174
* overriding the colors or you may lose the dynamic color support.
179175
*
180176
* @param application The target application.
@@ -261,7 +257,7 @@ private static void applyIfAvailable(
261257
theme = getDefaultThemeOverlay(activity);
262258
}
263259
if (theme != 0 && precondition.shouldApplyDynamicColors(activity, theme)) {
264-
applyDynamicColorThemeOverlay(activity, theme);
260+
ThemeUtils.applyThemeOverlay(activity, theme);
265261
onAppliedCallback.onApplied(activity);
266262
}
267263
}
@@ -327,34 +323,6 @@ private static int getDefaultThemeOverlay(@NonNull Context context) {
327323
return theme;
328324
}
329325

330-
private static void applyDynamicColorThemeOverlay(Activity activity, @StyleRes int theme) {
331-
// Use applyStyle() instead of setTheme() due to Force Dark issue.
332-
activity.getTheme().applyStyle(theme, /* force= */ true);
333-
334-
// Make sure theme is applied to the Window decorView similar to Activity#setTheme, to ensure
335-
// that the dynamic colors will be applied to things like ContextMenu using the DecorContext.
336-
Theme windowDecorViewTheme = getWindowDecorViewTheme(activity);
337-
if (windowDecorViewTheme != null) {
338-
windowDecorViewTheme.applyStyle(theme, /* force= */ true);
339-
}
340-
}
341-
342-
@Nullable
343-
private static Theme getWindowDecorViewTheme(@NonNull Activity activity) {
344-
Window window = activity.getWindow();
345-
if (window != null) {
346-
// Use peekDecorView() instead of getDecorView() to avoid locking the Window.
347-
View decorView = window.peekDecorView();
348-
if (decorView != null) {
349-
Context context = decorView.getContext();
350-
if (context != null) {
351-
return context.getTheme();
352-
}
353-
}
354-
}
355-
return null;
356-
}
357-
358326
/**
359327
* The interface that provides a precondition to decide if dynamic colors should be applied.
360328
*/
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (C) 2022 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+
24+
/**
25+
* A class for specifying color attributes for harmonization, which would contain an int array of
26+
* color attributes, with the option to specify a custom theme overlay.
27+
*/
28+
public final class HarmonizedColorAttributes {
29+
30+
private final int[] attributes;
31+
@StyleRes private final int themeOverlay;
32+
33+
private static final int[] HARMONIZED_MATERIAL_ATTRIBUTES =
34+
new int[] {
35+
R.attr.colorError,
36+
R.attr.colorOnError,
37+
R.attr.colorErrorContainer,
38+
R.attr.colorOnErrorContainer
39+
};
40+
41+
/** Create HarmonizedColorAttributes with an int array of color attributes. */
42+
@NonNull
43+
public static HarmonizedColorAttributes create(@NonNull int[] attributes) {
44+
return new HarmonizedColorAttributes(attributes, 0);
45+
}
46+
47+
/**
48+
* Create HarmonizedColorAttributes with a theme overlay, along with an int array of attributes in
49+
* the theme overlay.
50+
*/
51+
@NonNull
52+
public static HarmonizedColorAttributes create(
53+
@NonNull int[] attributes, @StyleRes int themeOverlay) {
54+
return new HarmonizedColorAttributes(attributes, themeOverlay);
55+
}
56+
57+
/** Create HarmonizedColorAttributes with Material default, with Error colors being harmonized. */
58+
@NonNull
59+
public static HarmonizedColorAttributes createMaterialDefaults() {
60+
return create(HARMONIZED_MATERIAL_ATTRIBUTES, R.style.ThemeOverlay_Material3_HarmonizedColors);
61+
}
62+
63+
private HarmonizedColorAttributes(@NonNull int[] attributes, @StyleRes int themeOverlay) {
64+
if (themeOverlay != 0 && attributes.length == 0) {
65+
throw new IllegalArgumentException(
66+
"Theme overlay should be used with the accompanying int[] attributes.");
67+
}
68+
this.attributes = attributes;
69+
this.themeOverlay = themeOverlay;
70+
}
71+
72+
/** Returns the int array of color attributes for harmonization. */
73+
@NonNull
74+
public int[] getAttributes() {
75+
return attributes;
76+
}
77+
78+
/** Returns the custom theme overlay for harmonization, default is 0 if not specified. */
79+
@StyleRes
80+
public int getThemeOverlay() {
81+
return themeOverlay;
82+
}
83+
}

0 commit comments

Comments
 (0)