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.qs.tileimpl.QSTileImpl.getColorForState;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.content.res.ColorStateList;
24 import android.content.res.Resources;
25 import android.graphics.Color;
26 import android.graphics.drawable.Animatable2;
27 import android.graphics.drawable.Animatable2.AnimationCallback;
28 import android.graphics.drawable.Drawable;
29 import android.view.View;
30 import android.widget.ImageView;
31 import android.widget.ImageView.ScaleType;
32 
33 import com.android.systemui.R;
34 import com.android.systemui.plugins.qs.QSIconView;
35 import com.android.systemui.plugins.qs.QSTile;
36 import com.android.systemui.plugins.qs.QSTile.State;
37 import com.android.systemui.qs.AlphaControlledSignalTileView.AlphaControlledSlashImageView;
38 
39 import java.util.Objects;
40 
41 public class QSIconViewImpl extends QSIconView {
42 
43     public static final long QS_ANIM_LENGTH = 350;
44 
45     protected final View mIcon;
46     protected final int mIconSizePx;
47     private boolean mAnimationEnabled = true;
48     private int mState = -1;
49     private int mTint;
50     private QSTile.Icon mLastIcon;
51 
QSIconViewImpl(Context context)52     public QSIconViewImpl(Context context) {
53         super(context);
54 
55         final Resources res = context.getResources();
56         mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_tile_icon_size);
57 
58         mIcon = createIcon();
59         addView(mIcon);
60     }
61 
disableAnimation()62     public void disableAnimation() {
63         mAnimationEnabled = false;
64     }
65 
getIconView()66     public View getIconView() {
67         return mIcon;
68     }
69 
70     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)71     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
72         final int w = MeasureSpec.getSize(widthMeasureSpec);
73         final int iconSpec = exactly(mIconSizePx);
74         mIcon.measure(MeasureSpec.makeMeasureSpec(w, getIconMeasureMode()), iconSpec);
75         setMeasuredDimension(w, mIcon.getMeasuredHeight());
76     }
77 
78     @Override
toString()79     public String toString() {
80         final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('[');
81         sb.append("state=" + mState);
82         sb.append(", tint=" + mTint);
83         if (mLastIcon != null) sb.append(", lastIcon=" + mLastIcon.toString());
84         sb.append("]");
85         return sb.toString();
86     }
87 
88     @Override
onLayout(boolean changed, int l, int t, int r, int b)89     protected void onLayout(boolean changed, int l, int t, int r, int b) {
90         final int w = getMeasuredWidth();
91         int top = 0;
92         final int iconLeft = (w - mIcon.getMeasuredWidth()) / 2;
93         layout(mIcon, iconLeft, top);
94     }
95 
setIcon(State state, boolean allowAnimations)96     public void setIcon(State state, boolean allowAnimations) {
97         setIcon((ImageView) mIcon, state, allowAnimations);
98     }
99 
updateIcon(ImageView iv, State state, boolean allowAnimations)100     protected void updateIcon(ImageView iv, State state, boolean allowAnimations) {
101         final QSTile.Icon icon = state.iconSupplier != null ? state.iconSupplier.get() : state.icon;
102         if (!Objects.equals(icon, iv.getTag(R.id.qs_icon_tag))
103                 || !Objects.equals(state.slash, iv.getTag(R.id.qs_slash_tag))) {
104             boolean shouldAnimate = allowAnimations && shouldAnimate(iv);
105             mLastIcon = icon;
106             Drawable d = icon != null
107                     ? shouldAnimate ? icon.getDrawable(mContext)
108                     : icon.getInvisibleDrawable(mContext) : null;
109             int padding = icon != null ? icon.getPadding() : 0;
110             if (d != null) {
111                 d.setAutoMirrored(false);
112                 d.setLayoutDirection(getLayoutDirection());
113             }
114 
115             if (iv instanceof SlashImageView) {
116                 ((SlashImageView) iv).setAnimationEnabled(shouldAnimate);
117                 ((SlashImageView) iv).setState(null, d);
118             } else {
119                 iv.setImageDrawable(d);
120             }
121 
122             iv.setTag(R.id.qs_icon_tag, icon);
123             iv.setTag(R.id.qs_slash_tag, state.slash);
124             iv.setPadding(0, padding, 0, padding);
125             if (d instanceof Animatable2) {
126                 Animatable2 a = (Animatable2) d;
127                 a.start();
128                 if (state.isTransient) {
129                     a.registerAnimationCallback(new AnimationCallback() {
130                         @Override
131                         public void onAnimationEnd(Drawable drawable) {
132                             a.start();
133                         }
134                     });
135                 }
136             }
137         }
138     }
139 
shouldAnimate(ImageView iv)140     private boolean shouldAnimate(ImageView iv) {
141         return mAnimationEnabled && iv.isShown() && iv.getDrawable() != null;
142     }
143 
setIcon(ImageView iv, QSTile.State state, boolean allowAnimations)144     protected void setIcon(ImageView iv, QSTile.State state, boolean allowAnimations) {
145         if (state.disabledByPolicy) {
146             iv.setColorFilter(getContext().getColor(R.color.qs_tile_disabled_color));
147         } else {
148             iv.clearColorFilter();
149         }
150         if (state.state != mState) {
151             int color = getColor(state.state);
152             mState = state.state;
153             if (mTint != 0 && allowAnimations && shouldAnimate(iv)) {
154                 animateGrayScale(mTint, color, iv, () -> updateIcon(iv, state, allowAnimations));
155                 mTint = color;
156             } else {
157                 if (iv instanceof AlphaControlledSlashImageView) {
158                     ((AlphaControlledSlashImageView)iv)
159                             .setFinalImageTintList(ColorStateList.valueOf(color));
160                 } else {
161                     setTint(iv, color);
162                 }
163                 mTint = color;
164                 updateIcon(iv, state, allowAnimations);
165             }
166         } else {
167             updateIcon(iv, state, allowAnimations);
168         }
169     }
170 
getColor(int state)171     protected int getColor(int state) {
172         return getColorForState(getContext(), state);
173     }
174 
animateGrayScale(int fromColor, int toColor, ImageView iv, final Runnable endRunnable)175     private void animateGrayScale(int fromColor, int toColor, ImageView iv,
176         final Runnable endRunnable) {
177         if (iv instanceof AlphaControlledSlashImageView) {
178             ((AlphaControlledSlashImageView)iv)
179                     .setFinalImageTintList(ColorStateList.valueOf(toColor));
180         }
181         if (mAnimationEnabled && ValueAnimator.areAnimatorsEnabled()) {
182             final float fromAlpha = Color.alpha(fromColor);
183             final float toAlpha = Color.alpha(toColor);
184             final float fromChannel = Color.red(fromColor);
185             final float toChannel = Color.red(toColor);
186 
187             ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
188             anim.setDuration(QS_ANIM_LENGTH);
189             anim.addUpdateListener(animation -> {
190                 float fraction = animation.getAnimatedFraction();
191                 int alpha = (int) (fromAlpha + (toAlpha - fromAlpha) * fraction);
192                 int channel = (int) (fromChannel + (toChannel - fromChannel) * fraction);
193 
194                 setTint(iv, Color.argb(alpha, channel, channel, channel));
195             });
196             anim.addListener(new AnimatorListenerAdapter() {
197                 @Override
198                 public void onAnimationEnd(Animator animation) {
199                     endRunnable.run();
200                 }
201             });
202             anim.start();
203         } else {
204             setTint(iv, toColor);
205             endRunnable.run();
206         }
207     }
208 
setTint(ImageView iv, int color)209     public static void setTint(ImageView iv, int color) {
210         iv.setImageTintList(ColorStateList.valueOf(color));
211     }
212 
213 
getIconMeasureMode()214     protected int getIconMeasureMode() {
215         return MeasureSpec.EXACTLY;
216     }
217 
createIcon()218     protected View createIcon() {
219         final ImageView icon = new SlashImageView(mContext);
220         icon.setId(android.R.id.icon);
221         icon.setScaleType(ScaleType.FIT_CENTER);
222         return icon;
223     }
224 
exactly(int size)225     protected final int exactly(int size) {
226         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
227     }
228 
layout(View child, int left, int top)229     protected final void layout(View child, int left, int top) {
230         child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
231     }
232 }
233