Loading res/layout/preference_circular_icons.xml +2 −2 Original line number Diff line number Diff line Loading @@ -58,8 +58,8 @@ android:lineBreakWordStyle="phrase" android:maxLines="10"/> <!-- Circular icons (32dp) will be ImageViews under this LinearLayout --> <LinearLayout <!-- Circular icons (32dp) will be ImageViews under this container --> <com.android.settings.notification.modes.CircularIconsView android:id="@+id/circles_container" android:importantForAccessibility="noHideDescendants" android:orientation="horizontal" Loading src/com/android/settings/notification/modes/AbstractZenModeHeaderController.java +8 −7 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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, Loading src/com/android/settings/notification/modes/CircularIconsPreference.java +19 −183 Original line number Diff line number Diff line Loading @@ -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; } mIconSet = iconSet; 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); } } src/com/android/settings/notification/modes/CircularIconsView.java 0 → 100644 +232 −0 Original line number Diff line number Diff line /* * 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; } } src/com/android/settings/notification/modes/IconUtil.java +13 −7 Original line number Diff line number Diff line Loading @@ -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), Loading @@ -89,6 +90,7 @@ class IconUtil { innerSizePx); Drawable selected = composeIcons( context.getResources(), background, Utils.getColorAttr(context, com.android.internal.R.attr.materialColorPrimary), outerSizePx, Loading @@ -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( Loading @@ -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), Loading @@ -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( Loading @@ -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), Loading Loading @@ -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 }); Loading Loading
res/layout/preference_circular_icons.xml +2 −2 Original line number Diff line number Diff line Loading @@ -58,8 +58,8 @@ android:lineBreakWordStyle="phrase" android:maxLines="10"/> <!-- Circular icons (32dp) will be ImageViews under this LinearLayout --> <LinearLayout <!-- Circular icons (32dp) will be ImageViews under this container --> <com.android.settings.notification.modes.CircularIconsView android:id="@+id/circles_container" android:importantForAccessibility="noHideDescendants" android:orientation="horizontal" Loading
src/com/android/settings/notification/modes/AbstractZenModeHeaderController.java +8 −7 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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, Loading
src/com/android/settings/notification/modes/CircularIconsPreference.java +19 −183 Original line number Diff line number Diff line Loading @@ -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; } mIconSet = iconSet; 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); } }
src/com/android/settings/notification/modes/CircularIconsView.java 0 → 100644 +232 −0 Original line number Diff line number Diff line /* * 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; } }
src/com/android/settings/notification/modes/IconUtil.java +13 −7 Original line number Diff line number Diff line Loading @@ -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), Loading @@ -89,6 +90,7 @@ class IconUtil { innerSizePx); Drawable selected = composeIcons( context.getResources(), background, Utils.getColorAttr(context, com.android.internal.R.attr.materialColorPrimary), outerSizePx, Loading @@ -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( Loading @@ -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), Loading @@ -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( Loading @@ -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), Loading Loading @@ -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 }); Loading