Android 中的 StateListDrawable

简介: Android 中的 StateListDrawable

前言-关于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文件的颜色值 ↩︎


相关文章
|
3月前
|
XML Android开发 数据格式
"探秘Android Drawable魔法:一篇文章教你玩转StateListDrawable与AnimationDrawable!"
【8月更文挑战第18天】Drawable是Android中用于屏幕绘制的图形对象,StateListDrawable与AnimationDrawable是两种实用类型。StateListDrawable可根据控件状态变化显示不同图形,如按钮的点击反馈;AnimationDrawable则用于实现帧动画效果,常用于加载提示或动态图标。两者均可通过XML定义或代码创建,并轻松应用于View的背景中,有效增强应用的交互性和视觉体验。
54 0
|
Android开发 索引 数据格式
|
Android开发
我的Android进阶之旅------&gt;Android中StateListDrawable支持的状态
Android中StateListDrawable支持的状态 android:state_active 代表是否处于激活状态 android:state_checked  代表是否处于已勾选状态 android:state_check...
1224 0
|
3天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
5天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
7天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
5天前
|
存储 API 开发工具
探索安卓开发:从基础到进阶
【10月更文挑战第37天】在这篇文章中,我们将一起探索安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和建议。我们将从安卓开发的基础开始,逐步深入到更复杂的主题,如自定义组件、性能优化等。最后,我们将通过一个代码示例来展示如何实现一个简单的安卓应用。让我们一起开始吧!
|
6天前
|
存储 XML JSON
探索安卓开发:从新手到专家的旅程
【10月更文挑战第36天】在这篇文章中,我们将一起踏上一段激动人心的旅程,从零基础开始,逐步深入安卓开发的奥秘。无论你是编程新手,还是希望扩展技能的老手,这里都有适合你的知识宝藏等待发掘。通过实际的代码示例和深入浅出的解释,我们将解锁安卓开发的关键技能,让你能够构建自己的应用程序,甚至贡献于开源社区。准备好了吗?让我们开始吧!
18 2