- Drawable 可以方便做出一些特殊的 UI 效果,它比自定义 View 的成本要低,非图片类的 Drawable 占用空间较小,这对减小 apk 的大小很有帮助。
- Drawable 通过 getIntrinsicWidth 和 getIntrinsicHeight 获取 Drawable 的内部宽高,比如一张图片形成的 Drawable 内部宽高就是图片的宽高,而一个颜色所形成的 Drawable 就没有内部宽高的概念,Drawable 的内部宽高不等同于他的大小。
- 在实际开发中, Drawable 常被作为 View 的背景使用,Drawable 会被拉伸至 View 同等大小。
- Drawable 通常被用作 View 的背景或者在 ImageView 等控件显示。
- 在 ImageView 中显示,xml 文件中设置 'android:src' 属性,或者在代码中调用
setImageDrawable()
方法,getDrawable()
方法获取到 Drawable。 - 作为 View 背景,在 xml 文件中设置
android:background
属性,或者在代码中调用setBackground
方法,通过getBackground
方法获取背景 Drawable;
Drawable
Drawable 是一个抽象类,提供了一些 API 方法去处理各种资源的绘制,但是又不具备 View 的事件与交互处理能力。再简单粗暴一点认为就是一个辅助绘制工具类,把各种东西都封装搞好以后直接给 Canvas 去画。
Drawable 类中的几个重要的方法
Drawable 类有四个抽象方法子类必须实现:
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,setColorFilter 等方法控制;
- setAlpha :给 Drawable 指定一个 alpha 值,在 0 - 255 之间;
- setColorFilter:设置滤镜效果,有时我们会在 Drawable 内部定义一个 Paint 对象,所以该方法的实现可以为
mPaint.setColorFilter(colorFilter)
; - getOpacity :返回 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 setBounds 方法中新旧 bounds 发生变化时回调,默认为空方法;
public int getIntrinsicWidth();
public int getIntrinsicHeight();
获取 Drawable 的内部宽高,包含 padding,一张图片形成的 Drawable 内部宽高就是图片的宽高,不同的 Drawable 子类是有不同的实现的,而一个颜色所形成的 Drawable 就没有内部宽高的概念,在用作 View 的 background 时自动拉伸为 View 大小。
public int getMinimumWidth()
public int getMinimumHeight()
返回 Drawable 建议的最小宽高,View 用作背景时要大于该最小宽高,默认的返回为内部宽高或 0;
Drawable 调用流程浅析
Drawable 一般用作 View 的背景,在基类 View 中声明了一个成员变量 Drawable mBackground
作为 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、我们也可以在代码中直接调用一系列 setBackgroundXXX 方法:
public void setBackgroundColor(@ColorInt int color);
public void setBackgroundResource(@DrawableRes int resid);
public void setBackground(Drawable background)
最终其实都是调用了 setBackgroundDrawable
方法:
public void setBackgroundDrawable(Drawable background) {
......
//每次设置新的background后就进行复位操作
if (mBackground != null) {
if (isAttachedToWindow()) {
//Drawable设置为不可见(这部就用上上面Drawable的分析了么)
mBackground.setVisible(false, false);
}
//Drawable回调断开(这部就用上上面Drawable的分析了么)
mBackground.setCallback(null);
//移除Drawable绘制队列,实质触发回调的该方法(这部就用上上面Drawable的分析了么)
unscheduleDrawable(mBackground);
}
if (background != null) {
//当有设置背景Drawable
......
//给Drawable设置View当前的布局方向(这部就用上上面Drawable的分析了么)
background.setLayoutDirection(getLayoutDirection());
//判断当前Drawable是否设置有padding(这部就用上上面Drawable的分析了么,有padding则返回true)
if (background.getPadding(padding)) {
//依据Drawable的这个padding去给当前View相关padding属性建议修改
......
}
//比较上次旧的(可能没有)和现在设置的Drawable的最小宽高,发现不一样就预示着我们接下来需要对View再次layout,做标记
if (mBackground == null
|| mBackground.getMinimumHeight() != background.getMinimumHeight()
|| mBackground.getMinimumWidth() != background.getMinimumWidth()) {
requestLayout = true;
}
//把要新设置的Drawable正式赋值给View的mBackground成员
mBackground = background;
//判断当状态改变时当前Drawable是否需要切换图片,一般在StateListDrawable实现中为true(这部就用上上面Drawable的分析了么)
if (background.isStateful()) {
//设置Drawable状态(这部就用上上面Drawable的分析了么)
background.setState(getDrawableState());
}
//如果View已经attached to window了就把Drawable设置为可见(这部就用上上面Drawable的分析了么)
if (isAttachedToWindow()) {
background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
}
//设置Drawable的callback,在View继承关系中有实现Drawable的callback
background.setCallback(this);
......
} else {
//当没有设置背景Drawable时清空背景Drawable,然后设置View的重新layout标志
mBackground = null;
......
requestLayout = true;
}
......
//需要重新布局,触发重新布局
if (requestLayout) {
requestLayout();
}
//通知重新绘制刷新操作
mBackgroundSizeChanged = true;
invalidate(true);
invalidateOutline();
}
每逢我们对控件通过 xml 或者 java 设置 background 后触发的其实就是上面这一堆操作,实质最后触发的就是 layout 或者 draw。在 View 的 draw 方法中调用 drawBackground 方法:
public void draw(Canvas canvas) {
/*
* 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)
*/
......
drawBackground(canvas);
......
}
private void drawBackground(Canvas canvas) {
......
//实质调用了Drawable的setBounds方法,把当前View测量好的矩形区域顶点赋值给Drawable,说明接下来Drawable绘制区域与View大小相同。
//该方法实质:mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
setBackgroundBounds();
//简单粗暴理解就是View要是能滑动,化到哪Drawable背景画到哪(其实就是每次滑动都按可见区域绘制Drawable,因为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方法,传入的是当前View的canvas而已。
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
最终调用了 mBackground 的 draw 方法完成绘制。
参考:
Android 应用层开发 Drawable 的一些叨叨絮