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