Files
app_Settings/src/com/android/settings/notification/modes/IconUtil.java
Matías Hernández b676ca3a62 Fix several issues related to CircularIconsPreference
* Sometimes would cause an unending stream of accessibility events (particularly if starting off-screen). This would break TalkBack and anything that depends on UiAutomator (although the system itself took it like a champ).
* Sometimes would not load images (because the ViewTreeObserver would never fire onGlobalLayout after being added because measured width was 0, even though a new width was calculated later).
* Would not recalculate the number of icons that fit if the width changes after the first layout.

Combining ViewHolders with waiting for measuring and/or ViewTreeObservers was always a wonky approach, even though it should've worked in theory. This should be more robust.

Also fixes the unwanted animation on mode screen load related to the absence of the header name being applied a tad too late.

Fixes: 359948417
Fixes: 360072876
Fixes: 360328804
Test: atest SettingsRoboTests + manual + adb shell uiautomator events
Flag: android.app.modes_ui
Change-Id: I7e5dfbdab220d1ebc1c68e5e87ce544ee86b6a65
2024-08-16 16:20:38 +02:00

236 lines
11 KiB
Java

/*
* Copyright (C) 2024 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.settings.notification.modes;
import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.StateListDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.util.StateSet;
import android.view.Gravity;
import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import com.android.settings.R;
import com.android.settingslib.Utils;
import com.google.common.base.Strings;
import java.util.Locale;
class IconUtil {
static Drawable applyNormalTint(@NonNull Context context, @NonNull Drawable icon) {
return applyTint(context, icon, android.R.attr.colorControlNormal);
}
static Drawable applyAccentTint(@NonNull Context context, @NonNull Drawable icon) {
return applyTint(context, icon, android.R.attr.colorAccent);
}
private static Drawable applyTint(@NonNull Context context, @NonNull Drawable icon,
@AttrRes int colorAttr) {
icon = icon.mutate();
icon.setTintList(Utils.getColorAttr(context, colorAttr));
return icon;
}
/**
* Returns a variant of the supplied mode icon to be used as the header in the mode page. The
* mode icon is contained in a 12-sided-cookie. The color combination is "material secondary"
* when unselected and "material primary" when selected; the switch between these two color sets
* is animated with a cross-fade. The selected colors should be used when the mode is currently
* active.
*/
static Drawable makeModeHeader(@NonNull Context context, Drawable modeIcon) {
Resources res = context.getResources();
Drawable background = checkNotNull(context.getDrawable(R.drawable.ic_zen_mode_icon_cookie));
@Px int outerSizePx = res.getDimensionPixelSize(R.dimen.zen_mode_header_size);
@Px int innerSizePx = res.getDimensionPixelSize(R.dimen.zen_mode_header_inner_icon_size);
Drawable base = composeIcons(
context.getResources(),
background,
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorSecondaryContainer),
outerSizePx,
modeIcon,
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorOnSecondaryContainer),
innerSizePx);
Drawable selected = composeIcons(
context.getResources(),
background,
Utils.getColorAttr(context, com.android.internal.R.attr.materialColorPrimary),
outerSizePx,
modeIcon,
Utils.getColorAttr(context, com.android.internal.R.attr.materialColorOnPrimary),
innerSizePx);
StateListDrawable result = new StateListDrawable();
result.setEnterFadeDuration(res.getInteger(android.R.integer.config_mediumAnimTime));
result.setExitFadeDuration(res.getInteger(android.R.integer.config_mediumAnimTime));
result.addState(new int[] { android.R.attr.state_selected }, selected);
result.addState(StateSet.WILD_CARD, base);
result.setBounds(0, 0, outerSizePx, outerSizePx);
return result;
}
/**
* Returns a variant of the supplied {@code icon} to be used as the header in the icon picker
* (large icon within large circle, with the "material secondary" color combination).
*/
static Drawable makeIconPickerHeader(@NonNull Context context, Drawable icon) {
return composeIconCircle(
context.getResources(),
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorSecondaryContainer),
context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_icon_list_header_circle_diameter),
icon,
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorOnSecondaryContainer),
context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_icon_list_header_icon_size));
}
/**
* Returns a variant of the supplied {@code icon} to be used as an option in the icon picker
* (small icon in small circle, with "material secondary" colors for the normal state and
* "material primary" colors for the selected state).
*/
static Drawable makeIconPickerItem(@NonNull Context context, @DrawableRes int iconResId) {
return composeIconCircle(
context.getResources(),
context.getColorStateList(R.color.modes_icon_selectable_background),
context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_icon_list_item_circle_diameter),
checkNotNull(context.getDrawable(iconResId)),
context.getColorStateList(R.color.modes_icon_selectable_icon),
context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_icon_list_item_icon_size));
}
/**
* Returns a variant of the supplied icon to be used in a {@link CircularIconsPreference}. The
* inner icon is 20x20 dp and it's contained in a circle of diameter 32dp, and is tinted
* with the "material secondary" color combination.
*/
static Drawable makeCircularIconPreferenceItem(@NonNull Context context,
@DrawableRes int iconResId) {
return composeIconCircle(
context.getResources(),
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorSecondaryContainer),
context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_circular_icon_diameter),
checkNotNull(context.getDrawable(iconResId)),
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorOnSecondaryContainer),
context.getResources().getDimensionPixelSize(
R.dimen.zen_mode_circular_icon_inner_icon_size));
}
/**
* Returns an icon representing a contact that doesn't have an associated photo, to be used in
* a {@link CircularIconsPreference}, tinted with the "material tertiary". If the contact's
* display name is not empty, it's the contact's monogram, otherwise it's a generic icon.
*/
static Drawable makeContactMonogram(@NonNull Context context, @Nullable String displayName) {
Resources res = context.getResources();
if (Strings.isNullOrEmpty(displayName)) {
return composeIconCircle(
context.getResources(),
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorTertiaryContainer),
res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter),
checkNotNull(context.getDrawable(R.drawable.ic_zen_mode_generic_contact)),
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorOnTertiaryContainer),
res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_inner_icon_size));
}
float diameter = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter);
Bitmap bitmap = Bitmap.createBitmap((int) diameter, (int) diameter,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint circlePaint = new Paint();
circlePaint.setColor(Utils.getColorAttrDefaultColor(context,
com.android.internal.R.attr.materialColorTertiaryContainer));
circlePaint.setFlags(Paint.ANTI_ALIAS_FLAG);
canvas.drawCircle(diameter / 2f, diameter / 2f, diameter / 2f, circlePaint);
Paint textPaint = new Paint();
textPaint.setColor(Utils.getColorAttrDefaultColor(context,
com.android.internal.R.attr.materialColorOnTertiaryContainer));
textPaint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
textPaint.setTextAlign(Paint.Align.LEFT);
textPaint.setTextSize(res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_text_size));
String text = displayName.substring(0, 1).toUpperCase(Locale.getDefault());
Rect textRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), textRect);
float textX = diameter / 2f - textRect.width() / 2f - textRect.left;
float textY = diameter / 2f + textRect.height() / 2f - textRect.bottom;
canvas.drawText(text, textX, textY, textPaint);
return new BitmapDrawable(context.getResources(), bitmap);
}
private static Drawable composeIconCircle(Resources res, ColorStateList circleColor,
@Px int circleDiameterPx, Drawable icon, ColorStateList iconColor, @Px int iconSizePx) {
return composeIcons(res, new ShapeDrawable(new OvalShape()), circleColor, circleDiameterPx,
icon, iconColor, iconSizePx);
}
private static Drawable composeIcons(Resources res, Drawable outer, ColorStateList outerColor,
@Px int outerSizePx, Drawable icon, ColorStateList iconColor, @Px int iconSizePx) {
Drawable background = checkNotNull(outer.getConstantState()).newDrawable(res).mutate();
background.setTintList(outerColor);
Drawable foreground = checkNotNull(icon.getConstantState()).newDrawable(res).mutate();
foreground.setTintList(iconColor);
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground });
layerDrawable.setLayerSize(0, outerSizePx, outerSizePx);
layerDrawable.setLayerGravity(1, Gravity.CENTER);
layerDrawable.setLayerSize(1, iconSizePx, iconSizePx);
layerDrawable.setBounds(0, 0, outerSizePx, outerSizePx);
return layerDrawable;
}
}