阅读 1704

android中drawable显示到view上的过程

前段时间一直整理java方面的知识了,先过渡一段时间到android上面来,后期还是会整理java相关的东西,至于整理什么方面的,还没想好。好了,先不说废话了,还是回到正片上来,说说android中用得比较多的drawable类,drawable类是一个抽象的类,其实我们平常开发的阶段用的就是它的各种子类,比如有ColorDrawableBitmapDrawable等等,后面所有相关的Drawable都会讲到。相信大家用Bitmap也是用得比较多的,那他两有啥区别呢。

Bitmap是专门存储图片的一种形式,是对位图的每一个像素的颜色存储器,而我们的颜色值是由ARGB来标示的,因此我们常常用16进制的6位数表示一个颜色值,而颜色模式一般有下面几种:

颜色模式 说明
ARGB8888 四通道高精度(32位)
ARGB4444 四通道低精度(24位)
RGB565 三通道(16位)
Alpha8 透明通道(8位)

所以对于Bitmap的使用是需要指明Bitmap使用的颜色通道模式,一般如果没有特殊要求最好是选择三通道的就行。好了,关于Bitmap的说明就这么多,还是言归正传,说说drawable是怎么一步步显示在view上的,下面还是通过一个简单的例子,说明drawable的用法:

定义了一个drawable文件:test_back.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/colorPrimary" android:state_pressed="true" />
    <item android:drawable="@color/colorPrimary" android:state_selected="true" />
    <item android:drawable="@color/colorAccent" />
</selector>
复制代码

接着在activity布局中使用:

<View
    android:id="@+id/view"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_marginTop="50dp"
    android:background="@drawable/test_back" />
复制代码

Drawable的生成

相信大家这个代码非常熟悉了,还有个疑问就是为什么在view设置了setOnClickListener才会有view按下的效果呢,所以带着这些疑问以前看下这些问题,大家都知道view的所有属性是通过定义在attrs文件中的,而view的attrs的name是View:

image.png

紧接着第一个属性就是获取background属性:

image.png
那咱们可以看下drawable是怎么获取的呢,最终会到TypedArray的getDrawableForDensity方法:

@Nullable
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {   
	//省略了代码
    return mResources.loadDrawable(value, value.resourceId, density, mTheme);
}
复制代码

可以看到上面调的是Resource类中的loadDrawable方法:

@NonNull
Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
        throws NotFoundException {
    return mResourcesImpl.loadDrawable(this, value, id, density, theme);
}
复制代码

很简单一句话,直接调了ResourcesImpl的loadDrawable方法:

@Nullable
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
        int density, @Nullable Resources.Theme theme)
        throws NotFoundException {
    //省略代码

    try {

        final boolean isColorDrawable;
        //省略代码
        //如果传过来的是#开头的属性值,直接返回colorDrawable
        Drawable dr;
        if (isColorDrawable) {
            dr = new ColorDrawable(value.data);
        } else {
        	//如果不是则调用该方法
            dr = loadDrawableForCookie(wrapper, value, id, density);
        }
        return dr;
    } catch (Exception e) {
        
    }
}
复制代码

上面代码已经到了最精简的代码了,前面一大堆的工作判断有没有缓存的drawable,如果有直接返回cachedDrawable,如果没有接着判断是不是以#开头的clor颜色值,如果是直接返回colorDrawable,先不说colorDrawable,后面会讲到,该节只是分析drawable的显示流程。那咱们看下loadDrawableForCookie:

private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
        int id, int density) {
    final String file = value.string.toString();

    final Drawable dr;
    try {
        try {
            //注意了如果drawable是一个xml文件定义的走这里
            if (file.endsWith(".xml")) {
                final XmlResourceParser rp = loadXmlResourceParser(
                        file, id, value.assetCookie, "drawable");
                dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
                rp.close();
            } else {
                //否则从asset输入流读取
                final InputStream is = mAssets.openNonAsset(
                        value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                AssetInputStream ais = (AssetInputStream) is;
                dr = decodeImageDrawable(ais, wrapper, value);
            }
        } finally {
            stack.pop();
        }
    } catch (Exception | StackOverflowError e) {
    
    }
    return dr;
}
复制代码

从上面可以看到,我们一般写的xml都是用Drawable.createFromXmlForDensity方法来获取的,接着看下该方法:

@NonNull
public static Drawable createFromXmlForDensity(@NonNull Resources r,
        @NonNull XmlPullParser parser, int density, @Nullable Theme theme)
        throws XmlPullParserException, IOException {
    AttributeSet attrs = Xml.asAttributeSet(parser);
    //省略了判断
    Drawable drawable = createFromXmlInnerForDensity(r, parser, attrs, density, theme);
    return drawable;
}
复制代码

该方法很简单,直接是调用了createFromXmlInnerForDensity方法:

@NonNull
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, attire
            density, theme);
}
复制代码

