Android 中的 StateListDrawable

Android 中的 StateListDrawable,第1张

前言-关于StateListDrawable

Android里面经常会在开发中在res/drawable目录下, 使用shape或者selector编写XML文件, 来定制一些View的背景, 而这些XML最终会转换为StateListDrawable

  *注: 并非所有的XML都是StateListDrawable* 

对于StateListDrawable文档中如此描述:
允许您将多个图形图像分配给单个 Drawable,并通过字符串 ID 值替换可见项。它可以在带有 元素的 XML 文件中定义。每个状态 Drawable 都在嵌套的 元素中定义.


常见的XML

<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) {
                        //默认无任何状态的
                        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  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(" 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文件的颜色值 ↩︎

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/993015.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存