0%

与 Drawable 相关的知识点

Drawable

Drawable 是绘制的基础单元,但它没有事件机制,不想 View 那么复杂。Drawable 的基本机制或属性包括

  • 使用 Bound 和 Padding 确定绘制的范围 , 且 Drawable 必须确定 Bound
  • 使用 Outline 确定轮廓,默认情况下与 Bound 相同
  • 使用 State 记录状态
  • 使用 Level(0-10000) 赋予一定的动态性,如定义变化中的进度条等
  • 使用 Callback 回调接口实现动画效果,与 View 的 invalidate() 方法配合使用
  • 绘制机制,需要实现抽象的 draw 方法

例如子类 BitmapDrawable 的绘制方法是绘制 Bitmap,且通过Bitmap来确定 Bound 和 Outline,其额外的属性(如抗锯齿等)均通过设置 Paint 对象和 Shader 对象来生效。

再比如子类 ColorDrawable 的本质是使用指定颜色绘制矩形。

1
2
3
4
5
@Override
public void draw(Canvas canvas) {
mPaint.setColor(mColorState.mUseColor);
canvas.drawRect(getBounds(), mPaint);
}

这些 Drawable 也可以用 XML 文件定义的,系统最终要使用 XML解析器将其解析成 Drawable 对象。

关于绘制,Drawable 还有以下效果设置

1
2
3
4
5
6
7
8
9
10
@IntRange(from=0,to=255) public int getAlpha();		//1.透明度
public void setXfermode(@Nullable Xfermode mode); //2. Xfermode
public abstract void setColorFilter(@Nullable ColorFilter colorFilter); //3.ColorFilter
public void setTint(@ColorInt int tintColor); //4.染色
public void setTintList(@Nullable ColorStateList tint) //4.染色 ColorStateList
public void setHotspot(float x, float y); //5.热点
public void setHotspotBounds(int left, int top, int right, int bottom); //5.热点区域
public @Nullable Region getTransparentRegion(); //6.透明区域
public abstract @PixelFormat.Opacity int getOpacity(); //7.设置像素格式,如是否包含表示透明度的bit
public @NonNull Drawable getCurrent(); //8.在 Drawable 容器中获取 Drawable

GradientDrawable与ShapeDrawable

GradientDrawable 可以绘制四种类型,并带有渐变和圆角特征,它与ShapeDrawable类似。

ShapeDrawable 即绘制“形状”,其绘制实际是“形状”对象(Shape)来代理

1
2
3
4
5
6
7
8
9
10
@Override
public void draw(Canvas canvas) {
final Rect r = getBounds();
final ShapeState state = mShapeState;
final Paint paint = state.mPaint;
final int count = canvas.save();
canvas.translate(r.left, r.top);
shape.draw(canvas, paint);
canvas.restoreToCount(count);
}

Shape 的基本形状是 RectShape ,其绘制实现如下

1
2
3
4
@Override
public void draw(Canvas canvas, Paint paint) {
canvas.drawRect(mRect, paint);
}

如果设置了 corners 标签,则使用子类 PaintDrawable 来解析

1
2
3
4
public void setCornerRadii(float[] radii) {
setShape(new RoundRectShape(radii, null, null));
invalidateSelf();
}

可见四角是通过圆角矩形这个形状(RoundRectShape)来实现的, 这个将在 Path 中详细讲。

DrawableWrapper

DrawableWrapper 表示对单个 Drawable 的包装,类似于装饰器模式。

1
2
3
4
public abstract class DrawableWrapper extends Drawable implements Drawable.Callback {
private DrawableWrapperState mState;
private Drawable mDrawable;
}

对 DrawableWrapper 的设置实际上都在处理被包装的 Drawable,包括最终的绘制

1
2
3
4
@Override
public void draw(@NonNull Canvas canvas) {
mDrawable.draw(canvas);
}
  • InsetDrawable 实际是通过 padding 来实现缩进的。
  • ScaleDrawable 实际会按照比例重新计算 Bound
  • RotateDrawable 很容易想到旋转是通过 canvas 来实现的。
  • ClipDrawable 主要要指定剪切的方向和 level,其绘制与 level 有关,需要该值计算出边界,再用canvas 的 clipRect() 方法来裁剪它。

DrawableContainer

DrawableContainer 表示 Drawable 的集合,但只显示一张,同样是装饰器模式。

1
2
3
4
5
@Override
public void draw(Canvas canvas) {
mCurrDrawable.draw(canvas);
mLastDrawable.draw(canvas);
}

关于如何在众多 Drawable 中选择一张,DrawableContainer 有如下子类

  1. LevelListDrawable