可以看到通过resources.getDrawableInflater().inflateFromXmlForDensity方法返回的drawable对象,可以直接看下resources.getDrawableInflater()返回的对象:

image.png
很一目了然,获取的是DrawableInfalter对象,还记得LayoutInflater对象吧,它是用来加载布局的,可以看出来各种xml都是通过各种****Inlfater加载出来的,直接看DrawableInfalter的inflateFromXmlForDensity方法:

@NonNull
Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
        @NonNull AttributeSet attrs, int density, @Nullable Theme theme)
        throws XmlPullParserException, IOException {
    //如果xml中根标签是drawable,那么直接解析它的class属性,一般如果是自定义drawable可以这么玩
    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的时候会讲到该方法
    drawable.inflate(mRes, parser, attrs, theme);
    return drawable;
}
复制代码

上面代码逻辑很清晰,如果获取到的标签是drawable,通过class属性获取到drawable对象,如果标签不是drawable通过inflateFromTag方法获取:

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;
    }
}
复制代码

我去,这也太明显了吧,整个drawable的子类都放出来了,像不像工厂方法呢,是的,没错,DrawableInflater类就是Drawable的工厂类,通过标签的name,返回不同的drawable。如果标签的名字是drawable的话,会调用inflateFromClass方法来生成drawable的:

@NonNull
private Drawable inflateFromClass(@NonNull String className) {
    try {
        Constructor<? extends Drawable> constructor;
        synchronized (CONSTRUCTOR_MAP) {
            constructor = CONSTRUCTOR_MAP.get(className);
            if (constructor == null) {
                final Class<? extends Drawable> clazz =
                        mClassLoader.loadClass(className).asSubclass(Drawable.class);
                constructor = clazz.getConstructor();
                CONSTRUCTOR_MAP.put(className, constructor);
            }
        }
        return constructor.newInstance();
    } catch (NoSuchMethodException e) {
        final InflateException ie = new InflateException(
                "Error inflating class " + className);
        ie.initCause(e);
        throw ie;
    } catch (ClassCastException e) {
        // If loaded class is not a Drawable subclass.
        final InflateException ie = new InflateException(
                "Class is not a Drawable " + className);
        ie.initCause(e);
        throw ie;
    } catch (ClassNotFoundException e) {
        // If loadClass fails, we should propagate the exception.
        final InflateException ie = new InflateException(
                "Class not found " + className);
        ie.initCause(e);
        throw ie;
    } catch (Exception e) {
        final InflateException ie = new InflateException(
                "Error inflating class " + className);
        ie.initCause(e);
        throw ie;
    }
}
复制代码

这里就不做过多的解释了,通过反射生成Drawable对象的,关于反射大家可以看我前面写的java反射整理 紧接着调用了drawable.inflate方法。该方法对于后面分析各种Drawable有很大的帮助。

小节

xml中定义的drawable是通过DrawableInflater生成不同的drawable,如果标签直接定义drawable,去解析class属性,class属性是drawable的全类名;否则解析相应的标签生成不同的drawable,比如ColorDrawableStateListDrawableGradientDrawable

通过上面的分析,例子中的Drawable实际是一个StateListDrawable,下面一起来看看他是如何显示到view上的

Drawable与view的关系

上面已经讲了drawable是如何通过xml生成drawable,下面要将的是Drawable是怎么作用到view上,在前面说到view中通过TypeArray.getDrawable获取到Background是一个StateListDrawable,后面在view四个参数的构造方法中设置了background:

if (background != null) {
    setBackground(background);
}
复制代码

接着调用了下面该方法:

