前言-关于StateListDrawable
Android里面经常会在开发中在res/drawable目录下, 使用shape或者selector编写XML文件, 来定制一些View的背景, 而这些XML最终会转换为StateListDrawable
*注: 并非所有的XML都是StateListDrawable*
对于StateListDrawable文档中如此描述:
允许您将多个图形图像分配给单个 Drawable,并通过字符串 ID 值替换可见项。它可以在带有 元素的 XML 文件中定义。每个状态 Drawable 都在嵌套的 元素中定义.
常见的XML
<?xml version="1.0" encoding="UTF-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_enabled="false" android:drawable="@drawable/btn_off" /> <item android:state_pressed="true" android:state_enabled="true" android:drawable="@drawable/btn_off" /> <item android:state_focused="true" android:state_enabled="true" android:drawable="@drawable/btn_on" /> <item android:state_enabled="true" android:drawable="@drawable/btn_on" /> </selector>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" > <shape> <solid android:color="#3498DB" /> <stroke android:width="1dp" android:color="#2980B9" /> <corners android:radius="0dp" /> <padding android:left="12dp" android:top="12dp" android:right="12dp" android:bottom="12dp" /> </shape> </item> <item> <shape> <solid android:color="#2980B9" /> <stroke android:width="1dp" android:color="#2980B9" /> <corners android:radius="0dp" /> <padding android:left="12dp" android:top="12dp" android:right="12dp" android:bottom="12dp" /> </shape> </item> </selector>
通过代码创建StateListDrawable1
注意注释: 注意该处的顺序,只要有一个状态与之相配,背景就会被换掉
private StateListDrawable addStateDrawable(Context context, int idNormal, int idPressed, int idFocused) { StateListDrawable sd = new StateListDrawable(); Drawable normal = idNormal == -1 ? null : context.getResources().getDrawable(idNormal) Drawable press(略);Drawable focus(略); //注意该处的顺序,只要有一个状态与之相配,背景就会被换掉 //所以不要把大范围放在前面了,如果sd.addState(new[]{},normal)放在第一个的话,就没有q 什么效果了 sd.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_focused}, focus); sd.addState(new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled}, pressed); sd.addState(new int[]{android.R.attr.state_focused}, focus); sd.addState(new int[]{android.R.attr.state_pressed}, pressed); sd.addState(new int[]{android.R.attr.state_enabled}, normal); sd.addState(new int[]{}, normal); return sd; }
动态修改背景 2
StateListDrawable mySelectorGrad = (StateListDrawable)view.getBackground(); try { Class slDraClass = StateListDrawable.class; Method getStateCountMethod = slDraClass.getDeclaredMethod("getStateCount", new Class[0]); Method getStateSetMethod = slDraClass.getDeclaredMethod("getStateSet", int.class); Method getDrawableMethod = slDraClass.getDeclaredMethod("getStateDrawable", int.class); int count = (Integer) getStateCountMethod.invoke(mySelectorGrad, new Object[]{});//对应item标签 Log.d(TAG, "state count ="+count); for(int i=0;i < count;i++) { int[] stateSet = (int[]) getStateSetMethod.invoke(mySelectorGrad, i);//对应item标签中的 android:state_xxxx if (stateSet == null || stateSet.length == 0) { Log.d(TAG, "state is null"); GradientDrawable drawable = (GradientDrawable) getDrawableMethod.invoke(mySelectorGrad, i);//这就是你要获得的Enabled为false时候的drawable drawable.setColor(Color.parseColor(checkColor)); } else { for (int j = 0; j < stateSet.length; j++) { Log.d(TAG, "state =" + stateSet[j]); Drawable drawable = (Drawable) getDrawableMethod.invoke(mySelectorGrad, i);//这就是你要获得的Enabled为false时候的drawable } } } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }
在上面的代码基础修改下, 丰富函数,满足更多的状态, 更换背景颜色:
public static void updateDrawableColor(Drawable drawable, int newColor) { updateDrawableColor(drawable, state_default, newColor); } //https://www.cnblogs.com/whycxb/p/7256109.html //改变默认无状态背景 //targetState为-1时代表修改默认状态,对应stateSet为空的状态 public static void updateDrawableColor(Drawable drawable, int targetState, int newColor) { if(drawable == null)return; if(!(drawable instanceof StateListDrawable)){ return; } StateListDrawable mySelectorGrad = (StateListDrawable) drawable; try { Method getStateCount = StateListDrawable.class.getDeclaredMethod("getStateCount"); Method getStateSet = StateListDrawable.class.getDeclaredMethod("getStateSet", int.class); Method getDrawable = StateListDrawable.class.getDeclaredMethod("getStateDrawable", int.class); Object val = getStateCount.invoke(mySelectorGrad);//对应item标签; if(val == null){ return; } int count = (Integer) val; //Logger.d("UiTools", "count=" + count); for (int i = 0; i < count; i++) { int[] stateSet = (int[]) getStateSet.invoke(mySelectorGrad, i);//对应item标签中的 android:state_xxxx boolean stateEmpty = stateSet == null || stateSet.length == 0; //一个item 存在 0个或多个 state_XXXX if (targetState == -1){//改变默认状态 if(stateEmpty) { //默认无任何状态的<item> GradientDrawable gd = (GradientDrawable) getDrawable.invoke(mySelectorGrad, i);//这就是你要获得的Enabled为false时候的drawable if(gd != null)gd.setColor(newColor); } }else if(!stateEmpty){//改定指定状态 for(int s : stateSet){ if(s == targetState){ Drawable d = (Drawable)getDrawable.invoke(mySelectorGrad, i); if(d instanceof GradientDrawable) { GradientDrawable gd = (GradientDrawable)d; gd.setColor(newColor); }else{ //Logger.w("UiTools", "updateDrawableColor: not GradientDrawable"); } } } } } } catch (Exception e) { e.printStackTrace(); } }
从View的background跟踪StateListDrawable的由来
控件的背景初始化过程如下:
frameworks/base/core/java/android/view/View.java
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { this(context); mSourceLayoutId = Resources.getAttributeSetSourceResId(attrs); final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); //... final int N = a.getIndexCount(); for (int i = 0; i < N; i++) { int attr = a.getIndex(i); switch (attr) { case com.android.internal.R.styleable.View_background: background = a.getDrawable(attr); break; //... } if (background != null) { setBackground(background); } }
frameworks/base/core/java/android/content/res/TypedArray.java
@Nullable public Drawable getDrawable(@StyleableRes int index) { return getDrawableForDensity(index, 0); } /** * Version of {@link #getDrawable(int)} that accepts an override density. * @hide */ @Nullable public Drawable getDrawableForDensity(@StyleableRes int index, int density) { if (mRecycled) { throw new RuntimeException("Cannot make calls to a recycled instance!"); } final TypedValue value = mValue; if (getValueAt(index * STYLE_NUM_ENTRIES, value)) { if (value.type == TypedValue.TYPE_ATTRIBUTE) { throw new UnsupportedOperationException( "Failed to resolve attribute at index " + index + ": " + value); } if (density > 0) { // If the density is overridden, the value in the TypedArray will not reflect this. // Do a separate lookup of the resourceId with the density override. mResources.getValueForDensity(value.resourceId, density, value, true); } return mResources.loadDrawable(value, value.resourceId, density, mTheme); } return null; }
frameworks/base/core/java/android/content/res/Resources.java
@NonNull @UnsupportedAppUsage Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme) throws NotFoundException { return mResourcesImpl.loadDrawable(this, value, id, density, theme); }
frameworks/base/
core/java/android/content/res/ResourcesImpl.java
@Nullable Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id, int density, @Nullable Resources.Theme theme) throws NotFoundException { // If the drawable's XML lives in our current density qualifier, // it's okay to use a scaled version from the cache. Otherwise, we // need to actually load the drawable from XML. final boolean useCache = density == 0 || value.density == mMetrics.densityDpi; // Pretend the requested density is actually the display density. If // the drawable returned is not the requested density, then force it // to be scaled later by dividing its density by the ratio of // requested density to actual device density. Drawables that have // undefined density or no density don't need to be handled here. if (density > 0 && value.density > 0 && value.density != TypedValue.DENSITY_NONE) { if (value.density == density) { value.density = mMetrics.densityDpi; } else { value.density = (value.density * mMetrics.densityDpi) / density; } } try { if (TRACE_FOR_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) { Log.d("PreloadDrawable", name); } } } final boolean isColorDrawable; final DrawableCache caches; final long key; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { isColorDrawable = true; caches = mColorDrawableCache; key = value.data; } else { isColorDrawable = false; caches = mDrawableCache; key = (((long) value.assetCookie) << 32) | value.data; } // First, check whether we have a cached version of this drawable // that was inflated against the specified theme. Skip the cache if // we're currently preloading or we're not using the cache. if (!mPreloading && useCache) { final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme); if (cachedDrawable != null) { cachedDrawable.setChangingConfigurations(value.changingConfigurations); return cachedDrawable; } } // Next, check preloaded drawables. Preloaded drawables may contain // unresolved theme attributes. final Drawable.ConstantState cs; if (isColorDrawable) { cs = sPreloadedColorDrawables.get(key); } else { cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); } Drawable dr; boolean needsNewDrawableAfterCache = false; if (cs != null) { if (TRACE_FOR_DETAILED_PRELOAD) { // Log only framework resources if (((id >>> 24) == 0x1) && (android.os.Process.myUid() != 0)) { final String name = getResourceName(id); if (name != null) { Log.d(TAG_PRELOAD, "Hit preloaded FW drawable #" + Integer.toHexString(id) + " " + name); } } } dr = cs.newDrawable(wrapper); } else if (isColorDrawable) { dr = new ColorDrawable(value.data); } else { dr = loadDrawableForCookie(wrapper, value, id, density); } // DrawableContainer' constant state has drawables instances. In order to leave the // constant state intact in the cache, we need to create a new DrawableContainer after // added to cache. if (dr instanceof DrawableContainer) { needsNewDrawableAfterCache = true; } // Determine if the drawable has unresolved theme attributes. If it // does, we'll need to apply a theme and store it in a theme-specific // cache. final boolean canApplyTheme = dr != null && dr.canApplyTheme(); if (canApplyTheme && theme != null) { dr = dr.mutate(); dr.applyTheme(theme); dr.clearMutated(); } // If we were able to obtain a drawable, store it in the appropriate // cache: preload, not themed, null theme, or theme-specific. Don't // pollute the cache with drawables loaded from a foreign density. if (dr != null) { dr.setChangingConfigurations(value.changingConfigurations); if (useCache) { cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr); if (needsNewDrawableAfterCache) { Drawable.ConstantState state = dr.getConstantState(); if (state != null) { dr = state.newDrawable(wrapper); } } } } return dr; } catch (Exception e) { String name; try { name = getResourceName(id); } catch (NotFoundException e2) { name = "(missing name)"; } // The target drawable might fail to load for any number of // reasons, but we always want to include the resource name. // Since the client already expects this method to throw a // NotFoundException, just throw one of those. final NotFoundException nfe = new NotFoundException("Drawable " + name + " with resource ID #0x" + Integer.toHexString(id), e); nfe.setStackTrace(new StackTraceElement[0]); throw nfe; } }
关于mDrawableCache有关的注释:
// First, check whether we have a cached version of this drawable
// that was inflated against the specified theme. Skip the cache if
// we’re currently preloading or we’re not using the cache.
这是一个缓冲区, 加载过的Drawable放存放在里面, 在取时会先检测是否已存在.
/** * Loads a drawable from XML or resources stream. * * @return Drawable, or null if Drawable cannot be decoded. */ @Nullable private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value, int id, int density) { //... try { if (file.endsWith(".xml")) { final String typeName = getResourceTypeName(id); if (typeName != null && typeName.equals("color")) { dr = loadColorOrXmlDrawable(wrapper, value, id, density, file); } else { dr = loadXmlDrawable(wrapper, value, id, density, file); } } else { final InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); final AssetInputStream ais = (AssetInputStream) is; dr = decodeImageDrawable(ais, wrapper, value); } } finally { stack.pop(); } } catch (Exception | StackOverflowError e) { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); final NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } //... return dr; }
如果是XML:
frameworks/base/graphics/java/android/graphics/drawable/DrawableInflater.java
/** * Version of {@link #inflateFromXml(String, XmlPullParser, AttributeSet, Theme)} that accepts * an override density. */ @NonNull Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density, @Nullable Theme theme) throws XmlPullParserException, IOException { // Inner classes must be referenced as Outer$Inner, but XML tag names // can't contain $, so the <drawable> tag allows developers to specify // the class in an attribute. We'll still run it through inflateFromTag // to stay consistent with how LayoutInflater works. if (name.equals("drawable")) { name = attrs.getAttributeValue(null, "class"); if (name == null) { throw new InflateException("<drawable> tag must specify class attribute"); } } Drawable drawable = inflateFromTag(name); if (drawable == null) { drawable = inflateFromClass(name); } drawable.setSrcDensityOverride(density); drawable.inflate(mRes, parser, attrs, theme); return drawable; } @NonNull @SuppressWarnings("deprecation") private Drawable inflateFromTag(@NonNull String name) { switch (name) { case "selector": return new StateListDrawable(); case "animated-selector": return new AnimatedStateListDrawable(); case "level-list": return new LevelListDrawable(); case "layer-list": return new LayerDrawable(); case "transition": return new TransitionDrawable(); case "ripple": return new RippleDrawable(); case "adaptive-icon": return new AdaptiveIconDrawable(); case "color": return new ColorDrawable(); case "shape": return new GradientDrawable(); case "vector": return new VectorDrawable(); case "animated-vector": return new AnimatedVectorDrawable(); case "scale": return new ScaleDrawable(); case "clip": return new ClipDrawable(); case "rotate": return new RotateDrawable(); case "animated-rotate": return new AnimatedRotateDrawable(); case "animation-list": return new AnimationDrawable(); case "inset": return new InsetDrawable(); case "bitmap": return new BitmapDrawable(); case "nine-patch": return new NinePatchDrawable(); case "animated-image": return new AnimatedImageDrawable(); default: return null; } }
假如是图片文件, 最终返回: BitmapDrawable
frameworks/base/graphics/java/android/graphics/ImageDecoder.java
@WorkerThread @NonNull private static Drawable decodeDrawableImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { //.... return new BitmapDrawable(res, bm); }
参考
StateListDrawable
Set android shape color programmatically
Android: How to create a StateListDrawable programmatically
How to change colors of a Drawable in Android?
StateListDrawable 动态更换背景 ↩︎
Java代码更改shape和selector文件的颜色值 ↩︎