1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.notification.modes; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.drawable.Drawable; 24 import android.util.AttributeSet; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.widget.ImageView; 28 import android.widget.LinearLayout; 29 import android.widget.TextView; 30 31 import androidx.annotation.Nullable; 32 import androidx.annotation.VisibleForTesting; 33 34 import com.android.settings.R; 35 36 import com.google.common.collect.ImmutableList; 37 import com.google.common.util.concurrent.Futures; 38 import com.google.common.util.concurrent.ListenableFuture; 39 40 import java.util.List; 41 import java.util.concurrent.Executor; 42 43 public class CircularIconsView extends LinearLayout { 44 45 private static final float DISABLED_ITEM_ALPHA = 0.3f; 46 Icons(ImmutableList<Drawable> icons, int extraItems)47 record Icons(ImmutableList<Drawable> icons, int extraItems) { } 48 49 private Executor mUiExecutor; 50 private int mNumberOfCirclesThatFit; 51 52 // Chronologically, fields will be set top-to-bottom. 53 @Nullable private CircularIconSet<?> mIconSet; 54 @Nullable private ListenableFuture<List<Drawable>> mPendingLoadIconsFuture; 55 @Nullable private Icons mDisplayedIcons; 56 CircularIconsView(Context context)57 public CircularIconsView(Context context) { 58 super(context); 59 setUiExecutor(context.getMainExecutor()); 60 } 61 CircularIconsView(Context context, AttributeSet attrs)62 public CircularIconsView(Context context, AttributeSet attrs) { 63 super(context, attrs); 64 setUiExecutor(context.getMainExecutor()); 65 } 66 CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr)67 public CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr) { 68 super(context, attrs, defStyleAttr); 69 setUiExecutor(context.getMainExecutor()); 70 } 71 CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)72 public CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr, 73 int defStyleRes) { 74 super(context, attrs, defStyleAttr, defStyleRes); 75 setUiExecutor(context.getMainExecutor()); 76 } 77 78 @VisibleForTesting setUiExecutor(Executor uiExecutor)79 void setUiExecutor(Executor uiExecutor) { 80 mUiExecutor = uiExecutor; 81 } 82 setIcons(CircularIconSet<T> iconSet)83 <T> void setIcons(CircularIconSet<T> iconSet) { 84 if (mIconSet != null && mIconSet.equals(iconSet)) { 85 return; 86 } 87 88 mIconSet = checkNotNull(iconSet); 89 cancelPendingTasks(); 90 if (getMeasuredWidth() != 0) { 91 startLoadingIcons(iconSet); 92 } 93 } 94 cancelPendingTasks()95 private void cancelPendingTasks() { 96 mDisplayedIcons = null; 97 if (mPendingLoadIconsFuture != null) { 98 mPendingLoadIconsFuture.cancel(true); 99 mPendingLoadIconsFuture = null; 100 } 101 } 102 103 @Override onLayout(boolean changed, int left, int top, int right, int bottom)104 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 105 super.onLayout(changed, left, top, right, bottom); 106 107 int numFitting = getNumberOfCirclesThatFit(); 108 if (mNumberOfCirclesThatFit != numFitting) { 109 // View has been measured for the first time OR its dimensions have changed since then. 110 // Keep track, because we want to reload stuff if more (or less) items fit. 111 mNumberOfCirclesThatFit = numFitting; 112 113 if (mIconSet != null) { 114 cancelPendingTasks(); 115 startLoadingIcons(mIconSet); 116 } 117 } 118 } 119 getNumberOfCirclesThatFit()120 private int getNumberOfCirclesThatFit() { 121 Resources res = getContext().getResources(); 122 int availableSpace = getMeasuredWidth(); 123 int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter) 124 + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); 125 return availableSpace / iconHorizontalSpace; 126 } 127 startLoadingIcons(CircularIconSet<?> iconSet)128 private void startLoadingIcons(CircularIconSet<?> iconSet) { 129 int numCirclesThatFit = getNumberOfCirclesThatFit(); 130 131 List<ListenableFuture<Drawable>> iconFutures; 132 int extraItems; 133 if (iconSet.size() > numCirclesThatFit) { 134 // Reserve one space for the (+xx) textview. 135 int numIconsToShow = numCirclesThatFit - 1; 136 if (numIconsToShow < 0) { 137 numIconsToShow = 0; 138 } 139 iconFutures = iconSet.getIcons(numIconsToShow); 140 extraItems = iconSet.size() - numIconsToShow; 141 } else { 142 // Fit exactly or with remaining space. 143 iconFutures = iconSet.getIcons(); 144 extraItems = 0; 145 } 146 147 // Display icons when all are ready (more consistent than randomly loading). 148 mPendingLoadIconsFuture = Futures.allAsList(iconFutures); 149 FutureUtil.whenDone( 150 mPendingLoadIconsFuture, 151 icons -> setDrawables(new Icons(ImmutableList.copyOf(icons), extraItems)), 152 mUiExecutor); 153 } 154 setDrawables(Icons icons)155 private void setDrawables(Icons icons) { 156 mDisplayedIcons = icons; 157 158 // Rearrange child views until we have <numImages> ImageViews... 159 LayoutInflater inflater = LayoutInflater.from(getContext()); 160 int numImages = icons.icons.size(); 161 int numImageViews = getChildCount(ImageView.class); 162 if (numImages > numImageViews) { 163 for (int i = 0; i < numImages - numImageViews; i++) { 164 ImageView imageView = (ImageView) inflater.inflate( 165 R.layout.preference_circular_icons_item, this, false); 166 addView(imageView, 0); 167 } 168 } else if (numImageViews > numImages) { 169 for (int i = 0; i < numImageViews - numImages; i++) { 170 removeViewAt(0); 171 } 172 } 173 // ... plus 0/1 TextViews at the end. 174 if (icons.extraItems > 0 && !(getLastChild() instanceof TextView)) { 175 TextView plusView = (TextView) inflater.inflate( 176 R.layout.preference_circular_icons_plus_item, this, false); 177 this.addView(plusView); 178 } else if (icons.extraItems == 0 && (getLastChild() instanceof TextView)) { 179 removeViewAt(getChildCount() - 1); 180 } 181 182 // Show images (and +n if needed). 183 for (int i = 0; i < numImages; i++) { 184 ImageView imageView = (ImageView) getChildAt(i); 185 imageView.setImageDrawable(icons.icons.get(i)); 186 } 187 if (icons.extraItems > 0) { 188 TextView textView = (TextView) checkNotNull(getLastChild()); 189 textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, 190 icons.extraItems)); 191 } 192 193 applyEnabledDisabledAppearance(isEnabled()); 194 } 195 196 @Override setEnabled(boolean enabled)197 public void setEnabled(boolean enabled) { 198 super.setEnabled(enabled); 199 applyEnabledDisabledAppearance(isEnabled()); 200 } 201 applyEnabledDisabledAppearance(boolean enabled)202 private void applyEnabledDisabledAppearance(boolean enabled) { 203 for (int i = 0; i < getChildCount(); i++) { 204 View child = getChildAt(i); 205 child.setAlpha(enabled ? 1.0f : DISABLED_ITEM_ALPHA); 206 } 207 } 208 getChildCount(Class<? extends View> childClass)209 private int getChildCount(Class<? extends View> childClass) { 210 int count = 0; 211 for (int i = 0; i < getChildCount(); i++) { 212 if (childClass.isInstance(getChildAt(i))) { 213 count++; 214 } 215 } 216 return count; 217 } 218 219 @Nullable getLastChild()220 private View getLastChild() { 221 if (getChildCount() == 0) { 222 return null; 223 } 224 return getChildAt(getChildCount() - 1); 225 } 226 227 @VisibleForTesting(otherwise = VisibleForTesting.NONE) 228 @Nullable getDisplayedIcons()229 Icons getDisplayedIcons() { 230 return mDisplayedIcons; 231 } 232 } 233