public void setBackground(Drawable background) {
    //noinspection deprecation
    setBackgroundDrawable(background);
}
复制代码
@Deprecated
public void setBackgroundDrawable(Drawable background) {
    computeOpaqueFlags();

    if (background == mBackground) {
        return;
    }

    boolean requestLayout = false;

    mBackgroundResource = 0;

    //1.销毁之前用到的drawable
    if (mBackground != null) {
        if (isAttachedToWindow()) {
            mBackground.setVisible(false, false);
        }
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }

    if (background != null) {
       	//2.需要刷新view的位置时候
        if (mBackground == null
                || mBackground.getMinimumHeight() != background.getMinimumHeight()
                || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
            requestLayout = true;
        }

        mBackground = background;
        if (background.isStateful()) {
        	//3.getDrawableState主要是获取到drawable状态的全集
            background.setState(getDrawableState());
        }
        if (isAttachedToWindow()) {
            background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
        }
        //添加着色的代码
        applyBackgroundTint();

        // 对当前的drawable设置回调
        background.setCallback(this);
    } else {
        //如果当前background为空,需要重新更新view在页面上的位置
        mBackground = null;
        requestLayout = true;
    }

    computeOpaqueFlags();

    if (requestLayout) {
        requestLayout();
    }

    mBackgroundSizeChanged = true;
    //重新走绘制
    invalidate(true);
    invalidateOutline();
}
复制代码

可以看到上面先是对之前的background进行销毁,调用了drawable.setCallback(null),可以看到将当前view实例传给了drawable对象:

public final void setCallback(@Nullable Callback cb) {
    mCallback = cb != null ? new WeakReference<>(cb) : null;
}
复制代码

此处看到了没,将view实例通过弱引用包装起来了,防止drawable长时间不释放view实例,所以在不用drawable的时候务必调用下setCallback(null)防止内存泄漏

紧接着注释三处通过drawable.isStateful来判断需要给drawale加各种状态不,在drawable默认中实现了isStateful方法:

 /**
  * 该drawable是否根据state来更改样式
  *
  */
 public boolean isStateful() {
     return false;
 }
复制代码

咋们看下ColorDrawable和StateListDrawable下是怎么实现该方法的:

//stateLIstDrawable直接返回true,说明它是根据状态改变样式的drawable
@Override
public boolean isStateful() {
    return true;
}
复制代码
//colorDrawable会根据mColorState.mTint.isStateful()来判断是不是根据状态来变样式
//mColorState.mTint是colorStateLIst对象
@Override
public boolean isStateful() {
    return mColorState.mTint != null && mColorState.mTint.isStateful();
}
复制代码
@Override
public boolean isStateful() {
    return mStateSpecs.length >= 1 && mStateSpecs[0].length > 0;
}
复制代码

判断mStateSpecs长度大于1,并且第一个length大于0

public ColorStateList(int[][] states, @ColorInt int[] colors) {
    mStateSpecs = states;
    mColors = colors;
    onColorsChanged();
}
复制代码

mStateSpecs数组是根据states二维数组传过来的,这里写一个简单的例子来看看isStateful方法的说明:

int pressed = Color.RED;
int focused = Color.RED;
int normal = Color.BLACK;
int unable = Color.GRAY;
//颜色数组值要和状态值对应上
int[] colors = new int[]{pressed, focused, normal, focused, unable, normal}
int[][] states = new int[6][];
//定义6个状态的数组
states[0] = new int[]{android.R.attr.state_pressed, android.R.attr.state_en
states[1] = new int[]{android.R.attr.state_enabled, android.R.attr.state_fo
states[2] = new int[]{android.R.attr.state_enabled};
states[3] = new int[]{android.R.attr.state_focused};
states[4] = new int[]{android.R.attr.state_window_focused};
states[5] = new int[]{};
ColorStateList colorStateList = new ColorStateList(states, colors);
ColorDrawable colorDrawable = new ColorDrawable();
//可以通过该方法设置colorStateList,先只需要知道用就行,后面会讲到
colorDrawable.setTintMode(PorterDuff.Mode.ADD);
colorDrawable.setTintList(colorStateList);
View test = findViewById(R.id.test);
test.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});
test.setBackground(colorDrawable);
boolean stateful = colorDrawable.isStateful();
Log.d(TAG, "stateful:" + stateful);
复制代码

此时获取到的stateful是true,再来看不加状态值的时候,代码改成如下:

ColorDrawable colorDrawable = new ColorDrawable(Color.BLACK);
View test = findViewById(R.id.test);
test.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});
test.setBackground(colorDrawable);
boolean stateful = colorDrawable.isStateful();
Log.d(TAG, "stateful:" + stateful);
复制代码