1
2
3
4
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/checked" android:minLevel="0" android:maxLevel="100" />
<item android:drawable="@drawable/checked" android:minLevel="100" android:maxLevel="200" />
</level-list>

LevelListDrawable 中挑选的标准是 level 值的大小,每一个 Drawable 对象指定一个范围,显示 level 值落入范围的 Drawable 对象。

  1. StateListDrawable 同理显示状态相符的 Drawable 对象。
  2. AnimationDrawable 即帧动画,用多个 Drawable 对象产生动画效果。
1
2
3
4
5
6
7
8
@Override
public void start() {
mAnimating = true;
if (!mRunning) {
selectDrawable(frame);
scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
}
}

这里帧会进行帧的步进,并触发 drawable 的动画刷新机制,直到播放完成。

LayerDrawable

LayerDrawable 维持一个 Drawable 数组,并全部显示,但下标最大的那个显示最完全,构造方法如下

1
public LayerDrawable(@NonNull Drawable[] layers)

绘制方法如下

1
2
3
4
5
6
7
8
9
10
11
@Override
public void draw(Canvas canvas) {
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
final Drawable dr = array[i].mDrawable;
if (dr != null) {
dr.draw(canvas);
}
}
}

这样可以通过为某一层设置 Padding 来进行层叠显示,如果设置合理,可以显示全部的 Drawable 。

TransitionDrawable

TransitionDrawable 是一个仅有两层的LayerDrawable,可以在切换时播放动画,一般用作切换动画。

1
2
3
4
5
6
7
8
9
public void startTransition(int durationMillis) {
mFrom = 0;
mTo = 255;
mAlpha = 0;
mDuration = mOriginalDuration = durationMillis;
mReverse = false;
mTransitionState = TRANSITION_STARTING;
invalidateSelf();
}

该方法设置的动画是关于透明度的,且起始值是255和0,如果要启用交叉透明效果,首先设置

1
public void setCrossFadeEnabled(boolean enabled)

在绘制方法 draw 中通过时间的流逝计算 alpha 值

1
2
3
4
float normalized = (float)(SystemClock.uptimeMillis() - mStartTimeMillis) / mDuration;
done = normalized >= 1.0f;
normalized = Math.min(normalized, 1.0f);
mAlpha = (int) (mFrom + (mTo - mFrom) * normalized);

如果时间流逝完,就将第1张显示出来,且停止刷新和动画,否则显示第0张。

1
2
3
4
5
6
7
8
9
if (done) {
if (!crossFade || alpha == 0) {
array[0].mDrawable.draw(canvas);
}
if (alpha == 0xFF) {
array[1].mDrawable.draw(canvas);
}
return;
}

如果时间没有流逝完,配置二者的透明度,并继续刷新出动画效果。

1
2
array[0].mDrawable.setAlpha(255 - alpha);
array[1].mDrawable.setAlpha(alpha);

RippleDrawable

最简单的添加 Ripple 效果的方法是采用如下属性

1
android:background="?attr/selectableItemBackground"

RippleDrawable 的XML标签为 ripple。

1
2
3
4
5
6
7
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/colorAccent" //触摸波纹颜色
android:radius="30dp">
<item android:drawable="@android:color/white" //蒙版颜色
android:id="@android:id/mask" />
<item android:drawable="@color/colorPrimary" /> //View 默认颜色
</ripple>

注意这里有三种颜色,注意ripple下的color标签将被解析成 ColorStateList 类,这样 RippleDrawable 将可以对状态改变做出波纹效果的响应

1
2
3
4
5
public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content,
@Nullable Drawable mask) {
addLayer(content, null, 0, 0, 0, 0, 0);
addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0);
}

从构造方法可以看出,子类 RippleDrawable 包含两个 Drawable:content 和 mask,后者的 id 是 android.R.id.mask。

当状态改变发生时,将构建 RippleForeground[] 和 RippleBackground 以备绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
protected boolean onStateChange(int[] stateSet) {
final boolean changed = super.onStateChange(stateSet);
boolean enabled = false;
boolean pressed = false;
boolean focused = false;
boolean hovered = false;
for (int state : stateSet) {
if (state == R.attr.state_enabled) {
enabled = true;
} else if (state == R.attr.state_focused) {
focused = true;
} else if (state == R.attr.state_pressed) {
pressed = true;
} else if (state == R.attr.state_hovered) {
hovered = true;
}
}
setRippleActive(enabled && pressed);
setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered);
return changed;
}

draw 绘制的方法先绘制内容,而后绘制 ripple 效果。

1.绘制内容是正常的 Drawable 绘制,但避免绘制 mask 这一层

