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