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