1
2
3
4
5
6
7
8
9
10
private void drawContent(Canvas canvas) {
// Draw everything except the mask.
final ChildDrawable[] array = mLayerState.mChildren;
final int count = mLayerState.mNum;
for (int i = 0; i < count; i++) {
if (array[i].mId != R.id.mask) {
array[i].mDrawable.draw(canvas);
}
}
}

2.绘制背景以及 Ripple 效果。

Ripple 效果是使用 Shader 来实现的,并将 mask 层绘制到同尺寸的 Bitmap 上去。

1
2
3
4
5
6
7
final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
final int halfAlpha = (Color.alpha(color) / 2) << 24;
final Paint p = getRipplePaint();
p.setColor(halfAlpha);
p.setShader(mMaskShader);
mBackground.draw(canvas, p);
mRipple.draw(canvas, p);

重新认识 Canvas,Path和Paint

Canvas

Canvas 记录绘制命令,使用下列方法能够将命令入栈

1
public int save();

而后再将命令出栈,这样就恢复到了 save 时的状态,其中的命令和状态改变就被丢弃

1
public void restore();

Canvas 的各种命令不再多说,只谈一个应该注意的方法

1
2
public boolean clipRect(@NonNull Rect rect);
public boolean clipRect(@NonNull Rect rect, @NonNull Region.Op op);

这个方法将截取某个区域来显示,其中 Op 表示区域间的叠加方式。

依靠 canvas.clipRect 方法实现的一个典型的例子是 ColorTrackView

核心代码非常简单如下

1
2
3
4
canvas.drawText("秦时明月汉时关", centerX, centerY, mPaint);
canvas.clipRect(0, 0, getBounds().width()*ratio, getBounds().height());
canvas.drawText("秦时明月汉时关", centerX, centerY, mColorPaint);
invalidateSelf();

Path

Canvas 的另一个方法 clipPath 能实现的效果更多

1
public boolean clipPath(@NonNull Path path)

上图中的 ArcNavigationView 就是通过创造 Path 绘制出来的,利用这一点可以将控件改造成任意形状,这里以经典的圆形图片为例,只需要重写绘制方法就可以了

1
2
3
4
5
@Override
protected void onDraw(Canvas canvas) {
canvas.clipPath(mPath);
super.onDraw(canvas);
}

唯一值得注意的是 Path 的建立位置是不能放在构造方法中的,这个时刻是不能获得控件尺寸的,因此应该将其放在 layout 方法中

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if(changed){
mPath = new Path();
int width = getWidth();
int height = getHeight();
int radius = Math.min(width/2, height/2);
mPath.addCircle(getWidth()/2, getHeight()/2, radius, Path.Direction.CW);
}
}

Path 的构建方法包括

1
2
3
4
5
public void moveTo(float x, float y)
public void lineTo(float x, float y)
public void quadTo(float x1, float y1, float x2, float y2)
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
public void arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)

Path 也可以通过添加形状来构建

1
2
3
4
5
6
public void addRect(float left, float top, float right, float bottom, Direction dir) // 使用OP.UNION
public void addOval(float left, float top, float right, float bottom, Direction dir)
public void addCircle(float x, float y, float radius, Direction dir)
public void addArc(float left, float top, float right, float bottom, float start, float sweep)
public void addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir)
public void addPath(Path src, float dx, float dy)

Path 内部包含区域(Region),如何合并两个 Path 包括如下方式

1
2
3
4
5
6
7
public enum Op {
DIFFERENCE, // PathA - PathB
INTERSECT, // 二者的交集
UNION, // PathA + PathB
XOR, // UNION - INTERSECT
REVERSE_DIFFERENCE // PathB - PathA
}

Path 的另一个主要方法是路径偏移

1
public void offset(float dx, float dy)

Paint

1.获取文字区域 Rect

1
public void getTextBounds(String text, int start, int end, Rect bounds)

2.获取文字路径 Path

1
public void getTextPath(String text, int start, int end, float x, float y, Path path)

使用画笔获取文字路径后,可以使用 Path 的偏移方法调整其位置。

3.画笔的众多 flag 效果,如添加下划线效果

1
2
public void setUnderlineText(boolean underlineText)
public static final int STRIKE_THRU_TEXT_FLAG = 0x10;

4.字体参数 FontMetrics,使用 ascent+descent 可以获得偏移量

1
2
3
4
5
6
7
public static class FontMetrics {
public float top;
public float ascent; //baseline到字顶,负值
public float descent; //baseline到字底,正值
public float bottom;
public float leading;
}

5.获得文本宽度

1
2
3
4
5
public float measureText(String text){
return measureText(text, 0, text.length());
}
//设置最大文本宽度
public int breakText(String text, boolean measureForwards, loat maxWidth, float[] measuredWidth) {}