blob: 21218a2dc8e223916d8df5ed4b7b8694fb099f67 [file] [log] [blame]
/*
* 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.android.systemui.monet.dynamiccolor;
import com.android.systemui.monet.dislike.DislikeAnalyzer;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.hct.ViewingConditions;
import com.android.systemui.monet.scheme.DynamicScheme;
import com.android.systemui.monet.scheme.Variant;
/** Named colors, otherwise known as tokens, or roles, in the Material Design system.*/
// Prevent lint for Function.apply not being available on Android before API level 14 (4.0.1).
// "AndroidJdkLibsChecker" for Function, "NewApi" for Function.apply().
// A java_library Bazel rule with an Android constraint cannot skip these warnings without this
// annotation; another solution would be to create an android_library rule and supply
// AndroidManifest with an SDK set higher than 14.
@SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
public final class MaterialDynamicColors {
private static final double CONTAINER_ACCENT_TONE_DELTA = 15.0;
public MaterialDynamicColors() {
}
/**
* These colors were present in Android framework before Android U, and used by MDC controls.
* They
* should be avoided, if possible. It's unclear if they're used on multiple backgrounds, and if
* they are, they can't be adjusted for contrast.* For now, they will be set with no background,
* and those won't adjust for contrast, avoiding issues.
*
* <p>* For example, if the same color is on a white background _and_ black background,
* there's no
* way to increase contrast with either without losing contrast with the other.
*/
// colorControlActivated documented as colorAccent in M3 & GM3.
// colorAccent documented as colorSecondary in M3 and colorPrimary in GM3.
// Android used Material's Container as Primary/Secondary/Tertiary at launch.
// Therefore, this is a duplicated version of Primary Container.
public static DynamicColor controlActivated() {
return DynamicColor.fromPalette((s) -> s.primaryPalette, (s) -> s.isDark ? 30.0 : 90.0, null);
}
// Compatibility Keys Colors for Android
public static DynamicColor primaryPaletteKeyColor() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette, (s) -> s.primaryPalette.getKeyColor().getTone());
}
public static DynamicColor secondaryPaletteKeyColor() {
return DynamicColor.fromPalette(
(s) -> s.secondaryPalette, (s) -> s.secondaryPalette.getKeyColor().getTone());
}
public static DynamicColor tertiaryPaletteKeyColor() {
return DynamicColor.fromPalette(
(s) -> s.tertiaryPalette, (s) -> s.tertiaryPalette.getKeyColor().getTone());
}
public static DynamicColor neutralPaletteKeyColor() {
return DynamicColor.fromPalette(
(s) -> s.neutralPalette, (s) -> s.neutralPalette.getKeyColor().getTone());
}
public static DynamicColor neutralVariantPaletteKeyColor() {
return DynamicColor.fromPalette(
(s) -> s.neutralVariantPalette,
(s) -> s.neutralVariantPalette.getKeyColor().getTone());
}
private static ViewingConditions viewingConditionsForAlbers(DynamicScheme scheme) {
return ViewingConditions.defaultWithBackgroundLstar(scheme.isDark ? 30.0 : 80.0);
}
private static boolean isFidelity(DynamicScheme scheme) {
return scheme.variant == Variant.FIDELITY || scheme.variant == Variant.CONTENT;
}
private static boolean isMonochrome(DynamicScheme scheme) {
return scheme.variant == Variant.MONOCHROME;
}
static double findDesiredChromaByTone(
double hue, double chroma, double tone, boolean byDecreasingTone) {
double answer = tone;
Hct closestToChroma = Hct.from(hue, chroma, tone);
if (closestToChroma.getChroma() < chroma) {
double chromaPeak = closestToChroma.getChroma();
while (closestToChroma.getChroma() < chroma) {
answer += byDecreasingTone ? -1.0 : 1.0;
Hct potentialSolution = Hct.from(hue, chroma, answer);
if (chromaPeak > potentialSolution.getChroma()) {
break;
}
if (Math.abs(potentialSolution.getChroma() - chroma) < 0.4) {
break;
}
double potentialDelta = Math.abs(potentialSolution.getChroma() - chroma);
double currentDelta = Math.abs(closestToChroma.getChroma() - chroma);
if (potentialDelta < currentDelta) {
closestToChroma = potentialSolution;
}
chromaPeak = Math.max(chromaPeak, potentialSolution.getChroma());
}
}
return answer;
}
static double performAlbers(Hct prealbers, DynamicScheme scheme) {
final Hct albersd = prealbers.inViewingConditions(viewingConditionsForAlbers(scheme));
if (DynamicColor.tonePrefersLightForeground(prealbers.getTone())
&& !DynamicColor.toneAllowsLightForeground(albersd.getTone())) {
return DynamicColor.enableLightForeground(prealbers.getTone());
} else {
return DynamicColor.enableLightForeground(albersd.getTone());
}
}
public static DynamicColor highestSurface(DynamicScheme s) {
return s.isDark ? surfaceBright() : surfaceDim();
}
public static DynamicColor background() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 98.0);
}
public static DynamicColor onBackground() {
return DynamicColor.fromPalette(
(s) -> s.neutralPalette, (s) -> s.isDark ? 90.0 : 10.0, (s) -> background());
}
public static DynamicColor surface() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 98.0);
}
public static DynamicColor inverseSurface() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 90.0 : 20.0);
}
public static DynamicColor surfaceBright() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 24.0 : 98.0);
}
public static DynamicColor surfaceDim() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 6.0 : 87.0);
}
public static DynamicColor surfaceContainerLowest() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 4.0 : 100.0);
}
public static DynamicColor surfaceContainerLow() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 96.0);
}
public static DynamicColor surfaceContainer() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 12.0 : 94.0);
}
public static DynamicColor surfaceContainerHigh() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 17.0 : 92.0);
}
public static DynamicColor surfaceContainerHighest() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 22.0 : 90.0);
}
public static DynamicColor onSurface() {
return DynamicColor.fromPalette(
(s) -> s.neutralPalette, (s) -> s.isDark ? 90.0 : 10.0,
MaterialDynamicColors::highestSurface);
}
public static DynamicColor inverseOnSurface() {
return DynamicColor.fromPalette(
(s) -> s.neutralPalette, (s) -> s.isDark ? 20.0 : 95.0, (s) -> inverseSurface());
}
public static DynamicColor surfaceVariant() {
return DynamicColor.fromPalette((s) -> s.neutralVariantPalette,
(s) -> s.isDark ? 30.0 : 90.0);
}
public static DynamicColor onSurfaceVariant() {
return DynamicColor.fromPalette(
(s) -> s.neutralVariantPalette, (s) -> s.isDark ? 80.0 : 30.0,
(s) -> surfaceVariant());
}
public static DynamicColor outline() {
return DynamicColor.fromPalette(
(s) -> s.neutralVariantPalette, (s) -> 50.0, MaterialDynamicColors::highestSurface);
}
public static DynamicColor outlineVariant() {
return DynamicColor.fromPalette(
(s) -> s.neutralVariantPalette, (s) -> s.isDark ? 30.0 : 80.0,
MaterialDynamicColors::highestSurface);
}
public static DynamicColor primaryContainer() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette,
(s) -> {
if (isFidelity(s)) {
return performAlbers(s.sourceColorHct, s);
}
if (isMonochrome(s)) {
return s.isDark ? 85.0 : 25.0;
}
return s.isDark ? 30.0 : 90.0;
},
MaterialDynamicColors::highestSurface);
}
public static DynamicColor onPrimaryContainer() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette,
(s) -> {
if (isFidelity(s)) {
return DynamicColor.contrastingTone(primaryContainer().tone.apply(s), 4.5);
}
if (isMonochrome(s)) {
return s.isDark ? 0.0 : 100.0;
}
return s.isDark ? 90.0 : 10.0;
},
(s) -> primaryContainer(),
null);
}
public static DynamicColor primary() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 100.0 : 0.0;
}
return s.isDark ? 80.0 : 40.0;
},
MaterialDynamicColors::highestSurface,
(s) ->
new ToneDeltaConstraint(
CONTAINER_ACCENT_TONE_DELTA,
primaryContainer(),
s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
}
public static DynamicColor inversePrimary() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette, (s) -> s.isDark ? 40.0 : 80.0, (s) -> inverseSurface());
}
public static DynamicColor onPrimary() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 10.0 : 90.0;
}
return s.isDark ? 20.0 : 100.0;
},
(s) -> primary());
}
public static DynamicColor secondaryContainer() {
return DynamicColor.fromPalette(
(s) -> s.secondaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 30.0 : 85.0;
}
final double initialTone = s.isDark ? 30.0 : 90.0;
if (!isFidelity(s)) {
return initialTone;
}
double answer =
findDesiredChromaByTone(
s.secondaryPalette.getHue(),
s.secondaryPalette.getChroma(),
initialTone,
!s.isDark);
answer = performAlbers(s.secondaryPalette.getHct(answer), s);
return answer;
},
MaterialDynamicColors::highestSurface);
}
public static DynamicColor onSecondaryContainer() {
return DynamicColor.fromPalette(
(s) -> s.secondaryPalette,
(s) -> {
if (!isFidelity(s)) {
return s.isDark ? 90.0 : 10.0;
}
return DynamicColor.contrastingTone(secondaryContainer().tone.apply(s), 4.5);
},
(s) -> secondaryContainer());
}
public static DynamicColor secondary() {
return DynamicColor.fromPalette(
(s) -> s.secondaryPalette,
(s) -> s.isDark ? 80.0 : 40.0,
MaterialDynamicColors::highestSurface,
(s) ->
new ToneDeltaConstraint(
CONTAINER_ACCENT_TONE_DELTA,
secondaryContainer(),
s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
}
public static DynamicColor onSecondary() {
return DynamicColor.fromPalette(
(s) -> s.secondaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 10.0 : 100.0;
}
return s.isDark ? 20.0 : 100.0;
},
(s) -> secondary());
}
public static DynamicColor tertiaryContainer() {
return DynamicColor.fromPalette(
(s) -> s.tertiaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 60.0 : 49.0;
}
if (!isFidelity(s)) {
return s.isDark ? 30.0 : 90.0;
}
final double albersTone =
performAlbers(s.tertiaryPalette.getHct(s.sourceColorHct.getTone()), s);
final Hct proposedHct = s.tertiaryPalette.getHct(albersTone);
return DislikeAnalyzer.fixIfDisliked(proposedHct).getTone();
},
MaterialDynamicColors::highestSurface);
}
public static DynamicColor onTertiaryContainer() {
return DynamicColor.fromPalette(
(s) -> s.tertiaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 0.0 : 100.0;
}
if (!isFidelity(s)) {
return s.isDark ? 90.0 : 10.0;
}
return DynamicColor.contrastingTone(tertiaryContainer().tone.apply(s), 4.5);
},
(s) -> tertiaryContainer());
}
public static DynamicColor tertiary() {
return DynamicColor.fromPalette(
(s) -> s.tertiaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 90.0 : 25.0;
}
return s.isDark ? 80.0 : 40.0;
},
MaterialDynamicColors::highestSurface,
(s) ->
new ToneDeltaConstraint(
CONTAINER_ACCENT_TONE_DELTA,
tertiaryContainer(),
s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
}
public static DynamicColor onTertiary() {
return DynamicColor.fromPalette(
(s) -> s.tertiaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 10.0 : 90.0;
}
return s.isDark ? 20.0 : 100.0;
},
(s) -> tertiary());
}
public static DynamicColor errorContainer() {
return DynamicColor.fromPalette(
(s) -> s.errorPalette, (s) -> s.isDark ? 30.0 : 90.0,
MaterialDynamicColors::highestSurface);
}
public static DynamicColor onErrorContainer() {
return DynamicColor.fromPalette(
(s) -> s.errorPalette, (s) -> s.isDark ? 90.0 : 10.0, (s) -> errorContainer());
}
public static DynamicColor error() {
return DynamicColor.fromPalette(
(s) -> s.errorPalette,
(s) -> s.isDark ? 80.0 : 40.0,
MaterialDynamicColors::highestSurface,
(s) ->
new ToneDeltaConstraint(
CONTAINER_ACCENT_TONE_DELTA,
errorContainer(),
s.isDark ? TonePolarity.DARKER : TonePolarity.LIGHTER));
}
public static DynamicColor onError() {
return DynamicColor.fromPalette(
(s) -> s.errorPalette, (s) -> s.isDark ? 20.0 : 100.0, (s) -> error());
}
public static DynamicColor primaryFixed() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 100.0 : 10.0;
}
return 90.0;
},
MaterialDynamicColors::highestSurface);
}
public static DynamicColor primaryFixedDim() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 90.0 : 20.0;
}
return 80.0;
},
MaterialDynamicColors::highestSurface);
}
public static DynamicColor onPrimaryFixed() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 10.0 : 90.0;
}
return 10.0;
},
(s) -> primaryFixedDim());
}
public static DynamicColor onPrimaryFixedVariant() {
return DynamicColor.fromPalette(
(s) -> s.primaryPalette,
(s) -> {
if (isMonochrome(s)) {
return s.isDark ? 30.0 : 70.0;
}
return 30.0;
},
(s) -> primaryFixedDim());
}
public static DynamicColor secondaryFixed() {
return DynamicColor.fromPalette(
(s) -> s.secondaryPalette, (s) -> isMonochrome(s) ? 80.0 : 90.0,
MaterialDynamicColors::highestSurface);
}
public static DynamicColor secondaryFixedDim() {
return DynamicColor.fromPalette(
(s) -> s.secondaryPalette, (s) -> isMonochrome(s) ? 70.0 : 80.0,
MaterialDynamicColors::highestSurface);
}
public static DynamicColor onSecondaryFixed() {
return DynamicColor.fromPalette(
(s) -> s.secondaryPalette, (s) -> 10.0, (s) -> secondaryFixedDim());
}
public static DynamicColor onSecondaryFixedVariant() {
return DynamicColor.fromPalette(
(s) -> s.secondaryPalette,
(s) -> isMonochrome(s) ? 25.0 : 30.0,
(s) -> secondaryFixedDim());
}
public static DynamicColor tertiaryFixed() {
return DynamicColor.fromPalette(
(s) -> s.tertiaryPalette, (s) -> isMonochrome(s) ? 40.0 : 90.0,
MaterialDynamicColors::highestSurface);
}
public static DynamicColor tertiaryFixedDim() {
return DynamicColor.fromPalette(
(s) -> s.tertiaryPalette, (s) -> isMonochrome(s) ? 30.0 : 80.0,
MaterialDynamicColors::highestSurface);
}
public static DynamicColor onTertiaryFixed() {
return DynamicColor.fromPalette(
(s) -> s.tertiaryPalette, (s) -> isMonochrome(s) ? 90.0 : 10.0,
(s) -> tertiaryFixedDim());
}
public static DynamicColor onTertiaryFixedVariant() {
return DynamicColor.fromPalette(
(s) -> s.tertiaryPalette, (s) -> isMonochrome(s) ? 70.0 : 30.0,
(s) -> tertiaryFixedDim());
}
// colorControlNormal documented as textColorSecondary in M3 & GM3.
// In Material, textColorSecondary points to onSurfaceVariant in the non-disabled state,
// which is Neutral Variant T30/80 in light/dark.
public static DynamicColor controlNormal() {
return DynamicColor.fromPalette((s) -> s.neutralVariantPalette,
(s) -> s.isDark ? 80.0 : 30.0);
}
// colorControlHighlight documented, in both M3 & GM3:
// Light mode: #1f000000 dark mode: #33ffffff.
// These are black and white with some alpha.
// 1F hex = 31 decimal; 31 / 255 = 12% alpha.
// 33 hex = 51 decimal; 51 / 255 = 20% alpha.
// DynamicColors do not support alpha currently, and _may_ not need it for this use case,
// depending on how MDC resolved alpha for the other cases.
// Returning black in dark mode, white in light mode.
public static DynamicColor controlHighlight() {
return new DynamicColor(
s -> 0.0,
s -> 0.0,
s -> s.isDark ? 100.0 : 0.0,
s -> s.isDark ? 0.20 : 0.12,
null,
scheme ->
DynamicColor.toneMinContrastDefault((s) -> s.isDark ? 100.0 : 0.0, null,
scheme, null),
scheme ->
DynamicColor.toneMaxContrastDefault((s) -> s.isDark ? 100.0 : 0.0, null,
scheme, null),
null);
}
// textColorPrimaryInverse documented, in both M3 & GM3, documented as N10/N90.
public static DynamicColor textPrimaryInverse() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
}
// textColorSecondaryInverse and textColorTertiaryInverse both documented, in both M3 & GM3, as
// NV30/NV80
public static DynamicColor textSecondaryAndTertiaryInverse() {
return DynamicColor.fromPalette((s) -> s.neutralVariantPalette,
(s) -> s.isDark ? 30.0 : 80.0);
}
// textColorPrimaryInverseDisableOnly documented, in both M3 & GM3, as N10/N90
public static DynamicColor textPrimaryInverseDisableOnly() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
}
// textColorSecondaryInverse and textColorTertiaryInverse in disabled state both documented,
// in both M3 & GM3, as N10/N90
public static DynamicColor textSecondaryAndTertiaryInverseDisabled() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
}
// textColorHintInverse documented, in both M3 & GM3, as N10/N90
public static DynamicColor textHintInverse() {
return DynamicColor.fromPalette((s) -> s.neutralPalette, (s) -> s.isDark ? 10.0 : 90.0);
}
}