1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.graphics.drawable; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.res.Resources; 23 import android.content.res.Resources.Theme; 24 import android.content.res.TypedArray; 25 import android.graphics.Canvas; 26 import android.graphics.PixelFormat; 27 import android.graphics.Rect; 28 import android.os.Build; 29 import android.util.AttributeSet; 30 import android.util.TypedValue; 31 import android.view.Gravity; 32 33 import com.android.internal.R; 34 35 import org.xmlpull.v1.XmlPullParser; 36 import org.xmlpull.v1.XmlPullParserException; 37 38 import java.io.IOException; 39 40 /** 41 * A Drawable that changes the size of another Drawable based on its current 42 * level value. You can control how much the child Drawable changes in width 43 * and height based on the level, as well as a gravity to control where it is 44 * placed in its overall container. Most often used to implement things like 45 * progress bars. 46 * <p> 47 * The default level may be specified from XML using the 48 * {@link android.R.styleable#ScaleDrawable_level android:level} property. When 49 * this property is not specified, the default level is 0, which corresponds to 50 * zero height and/or width depending on the values specified for 51 * {@code android.R.styleable#ScaleDrawable_scaleWidth scaleWidth} and 52 * {@code android.R.styleable#ScaleDrawable_scaleHeight scaleHeight}. At run 53 * time, the level may be set via {@link #setLevel(int)}. 54 * <p> 55 * A scale drawable may be defined in an XML file with the {@code <scale>} 56 * element. For more information, see the guide to 57 * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable 58 * Resources</a>. 59 * 60 * @attr ref android.R.styleable#ScaleDrawable_scaleWidth 61 * @attr ref android.R.styleable#ScaleDrawable_scaleHeight 62 * @attr ref android.R.styleable#ScaleDrawable_scaleGravity 63 * @attr ref android.R.styleable#ScaleDrawable_drawable 64 * @attr ref android.R.styleable#ScaleDrawable_level 65 */ 66 public class ScaleDrawable extends DrawableWrapper { 67 private static final int MAX_LEVEL = 10000; 68 69 private final Rect mTmpRect = new Rect(); 70 71 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 72 private ScaleState mState; 73 ScaleDrawable()74 ScaleDrawable() { 75 this(new ScaleState(null, null), null); 76 } 77 78 /** 79 * Creates a new scale drawable with the specified gravity and scale 80 * properties. 81 * 82 * @param drawable the drawable to scale 83 * @param gravity gravity constant (see {@link Gravity} used to position 84 * the scaled drawable within the parent container 85 * @param scaleWidth width scaling factor [0...1] to use then the level is 86 * at the maximum value, or -1 to not scale width 87 * @param scaleHeight height scaling factor [0...1] to use then the level 88 * is at the maximum value, or -1 to not scale height 89 */ ScaleDrawable(Drawable drawable, int gravity, float scaleWidth, float scaleHeight)90 public ScaleDrawable(Drawable drawable, int gravity, float scaleWidth, float scaleHeight) { 91 this(new ScaleState(null, null), null); 92 93 mState.mGravity = gravity; 94 mState.mScaleWidth = scaleWidth; 95 mState.mScaleHeight = scaleHeight; 96 97 setDrawable(drawable); 98 } 99 100 @Override inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)101 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 102 @NonNull AttributeSet attrs, @Nullable Theme theme) 103 throws XmlPullParserException, IOException { 104 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ScaleDrawable); 105 106 // Inflation will advance the XmlPullParser and AttributeSet. 107 super.inflate(r, parser, attrs, theme); 108 109 updateStateFromTypedArray(a); 110 verifyRequiredAttributes(a); 111 a.recycle(); 112 113 updateLocalState(); 114 } 115 116 @Override applyTheme(@onNull Theme t)117 public void applyTheme(@NonNull Theme t) { 118 super.applyTheme(t); 119 120 final ScaleState state = mState; 121 if (state == null) { 122 return; 123 } 124 125 if (state.mThemeAttrs != null) { 126 final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ScaleDrawable); 127 try { 128 updateStateFromTypedArray(a); 129 verifyRequiredAttributes(a); 130 } catch (XmlPullParserException e) { 131 rethrowAsRuntimeException(e); 132 } finally { 133 a.recycle(); 134 } 135 } 136 137 updateLocalState(); 138 } 139 verifyRequiredAttributes(@onNull TypedArray a)140 private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException { 141 // If we're not waiting on a theme, verify required attributes. 142 if (getDrawable() == null && (mState.mThemeAttrs == null 143 || mState.mThemeAttrs[R.styleable.ScaleDrawable_drawable] == 0)) { 144 throw new XmlPullParserException(a.getPositionDescription() 145 + ": <scale> tag requires a 'drawable' attribute or " 146 + "child tag defining a drawable"); 147 } 148 } 149 updateStateFromTypedArray(@onNull TypedArray a)150 private void updateStateFromTypedArray(@NonNull TypedArray a) { 151 final ScaleState state = mState; 152 if (state == null) { 153 return; 154 } 155 156 // Account for any configuration changes. 157 state.mChangingConfigurations |= a.getChangingConfigurations(); 158 159 // Extract the theme attributes, if any. 160 state.mThemeAttrs = a.extractThemeAttrs(); 161 162 state.mScaleWidth = getPercent(a, 163 R.styleable.ScaleDrawable_scaleWidth, state.mScaleWidth); 164 state.mScaleHeight = getPercent(a, 165 R.styleable.ScaleDrawable_scaleHeight, state.mScaleHeight); 166 state.mGravity = a.getInt( 167 R.styleable.ScaleDrawable_scaleGravity, state.mGravity); 168 state.mUseIntrinsicSizeAsMin = a.getBoolean( 169 R.styleable.ScaleDrawable_useIntrinsicSizeAsMinimum, state.mUseIntrinsicSizeAsMin); 170 state.mInitialLevel = a.getInt( 171 R.styleable.ScaleDrawable_level, state.mInitialLevel); 172 } 173 getPercent(TypedArray a, int index, float defaultValue)174 private static float getPercent(TypedArray a, int index, float defaultValue) { 175 final int type = a.getType(index); 176 if (type == TypedValue.TYPE_FRACTION || type == TypedValue.TYPE_NULL) { 177 return a.getFraction(index, 1, 1, defaultValue); 178 } 179 180 // Coerce to float. 181 final String s = a.getString(index); 182 if (s != null) { 183 if (s.endsWith("%")) { 184 final String f = s.substring(0, s.length() - 1); 185 return Float.parseFloat(f) / 100.0f; 186 } 187 } 188 189 return defaultValue; 190 } 191 192 @Override draw(Canvas canvas)193 public void draw(Canvas canvas) { 194 final Drawable d = getDrawable(); 195 if (d != null && d.getLevel() != 0) { 196 d.draw(canvas); 197 } 198 } 199 200 @Override getOpacity()201 public int getOpacity() { 202 final Drawable d = getDrawable(); 203 if (d.getLevel() == 0) { 204 return PixelFormat.TRANSPARENT; 205 } 206 207 final int opacity = d.getOpacity(); 208 if (opacity == PixelFormat.OPAQUE && d.getLevel() < MAX_LEVEL) { 209 return PixelFormat.TRANSLUCENT; 210 } 211 212 return opacity; 213 } 214 215 @Override onLevelChange(int level)216 protected boolean onLevelChange(int level) { 217 super.onLevelChange(level); 218 onBoundsChange(getBounds()); 219 invalidateSelf(); 220 return true; 221 } 222 223 @Override onBoundsChange(Rect bounds)224 protected void onBoundsChange(Rect bounds) { 225 final Drawable d = getDrawable(); 226 final Rect r = mTmpRect; 227 final boolean min = mState.mUseIntrinsicSizeAsMin; 228 final int level = getLevel(); 229 230 int w = bounds.width(); 231 if (mState.mScaleWidth > 0) { 232 final int iw = min ? d.getIntrinsicWidth() : 0; 233 w -= (int) ((w - iw) * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL); 234 } 235 236 int h = bounds.height(); 237 if (mState.mScaleHeight > 0) { 238 final int ih = min ? d.getIntrinsicHeight() : 0; 239 h -= (int) ((h - ih) * (MAX_LEVEL - level) * mState.mScaleHeight / MAX_LEVEL); 240 } 241 242 final int layoutDirection = getLayoutDirection(); 243 Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection); 244 245 if (w > 0 && h > 0) { 246 d.setBounds(r.left, r.top, r.right, r.bottom); 247 } 248 } 249 250 @Override mutateConstantState()251 DrawableWrapperState mutateConstantState() { 252 mState = new ScaleState(mState, null); 253 return mState; 254 } 255 256 static final class ScaleState extends DrawableWrapper.DrawableWrapperState { 257 /** Constant used to disable scaling for a particular dimension. */ 258 private static final float DO_NOT_SCALE = -1.0f; 259 260 private int[] mThemeAttrs; 261 262 float mScaleWidth = DO_NOT_SCALE; 263 float mScaleHeight = DO_NOT_SCALE; 264 int mGravity = Gravity.LEFT; 265 boolean mUseIntrinsicSizeAsMin = false; 266 int mInitialLevel = 0; 267 ScaleState(ScaleState orig, Resources res)268 ScaleState(ScaleState orig, Resources res) { 269 super(orig, res); 270 271 if (orig != null) { 272 mScaleWidth = orig.mScaleWidth; 273 mScaleHeight = orig.mScaleHeight; 274 mGravity = orig.mGravity; 275 mUseIntrinsicSizeAsMin = orig.mUseIntrinsicSizeAsMin; 276 mInitialLevel = orig.mInitialLevel; 277 } 278 } 279 280 @Override newDrawable(Resources res)281 public Drawable newDrawable(Resources res) { 282 return new ScaleDrawable(this, res); 283 } 284 } 285 286 /** 287 * Creates a new ScaleDrawable based on the specified constant state. 288 * <p> 289 * The resulting drawable is guaranteed to have a new constant state. 290 * 291 * @param state constant state from which the drawable inherits 292 */ ScaleDrawable(ScaleState state, Resources res)293 private ScaleDrawable(ScaleState state, Resources res) { 294 super(state, res); 295 296 mState = state; 297 298 updateLocalState(); 299 } 300 updateLocalState()301 private void updateLocalState() { 302 setLevel(mState.mInitialLevel); 303 } 304 } 305 306