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