1 /* 2 * Copyright (C) 2007 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.content.res; 18 19 import android.graphics.Color; 20 21 import com.android.internal.util.ArrayUtils; 22 import com.android.internal.util.GrowingArrayUtils; 23 24 import org.xmlpull.v1.XmlPullParser; 25 import org.xmlpull.v1.XmlPullParserException; 26 27 import android.util.AttributeSet; 28 import android.util.MathUtils; 29 import android.util.SparseArray; 30 import android.util.StateSet; 31 import android.util.Xml; 32 import android.os.Parcel; 33 import android.os.Parcelable; 34 35 import java.io.IOException; 36 import java.lang.ref.WeakReference; 37 import java.util.Arrays; 38 39 /** 40 * 41 * Lets you map {@link android.view.View} state sets to colors. 42 * 43 * {@link android.content.res.ColorStateList}s are created from XML resource files defined in the 44 * "color" subdirectory directory of an application's resource directory. The XML file contains 45 * a single "selector" element with a number of "item" elements inside. For example: 46 * 47 * <pre> 48 * <selector xmlns:android="http://schemas.android.com/apk/res/android"> 49 * <item android:state_focused="true" android:color="@color/testcolor1"/> 50 * <item android:state_pressed="true" android:state_enabled="false" android:color="@color/testcolor2" /> 51 * <item android:state_enabled="false" android:color="@color/testcolor3" /> 52 * <item android:color="@color/testcolor5"/> 53 * </selector> 54 * </pre> 55 * 56 * This defines a set of state spec / color pairs where each state spec specifies a set of 57 * states that a view must either be in or not be in and the color specifies the color associated 58 * with that spec. The list of state specs will be processed in order of the items in the XML file. 59 * An item with no state spec is considered to match any set of states and is generally useful as 60 * a final item to be used as a default. Note that if you have such an item before any other items 61 * in the list then any subsequent items will end up being ignored. 62 * <p>For more information, see the guide to <a 63 * href="{@docRoot}guide/topics/resources/color-list-resource.html">Color State 64 * List Resource</a>.</p> 65 */ 66 public class ColorStateList implements Parcelable { 67 private int[][] mStateSpecs; // must be parallel to mColors 68 private int[] mColors; // must be parallel to mStateSpecs 69 private int mDefaultColor = 0xffff0000; 70 71 private static final int[][] EMPTY = new int[][] { new int[0] }; 72 private static final SparseArray<WeakReference<ColorStateList>> sCache = 73 new SparseArray<WeakReference<ColorStateList>>(); 74 ColorStateList()75 private ColorStateList() { } 76 77 /** 78 * Creates a ColorStateList that returns the specified mapping from 79 * states to colors. 80 */ ColorStateList(int[][] states, int[] colors)81 public ColorStateList(int[][] states, int[] colors) { 82 mStateSpecs = states; 83 mColors = colors; 84 85 if (states.length > 0) { 86 mDefaultColor = colors[0]; 87 88 for (int i = 0; i < states.length; i++) { 89 if (states[i].length == 0) { 90 mDefaultColor = colors[i]; 91 } 92 } 93 } 94 } 95 96 /** 97 * Creates or retrieves a ColorStateList that always returns a single color. 98 */ valueOf(int color)99 public static ColorStateList valueOf(int color) { 100 // TODO: should we collect these eventually? 101 synchronized (sCache) { 102 final WeakReference<ColorStateList> ref = sCache.get(color); 103 104 ColorStateList csl = ref != null ? ref.get() : null; 105 if (csl != null) { 106 return csl; 107 } 108 109 csl = new ColorStateList(EMPTY, new int[] { color }); 110 sCache.put(color, new WeakReference<ColorStateList>(csl)); 111 return csl; 112 } 113 } 114 115 /** 116 * Create a ColorStateList from an XML document, given a set of {@link Resources}. 117 */ createFromXml(Resources r, XmlPullParser parser)118 public static ColorStateList createFromXml(Resources r, XmlPullParser parser) 119 throws XmlPullParserException, IOException { 120 final AttributeSet attrs = Xml.asAttributeSet(parser); 121 122 int type; 123 while ((type=parser.next()) != XmlPullParser.START_TAG 124 && type != XmlPullParser.END_DOCUMENT) { 125 } 126 127 if (type != XmlPullParser.START_TAG) { 128 throw new XmlPullParserException("No start tag found"); 129 } 130 131 return createFromXmlInner(r, parser, attrs); 132 } 133 134 /** 135 * Create from inside an XML document. Called on a parser positioned at a 136 * tag in an XML document, tries to create a ColorStateList from that tag. 137 * 138 * @throws XmlPullParserException if the current tag is not <selector> 139 * @return A color state list for the current tag. 140 */ createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs)141 private static ColorStateList createFromXmlInner(Resources r, XmlPullParser parser, 142 AttributeSet attrs) throws XmlPullParserException, IOException { 143 final ColorStateList colorStateList; 144 final String name = parser.getName(); 145 if (name.equals("selector")) { 146 colorStateList = new ColorStateList(); 147 } else { 148 throw new XmlPullParserException( 149 parser.getPositionDescription() + ": invalid drawable tag " + name); 150 } 151 152 colorStateList.inflate(r, parser, attrs); 153 return colorStateList; 154 } 155 156 /** 157 * Creates a new ColorStateList that has the same states and 158 * colors as this one but where each color has the specified alpha value 159 * (0-255). 160 */ withAlpha(int alpha)161 public ColorStateList withAlpha(int alpha) { 162 final int[] colors = new int[mColors.length]; 163 final int len = colors.length; 164 for (int i = 0; i < len; i++) { 165 colors[i] = (mColors[i] & 0xFFFFFF) | (alpha << 24); 166 } 167 168 return new ColorStateList(mStateSpecs, colors); 169 } 170 171 /** 172 * Fill in this object based on the contents of an XML "selector" element. 173 */ inflate(Resources r, XmlPullParser parser, AttributeSet attrs)174 private void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) 175 throws XmlPullParserException, IOException { 176 int type; 177 178 final int innerDepth = parser.getDepth()+1; 179 int depth; 180 181 int[][] stateSpecList = ArrayUtils.newUnpaddedArray(int[].class, 20); 182 int[] colorList = new int[stateSpecList.length]; 183 int listSize = 0; 184 185 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 186 && ((depth=parser.getDepth()) >= innerDepth 187 || type != XmlPullParser.END_TAG)) { 188 if (type != XmlPullParser.START_TAG) { 189 continue; 190 } 191 192 if (depth > innerDepth || !parser.getName().equals("item")) { 193 continue; 194 } 195 196 int alphaRes = 0; 197 float alpha = 1.0f; 198 int colorRes = 0; 199 int color = 0xffff0000; 200 boolean haveColor = false; 201 202 int i; 203 int j = 0; 204 final int numAttrs = attrs.getAttributeCount(); 205 int[] stateSpec = new int[numAttrs]; 206 for (i = 0; i < numAttrs; i++) { 207 final int stateResId = attrs.getAttributeNameResource(i); 208 if (stateResId == 0) break; 209 if (stateResId == com.android.internal.R.attr.alpha) { 210 alphaRes = attrs.getAttributeResourceValue(i, 0); 211 if (alphaRes == 0) { 212 alpha = attrs.getAttributeFloatValue(i, 1.0f); 213 } 214 } else if (stateResId == com.android.internal.R.attr.color) { 215 colorRes = attrs.getAttributeResourceValue(i, 0); 216 if (colorRes == 0) { 217 color = attrs.getAttributeIntValue(i, color); 218 haveColor = true; 219 } 220 } else { 221 stateSpec[j++] = attrs.getAttributeBooleanValue(i, false) 222 ? stateResId : -stateResId; 223 } 224 } 225 stateSpec = StateSet.trimStateSet(stateSpec, j); 226 227 if (colorRes != 0) { 228 color = r.getColor(colorRes); 229 } else if (!haveColor) { 230 throw new XmlPullParserException( 231 parser.getPositionDescription() 232 + ": <item> tag requires a 'android:color' attribute."); 233 } 234 235 if (alphaRes != 0) { 236 alpha = r.getFloat(alphaRes); 237 } 238 239 // Apply alpha modulation. 240 final int alphaMod = MathUtils.constrain((int) (Color.alpha(color) * alpha), 0, 255); 241 color = (color & 0xFFFFFF) | (alphaMod << 24); 242 243 if (listSize == 0 || stateSpec.length == 0) { 244 mDefaultColor = color; 245 } 246 247 colorList = GrowingArrayUtils.append(colorList, listSize, color); 248 stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec); 249 listSize++; 250 } 251 252 mColors = new int[listSize]; 253 mStateSpecs = new int[listSize][]; 254 System.arraycopy(colorList, 0, mColors, 0, listSize); 255 System.arraycopy(stateSpecList, 0, mStateSpecs, 0, listSize); 256 } 257 258 /** 259 * Indicates whether this color state list contains more than one state spec 260 * and will change color based on state. 261 * 262 * @return True if this color state list changes color based on state, false 263 * otherwise. 264 * @see #getColorForState(int[], int) 265 */ isStateful()266 public boolean isStateful() { 267 return mStateSpecs.length > 1; 268 } 269 270 /** 271 * Indicates whether this color state list is opaque, which means that every 272 * color returned from {@link #getColorForState(int[], int)} has an alpha 273 * value of 255. 274 * 275 * @return True if this color state list is opaque. 276 */ isOpaque()277 public boolean isOpaque() { 278 final int n = mColors.length; 279 for (int i = 0; i < n; i++) { 280 if (Color.alpha(mColors[i]) != 0xFF) { 281 return false; 282 } 283 } 284 return true; 285 } 286 287 /** 288 * Return the color associated with the given set of {@link android.view.View} states. 289 * 290 * @param stateSet an array of {@link android.view.View} states 291 * @param defaultColor the color to return if there's not state spec in this 292 * {@link ColorStateList} that matches the stateSet. 293 * 294 * @return the color associated with that set of states in this {@link ColorStateList}. 295 */ getColorForState(int[] stateSet, int defaultColor)296 public int getColorForState(int[] stateSet, int defaultColor) { 297 final int setLength = mStateSpecs.length; 298 for (int i = 0; i < setLength; i++) { 299 int[] stateSpec = mStateSpecs[i]; 300 if (StateSet.stateSetMatches(stateSpec, stateSet)) { 301 return mColors[i]; 302 } 303 } 304 return defaultColor; 305 } 306 307 /** 308 * Return the default color in this {@link ColorStateList}. 309 * 310 * @return the default color in this {@link ColorStateList}. 311 */ getDefaultColor()312 public int getDefaultColor() { 313 return mDefaultColor; 314 } 315 316 /** 317 * Return the states in this {@link ColorStateList}. 318 * @return the states in this {@link ColorStateList} 319 * @hide 320 */ getStates()321 public int[][] getStates() { 322 return mStateSpecs; 323 } 324 325 /** 326 * Return the colors in this {@link ColorStateList}. 327 * @return the colors in this {@link ColorStateList} 328 * @hide 329 */ getColors()330 public int[] getColors() { 331 return mColors; 332 } 333 334 /** 335 * If the color state list does not already have an entry matching the 336 * specified state, prepends a state set and color pair to a color state 337 * list. 338 * <p> 339 * This is a workaround used in TimePicker and DatePicker until we can 340 * add support for theme attributes in ColorStateList. 341 * 342 * @param colorStateList the source color state list 343 * @param state the state to prepend 344 * @param color the color to use for the given state 345 * @return a new color state list, or the source color state list if there 346 * was already a matching state set 347 * 348 * @hide Remove when we can support theme attributes. 349 */ addFirstIfMissing( ColorStateList colorStateList, int state, int color)350 public static ColorStateList addFirstIfMissing( 351 ColorStateList colorStateList, int state, int color) { 352 final int[][] inputStates = colorStateList.getStates(); 353 for (int i = 0; i < inputStates.length; i++) { 354 final int[] inputState = inputStates[i]; 355 for (int j = 0; j < inputState.length; j++) { 356 if (inputState[j] == state) { 357 return colorStateList; 358 } 359 } 360 } 361 362 final int[][] outputStates = new int[inputStates.length + 1][]; 363 System.arraycopy(inputStates, 0, outputStates, 1, inputStates.length); 364 outputStates[0] = new int[] { state }; 365 366 final int[] inputColors = colorStateList.getColors(); 367 final int[] outputColors = new int[inputColors.length + 1]; 368 System.arraycopy(inputColors, 0, outputColors, 1, inputColors.length); 369 outputColors[0] = color; 370 371 return new ColorStateList(outputStates, outputColors); 372 } 373 374 @Override toString()375 public String toString() { 376 return "ColorStateList{" + 377 "mStateSpecs=" + Arrays.deepToString(mStateSpecs) + 378 "mColors=" + Arrays.toString(mColors) + 379 "mDefaultColor=" + mDefaultColor + '}'; 380 } 381 382 @Override describeContents()383 public int describeContents() { 384 return 0; 385 } 386 387 @Override writeToParcel(Parcel dest, int flags)388 public void writeToParcel(Parcel dest, int flags) { 389 final int N = mStateSpecs.length; 390 dest.writeInt(N); 391 for (int i = 0; i < N; i++) { 392 dest.writeIntArray(mStateSpecs[i]); 393 } 394 dest.writeIntArray(mColors); 395 } 396 397 public static final Parcelable.Creator<ColorStateList> CREATOR = 398 new Parcelable.Creator<ColorStateList>() { 399 @Override 400 public ColorStateList[] newArray(int size) { 401 return new ColorStateList[size]; 402 } 403 404 @Override 405 public ColorStateList createFromParcel(Parcel source) { 406 final int N = source.readInt(); 407 final int[][] stateSpecs = new int[N][]; 408 for (int i = 0; i < N; i++) { 409 stateSpecs[i] = source.createIntArray(); 410 } 411 final int[] colors = source.createIntArray(); 412 return new ColorStateList(stateSpecs, colors); 413 } 414 }; 415 } 416