• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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