Skip to content

Commit

Permalink
[M3][Color] Add support for color resources harmonization in XML
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 430757816
  • Loading branch information
Material Design Team authored and hunterstich committed Feb 25, 2022
1 parent 43114c4 commit 869d943
Show file tree
Hide file tree
Showing 9 changed files with 559 additions and 45 deletions.
@@ -0,0 +1,75 @@
/*
* 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.color;

import android.content.Context;
import android.content.res.loader.ResourcesLoader;
import android.content.res.loader.ResourcesProvider;
import android.os.Build.VERSION_CODES;
import android.os.ParcelFileDescriptor;
import android.system.Os;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Map;

/** This class creates a Resources Table at runtime and helps replace color Resources on the fly. */
@RequiresApi(VERSION_CODES.R)
final class ColorResourcesLoaderCreator {

private ColorResourcesLoaderCreator() {}

private static final String TAG = ColorResourcesLoaderCreator.class.getSimpleName();

@Nullable
static ResourcesLoader create(
@NonNull Context context, @NonNull Map<Integer, Integer> colorMapping) {
try {
byte[] contentBytes = ColorResourcesTableCreator.create(context, colorMapping);
Log.i(TAG, "Table created, length: " + contentBytes.length);
if (contentBytes.length == 0) {
return null;
}
FileDescriptor arscFile = null;
try {
arscFile = Os.memfd_create("temp.arsc", /* flags= */ 0);
// Note: This must not be closed through the OutputStream.
try (OutputStream pipeWriter = new FileOutputStream(arscFile)) {
pipeWriter.write(contentBytes);

try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(arscFile)) {
ResourcesLoader colorsLoader = new ResourcesLoader();
colorsLoader.addProvider(
ResourcesProvider.loadFromTable(pfd, /* assetsProvider= */ null));
return colorsLoader;
}
}
} finally {
if (arscFile != null) {
Os.close(arscFile);
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to create the ColorResourcesTableCreator.", e);
}
return null;
}
}
Expand Up @@ -74,7 +74,7 @@ static byte[] create(Context context, Map<Integer, Integer> colorMapping) throws
context.getResources().getResourceName(entry.getKey()),
entry.getValue());
if (colorResource.typeId != TYPE_ID_COLOR) {
throw new IllegalArgumentException("Expected color resource not found.");
throw new IllegalArgumentException("Non color resource found: " + colorResource.name);
}
PackageInfo packageInfo;
if (colorResource.packageId == ANDROID_PACKAGE_ID) {
Expand Down
56 changes: 12 additions & 44 deletions lib/java/com/google/android/material/color/DynamicColors.java
Expand Up @@ -22,15 +22,12 @@
import android.app.Application;
import android.app.Application.ActivityLifecycleCallbacks;
import android.content.Context;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.Window;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -40,9 +37,7 @@
import java.util.HashMap;
import java.util.Map;

/**
* Utility for applying dynamic colors to application/activities.
*/
/** Utility for applying dynamic colors to application/activities. */
public class DynamicColors {
private static final int[] DYNAMIC_COLOR_THEME_OVERLAY_ATTRIBUTE =
new int[] { R.attr.dynamicColorThemeOverlay };
Expand Down Expand Up @@ -127,12 +122,11 @@ public static void applyToActivitiesIfAvailable(@NonNull Application application
}

/**
* Applies dynamic colors to all activities with the given theme overlay by registering a
* {@link ActivityLifecycleCallbacks} to your application.
* Applies dynamic colors to all activities with the given theme overlay by registering a {@link
* ActivityLifecycleCallbacks} to your application.
*
* @see #applyToActivitiesIfAvailable(Application, int, Precondition) for more detailed info and
* examples.
*
* examples.
* @param application The target application.
* @param theme The resource ID of the theme overlay that provides dynamic color definition.
*/
Expand Down Expand Up @@ -161,8 +155,9 @@ public static void applyToActivitiesIfAvailable(
* Applies dynamic colors to all activities with the given theme overlay according to the given
* precondition by registering a {@link ActivityLifecycleCallbacks} to your application.
*
* 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:
* <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 {
* &#64;Override
Expand All @@ -172,9 +167,10 @@ public static void applyToActivitiesIfAvailable(
* }
* }
* </pre>
* This method will try to apply the given dynamic color theme overlay in every activity's
* {@link ActivityLifecycleCallbacks#onActivityPreCreated(Activity, Bundle)} callback. Therefore,
* if you are applying any other theme overlays after that, you will need to be careful about not
*
* This method will try to apply the given dynamic color theme overlay in every activity's {@link
* ActivityLifecycleCallbacks#onActivityPreCreated(Activity, Bundle)} callback. Therefore, if you
* are applying any other theme overlays after that, you will need to be careful about not
* overriding the colors or you may lose the dynamic color support.
*
* @param application The target application.
Expand Down Expand Up @@ -261,7 +257,7 @@ private static void applyIfAvailable(
theme = getDefaultThemeOverlay(activity);
}
if (theme != 0 && precondition.shouldApplyDynamicColors(activity, theme)) {
applyDynamicColorThemeOverlay(activity, theme);
ThemeUtils.applyThemeOverlay(activity, theme);
onAppliedCallback.onApplied(activity);
}
}
Expand Down Expand Up @@ -327,34 +323,6 @@ private static int getDefaultThemeOverlay(@NonNull Context context) {
return theme;
}

private static void applyDynamicColorThemeOverlay(Activity activity, @StyleRes int theme) {
// Use applyStyle() instead of setTheme() due to Force Dark issue.
activity.getTheme().applyStyle(theme, /* force= */ true);

// Make sure theme is applied to the Window decorView similar to Activity#setTheme, to ensure
// that the dynamic colors will be applied to things like ContextMenu using the DecorContext.
Theme windowDecorViewTheme = getWindowDecorViewTheme(activity);
if (windowDecorViewTheme != null) {
windowDecorViewTheme.applyStyle(theme, /* force= */ true);
}
}

@Nullable
private static Theme getWindowDecorViewTheme(@NonNull Activity activity) {
Window window = activity.getWindow();
if (window != null) {
// Use peekDecorView() instead of getDecorView() to avoid locking the Window.
View decorView = window.peekDecorView();
if (decorView != null) {
Context context = decorView.getContext();
if (context != null) {
return context.getTheme();
}
}
}
return null;
}

/**
* The interface that provides a precondition to decide if dynamic colors should be applied.
*/
Expand Down
@@ -0,0 +1,83 @@
/*
* 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.color;

import com.google.android.material.R;

import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;

/**
* A class for specifying color attributes for harmonization, which would contain an int array of
* color attributes, with the option to specify a custom theme overlay.
*/
public final class HarmonizedColorAttributes {

private final int[] attributes;
@StyleRes private final int themeOverlay;

private static final int[] HARMONIZED_MATERIAL_ATTRIBUTES =
new int[] {
R.attr.colorError,
R.attr.colorOnError,
R.attr.colorErrorContainer,
R.attr.colorOnErrorContainer
};

/** Create HarmonizedColorAttributes with an int array of color attributes. */
@NonNull
public static HarmonizedColorAttributes create(@NonNull int[] attributes) {
return new HarmonizedColorAttributes(attributes, 0);
}

/**
* Create HarmonizedColorAttributes with a theme overlay, along with an int array of attributes in
* the theme overlay.
*/
@NonNull
public static HarmonizedColorAttributes create(
@NonNull int[] attributes, @StyleRes int themeOverlay) {
return new HarmonizedColorAttributes(attributes, themeOverlay);
}

/** Create HarmonizedColorAttributes with Material default, with Error colors being harmonized. */
@NonNull
public static HarmonizedColorAttributes createMaterialDefaults() {
return create(HARMONIZED_MATERIAL_ATTRIBUTES, R.style.ThemeOverlay_Material3_HarmonizedColors);
}

private HarmonizedColorAttributes(@NonNull int[] attributes, @StyleRes int themeOverlay) {
if (themeOverlay != 0 && attributes.length == 0) {
throw new IllegalArgumentException(
"Theme overlay should be used with the accompanying int[] attributes.");
}
this.attributes = attributes;
this.themeOverlay = themeOverlay;
}

/** Returns the int array of color attributes for harmonization. */
@NonNull
public int[] getAttributes() {
return attributes;
}

/** Returns the custom theme overlay for harmonization, default is 0 if not specified. */
@StyleRes
public int getThemeOverlay() {
return themeOverlay;
}
}

0 comments on commit 869d943

Please sign in to comment.