android中drawable显示到view上的过程
前段时间一直整理java方面的知识了,先过渡一段时间到android上面来,后期还是会整理java相关的东西,至于整理什么方面的,还没想好。好了,先不说废话了,还是回到正片上来,说说android中用得比较多的drawable类,drawable类是一个抽象的类,其实我们平常开发的阶段用的就是它的各种子类,比如有ColorDrawable
、BitmapDrawable
等等,后面所有相关的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:
紧接着第一个属性就是获取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> class =
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 i.e
} 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 i.e
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
final InflateException ie = new InflateException(
"Class not found " + className);
ie.initCause(e);
throw i.e
} catch (Exception e) {
final InflateException ie = new InflateException(
"Error inflating class " + className);
ie.initCause(e);
throw i.e
}
}
这里就不做过多的解释了,通过反射生成Drawable对象的,关于反射大家可以看我前面写的java反射整理
紧接着调用了drawable.inflate方法。该方法对于后面分析各种Drawable有很大的帮助。
小节
xml中定义的drawable是通过DrawableInflater生成不同的drawable,如果标签直接定义drawable,去解析class属性,class属性是drawable的全类名;否则解析相应的标签生成不同的drawable,比如
ColorDrawable
、StateListDrawable
、GradientDrawable
。
通过上面的分析,例子中的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:" + stageful);
此时获取到的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:" + stageful);
看到上面获取的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());
}
}
}
}
看到上面的代码是不是很熟悉上面事例中setTintList
和setTintMode
的使用,没错上面代码也可以通过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中
background
,foreground
其实都是对应的drawable,如果我们设置的background
只是一个color,那么此时background就是一个colorDrawable
,如果是一个selector标签的话,此时是一个StateListDrawable
。 - background的isStateful方法判断drawable是不是带有state的drawl。.;
- 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主页菜单效果
.;.;.;