• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.systemui.qs.tileimpl;
16 
17 import static com.android.systemui.Flags.qsNewTiles;
18 import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.ArgbEvaluator;
23 import android.animation.PropertyValuesHolder;
24 import android.animation.ValueAnimator;
25 import android.annotation.Nullable;
26 import android.content.Context;
27 import android.content.res.ColorStateList;
28 import android.content.res.Configuration;
29 import android.content.res.Resources;
30 import android.graphics.drawable.Animatable2;
31 import android.graphics.drawable.Animatable2.AnimationCallback;
32 import android.graphics.drawable.Drawable;
33 import android.service.quicksettings.Tile;
34 import android.util.Log;
35 import android.view.View;
36 import android.widget.ImageView;
37 import android.widget.ImageView.ScaleType;
38 
39 import androidx.annotation.VisibleForTesting;
40 
41 import com.android.settingslib.Utils;
42 import com.android.systemui.plugins.qs.QSIconView;
43 import com.android.systemui.plugins.qs.QSTile;
44 import com.android.systemui.plugins.qs.QSTile.State;
45 import com.android.systemui.res.R;
46 
47 import java.util.Objects;
48 
49 public class QSIconViewImpl extends QSIconView {
50 
51     public static final long QS_ANIM_LENGTH = 350;
52 
53     private static final long ICON_APPLIED_TRANSACTION_ID = -1;
54 
55     protected final View mIcon;
56     protected int mIconSizePx;
57     private boolean mAnimationEnabled = true;
58     private int mState = -1;
59     private boolean mDisabledByPolicy = false;
60     private int mTint;
61     @Nullable
62     @VisibleForTesting
63     QSTile.Icon mLastIcon;
64 
65     private long mScheduledIconChangeTransactionId = ICON_APPLIED_TRANSACTION_ID;
66     private long mHighestScheduledIconChangeTransactionId = ICON_APPLIED_TRANSACTION_ID;
67 
68     private ValueAnimator mColorAnimator = new ValueAnimator();
69 
70     private int mColorUnavailable;
71     private int mColorInactive;
72     private int mColorActive;
73 
QSIconViewImpl(Context context)74     public QSIconViewImpl(Context context) {
75         super(context);
76 
77         final Resources res = context.getResources();
78         mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_icon_size);
79 
80         if (qsNewTiles()) { // pre-load icon tint colors
81             mColorUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.outline);
82             mColorInactive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant);
83             mColorActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive);
84         }
85 
86         mIcon = createIcon();
87         addView(mIcon);
88         mColorAnimator.setDuration(QS_ANIM_LENGTH);
89     }
90 
91     @Override
onConfigurationChanged(Configuration newConfig)92     protected void onConfigurationChanged(Configuration newConfig) {
93         super.onConfigurationChanged(newConfig);
94         mIconSizePx = getContext().getResources().getDimensionPixelSize(R.dimen.qs_icon_size);
95     }
96 
disableAnimation()97     public void disableAnimation() {
98         mAnimationEnabled = false;
99     }
100 
getIconView()101     public View getIconView() {
102         return mIcon;
103     }
104 
105     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)106     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
107         final int w = MeasureSpec.getSize(widthMeasureSpec);
108         final int iconSpec = exactly(mIconSizePx);
109         mIcon.measure(MeasureSpec.makeMeasureSpec(w, getIconMeasureMode()), iconSpec);
110         setMeasuredDimension(w, mIcon.getMeasuredHeight());
111     }
112 
113     @Override
toString()114     public String toString() {
115         final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('[');
116         sb.append("state=" + mState);
117         sb.append(", tint=" + mTint);
118         if (mLastIcon != null) sb.append(", lastIcon=" + mLastIcon.toString());
119         sb.append("]");
120         return sb.toString();
121     }
122 
123     @Override
onLayout(boolean changed, int l, int t, int r, int b)124     protected void onLayout(boolean changed, int l, int t, int r, int b) {
125         final int w = getMeasuredWidth();
126         int top = 0;
127         final int iconLeft = (w - mIcon.getMeasuredWidth()) / 2;
128         layout(mIcon, iconLeft, top);
129     }
130 
setIcon(State state, boolean allowAnimations)131     public void setIcon(State state, boolean allowAnimations) {
132         setIcon((ImageView) mIcon, state, allowAnimations);
133     }
134 
updateIcon(ImageView iv, State state, boolean allowAnimations)135     protected void updateIcon(ImageView iv, State state, boolean allowAnimations) {
136         mScheduledIconChangeTransactionId = ICON_APPLIED_TRANSACTION_ID;
137         final QSTile.Icon icon = state.iconSupplier != null ? state.iconSupplier.get() : state.icon;
138         if (!Objects.equals(icon, iv.getTag(R.id.qs_icon_tag))) {
139             boolean shouldAnimate = allowAnimations && shouldAnimate(iv);
140             mLastIcon = icon;
141             Drawable d = icon != null
142                     ? shouldAnimate ? icon.getDrawable(mContext)
143                     : icon.getInvisibleDrawable(mContext) : null;
144             int padding = icon != null ? icon.getPadding() : 0;
145             if (d != null) {
146                 if (d.getConstantState() != null) {
147                     d = d.getConstantState().newDrawable();
148                 }
149                 d.setAutoMirrored(false);
150                 d.setLayoutDirection(getLayoutDirection());
151             }
152 
153             final Drawable lastDrawable = iv.getDrawable();
154             if (lastDrawable instanceof Animatable2) {
155                 ((Animatable2) lastDrawable).clearAnimationCallbacks();
156             }
157 
158             iv.setImageDrawable(d);
159 
160             iv.setTag(R.id.qs_icon_tag, icon);
161             iv.setPadding(0, padding, 0, padding);
162             if (d instanceof Animatable2) {
163                 Animatable2 a = (Animatable2) d;
164                 a.start();
165                 if (shouldAnimate) {
166                     if (state.isTransient) {
167                         a.registerAnimationCallback(new AnimationCallback() {
168                             @Override
169                             public void onAnimationEnd(Drawable drawable) {
170                                 a.start();
171                             }
172                         });
173                     }
174                 } else {
175                     // Sends animator to end of animation. Needs to be called after calling start.
176                     a.stop();
177                 }
178             }
179         }
180     }
181 
shouldAnimate(ImageView iv)182     private boolean shouldAnimate(ImageView iv) {
183         return mAnimationEnabled && iv.isShown() && iv.getDrawable() != null;
184     }
185 
setIcon(ImageView iv, QSTile.State state, boolean allowAnimations)186     protected void setIcon(ImageView iv, QSTile.State state, boolean allowAnimations) {
187         if (state.state != mState || state.disabledByPolicy != mDisabledByPolicy) {
188             int color = getColor(state);
189             mState = state.state;
190             mDisabledByPolicy = state.disabledByPolicy;
191             if (mTint != 0 && allowAnimations && shouldAnimate(iv)) {
192                 final long iconTransactionId = getNextIconTransactionId();
193                 mScheduledIconChangeTransactionId = iconTransactionId;
194                 animateGrayScale(mTint, color, iv, () -> {
195                     if (mScheduledIconChangeTransactionId == iconTransactionId) {
196                         updateIcon(iv, state, allowAnimations);
197                     }
198                 });
199             } else {
200                 setTint(iv, color);
201                 updateIcon(iv, state, allowAnimations);
202             }
203         } else {
204             updateIcon(iv, state, allowAnimations);
205         }
206     }
207 
getColor(QSTile.State state)208     protected int getColor(QSTile.State state) {
209         if (qsNewTiles()) {
210             return getCachedIconColorForState(state);
211         } else {
212             return getIconColorForState(getContext(), state);
213         }
214     }
215 
animateGrayScale(int fromColor, int toColor, ImageView iv, final Runnable endRunnable)216     private void animateGrayScale(int fromColor, int toColor, ImageView iv,
217             final Runnable endRunnable) {
218         mColorAnimator.cancel();
219         if (mAnimationEnabled && ValueAnimator.areAnimatorsEnabled()) {
220             PropertyValuesHolder values = PropertyValuesHolder.ofInt("color", fromColor, toColor);
221             values.setEvaluator(ArgbEvaluator.getInstance());
222             mColorAnimator.setValues(values);
223             mColorAnimator.removeAllListeners();
224             if (removeUpdateListenerInQsIconViewImpl()) {
225                 mColorAnimator.removeAllUpdateListeners();
226             }
227             mColorAnimator.addUpdateListener(animation -> {
228                 setTint(iv, (int) animation.getAnimatedValue());
229             });
230             mColorAnimator.addListener(new EndRunnableAnimatorListener(endRunnable));
231 
232             mColorAnimator.start();
233         } else {
234 
235             setTint(iv, toColor);
236             endRunnable.run();
237         }
238     }
239 
setTint(ImageView iv, int color)240     public void setTint(ImageView iv, int color) {
241         iv.setImageTintList(ColorStateList.valueOf(color));
242         mTint = color;
243     }
244 
getIconMeasureMode()245     protected int getIconMeasureMode() {
246         return MeasureSpec.EXACTLY;
247     }
248 
createIcon()249     protected View createIcon() {
250         final ImageView icon = new ImageView(mContext);
251         icon.setId(android.R.id.icon);
252         icon.setScaleType(ScaleType.FIT_CENTER);
253         return icon;
254     }
255 
exactly(int size)256     protected final int exactly(int size) {
257         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
258     }
259 
layout(View child, int left, int top)260     protected final void layout(View child, int left, int top) {
261         child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
262     }
263 
getNextIconTransactionId()264     private long getNextIconTransactionId() {
265         mHighestScheduledIconChangeTransactionId++;
266         return mHighestScheduledIconChangeTransactionId;
267     }
268 
269     /**
270      * Color to tint the tile icon based on state
271      */
getIconColorForState(Context context, QSTile.State state)272     private static int getIconColorForState(Context context, QSTile.State state) {
273         if (state.disabledByPolicy || state.state == Tile.STATE_UNAVAILABLE) {
274             return Utils.getColorAttrDefaultColor(context, R.attr.outline);
275         } else if (state.state == Tile.STATE_INACTIVE) {
276             return Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant);
277         } else if (state.state == Tile.STATE_ACTIVE) {
278             return Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive);
279         } else {
280             Log.e("QSIconView", "Invalid state " + state);
281             return 0;
282         }
283     }
284 
getCachedIconColorForState(QSTile.State state)285     private int getCachedIconColorForState(QSTile.State state) {
286         if (state.disabledByPolicy || state.state == Tile.STATE_UNAVAILABLE) {
287             return mColorUnavailable;
288         } else if (state.state == Tile.STATE_INACTIVE) {
289             return mColorInactive;
290         } else if (state.state == Tile.STATE_ACTIVE) {
291             return mColorActive;
292         } else {
293             Log.e("QSIconView", "Invalid state " + state);
294             return 0;
295         }
296     }
297 
298     private static class EndRunnableAnimatorListener extends AnimatorListenerAdapter {
299         private Runnable mRunnable;
300 
EndRunnableAnimatorListener(Runnable endRunnable)301         EndRunnableAnimatorListener(Runnable endRunnable) {
302             super();
303             mRunnable = endRunnable;
304         }
305 
306         @Override
onAnimationCancel(Animator animation)307         public void onAnimationCancel(Animator animation) {
308             super.onAnimationCancel(animation);
309             mRunnable.run();
310         }
311 
312         @Override
onAnimationEnd(Animator animation)313         public void onAnimationEnd(Animator animation) {
314             super.onAnimationEnd(animation);
315             mRunnable.run();
316         }
317     }
318 }
319