看到上面获取的stateful为false,因为此时我们没有传state的数组,所以验证了上面的代码。关于isStateful方法说明到这里,下面继续回到 setBackgroundDrawable方法的background.setState(getDrawableState())这一行,大家可以看下drawable.setState方法:

public boolean setState(@NonNull final int[] stateSet) {
    if (!Arrays.equals(mStateSet, stateSet)) {
        mStateSet = stateSet;
        return onStateChange(stateSet);
    }
    return false;
}
复制代码

该方法表示当前drawable是什么状态的,如果状态不一样,则会触发到onStateChange方法,drawable默认的onStateChange是一个空的实现,因此需要子类自己实现,大家这会只需要知道这么个流程,后面会仔细介绍drawable的子类时候再讲该方法,继续看view的getDrawableState方法:

public final int[] getDrawableState() {
      //如果已经获取过mDrawableState的状态值,并且mPrivateFlags等于标志
    if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
        return mDrawableState;
    } else {
        mDrawableState = onCreateDrawableState(0);
        mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
        return mDrawableState;
    }
}
复制代码

刚开始mDrawableState变量为空,那么此时通过onCreateDrawableState方法来获取mDrawableState

protected int[] onCreateDrawableState(int extraSpace) {
    int[] drawableState;
    int privateFlags = mPrivateFlags;
    int viewStateIndex = 0;
    //下面这些操作都是通过当前view的状态来给viewStateIndex设置不同状态的值
    if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED
    if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENAB
    if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED;
    if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= StateSet.VIEW_STATE_SELECT
    if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED;
    if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIV
    if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested &&
            ThreadedRenderer.isAvailable()) {
        viewStateIndex |= StateSet.VIEW_STATE_ACCELERATED;
    }
    if ((privateFlags & PFLAG_HOVERED) != 0) viewStateIndex |= StateSet.VIEW_STATE_HOVERED
    final int privateFlags2 = mPrivateFlags2;
    if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0) {
        viewStateIndex |= StateSet.VIEW_STATE_DRAG_CAN_ACCEPT;
    }
    if ((privateFlags2 & PFLAG2_DRAG_HOVERED) != 0) {
        viewStateIndex |= StateSet.VIEW_STATE_DRAG_HOVERED;
    }
    //获取到上面的不同状态的值后,获取到当前drawable的状态,以此来设置drawable在不同状态下的样式
    drawableState = StateSet.get(viewStateIndex);
   
    if (extraSpace == 0) {
        return drawableState;
    }
    //省略代码
}
复制代码

上面的操作是通过全局mPrivateFlags标志获取到各种状态下的索引,并且位运算或放到viewStateIndex中,最后通过StateSet.get(viewStateIndex)赋值给drawableState。关于StateSet.get(viewStateIndex)获取到的都是R.attr.state_****,关于通过view的状态设置drawable的状态就是这么来的,下面继续回到view的setBackgroundDrawable方法,接着会调用applyBackgroundTint方法,

private void applyBackgroundTint() {
    if (mBackground != null && mBackgroundTint != null) {
        //如果view设置了backgroundTint属性,那么mBackgroundTint就不会为空
        final TintInfo tintInfo = mBackgroundTint;
        if (tintInfo.mHasTintList || tintInfo.mHasTintMode) {
            mBackground = mBackground.mutate();
            if (tintInfo.mHasTintList) {
                //传入colorStateList实现效果
                mBackground.setTintList(tintInfo.mTintList);
            }
            if (tintInfo.mHasTintMode) {
                //设置tintMode
                mBackground.setTintMode(tintInfo.mTintMode);
            }
            if (mBackground.isStateful()) {
                mBackground.setState(getDrawableState());
            }
        }
    }
}
复制代码

看到上面的代码是不是很熟悉上面事例中setTintListsetTintMode的使用,没错上面代码也可以通过xml来实现setTintList的效果:

//定义一个drawable文件,实质是一个StateListDrawable,名字叫test.xml:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/holo_red_dark" android:state_enabled="true" android:state_pressed="true" />
    <item android:drawable="@android:color/holo_red_dark" android:state_enabled="true" android:state_focused="true" />
</selector>
复制代码

在相应的view布局上使用如下:

