Merge "Fix several issues related to CircularIconsPreference" into main

This commit is contained in:
Matías Hernández
2024-08-16 16:44:24 +00:00
committed by Android (Google) Code Review
13 changed files with 414 additions and 299 deletions

View File

@@ -61,13 +61,6 @@ abstract class AbstractZenModeHeaderController extends AbstractZenModePreference
LayoutPreference preference = checkNotNull(screen.findPreference(getPreferenceKey()));
preference.setSelectable(false);
if (mHeaderController == null) {
mHeaderController = EntityHeaderController.newInstance(
mFragment.getActivity(),
mFragment,
preference.findViewById(R.id.entity_header));
}
ImageView iconView = checkNotNull(preference.findViewById(R.id.entity_header_icon));
ViewGroup.LayoutParams layoutParams = iconView.getLayoutParams();
if (layoutParams.width != iconSizePx || layoutParams.height != iconSizePx) {
@@ -75,6 +68,14 @@ abstract class AbstractZenModeHeaderController extends AbstractZenModePreference
layoutParams.height = iconSizePx;
iconView.setLayoutParams(layoutParams);
}
if (mHeaderController == null) {
mHeaderController = EntityHeaderController.newInstance(
mFragment.getActivity(),
mFragment,
preference.findViewById(R.id.entity_header));
mHeaderController.done(false); // Make the space for the (unused) name go away.
}
}
protected void updateIcon(Preference preference, @NonNull ZenMode zenMode,

View File

@@ -16,236 +16,72 @@
package com.android.settings.notification.modes;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.RestrictedPreference;
import com.google.common.base.Equivalence;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.concurrent.Executor;
public class CircularIconsPreference extends RestrictedPreference {
private static final float DISABLED_ITEM_ALPHA = 0.3f;
record LoadedIcons(ImmutableList<Drawable> icons, int extraItems) {
static final LoadedIcons EMPTY = new LoadedIcons(ImmutableList.of(), 0);
}
private Executor mUiExecutor;
// Chronologically, fields will be set top-to-bottom.
@Nullable private CircularIconSet<?> mIconSet;
@Nullable private ListenableFuture<List<Drawable>> mPendingLoadIconsFuture;
@Nullable private LoadedIcons mLoadedIcons;
private CircularIconSet<?> mIconSet = CircularIconSet.EMPTY;
public CircularIconsPreference(Context context) {
super(context);
init(context);
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public CircularIconsPreference(Context context, Executor uiExecutor) {
this(context);
mUiExecutor = uiExecutor;
init();
}
public CircularIconsPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
init();
}
public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
init();
}
public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
init();
}
private void init(Context context) {
mUiExecutor = context.getMainExecutor();
private void init() {
setLayoutResource(R.layout.preference_circular_icons);
}
<T> void displayIcons(CircularIconSet<T> iconSet) {
displayIcons(iconSet, null);
<T> void setIcons(CircularIconSet<T> iconSet) {
setIcons(iconSet, null);
}
<T> void displayIcons(CircularIconSet<T> iconSet, @Nullable Equivalence<T> itemEquivalence) {
if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) {
<T> void setIcons(CircularIconSet<T> iconSet, @Nullable Equivalence<T> itemEquivalence) {
if (mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) {
return;
}
mIconSet = iconSet;
mLoadedIcons = null;
if (mPendingLoadIconsFuture != null) {
mPendingLoadIconsFuture.cancel(true);
mPendingLoadIconsFuture = null;
}
notifyChanged();
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
CircularIconsView iconContainer = checkNotNull(
(CircularIconsView) holder.findViewById(R.id.circles_container));
LinearLayout iconContainer = checkNotNull(
(LinearLayout) holder.findViewById(R.id.circles_container));
bindIconContainer(iconContainer);
}
private void bindIconContainer(LinearLayout container) {
if (mLoadedIcons != null) {
// We have the icons ready to display already, show them.
setDrawables(container, mLoadedIcons);
} else if (mIconSet != null) {
// We know what icons we want, but haven't yet loaded them.
if (mIconSet.size() == 0) {
container.setVisibility(View.GONE);
mLoadedIcons = LoadedIcons.EMPTY;
return;
}
container.setVisibility(View.VISIBLE);
if (container.getMeasuredWidth() != 0) {
startLoadingIcons(container, mIconSet);
} else {
container.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
container.getViewTreeObserver().removeOnGlobalLayoutListener(this);
notifyChanged();
}
}
);
}
}
}
private void startLoadingIcons(LinearLayout container, CircularIconSet<?> iconSet) {
Resources res = getContext().getResources();
int availableSpace = container.getMeasuredWidth();
int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter)
+ res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between);
int numIconsThatFit = availableSpace / iconHorizontalSpace;
List<ListenableFuture<Drawable>> iconFutures;
int extraItems;
if (iconSet.size() > numIconsThatFit) {
// Reserve one space for the (+xx) textview.
int numIconsToShow = numIconsThatFit - 1;
if (numIconsToShow < 0) {
numIconsToShow = 0;
}
iconFutures = iconSet.getIcons(numIconsToShow);
extraItems = iconSet.size() - numIconsToShow;
} else {
// Fit exactly or with remaining space.
iconFutures = iconSet.getIcons();
extraItems = 0;
}
// Display icons when all are ready (more consistent than randomly loading).
mPendingLoadIconsFuture = Futures.allAsList(iconFutures);
FutureUtil.whenDone(
mPendingLoadIconsFuture,
icons -> {
mLoadedIcons = new LoadedIcons(ImmutableList.copyOf(icons), extraItems);
notifyChanged(); // So that view is rebound and icons actually shown.
},
mUiExecutor);
}
private void setDrawables(LinearLayout container, LoadedIcons loadedIcons) {
// Rearrange child views until we have <numImages> ImageViews...
LayoutInflater inflater = LayoutInflater.from(getContext());
int numImages = loadedIcons.icons.size();
int numImageViews = getChildCount(container, ImageView.class);
if (numImages > numImageViews) {
for (int i = 0; i < numImages - numImageViews; i++) {
ImageView imageView = (ImageView) inflater.inflate(
R.layout.preference_circular_icons_item, container, false);
container.addView(imageView, 0);
}
} else if (numImageViews > numImages) {
for (int i = 0; i < numImageViews - numImages; i++) {
container.removeViewAt(0);
}
}
// ... plus 0/1 TextViews at the end.
if (loadedIcons.extraItems > 0 && !(getLastChild(container) instanceof TextView)) {
TextView plusView = (TextView) inflater.inflate(
R.layout.preference_circular_icons_plus_item, container, false);
container.addView(plusView);
} else if (loadedIcons.extraItems == 0 && (getLastChild(container) instanceof TextView)) {
container.removeViewAt(container.getChildCount() - 1);
}
// Show images (and +n if needed).
for (int i = 0; i < numImages; i++) {
ImageView imageView = (ImageView) container.getChildAt(i);
imageView.setImageDrawable(loadedIcons.icons.get(i));
}
if (loadedIcons.extraItems > 0) {
TextView textView = (TextView) checkNotNull(getLastChild(container));
textView.setText(getContext().getString(R.string.zen_mode_plus_n_items,
loadedIcons.extraItems));
}
// Apply enabled/disabled style.
for (int i = 0; i < container.getChildCount(); i++) {
View child = container.getChildAt(i);
child.setAlpha(isEnabled() ? 1.0f : DISABLED_ITEM_ALPHA);
}
}
private static int getChildCount(ViewGroup parent, Class<? extends View> childClass) {
int count = 0;
for (int i = 0; i < parent.getChildCount(); i++) {
if (childClass.isInstance(parent.getChildAt(i))) {
count++;
}
}
return count;
}
@Nullable
private static View getLastChild(ViewGroup parent) {
if (parent.getChildCount() == 0) {
return null;
}
return parent.getChildAt(parent.getChildCount() - 1);
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
@Nullable
LoadedIcons getLoadedIcons() {
return mLoadedIcons;
iconContainer.setVisibility(mIconSet != null && mIconSet.size() == 0 ? GONE : VISIBLE);
iconContainer.setEnabled(isEnabled());
iconContainer.setIcons(mIconSet);
}
}

