1 Drawable可以方便做出一些特殊的UI效果,它比自定义View的成本要低,非图片类的Drawable占用空间较小,这对减小apk的大小很有帮助。
2 Drawable通过获取Drawable的宽高,比如一张图片形成的Drawable内部宽高就是图片的宽高,而一个颜色形成的Drawable就没有内部宽高的概念,Drawable的内部宽高不等于它的大小。
public int getIntrinsicWidth() {
return -1;
}
/**
* Return the intrinsic height of the underlying drawable object. Returns
* -1 if it has no intrinsic height, such as with a solid color.
*/
public int getIntrinsicHeight() {
return -1;
}
3 在实际开发中,Drawable常被作为View的背景使用,Drawable会被拉伸至View同等大小。 4 Drawable通常被用作View的背景或者在ImageView等控件显示。 5 在ImageView中显示,xml文件中设置'android:src'属性,或者在代码中调用setImageDrawable()方法,getDrawable()方法获取到Drawable。 6 作为View背景,在xnl文件中设置android:background属性,或者在代码中调用setBackground方法,通过getBackground方法获取背景Drawable。
它是一个抽象类,提供了一些API方法去处理各种资源的绘制,但是又不具备View的事件与交互处理能力。可以简单认为是一个辅助绘制工具类,把各种东西都封装好直接给Canvas去画。
它有4个抽象方法子类必须实现:
public abstract void draw(Canvas canvas);
public abstract void setAlpha(@IntRange(from=0,to=255) int alpha);
public abstract void setColorFilter(ColorFilter colorFilter);
public abstract @PixelFormat.Opacity int getOpacity();
draw:在setBounds方法设置的区域内的Canvas中进行Drawable的绘制。 setAlpha:给Drawable指定一个alpha值,在0—255之间。 setColorFilter:设置滤镜效果 getOpactiy:返回Drawable的透明度,取值为PixelFormat.UNKNOWN,PixelFormat.TRANSLUCENT,PixelFormat.TRANSPARENT,PixelFormat.OPAQUE 中的一个。
public void setBounds(int left, int top, int right, int bottom);
public void setBounds(@NonNull Rect bounds);
protected void onBoundsChange(Rect bounds)
setBounds设置绘制绘制区域边界,draw方法调用时会用到其设置的值,不设置默认边界均为0,所以自定义Drawable时要重写该方法。 onBoundsChange 方法中新旧bounds方法变化时回调,默认为空方法。
public int getIntrinsicWidth();
public int getIntrinsicHeight();
获取Drawable的内部宽高,它是包含padding,一张图片形成的Drawable内部宽高就是图片的宽高,不同的Drawable子类是有不同的实现的,而一个颜色所形成的Drawable就没有内部宽高的概念,在用作View的background时自动拉伸成View大小。
下面我们来看下Drawable调用流程 Drawable一般用作View的背景,在基类View中声明了一个成员变量Drawable mBackgrounf作为View的背景。
1 在xml文件中通过android:background属性给View设置背景
//View构造方法中间省略代码
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
......
//获取View的属性
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
//定义一个局部背景变量Drawable
Drawable background = null;
......
//获取到设置的background属性,也就是我们平时给各个控件在xml中设置的android:background属性值
case com.android.internal.R.styleable.View_background:
background = a.getDrawable(attr);
break;
......
//当设置有背景Drawable属性时调用setBackground方法
if (background != null) {
setBackground(background);
}
......
}
这里获取xml中设置的背景后最终调用setBackground方法。 2 我们可以在代码中直接调用一系列的setBackgroundXX方法:
public void setBackgroundColor(@ColorInt int color) {
if (mBackground instanceof ColorDrawable) {
((ColorDrawable) mBackground.mutate()).setColor(color);
computeOpaqueFlags();
mBackgroundResource = 0;
} else {
setBackground(new ColorDrawable(color));
}
}
public void setBackgroundResource(@DrawableRes int resid) {
if (resid != 0 && resid == mBackgroundResource) {
return;
}
Drawable d = null;
if (resid != 0) {
d = mContext.getDrawable(resid);
}
setBackground(d);
mBackgroundResource = resid;
}
都是最终调用setBackground方法,而它又是调用setBackgroundDrawable方法
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;
/*
* Regardless of whether we're setting a new background or not, we want
* to clear the previous drawable.
*/
//每次设置新的background时先进行复位操作
if (mBackground != null) {
//Drawable回调断开
mBackground.setCallback(null);
//移除Drawable绘制队列,实质触发回调的该方法
unscheduleDrawable(mBackground);
}
if (background != null) {//当有设置背景Drawable
Rect padding = sThreadLocal.get();
if (padding == null) {
padding = new Rect();
sThreadLocal.set(padding);
}
resetResolvedDrawablesInternal();
//给Drawable设置View当前的布局方向
background.setLayoutDirection(getLayoutDirection());
//判断当前Drawable是否设置有padding
if (background.getPadding(padding)) {
//这里面代码就是依据Drawable的这个padding去给当前View相关padding属性进行修改
resetResolvedPaddingInternal();
switch (background.getLayoutDirection()) {
case LAYOUT_DIRECTION_RTL:
mUserPaddingLeftInitial = padding.right;
mUserPaddingRightInitial = padding.left;
internalSetPadding(padding.right, padding.top, padding.left, padding.bottom);
break;
case LAYOUT_DIRECTION_LTR:
default:
mUserPaddingLeftInitial = padding.left;
mUserPaddingRightInitial = padding.right;
internalSetPadding(padding.left, padding.top, padding.right, padding.bottom);
}
mLeftPaddingDefined = false;
mRightPaddingDefined = false;
}
// Compare the minimum sizes of the old Drawable and the new. If there isn't an old or
// if it has a different minimum size, we should layout again
//比较上次(可能没有)和现在设置的Drawable的最小高度,发现不一样就需要给requestLayout标记赋值为true。
if (mBackground == null
|| mBackground.getMinimumHeight() != background.getMinimumHeight()
|| mBackground.getMinimumWidth() != background.getMinimumWidth()) {
requestLayout = true;
}
//设置Drawable的callback,在View继承关系中有实现Drawable的callback
background.setCallback(this);
if (background.isStateful()) {
background.setState(getDrawableState());
}
background.setVisible(getVisibility() == VISIBLE, false);
mBackground = background;
applyBackgroundTint();
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
requestLayout = true;
}
} else {
/* Remove the background */
mBackground = null;
if ((mViewFlags & WILL_NOT_DRAW) != 0
&& (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
/*
* When the background is set, we try to apply its padding to this
* View. When the background is removed, we don't touch this View's
* padding. This is noted in the Javadocs. Hence, we don't need to
* requestLayout(), the invalidate() below is sufficient.
*/
// The old background's minimum size could have affected this
// View's layout, so let's requestLayout
requestLayout = true;
}
computeOpaqueFlags();
//如果需要重新布局,触发重新布局
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
//通知重新绘制刷新操作
invalidate(true);
}
每次当我们对控件通过xml或者java设置background后触发的其实就是上面这一堆操作,实质最后触发的就是layout或者draw。在View的draw方法中调用drawBackground方法。
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
//实质调用了Drawable的setBounds方法,把当前View测量好的矩形区域顶点赋值给Drawable,说明接下来Drawable绘制区域与View大小相同。
setBackgroundBounds();
// Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mHardwareRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
//View要是能滑动,当View滑到哪里,Drawable背景画到哪(其实就是每次滑动都按照可见区域绘制Drawabl,Canvas在平移了)
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
//这就是Drawable的draw方法,最后看来View会调用Drawable的draw方法
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
void setBackgroundBounds() {
if (mBackgroundSizeChanged && mBackground != null) {
mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
}
学习Drawable好处,可以在自定义View时如果能用Drawable实现会不会更简单,更优化呢?
这个效果是某个APP一张图标上左边有个角标,右边有个图标加文字,同时顶部有一段渐变阴影。如果我们当初直接用一个相对布局,顶部采用ImageView放一段阴影,左上角再放一个ImageView,右上角放一个TextView,使用drawableLeft设一个图标。
但是我们如果对Drawable理解够深刻,它是可以绘制任何东西的,毕竟重写的draw方法都给你canvas了。在平时的开发过程中要将以前固定的思维方式转向到Drawable上面来。
这样我们可以将这几个View都转换成一个自定义Drawable,这样一来View的数量少了,View的层级也减少了,性能也优化了,代码也更优雅了。(但一般而言图片上面叠加图片的推荐使用一个Drawable来自己画,比如阴影,角标等。如果遇到文字很多,那还是建议用TextView来实现)。
Drawable系统中有很多子类可以简化开发。
LayerDrawable 包含多个Drawable的Drawable,是一个可以管理一组drawable对象的drawable。在LayerDrawable的drawable资源按照列表的顺序绘制,所以列表的最后绘制在最上层。
badgeForeground下面垫了一个bdgeColor颜色背景。
InsetDrawale有时候我们希望点击区域不只是图片本身的大小,那么这个Drawable就可以设置外面一圈空白区域来调整Drawable实际大小,让点击更轻松。
VectorDrawable定义一个静态的drawable,类似svg格式,每个矢量图被定义成由path和group对象构成的树状结构。 每个path包含了对象的几何轮廓,group包含了变化的具体规则,所有的path会按照xml中定义的顺序依次绘制。
还有很多系统Drawable等等。主要还是思维的转换,脑子里要想到Drawable这个概念。