<View
    android:id="@+id/test"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@color/black"
    android:backgroundTint="@drawable/test"
    android:backgroundTintMode="add"/>
复制代码

简单来说,backgroundTint是往background上着色,相当于网上涂层,后面细讲drawable的时候,会说到他的各种子类的情况是如何控制backgroundTint的。继续回到view的setBackgroundDrawable方法上来,上面说完了applyBackgroundTint方法,后面紧接着到了background.setCallback(this),此处是控制drawable绘制的回调,将view当前的实例传给drawable。此时drawable的setState、setTintList、setCallback都已经完成了,紧接着就是绘制了,因此在最后调用了invalidate(true),最终会触发view的重新绘制了。

view绘制怎么绘制drawable

大家知道view的绘制方法在draw---->onDraw,draw里面做的就是系统的一些绘制,而onDraw是view的子类绘制的方法,所以我们一般写的绘制都是在onDraw里面,下面来看看draw里面绘制background,可以看到draw里面有一句drawBackground(canvas):

private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    //设置drawable的大小,这个很重要
    setBackgroundBounds();

    //省略代码
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    //最终会走drawable.draw方法,因此drawable的绘制,其实是drawable自己完成的
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}
复制代码

上面绘制drawable很简单,首先调用了setBackgroundBounds方法,然后调用了drawable.draw(canvas),最终的绘制还是交给了drawable自己去绘制。下面看下setBackgroundBounds方法:

void setBackgroundBounds() {
    if (mBackgroundSizeChanged && mBackground != null) {
        //设置drawable的大小,大小其实就是view的大小
        mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}
复制代码

可以看到setBackgroundBounds其实是调用了mBackground.setBounds方法,在drawable的setBounds方法中只是设置了全局的mBounds变量:

public void setBounds(int left, int top, int right, int bottom) {
    Rect oldBounds = mBounds;

    if (oldBounds == ZERO_BOUNDS_RECT) {
        oldBounds = mBounds = new Rect();
    }

    if (oldBounds.left != left || oldBounds.top != top ||
            oldBounds.right != right || oldBounds.bottom != bottom) {
        if (!oldBounds.isEmpty()) {
            // first invalidate the previous bounds
            invalidateSelf();
        }
        mBounds.set(left, top, right, bottom);
        onBoundsChange(mBounds);
    }
}
复制代码

所以说drawable中只是设置了个全局变量mBounds,供子类使用,所以从这里看得出来,如果要使用drawable必须调用drawable的setBounds方法,要不然在绘制drawable的时候显示不出来。到最后就是background的绘制了,也就是drawable.draw(canvas)方法,可以看出来drawable是不负责绘制的,绘制工作都交给了自己的子类。

view点击的时候drawable状态改变

view的点击都是在ontouchEvent里面,我们主要看关键点就行

//手指在抬起的时候,并且mPrivateFlags等于PFLAG_PRESSED标志
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    setPressed(false);
}
复制代码

这里可以看出来,如果手抬起来了,并且状态是PFLAG_PRESSED,调用了setPressed(false),看看该方法:

public void setPressed(boolean pressed) {
    //这句标明在按下的时候needsRefresh=true,因为刚开始mPrivateFlags&PFLAG_PRESSED!=PFLAG_PRESSED
    //因此后面为false,当pressed=true的时候,那么needsRefresh=true,反之pressed=false的时候,
    //因此此时mPrivateFlags&PFLAG_PRESSED=PFLAG_PRESSED,因此needsRefresh还是为true
    //所以在按下和松开view的时候needsRefresh都为true
    final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
    //如果按下了mPrivateFlags|PFLAG_PRESSED,那此时mPrivateFlags=PFLAG_PRESSED了
    //反之松开的时候mPrivateFlags!=PFLAG_PRESSED了
    if (pressed) {
        mPrivateFlags |= PFLAG_PRESSED;
    } else {
        mPrivateFlags &= ~PFLAG_PRESSED;
    }
    //如果needsRefresh为true,此时需要刷新drawable
    if (needsRefresh) {
        refreshDrawableState();
    }
    dispatchSetPressed(pressed);
}
复制代码

注释写得很清楚了,抬起和按下view都会触发refreshDrawableState方法,并且在按下的时候mPrivateFlags= PFLAG_PRESSED,抬起的时候mPrivateFlags!= PFLAG_PRESSED,下面来看看refreshDrawableState方法:

