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 android.animation.Animator;
18 import android.animation.AnimatorListenerAdapter;
19 import android.animation.ArgbEvaluator;
20 import android.animation.PropertyValuesHolder;
21 import android.animation.ValueAnimator;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.content.res.ColorStateList;
25 import android.content.res.Configuration;
26 import android.content.res.Resources;
27 import android.graphics.drawable.Animatable2;
28 import android.graphics.drawable.Animatable2.AnimationCallback;
29 import android.graphics.drawable.Drawable;
30 import android.service.quicksettings.Tile;
31 import android.util.Log;
32 import android.view.View;
33 import android.widget.ImageView;
34 import android.widget.ImageView.ScaleType;
35 
36 import androidx.annotation.VisibleForTesting;
37 
38 import com.android.settingslib.Utils;
39 import com.android.systemui.plugins.qs.QSIconView;
40 import com.android.systemui.plugins.qs.QSTile;
41 import com.android.systemui.plugins.qs.QSTile.State;
42 import com.android.systemui.res.R;
43 
44 import java.util.Objects;
45 
46 public class QSIconViewImpl extends QSIconView {
47 
48     public static final long QS_ANIM_LENGTH = 350;
49 
50     private static final long ICON_APPLIED_TRANSACTION_ID = -1;
51 
52     protected final View mIcon;
53     protected int mIconSizePx;
54     private boolean mAnimationEnabled = true;
55     private int mState = -1;
56     private boolean mDisabledByPolicy = false;
57     private int mTint;
58     @Nullable
59     @VisibleForTesting
60     QSTile.Icon mLastIcon;
61 
62     private long mScheduledIconChangeTransactionId = ICON_APPLIED_TRANSACTION_ID;
63     private long mHighestScheduledIconChangeTransactionId = ICON_APPLIED_TRANSACTION_ID;
64 
65     private ValueAnimator mColorAnimator = new ValueAnimator();
66 
QSIconViewImpl(Context context)67     public QSIconViewImpl(Context context) {
68         super(context);
69 
70         final Resources res = context.getResources();
71         mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_icon_size);
72 
73         mIcon = createIcon();
74         addView(mIcon);
75         mColorAnimator.setDuration(QS_ANIM_LENGTH);
76     }
77 
78     @Override
onConfigurationChanged(Configuration newConfig)79     protected void onConfigurationChanged(Configuration newConfig) {
80         super.onConfigurationChanged(newConfig);
81         mIconSizePx = getContext().getResources().getDimensionPixelSize(R.dimen.qs_icon_size);
82     }
83 
disableAnimation()84     public void disableAnimation() {
85         mAnimationEnabled = false;
86     }
87 
getIconView()88     public View getIconView() {
89         return mIcon;
90     }
91 
92     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)93     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
94         final int w = MeasureSpec.getSize(widthMeasureSpec);
95         final int iconSpec = exactly(mIconSizePx);
96         mIcon.measure(MeasureSpec.makeMeasureSpec(w, getIconMeasureMode()), iconSpec);
97         setMeasuredDimension(w, mIcon.getMeasuredHeight());
98     }
99 
100     @Override
toString()101     public String toString() {
102         final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('[');
103         sb.append("state=" + mState);
104         sb.append(", tint=" + mTint);
105         if (mLastIcon != null) sb.append(", lastIcon=" + mLastIcon.toString());
106         sb.append("]");
107         return sb.toString();
108     }
109 
110     @Override
onLayout(boolean changed, int l, int t, int r, int b)111     protected void onLayout(boolean changed, int l, int t, int r, int b) {
112         final int w = getMeasuredWidth();
113         int top = 0;
114         final int iconLeft = (w - mIcon.getMeasuredWidth()) / 2;
115         layout(mIcon, iconLeft, top);
116     }
117 
setIcon(State state, boolean allowAnimations)118     public void setIcon(State state, boolean allowAnimations) {
119         setIcon((ImageView) mIcon, state, allowAnimations);
120     }
121 
updateIcon(ImageView iv, State state, boolean allowAnimations)122     protected void updateIcon(ImageView iv, State state, boolean allowAnimations) {
123         mScheduledIconChangeTransactionId = ICON_APPLIED_TRANSACTION_ID;
124         final QSTile.Icon icon = state.iconSupplier != null ? state.iconSupplier.get() : state.icon;
125         if (!Objects.equals(icon, iv.getTag(R.id.qs_icon_tag))) {
126             boolean shouldAnimate = allowAnimations && shouldAnimate(iv);
127             mLastIcon = icon;
128             Drawable d = icon != null
129                     ? shouldAnimate ? icon.getDrawable(mContext)
130                     : icon.getInvisibleDrawable(mContext) : null;
131             int padding = icon != null ? icon.getPadding() : 0;
132             if (d != null) {
133                 if (d.getConstantState() != null) {
134                     d = d.getConstantState().newDrawable();
135                 }
136                 d.setAutoMirrored(false);
137                 d.setLayoutDirection(getLayoutDirection());
138             }
139 
140             final Drawable lastDrawable = iv.getDrawable();
141             if (lastDrawable instanceof Animatable2) {
142                 ((Animatable2) lastDrawable).clearAnimationCallbacks();
143             }
144 
145             iv.setImageDrawable(d);
146 
147             iv.setTag(R.id.qs_icon_tag, icon);
148             iv.setPadding(0, padding, 0, padding);
149             if (d instanceof Animatable2) {
150                 Animatable2 a = (Animatable2) d;
151                 a.start();
152                 if (shouldAnimate) {
153                     if (state.isTransient) {
154                         a.registerAnimationCallback(new AnimationCallback() {
155                             @Override
156                             public void onAnimationEnd(Drawable drawable) {
157                                 a.start();
158                             }
159                         });
160                     }
161                 } else {
162                     // Sends animator to end of animation. Needs to be called after calling start.
163                     a.stop();
164                 }
165             }
166         }
167     }
168 
shouldAnimate(ImageView iv)169     private boolean shouldAnimate(ImageView iv) {
170         return mAnimationEnabled && iv.isShown() && iv.getDrawable() != null;
171     }
172 
setIcon(ImageView iv, QSTile.State state, boolean allowAnimations)173     protected void setIcon(ImageView iv, QSTile.State state, boolean allowAnimations) {
174         if (state.state != mState || state.disabledByPolicy != mDisabledByPolicy) {
175             int color = getColor(state);
176             mState = state.state;
177             mDisabledByPolicy = state.disabledByPolicy;
178             if (mTint != 0 && allowAnimations && shouldAnimate(iv)) {
179                 final long iconTransactionId = getNextIconTransactionId();
180                 mScheduledIconChangeTransactionId = iconTransactionId;
181                 animateGrayScale(mTint, color, iv, () -> {
182                     if (mScheduledIconChangeTransactionId == iconTransactionId) {
183                         updateIcon(iv, state, allowAnimations);
184                     }
185                 });
186             } else {
187                 setTint(iv, color);
188                 updateIcon(iv, state, allowAnimations);
189             }
190         } else {
191             updateIcon(iv, state, allowAnimations);
192         }
193     }
194 
getColor(QSTile.State state)195     protected int getColor(QSTile.State state) {
196         return getIconColorForState(getContext(), state);
197     }
198 
animateGrayScale(int fromColor, int toColor, ImageView iv, final Runnable endRunnable)199     private void animateGrayScale(int fromColor, int toColor, ImageView iv,
200             final Runnable endRunnable) {
201         mColorAnimator.cancel();
202         if (mAnimationEnabled && ValueAnimator.areAnimatorsEnabled()) {
203             PropertyValuesHolder values = PropertyValuesHolder.ofInt("color", fromColor, toColor);
204             values.setEvaluator(ArgbEvaluator.getInstance());
205             mColorAnimator.setValues(values);
206             mColorAnimator.removeAllListeners();
207             mColorAnimator.addUpdateListener(animation -> {
208                 setTint(iv, (int) animation.getAnimatedValue());
209             });
210             mColorAnimator.addListener(new EndRunnableAnimatorListener(endRunnable));
211 
212             mColorAnimator.start();
213         } else {
214 
215             setTint(iv, toColor);
216             endRunnable.run();
217         }
218     }
219 
setTint(ImageView iv, int color)220     public void setTint(ImageView iv, int color) {
221         iv.setImageTintList(ColorStateList.valueOf(color));
222         mTint = color;
223     }
224 
getIconMeasureMode()225     protected int getIconMeasureMode() {
226         return MeasureSpec.EXACTLY;
227     }
228 
createIcon()229     protected View createIcon() {
230         final ImageView icon = new ImageView(mContext);
231         icon.setId(android.R.id.icon);
232         icon.setScaleType(ScaleType.FIT_CENTER);
233         return icon;
234     }
235 
exactly(int size)236     protected final int exactly(int size) {
237         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
238     }
239 
layout(View child, int left, int top)240     protected final void layout(View child, int left, int top) {
241         child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
242     }
243 
getNextIconTransactionId()244     private long getNextIconTransactionId() {
245         mHighestScheduledIconChangeTransactionId++;
246         return mHighestScheduledIconChangeTransactionId;
247     }
248 
249     /**
250      * Color to tint the tile icon based on state
251      */
getIconColorForState(Context context, QSTile.State state)252     private static int getIconColorForState(Context context, QSTile.State state) {
253         if (state.disabledByPolicy || state.state == Tile.STATE_UNAVAILABLE) {
254             return Utils.getColorAttrDefaultColor(context, R.attr.outline);
255         } else if (state.state == Tile.STATE_INACTIVE) {
256             return Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant);
257         } else if (state.state == Tile.STATE_ACTIVE) {
258             return Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive);
259         } else {
260             Log.e("QSIconView", "Invalid state " + state);
261             return 0;
262         }
263     }
264 
265     private static class EndRunnableAnimatorListener extends AnimatorListenerAdapter {
266         private Runnable mRunnable;
267 
EndRunnableAnimatorListener(Runnable endRunnable)268         EndRunnableAnimatorListener(Runnable endRunnable) {
269             super();
270             mRunnable = endRunnable;
271         }
272 
273         @Override
onAnimationCancel(Animator animation)274         public void onAnimationCancel(Animator animation) {
275             super.onAnimationCancel(animation);
276             mRunnable.run();
277         }
278 
279         @Override
onAnimationEnd(Animator animation)280         public void onAnimationEnd(Animator animation) {
281             super.onAnimationEnd(animation);
282             mRunnable.run();
283         }
284     }
285 }
286