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