public void refreshDrawableState() {
    mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
    drawableStateChanged();

    ViewParent parent = mParent;
    if (parent != null) {
        parent.childDrawableStateChanged(this);
    }
}
复制代码

接着看drawableStateChanged方法:

protected void drawableStateChanged() {
    //可以看到先去根据getDrawableState获取到drawable的状态,然后调用了各个drawable的setState方法
    final int[] state = getDrawableState();
    boolean changed = false;
    //首先就是设置background的state
    final Drawable bg = mBackground;
    if (bg != null && bg.isStateful()) {
        changed |= bg.setState(state);
    }

    //省略代码
    //foreground的state,后面会讲到foreground水波效果是怎么形成的
    final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (fg != null && fg.isStateful()) {
        changed |= fg.setState(state);
    }
    //省略代码
    
    //最后刷新view来调用view的draw方法,最后调用drawable的draw方法
    if (changed) {
        invalidate();
    }
}
复制代码

前面讲过,getDrawableState方法会根据当前view的状态的flag值,去StateSet类中去找各个状态下对应的drawable的state,找到后,调用drawable的setState方法,在setState中会通过mStateSet和传进来的stateSet对比,如果两者相等,那么触发drawable的onStateChange方法,后面讲drawable的时候细讲,先明白是设置state就行。上面可以看到foreground设置背景也是这么干的,后面会介绍foreground是怎么来的。最后调用了invalidate方法,重新绘制drawable。上面在整个ontouchEvent的up事件中调用了setPress(false),那什么时候调用的setPress(true)的呢,很简单,在view的down事件中触发的,回到ontouchEvent里面,在里面有这么一句if (clickable || (viewFlags & TOOLTIP) == TOOLTIP)也就是说只有在clickable为true的情况下,drawable点击才有效果,所以在开篇的例子中,只有设置了view的监听器,drawable点击样式才起效果。关于view的事件传递可以看我之前写的该篇android中view事件传递,在view的down事件中有这么一句setPressed(true, x, y),也就是会触发drawable按下的时候样式。 在上面我们提到过drawable.,setbackCall(this),这句代码,此处是把view的事例传给了drawable,在drawale的invalidateSelf方法中会回调到view:

//该方法是drawale的重绘方法,回调到view的invalidateDrawable方法
public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}
复制代码

看下view中回调的invalidateDrawable方法:

@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();
    }
}
复制代码

看到了没,实际上调用了view的重绘,而view的重绘又会调用drawable的draw方法,因此上面的drawable.setbackCall(this),最终是绘制自己。好了,介绍到这里,drawable显示到view上基本梳理完了,后面还会介绍foreground是怎么有波纹点击效果,以及介绍drawable在xml中定义的state_****是如何加载到drawale的state上来的,这个就涉及到xml解析了,其实跟xml布局的解析是差不多的,后面还会介绍各种drawable的使用,以及如何自定义一个drawable。

总结

  • xml中的各种drawable,其实每一个标签对应了各种drawable,该工作交给了DrawableInflater来完成的
  • 解析到各种drawable后,会触发drawable的inflate方法,后面会讲drawable的state获取时会讲到
  • view中backgroundforeground其实都是对应的drawable,如果我们设置的background只是一个color,那么此时background就是一个colorDrawable,如果是一个selector标签的话,此时是一个StateListDrawable
  • background的isStateful方法判断drawable是不是带有state的drawble。
  • drawable的setState方法是设置drawable当前的state的
  • drawable.setTintList是设置drawable的着色,它的实质是给paint设置setColorFilter,关于ColorFilter有三个子类,后面会讲到
  • drawable.setTintMode是设置上面着色的模式,关于模式后面也会讲到
  • 在view按下和松开的时候会通过view的FLAG_PRESSED来给drawble设置state_pressed状态,最终调用到drawable的draw方法,drawable的draw方法需要子类自己去实现。
  • drawable.setCallback(this)是将view的事例传到drawable的WeakReference弱引用包装起来了,在invalidateSelf方法中会触发callback的invalidateDrawable方法,从而回调到view的重绘,而最终会重新调用了drawable的draw方法,从而达到drawble的重新绘制。

关于drawable的打造可以看我另外一篇实战仿苹果版小黄车(ofo)app主页菜单效果

文章分类
Android
文章标签