美文网首页
selector之StateListDrawable

selector之StateListDrawable

作者: 行走缓慢的程序媛 | 来源:发表于2019-04-09 14:06 被阅读0次

    1 前言

    在文章开头,有一点我要说明,这篇文章很长,请耐心看完,有什么不对的地方,欢迎指正哦。以下分析是基于android 9.0,全文概述了系统从selectorxml的解析,到当前系统接收到来自底层的按压事件的上报,再到view响应事件,并根据具体的状态完成背景图切换的一系列流程。

    2 selector 的使用

    androidselector 对于android开发者而言再熟悉不过了,只需在drawable目录下定义一个selectorxml文件,在布局文件中引用这个xml文件或者在代码中setBackgroundDrawable的时候使用此xml就可以实现控件按下或有焦点等不同状态的效果。

    在此之前,我们先了解一些基础知识。首先要了解一个Drawable类,Drawable是一个抽象的可绘制的图片类,这个类可以从一个本地路径中创建一个图片,也可以使用从定义好的xml中创建,从路径中创建一个Bitmap对象并将它转换成BitmapDrawable;当图为.9.png时,则转换成NinePatchDrawable;当从xml中定义的selector标签转换为StateListDrawable。可以总结为以下几点。

    • bitmap --> BitmapDrawable
    • color --> ColorDrawable
    • shape --> GradientDrawable
    • selector --> StateListDrawable

    更多细节,具体可以查看官网可绘制对象资源的介绍。

    3 从 xml 文件解析 selector

    selector是给view作为背景用的,如果要找源头,自然先从view找起。我们先来看一下view的构造方法,里面解析了view的属性background并赋值给变量backgrounda我们知道是TypedArray,进到它的getDrawable()看看具体干了啥。

    package android.view;
    
    public class View implements Drawable.Callback, KeyEvent.Callback,
            AccessibilityEventSource {
    
        public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    
        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 属性赋值给 drawable
                        background = a.getDrawable(attr);
                        break;
                ...
    
                }
            ...
            }
    }
    

    进来之后,发现它调用了getDrawableForDensity(),在此方法里,它调用Resources类的loadDrawable()

    package android.content.res;
    
    public class TypedArray {
    
        @Nullable
        public Drawable getDrawable(@StyleableRes int index) {
            return getDrawableForDensity(index, 0);
        }
    
        @Nullable
        public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
    
            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) {
                    mResources.getValueForDensity(value.resourceId, density, value, true);
                }
                return mResources.loadDrawable(value, value.resourceId, density, mTheme);
            }
            return null;
        }
    
    }
    
    

    进到Resources类,我们继续跟踪,结果只调用了ResourcesImpl类的loadDrawable()方法。

    package android.content.res;
    
    public class Resources {
    
       @NonNull
        Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
                throws NotFoundException {
                //this,把当前 Resources 作为参数传进去了
            return mResourcesImpl.loadDrawable(this, value, id, density, theme);
        }
    
    }
    

    不急,我们继续走。进到loadDrawable()方法。哇,写了一堆代码,我们提取出我们最想要的信息。它调用了loadDrawableForCookie()方法来加载图片。

    package android.content.res;
    
    public class ResourcesImpl {
    
        Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
                int density, @Nullable Resources.Theme theme)
                throws NotFoundException {
    
            final boolean useCache = density == 0 || value.density == mMetrics.densityDpi;
    
            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;
                }
    
                if (!mPreloading && useCache) {
                    final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                    if (cachedDrawable != null) {
                        cachedDrawable.setChangingConfigurations(value.changingConfigurations);
                        return cachedDrawable;
                    }
                    /// M: Boost cache on system @{
                    synchronized (mAccessLock) {
                        final Drawable boostDrawable = mMtkBoostDrawableCache
                                .getBoostCachedDrawable(wrapper, key);
                        if (boostDrawable != null) {
                            Slog.w(TAG, "Using Boost");
                            return boostDrawable;
                        }
                    }
                    /// @}
                }
    
                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 {
                    //有用信息在这里,wrapper 就是 Resources
                    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)";
                }
    
                final NotFoundException nfe = new NotFoundException("Drawable " + name
                        + " with resource ID #0x" + Integer.toHexString(id), e);
                nfe.setStackTrace(new StackTraceElement[0]);
                throw nfe;
            }
        }
    

    继续往下走,它会先判断文件是否以xml结尾,并且构造了一个XmlResourceParserrp,并把它作为参数传给了DrawablecreateFromXmlForDensity()静态方法。最后是通过这个rp创造了Drawable

        private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,int id, int density) {
    
            if (file.endsWith(".xml")) {
                        final XmlResourceParser rp = loadXmlResourceParser(
                                file, id, value.assetCookie, "drawable");
                        dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
                        rp.close();
                    } else {
                        final InputStream is = mAssets.openNonAsset(
                                value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                        AssetInputStream ais = (AssetInputStream) is;
                        dr = decodeImageDrawable(ais, wrapper, value);
            }
    
        }
    

    继续进到createFromXmlInnerForDensity()方法。

    public abstract class Drawable {
    
        public static Drawable createFromXmlForDensity(@NonNull Resources r,
                @NonNull XmlPullParser parser, int density, @Nullable Theme theme)
                throws XmlPullParserException, IOException {
            AttributeSet attrs = Xml.asAttributeSet(parser);
    
            int type;
            //noinspection StatementWithEmptyBody
            //为了只过滤出 START_TAG 标签,也就是 selector
            while ((type=parser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
                // Empty loop.
            }
    
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException("No start tag found");
            }
    
            Drawable drawable = createFromXmlInnerForDensity(r, parser, attrs, density, theme);
    
            if (drawable == null) {
                throw new RuntimeException("Unknown initial tag: " + parser.getName());
            }
    
            return drawable;
        }
    

    我们发现这个方法只调用了DrawableInflaterinflateFromXmlForDensity()方法。接着走。

        static Drawable createFromXmlInnerForDensity(@NonNull Resources r,
                @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
                @Nullable Theme theme) throws XmlPullParserException, IOException {
            return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attrs,
                    density, theme);
        }
    
    
    }
    

    我们看到inflateFromTag()方法,并把解析器解析出来的tag名字作为参数传进去了。

    package android.graphics.drawable;
    
    public final class DrawableInflater {
    
        Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
                @NonNull AttributeSet attrs, int density, @Nullable Theme theme)
                throws XmlPullParserException, IOException {
            if (name.equals("drawable")) {
                name = attrs.getAttributeValue(null, "class");
                if (name == null) {
                    throw new InflateException("<drawable> tag must specify class attribute");
                }
            }
    
            //name = parse.getName()
            Drawable drawable = inflateFromTag(name);
            if (drawable == null) {
                drawable = inflateFromClass(name);
            }
            drawable.setSrcDensityOverride(density);
            //会调用 inflate() 方法
            drawable.inflate(mRes, parser, attrs, theme);
            return drawable;
        }
    

    跟踪了这么久,终于在这里看到了,当识别到selector标签时,会实例化一个StateListDrawable对象返回。

        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;
            }
        }
    
    }
    

    至此,我们终于理解了为什么文章开头讲selector最后实例化一个StateListDrawable。至于里面的状态以及对应的图是如何存取的,接着看接下来的分析。

    4 selector 状态图的存储

    由于上面调用了inflateFromTag()方法返回drawable之后,会立即调用它的inflate()方法,也就是StateListDrawable,我们继续跟踪。

    public class StateListDrawable extends DrawableContainer {
    
        public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
                throws XmlPullParserException, IOException {
            final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.StateListDrawable);
            super.inflateWithAttributes(r, parser, a, R.styleable.StateListDrawable_visible);
            updateStateFromTypedArray(a);
            updateDensity(r);
            a.recycle();
    
            inflateChildElements(r, parser, attrs, theme);
    
            onStateChange(getState());
        }
    

    进到updateStateFromTypedArray()方法,它会解析StateListDrawable的各种属性。

        private void updateStateFromTypedArray(TypedArray a) {
            final StateListState state = mStateListState;
    
            // Account for any configuration changes.
            state.mChangingConfigurations |= a.getChangingConfigurations();
    
            // Extract the theme attributes, if any.
            state.mThemeAttrs = a.extractThemeAttrs();
    
            state.mVariablePadding = a.getBoolean(
                    R.styleable.StateListDrawable_variablePadding, state.mVariablePadding);
            state.mConstantSize = a.getBoolean(
                    R.styleable.StateListDrawable_constantSize, state.mConstantSize);
            state.mEnterFadeDuration = a.getInt(
                    R.styleable.StateListDrawable_enterFadeDuration, state.mEnterFadeDuration);
            state.mExitFadeDuration = a.getInt(
                    R.styleable.StateListDrawable_exitFadeDuration, state.mExitFadeDuration);
            state.mDither = a.getBoolean(
                    R.styleable.StateListDrawable_dither, state.mDither);
            state.mAutoMirrored = a.getBoolean(
                    R.styleable.StateListDrawable_autoMirrored, state.mAutoMirrored);
        }
    

    继续看inflate()方法里调用的inflateChildElements()方法。当检测到不是START_TAG,就跳过;当不是item标签也跳过。然后把drawable标签对应的图片取出来,赋值给变量dr,然后调用extractStateSet()得到对应的状态states,最后将状态和图片存起来。

        private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
                Theme theme) throws XmlPullParserException, IOException {
            final StateListState state = mStateListState;
            final int innerDepth = parser.getDepth() + 1;
            int type;
            int depth;
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                    && ((depth = parser.getDepth()) >= innerDepth
                    || type != XmlPullParser.END_TAG)) {
                //不是 START_TAG,就跳过
                if (type != XmlPullParser.START_TAG) {
                    continue;
                }
    
                //不是 item 标签也跳过
                if (depth > innerDepth || !parser.getName().equals("item")) {
                    continue;
                }
    
                // This allows state list drawable item elements to be themed at
                // inflation time but does NOT make them work for Zygote preload.
                final TypedArray a = obtainAttributes(r, theme, attrs,
                        R.styleable.StateListDrawableItem);
                //把 drawable 标签的图片从 xml 里读出来
                Drawable dr = a.getDrawable(R.styleable.StateListDrawableItem_drawable);
                a.recycle();
    
                //取出状态值
                final int[] states = extractStateSet(attrs);
    
                // Loading child elements modifies the state of the AttributeSet's
                // underlying parser, so it needs to happen after obtaining
                // attributes and extracting states.
                if (dr == null) {
                    while ((type = parser.next()) == XmlPullParser.TEXT) {
                    }
                    if (type != XmlPullParser.START_TAG) {
                        throw new XmlPullParserException(
                                parser.getPositionDescription()
                                        + ": <item> tag requires a 'drawable' attribute or "
                                        + "child tag defining a drawable");
                    }
                    dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
                }
    
                //把状态值和图片存起来
                state.addStateSet(states, dr);
            }
        }
    
    }
    

    我们进到attrs文件里查看,定义了名为StateListDrawableItem的样式,属性名为drawable。之所以我们在xml里用andorid:drawable的形式,就是因为这里限定了。

    frameworks/base/core/res/res/values/attrs.xml

    <!-- Represents a single state inside a StateListDrawable. -->
    <declare-styleable name="StateListDrawableItem">
            <!-- Reference to a drawable resource to use for the state. If not
                 given, the drawable must be defined by the first child tag. -->
            <attr name="drawable" />
    </declare-styleable>
    

    知道了如何取出drawable,我们继续研究,它是如何取出state状态值的。
    继续进到extractStateSet()方法。发现它过滤掉了drawableid,只读取状态值,当状态设置为true时,则返回stateResId,若为false时,则为-stateResId

    public class StateListDrawable extends DrawableContainer {
    
        int[] extractStateSet(AttributeSet attrs) {
            int j = 0;
            final int numAttrs = attrs.getAttributeCount();
            int[] states = new int[numAttrs];
            for (int i = 0; i < numAttrs; i++) {
                final int stateResId = attrs.getAttributeNameResource(i);
                switch (stateResId) {
                    case 0:
                        break;
                    case R.attr.drawable:
                    case R.attr.id:
                        // Ignore attributes from StateListDrawableItem and
                        // AnimatedStateListDrawableItem.
                        continue;
                    default:
                        //把状态的值取出来,当为true时,则为stateResId,当为false时,则为-stateResId
                        states[j++] = attrs.getAttributeBooleanValue(i, false)
                                ? stateResId : -stateResId;
                }
            }
            states = StateSet.trimStateSet(states, j);
            return states;
        }
    
    }
    

    什么意思呢?就是当我们在xml如下表示的时候

    <?xml version="1.0" encoding="UTF-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
     <item android:state_pressed="false" android:drawable="@drawable/btn_style_one_pressed_false" />
     <item android:state_pressed="true" android:drawable="@drawable/btn_style_one_pressed_true" />
    
     <item android:drawable="@drawable/btn_style_default" />
    </selector>
    

    转换成java代码,就会变成如下所示,当为false时,状态值表示的时候,会有一个减号(减号):

    StateListDrawable stalistDrawable = new StateListDrawable();
    
    stalistDrawable.addState(new int []{-android.R.attr.state_pressed}, getResources().getDrawable(R.drawable.btn_style_one_pressed_false));
    stalistDrawable.addState(new int []{android.R.attr.state_pressed}, getResources().getDrawable(R.drawable.btn_style_one_pressed_true));
    
    //默认状态时,用一个空数组表示
    stalistDrawable.addState(new int []{}, getResources().getDrawable(R.drawable.btn_style_default));
    

    首先进到addState()方法。发现它调用了StateListStateaddStateSet()方法。而上面追踪代码到最后,它也是调用addStateSet()方法,和java代码如出一辙。StateListStateStateListDrawable的内部类。

    public class StateListDrawable extends DrawableContainer {
    
        public void addState(int[] stateSet, Drawable drawable) {
            if (drawable != null) {
                mStateListState.addStateSet(stateSet, drawable);
                // in case the new state matches our current state...
                onStateChange(getState());
            }
        }
    
    }
    

    为了更好的理解下面的讲解,首先我们先了解一下类关系图。

    class.png

    进到类StateListState,在addStateSet()方法里,又调用了addChild()方法。此方法写在父类DrawableContainerState里。把drawable参数传进去,返回一个下标,并将stateSet状态保存在对应下标的mStateSets二维数组中。

    static class StateListState extends DrawableContainerState {
    
        int addStateSet(int[] stateSet, Drawable drawable) {
                final int pos = addChild(drawable);
                mStateSets[pos] = stateSet;
                return pos;
        }
    
    }
    

    进到addChild()方法查看,它把dr存在mDrawables一维数组变量中。并将下标值返回。

    protected DrawableContainerState(DrawableContainerState orig, DrawableContainer owner,Resources res) {
    
        public final int addChild(Drawable dr) {
                final int pos = mNumChildren;
                if (pos >= mDrawables.length) {
                    growArray(pos, pos+10);
                }
    
                dr.mutate();
                dr.setVisible(false, true);
                dr.setCallback(mOwner);
    
                mDrawables[pos] = dr;
                mNumChildren++;
                mChildrenChangingConfigurations |= dr.getChangingConfigurations();
    
                invalidateCache();
    
                mConstantPadding = null;
                mCheckedPadding = false;
                mCheckedConstantSize = false;
                mCheckedConstantState = false;
    
                return pos;
            }
    
    }
    

    至此,通过上面的分析得出:“状态”用一维数组stateSet表示,根据上面的例子也能够看出。且把这个表示状态的一维数组stateSet保存在StateListState类的变量mStateSets二维数组中;“状态图片”保存在父类DrawableContainerState的变量mDrawables一维数组中,此下标与mStateSets保存状态的下标一一对应。

    在知道了状态和图片是如何存储之后,我们再来研究:当状态改变后,它是如何更新view视图的呢?也就是如何更新背景图的呢?

    5 view 视图的更新

    首先,TP会把我们的触摸事件上报,就拿press按压来举例子。当我们按下屏幕,底层接收到按压事件,然后上报给上层,上层再处理这个按压事件,经过一些列复杂的事件传递,最终会调用view类的setPressed()方法。这时候会根据传进来的按压布尔值,对mPrivateFlags进行位运算,并且判断当前状态是否改变,当改变了就需要调用refreshDrawableState()方法进行刷新状态。

    public void setPressed(boolean pressed) {
            //判断按压状态是否更新
            final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
    
            if (pressed) {
                mPrivateFlags |= PFLAG_PRESSED;
            } else {
                mPrivateFlags &= ~PFLAG_PRESSED;
            }
    
            if (needsRefresh) {
                refreshDrawableState();
            }
            dispatchSetPressed(pressed);
    }
    

    进到viewrefreshDrawableState()方法中,又调用了drawableStateChanged()方法,我们继续跟踪。

    public void refreshDrawableState() {
            mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
            drawableStateChanged();
    
            ViewParent parent = mParent;
            if (parent != null) {
                parent.childDrawableStateChanged(this);
            }
    }
    

    它会调用getDrawableState()获取当前的状态数组,然后设置一下drawable的状态,当设置成功,就会返回true,否则就返回false,当成功的时候,会调用invalidate()进行重绘。

    protected void drawableStateChanged() {
            final int[] state = getDrawableState();
            boolean changed = false;
    
            final Drawable bg = mBackground;
            if (bg != null && bg.isStateful()) {
                changed |= bg.setState(state);
            }
    
            ...
    
            if (changed) {
                invalidate();
            }
    
    }
    

    我们一步步解析这个drawableStateChanged()方法。首先我们看一下getDrawableState()是如何获取当前状态的。初次进来的时候,会调用onCreateDrawableState(0)来给状态数组赋值。

    public final int[] getDrawableState() {
            if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
                return mDrawableState;
            } else {
                mDrawableState = onCreateDrawableState(0);
                mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
                return mDrawableState;
            }
    }
    

    在这个方法里,会根据当前的flag和各种状态进行与运算,当不等于0的时候,会对viewStateIndex进行或运算,将标志位一个个加进去。最终会调用StateSet.get()方法,将viewStateIndex作为参数传进去,最后得到对应的状态值。这里面的运算可就有意思了。这里不得不赞叹一下google的大神们。

    protected int[] onCreateDrawableState(int extraSpace) {
         ...
    
            int[] drawableState;
    
            int privateFlags = mPrivateFlags;
    
            int viewStateIndex = 0;
            if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED;
            if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENABLED;
            if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED;
            if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= StateSet.VIEW_STATE_SELECTED;
            if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED;
            if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIVATED;
    
         ...
    
            //把 viewStateIndex 传进去得到对应的状态值
            drawableState = StateSet.get(viewStateIndex);
    
            if (extraSpace == 0) {
                return drawableState;
            }
    
            final int[] fullState;
            if (drawableState != null) {
                fullState = new int[drawableState.length + extraSpace];
                System.arraycopy(drawableState, 0, fullState, 0, drawableState.length);
            } else {
                fullState = new int[extraSpace];
            }
    
            return fullState;
    }
    

    StateSet.get()代码我贴出来了,有兴趣的可以自己研究下。总而言之,就是传进去的是实则是VIEW_STATE_IDS偶数下标项的位组合,返回的是VIEW_STATE_IDS奇数下标项的组合数组。

    public class StateSet {
    
        private static final int[][] VIEW_STATE_SETS;
    
        public static final int VIEW_STATE_WINDOW_FOCUSED = 1;
        public static final int VIEW_STATE_SELECTED = 1 << 1;
        public static final int VIEW_STATE_FOCUSED = 1 << 2;
        public static final int VIEW_STATE_ENABLED = 1 << 3;
        public static final int VIEW_STATE_PRESSED = 1 << 4;
        public static final int VIEW_STATE_ACTIVATED = 1 << 5;
        public static final int VIEW_STATE_ACCELERATED = 1 << 6;
        public static final int VIEW_STATE_HOVERED = 1 << 7;
        public static final int VIEW_STATE_DRAG_CAN_ACCEPT = 1 << 8;
        public static final int VIEW_STATE_DRAG_HOVERED = 1 << 9;
    
    
        static final int[] VIEW_STATE_IDS = new int[] {
                R.attr.state_window_focused,    VIEW_STATE_WINDOW_FOCUSED,
                R.attr.state_selected,          VIEW_STATE_SELECTED,
                R.attr.state_focused,           VIEW_STATE_FOCUSED,
                R.attr.state_enabled,           VIEW_STATE_ENABLED,
                R.attr.state_pressed,           VIEW_STATE_PRESSED,
                R.attr.state_activated,         VIEW_STATE_ACTIVATED,
                R.attr.state_accelerated,       VIEW_STATE_ACCELERATED,
                R.attr.state_hovered,           VIEW_STATE_HOVERED,
                R.attr.state_drag_can_accept,   VIEW_STATE_DRAG_CAN_ACCEPT,
                R.attr.state_drag_hovered,      VIEW_STATE_DRAG_HOVERED
        };
    
    
        static {
            final int[] orderedIds = new int[VIEW_STATE_IDS.length];
            for (int i = 0; i < R.styleable.ViewDrawableStates.length; i++) {
                final int viewState = R.styleable.ViewDrawableStates[i];
                for (int j = 0; j < VIEW_STATE_IDS.length; j += 2) {
                    if (VIEW_STATE_IDS[j] == viewState) {
                        orderedIds[i * 2] = viewState;
                        orderedIds[i * 2 + 1] = VIEW_STATE_IDS[j + 1];
                    }
                }
            }
    
            final int NUM_BITS = VIEW_STATE_IDS.length / 2;
            VIEW_STATE_SETS = new int[1 << NUM_BITS][];
            for (int i = 0; i < VIEW_STATE_SETS.length; i++) {
                final int numBits = Integer.bitCount(i);
                final int[] set = new int[numBits];
                int pos = 0;
                for (int j = 0; j < orderedIds.length; j += 2) {
                    if ((i & orderedIds[j + 1]) != 0) {
                        set[pos++] = orderedIds[j];
                    }
                }
                VIEW_STATE_SETS[i] = set;
            }
        }
    
        //我们将状态值传进去,返回VIEW_STATE_IDS的奇数项的一项或多项
        public static int[] get(int mask) {
            if (mask >= VIEW_STATE_SETS.length) {
                throw new IllegalArgumentException("Invalid state set mask");
            }
            return VIEW_STATE_SETS[mask];
        }
    
    }
    

    扯远了,好,我们再回到上面的drawableStateChanged()方法继续看,刚刚我们从getDrawableState()一路追过来,现在我们接着说下面setState()方法。把刚才得到的状态值,调用DrawablesetState()方法存进去。

    protected void drawableStateChanged() {
            final int[] state = getDrawableState();
            boolean changed = false;
    
            final Drawable bg = mBackground;
            if (bg != null && bg.isStateful()) {
                changed |= bg.setState(state);
            }
    
            ...
    
            if (changed) {
                invalidate();
            }
    
    }
    

    把当前状态赋值给成员变量mStateSet,并且调用onStateChange()方法通知状态改变。并把状态值赋值给成员变量mStateSet

    public abstract class Drawable {
    
        public boolean setState(@NonNull final int[] stateSet) {
            if (!Arrays.equals(mStateSet, stateSet)) {
    
                mStateSet = stateSet;
                //通知状态改变
                return onStateChange(stateSet);
            }
            return false;
        }
    
    }
    

    这个onStateChange()方法是在父类Drawable里定义的,子类StateListDrawable重写了该方法,所以我们直接看子类的方法即可。

    首先在该方法里,调用indexOfStateSet()方法,并把状态作为参数传进去了,并返回该状态对应的下标值。

    public class StateListDrawable extends DrawableContainer {
    
        @Override
        protected boolean onStateChange(int[] stateSet) {
            final boolean changed = super.onStateChange(stateSet);
    
            int idx = mStateListState.indexOfStateSet(stateSet);
            if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "
                    + Arrays.toString(stateSet) + " found " + idx);
            if (idx < 0) {
                idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
            }
    
            return selectDrawable(idx) || changed;
        }
    
    }
    

    我们先看具体它是怎么返回下标的。indexOfStateSet()是通过mStateSets二维数组里的每一项逐一和参数进行对比,将匹配的下标返回。

        int indexOfStateSet(int[] stateSet) {
                final int[][] stateSets = mStateSets;
                final int N = getChildCount();
                for (int i = 0; i < N; i++) {
                    if (StateSet.stateSetMatches(stateSets[i], stateSet)) {
                        return i;
                    }
                }
                return -1;
        }
    

    最后通过返回的id下标,调用selectDrawable(),并把下标值作为参数传进去了,下面我们再看selectDrawable()方法,根据下标,选出对应的drawable,更新mCurrDrawablemCurIndex,并且最后调用invalidateSelf()进行自我刷新。

    public class DrawableContainer extends Drawable implements Drawable.Callback {
    
        public boolean selectDrawable(int index) {
        ...
    
        if (index >= 0 && index < mDrawableContainerState.mNumChildren) {
                final Drawable d = mDrawableContainerState.getChild(index);
                mCurrDrawable = d;
                mCurIndex = index;
                if (d != null) {
                    if (mDrawableContainerState.mEnterFadeDuration > 0) {
                        mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;
                    }
                    initializeDrawableForDisplay(d);
                }
            } else {
                mCurrDrawable = null;
                mCurIndex = -1;
            }
        ...
    
        invalidateSelf();
    
        }
    
    }
    

    invalidateSelf()刷新背景图,是调用的父类Drawable中定义的方法。里面调用的callbackinvalidateDrawable()

    public abstract class Drawable {
    
        public void invalidateSelf() {
            final Callback callback = getCallback();
            if (callback != null) {
                callback.invalidateDrawable(this);
            }
        }
    
    }
    

    那么这个callback是什么呢?请看View中的setBackgroundDrawable()方法里,调用了background.setCallback(this),这里view把自己传给了Callback。所以最终调用的是viewinvalidateDrawable()方法进行背景刷新。

    setBackgroundDrawable()方法里,会调用isStateful()方法进行判断,多状态是否可用,父类Drawable类里,此方法返回false,子类StateListDrawable里重写了此方法,返回true

    public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    
        @Override
        public void invalidateDrawable(@NonNull Drawable drawable) {
            if (verifyDrawable(drawable)) {
                final Rect dirty = drawable.getDirtyBounds();
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;
    
                invalidate(dirty.left + scrollX, dirty.top + scrollY,
                        dirty.right + scrollX, dirty.bottom + scrollY);
                rebuildOutline();
            }
        }
    
    }
    

    终于快看到胜利的曙光了,invalidateDrawable()方法里调用了invalidate()方法,重绘,会调用draw()方法。这个方法里面会有一堆的代码,我们捡重点看,主要是drawBackground()方法。

    
    public void draw(Canvas canvas) {
    
        drawBackground(canvas);
    
    }
    
    

    最后,我们终于在viewdrawBackground()方法里,我们看到background.draw(canvas),把view的背景重新绘制了。如果你以为到这里就结束了,那你肯定忘了一件事,那就是:具体绘制哪一张背景图,我们到现在还没有说到。接着看。

    private void drawBackground(Canvas canvas) {
            final Drawable background = mBackground;
            if (background == null) {
                return;
            }
    
            setBackgroundBounds();
    
            ...
    
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;
            if ((scrollX | scrollY) == 0) {
                //重新绘制背景
                background.draw(canvas);
            } else {
                canvas.translate(scrollX, scrollY);
                background.draw(canvas);
                canvas.translate(-scrollX, -scrollY);
            }
    }
    
    

    我们知道,最后调用了drawabledraw()进行背景图的绘制,这个方法,由子类DrawableContainer重写了。哇,它调用了mCurrDrawable的绘制方法,还记得,它什么时候被赋值的嘛,我们上面讲过。是在状态改变时,在selectDrawable()方法里进行的赋值,它把表示当前状态的背景图赋值给了mCurrDrawable变量。当通知view需要更新背景,这时view又反过来调用了drawabledraw()方法。

    public class DrawableContainer extends Drawable implements Drawable.Callback {
    
        @Override
        public void draw(Canvas canvas) {
            if (mCurrDrawable != null) {
                mCurrDrawable.draw(canvas);
            }
            if (mLastDrawable != null) {
                mLastDrawable.draw(canvas);
            }
        }
    
    }
    

    至此,是真的分析结束了。喜大普奔,喜大普奔。最后,可以用下面一张图来说明整个过程。

    summary.png

    相关文章

      网友评论

          本文标题:selector之StateListDrawable

          本文链接:https://www.haomeiwen.com/subject/njpbiqtx.html