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


相关文章
|
Android开发
我的Android进阶之旅------&gt;Android中StateListDrawable支持的状态
Android中StateListDrawable支持的状态 android:state_active 代表是否处于激活状态 android:state_checked  代表是否处于已勾选状态 android:state_check...
1145 0
|
15天前
|
Java 数据库 Android开发
【专栏】Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理
【4月更文挑战第27天】本文探讨了Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理。通过案例分析展示了网络请求、图像处理和数据库操作的优化实践。同时,文章指出并发编程的挑战,如性能评估、调试及兼容性问题,并强调了多线程优化对提升应用性能的重要性。开发者应持续学习和探索新的优化策略,以适应移动应用市场的竞争需求。
|
9天前
|
存储 安全 Android开发
安卓应用开发:构建一个高效的用户登录系统
【5月更文挑战第3天】在移动应用开发中,用户登录系统的设计与实现是至关重要的一环。对于安卓平台而言,一个高效、安全且用户体验友好的登录系统能够显著提升应用的用户留存率和市场竞争力。本文将探讨在安卓平台上实现用户登录系统的最佳实践,包括对最新身份验证技术的应用、安全性考量以及性能优化策略。
|
12天前
|
前端开发 Android开发 iOS开发
【Flutter前端技术开发专栏】Flutter在Android与iOS上的性能对比
【4月更文挑战第30天】Flutter 框架实现跨平台移动应用,通过一致的 UI 渲染(Skia 引擎)、热重载功能和响应式框架提高开发效率和用户体验。然而,Android 和 iOS 的系统差异、渲染机制及编译过程影响性能。性能对比显示,iOS 可能因硬件优化提供更流畅体验,而 Android 更具灵活性和广泛硬件支持。开发者可采用代码、资源优化和特定平台优化策略,利用性能分析工具提升应用性能。
【Flutter前端技术开发专栏】Flutter在Android与iOS上的性能对比
|
13天前
|
监控 Java Android开发
安卓应用开发:打造高效用户界面的五大策略
【4月更文挑战第29天】 在安卓应用开发的世界中,构建一个既美观又高效的用户界面(UI)对于吸引和保留用户至关重要。本文将深入探讨五种策略,这些策略可以帮助开发者优化安卓应用的UI性能。我们将从布局优化讲起,逐步过渡到绘制优化、内存管理、异步处理以及最终的用户交互细节调整。通过这些实践技巧,你将能够为用户提供流畅而直观的体验,确保你的应用在竞争激烈的市场中脱颖而出。
|
2天前
|
Java Android开发
Android开发--Intent-filter属性详解
Android开发--Intent-filter属性详解
|
2天前
|
物联网 Java 开发工具
安卓应用开发:打造未来移动生活
【5月更文挑战第10天】 随着科技的飞速发展,智能手机已成为我们日常生活中不可或缺的一部分。作为智能手机市场的两大巨头,安卓和iOS分别占据了一定的市场份额。在这篇文章中,我们将重点关注安卓应用开发,探讨如何利用先进的技术和创新思维,为用户打造更加便捷、智能的移动生活。文章将涵盖安卓应用开发的基本概念、关键技术、以及未来发展趋势等方面的内容。
|
3天前
|
Java API 开发工具
java与Android开发入门指南
java与Android开发入门指南
11 0
|
5天前
|
Android开发 Kotlin
Kotlin开发Android之基础问题记录
Kotlin开发Android之基础问题记录
16 1