View File

@@ -0,0 +1,232 @@
/*
* 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.Resources;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.concurrent.Executor;
public class CircularIconsView extends LinearLayout {
private static final float DISABLED_ITEM_ALPHA = 0.3f;
record Icons(ImmutableList<Drawable> icons, int extraItems) { }
private Executor mUiExecutor;
private int mNumberOfCirclesThatFit;
// Chronologically, fields will be set top-to-bottom.
@Nullable private CircularIconSet<?> mIconSet;
@Nullable private ListenableFuture<List<Drawable>> mPendingLoadIconsFuture;
@Nullable private Icons mDisplayedIcons;
public CircularIconsView(Context context) {
super(context);
setUiExecutor(context.getMainExecutor());
}
public CircularIconsView(Context context, AttributeSet attrs) {
super(context, attrs);
setUiExecutor(context.getMainExecutor());
}
public CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setUiExecutor(context.getMainExecutor());
}
public CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setUiExecutor(context.getMainExecutor());
}
@VisibleForTesting
void setUiExecutor(Executor uiExecutor) {
mUiExecutor = uiExecutor;
}
<T> void setIcons(CircularIconSet<T> iconSet) {
if (mIconSet != null && mIconSet.equals(iconSet)) {
return;
}
mIconSet = checkNotNull(iconSet);
cancelPendingTasks();
if (getMeasuredWidth() != 0) {
startLoadingIcons(iconSet);
}
}
private void cancelPendingTasks() {
mDisplayedIcons = null;
if (mPendingLoadIconsFuture != null) {
mPendingLoadIconsFuture.cancel(true);
mPendingLoadIconsFuture = null;
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
int numFitting = getNumberOfCirclesThatFit();
if (mNumberOfCirclesThatFit != numFitting) {
// View has been measured for the first time OR its dimensions have changed since then.
// Keep track, because we want to reload stuff if more (or less) items fit.
mNumberOfCirclesThatFit = numFitting;
if (mIconSet != null) {
cancelPendingTasks();
startLoadingIcons(mIconSet);
}
}
}
private int getNumberOfCirclesThatFit() {
Resources res = getContext().getResources();
int availableSpace = getMeasuredWidth();
int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter)
+ res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between);
return availableSpace / iconHorizontalSpace;
}
private void startLoadingIcons(CircularIconSet<?> iconSet) {
int numCirclesThatFit = getNumberOfCirclesThatFit();
List<ListenableFuture<Drawable>> iconFutures;
int extraItems;
if (iconSet.size() > numCirclesThatFit) {
// Reserve one space for the (+xx) textview.
int numIconsToShow = numCirclesThatFit - 1;
if (numIconsToShow < 0) {
numIconsToShow = 0;
}
iconFutures = iconSet.getIcons(numIconsToShow);
extraItems = iconSet.size() - numIconsToShow;
} else {
// Fit exactly or with remaining space.
iconFutures = iconSet.getIcons();
extraItems = 0;
}
// Display icons when all are ready (more consistent than randomly loading).
mPendingLoadIconsFuture = Futures.allAsList(iconFutures);
FutureUtil.whenDone(
mPendingLoadIconsFuture,
icons -> setDrawables(new Icons(ImmutableList.copyOf(icons), extraItems)),
mUiExecutor);
}
private void setDrawables(Icons icons) {
mDisplayedIcons = icons;
// Rearrange child views until we have <numImages> ImageViews...
LayoutInflater inflater = LayoutInflater.from(getContext());
int numImages = icons.icons.size();
int numImageViews = getChildCount(ImageView.class);
if (numImages > numImageViews) {
for (int i = 0; i < numImages - numImageViews; i++) {
ImageView imageView = (ImageView) inflater.inflate(
R.layout.preference_circular_icons_item, this, false);
addView(imageView, 0);
}
} else if (numImageViews > numImages) {
for (int i = 0; i < numImageViews - numImages; i++) {
removeViewAt(0);
}
}
// ... plus 0/1 TextViews at the end.
if (icons.extraItems > 0 && !(getLastChild() instanceof TextView)) {
TextView plusView = (TextView) inflater.inflate(
R.layout.preference_circular_icons_plus_item, this, false);
this.addView(plusView);
} else if (icons.extraItems == 0 && (getLastChild() instanceof TextView)) {
removeViewAt(getChildCount() - 1);
}
// Show images (and +n if needed).
for (int i = 0; i < numImages; i++) {
ImageView imageView = (ImageView) getChildAt(i);
imageView.setImageDrawable(icons.icons.get(i));
}
if (icons.extraItems > 0) {
TextView textView = (TextView) checkNotNull(getLastChild());
textView.setText(getContext().getString(R.string.zen_mode_plus_n_items,
icons.extraItems));
}
applyEnabledDisabledAppearance(isEnabled());
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
applyEnabledDisabledAppearance(isEnabled());
}
private void applyEnabledDisabledAppearance(boolean enabled) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.setAlpha(enabled ? 1.0f : DISABLED_ITEM_ALPHA);
}
}
private int getChildCount(Class<? extends View> childClass) {
int count = 0;
for (int i = 0; i < getChildCount(); i++) {
if (childClass.isInstance(getChildAt(i))) {
count++;
}
}
return count;
}
@Nullable
private View getLastChild() {
if (getChildCount() == 0) {
return null;
}
return getChildAt(getChildCount() - 1);
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
@Nullable
Icons getDisplayedIcons() {
return mDisplayedIcons;
}
}

View File

@@ -79,6 +79,7 @@ class IconUtil {
@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),
@@ -89,6 +90,7 @@ class IconUtil {
innerSizePx);
Drawable selected = composeIcons(
context.getResources(),
background,
Utils.getColorAttr(context, com.android.internal.R.attr.materialColorPrimary),
outerSizePx,
@@ -111,6 +113,7 @@ class IconUtil {
*/
static Drawable makeIconPickerHeader(@NonNull Context context, Drawable icon) {
return composeIconCircle(
context.getResources(),
Utils.getColorAttr(context,
com.android.internal.R.attr.materialColorSecondaryContainer),
context.getResources().getDimensionPixelSize(
@@ -129,6 +132,7 @@ class IconUtil {
*/
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),
@@ -146,6 +150,7 @@ class IconUtil {
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(
@@ -166,6 +171,7 @@ class IconUtil {
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),
@@ -204,17 +210,17 @@ class IconUtil {
return new BitmapDrawable(context.getResources(), bitmap);
}
private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx,
Drawable icon, ColorStateList iconColor, @Px int iconSizePx) {
return composeIcons(new ShapeDrawable(new OvalShape()), circleColor, circleDiameterPx, icon,
iconColor, iconSizePx);
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(Drawable outer, ColorStateList outerColor,
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().mutate();
Drawable background = checkNotNull(outer.getConstantState()).newDrawable(res).mutate();
background.setTintList(outerColor);
Drawable foreground = checkNotNull(icon.getConstantState()).newDrawable().mutate();
Drawable foreground = checkNotNull(icon.getConstantState()).newDrawable(res).mutate();
foreground.setTintList(iconColor);
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground });

View File

@@ -109,7 +109,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) {
mPreference.setSummary(R.string.zen_mode_apps_none_apps);
mPreference.displayIcons(CircularIconSet.EMPTY);
mPreference.setIcons(CircularIconSet.EMPTY);
if (mAppSession != null) {
mAppSession.deactivateSession();
}
@@ -151,7 +151,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
ImmutableList<AppEntry> apps = getAppsBypassingDndSortedByName(allApps);
mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps));
mPreference.displayIcons(new CircularIconSet<>(apps,
mPreference.setIcons(new CircularIconSet<>(apps,
app -> mAppIconRetriever.apply(app.info)),
APP_ENTRY_EQUIVALENCE);
}

View File

@@ -72,7 +72,7 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont
preference.setEnabled(zenMode.isEnabled());
preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode));
((CircularIconsPreference) preference).displayIcons(getSoundIcons(zenMode.getPolicy()));
((CircularIconsPreference) preference).setIcons(getSoundIcons(zenMode.getPolicy()));
}
private CircularIconSet<Integer> getSoundIcons(ZenPolicy policy) {

View File

@@ -96,7 +96,7 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon
preference.setEnabled(zenMode.isEnabled());
preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy()));
((CircularIconsPreference) preference).displayIcons(getPeopleIcons(zenMode.getPolicy()),
((CircularIconsPreference) preference).setIcons(getPeopleIcons(zenMode.getPolicy()),
PEOPLE_ITEM_EQUIVALENCE);
}