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