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