1、与shape相关类的功能及关系概述
在Material Design的shape包下的MaterialShapeDrawable类是用来展示Material Shape的阴影,高程,比例和颜色的。MaterialShapeDrawable是Drawable的子类,其主要的展示逻辑在draw()方法里,先看一下与shape有关的类图:
MaterialShapeDrawable类里有一个内部类MaterialShapeDrawableState,该类是用来保存Drawable之间的状态及数据等信息的。MaterialShapeDrawable对象中有一个重要的成员strokeShapeAppearance,其类型为ShapeAppearanceModel,该类是用来对Corner和Edge进行建模的,并用来产生和渲染形状背景的。ShapeAppearanceModel内部又包含三种类型的对象,分别是:CornerTreatment、EdgeTreatment和CornerSize。CornerTreatment和CornerSize是用来处理四个角的,EdgeTreatment是用来处理四条边的,因此ShapeAppearanceModel内部分别包含长度为四的元素类型为以上三种类型的列表。CornerTreatment有两个子类CutCornerTreatment和RoundCornerTreatment,分别是用来处理切割角和圆角的。TriangleEdgeTreatment和OffsetEdgeTreatment是EdgeTreatment的子类,分别用来处理三角情形和相对其他边有偏移情形的边。CornerTreatment和EdgeTreatment处理的对象均是ShapePath,其代表形状路径的描述。ShapePath内部的类型为List<PathOperation>的成员operations与类型为List<ShadowCompatOperation>的成员shadowCompatOperations,其分别表示对路径的一系列操作和对兼容阴影的一系列操作。另外还有一个很重要的类ShapeAppearancePathProvider,它是用来将ShapeAppearanceModel转换为Path,主要是边角路径的计算(其中会包含各种转换),ShapeAppearanceProvider中的内部类ShapeAppearancePathSpec是用来描述将ShapeAppearanceModel转换为Path所需的必要信息的。
了解了shape包下主要类的功能及关系后再来看一下MaterialShapeDrawable绘制的主流程,也就是draw()方法:
public void draw(@NonNull Canvas canvas) {
fillPaint.setColorFilter(tintFilter);
final int prevAlpha = fillPaint.getAlpha();
fillPaint.setAlpha(modulateAlpha(prevAlpha, drawableState.alpha));
strokePaint.setColorFilter(strokeTintFilter);
strokePaint.setStrokeWidth(drawableState.strokeWidth);
final int prevStrokeAlpha = strokePaint.getAlpha();
strokePaint.setAlpha(modulateAlpha(prevStrokeAlpha, drawableState.alpha));
//1.计算需要绘制的边框路径与路径
if (pathDirty) {
calculateStrokePath();
calculatePath(getBoundsAsRectF(), path);
pathDirty = false;
}
//2.绘制兼容阴影
maybeDrawCompatShadow(canvas);
//3.绘制填充形状
if (hasFill()) {
drawFillShape(canvas);
}
//4.绘制边框形状
if (hasStroke()) {
drawStrokeShape(canvas);
}
fillPaint.setAlpha(prevAlpha);
strokePaint.setAlpha(prevStrokeAlpha);
}
2、边角绘制流程
先看一下计算边框路径的代码:
private void calculateStrokePath() {
// Adjust corner radius in order to draw the stroke so that the corners of the background are
// drawn on top of the edges.
final float strokeInsetLength = -getStrokeInsetLength();
strokeShapeAppearance =
getShapeAppearanceModel()
.withTransformedCornerSizes(
new CornerSizeUnaryOperator() {
@NonNull
@Override
public CornerSize apply(@NonNull CornerSize cornerSize) {
// Don't adjust for relative corners they will change by themselves when the
// bounds change.
return cornerSize instanceof RelativeCornerSize
? cornerSize
: new AdjustedCornerSize(strokeInsetLength, cornerSize);
}
});
pathProvider.calculatePath(
strokeShapeAppearance,
drawableState.interpolation,
getBoundsInsetByStroke(),
pathInsetByStroke);
}
先将strokeShapeAppearance适配圆角,载通过pathProvider将strokeShapeAppearance转换为类型为Path的pathInsetByStroke。再看一下绘制边框路径的代码:
private void drawStrokeShape(@NonNull Canvas canvas) {
drawShape(
canvas, strokePaint, pathInsetByStroke, strokeShapeAppearance, getBoundsInsetByStroke());
}
即是将上一步计算得到的pathInsetByStroke对象绘制载Canvas上。下面看一下计算path的代码:
private void calculatePath(@NonNull RectF bounds, @NonNull Path path) {
calculatePathForSize(bounds, path);
if (drawableState.scale != 1f) {
matrix.reset();
matrix.setScale(
drawableState.scale, drawableState.scale, bounds.width() / 2.0f, bounds.height() / 2.0f);
path.transform(matrix);
}
// Since the path has just been computed, we update the path bounds.
path.computeBounds(pathBounds, true);
}
protected final void calculatePathForSize(@NonNull RectF bounds, @NonNull Path path) {
pathProvider.calculatePath(
drawableState.shapeAppearanceModel,
drawableState.interpolation,
bounds,
pathShadowListener,
path);
}
先是通过pathProvider将保存载drawableState中的shapeAppearanceModel转换为path,然后再做一些缩放及平移的变换并更新path的bounds。可以看到计算边框路径与路径的过程均是依靠ShapeAppearancePathProvider对象将ShapeAppearanceModel转换为Path。下面看一下绘制填充形状的代码:
private void drawFillShape(@NonNull Canvas canvas) {
drawShape(canvas, fillPaint, path, drawableState.shapeAppearanceModel, getBoundsAsRectF());
}
即是将上一步计算得到的path绘制再Canvas上。
3、兼容shadow绘制流程
下面再来看一下绘制兼容阴影的绘制:
private void maybeDrawCompatShadow(@NonNull Canvas canvas) {
if (!hasCompatShadow()) {
return;
}
// Save the canvas before changing the clip bounds.
canvas.save();
prepareCanvasForShadow(canvas);
if (!shadowBitmapDrawingEnable) {
drawCompatShadow(canvas);
canvas.restore();
return;
}
// The extra height is the amount that the path draws outside of the bounds of the shape. This
// happens for some shapes like TriangleEdgeTreament when it draws a triangle outside.
int pathExtraWidth = (int) (pathBounds.width() - getBounds().width());
int pathExtraHeight = (int) (pathBounds.height() - getBounds().height());
if (pathExtraWidth < 0 || pathExtraHeight < 0) {
throw new IllegalStateException(
"Invalid shadow bounds. Check that the treatments result in a valid path.");
}
// Drawing the shadow in a bitmap lets us use the clear paint rather than using clipPath to
// prevent drawing shadow under the shape. clipPath has problems :-/
Bitmap shadowLayer =
Bitmap.createBitmap(
(int) pathBounds.width() + drawableState.shadowCompatRadius * 2 + pathExtraWidth,
(int) pathBounds.height() + drawableState.shadowCompatRadius * 2 + pathExtraHeight,
Bitmap.Config.ARGB_8888);
Canvas shadowCanvas = new Canvas(shadowLayer);
// Top Left of shadow (left - shadowCompatRadius, top - shadowCompatRadius) should be drawn at
// (0, 0) on shadowCanvas. Offset is handled by prepareCanvasForShadow and drawCompatShadow.
float shadowLeft = getBounds().left - drawableState.shadowCompatRadius - pathExtraWidth;
float shadowTop = getBounds().top - drawableState.shadowCompatRadius - pathExtraHeight;
shadowCanvas.translate(-shadowLeft, -shadowTop);
drawCompatShadow(shadowCanvas);
canvas.drawBitmap(shadowLayer, shadowLeft, shadowTop, null);
// Because we create the bitmap every time, we can recycle it. We may need to stop doing this
// if we end up keeping the bitmap in memory for performance.
shadowLayer.recycle();
// Restore the canvas to the same size it was before drawing any shadows.
canvas.restore();
}
这段代码的逻辑是先判断是否需要画兼容阴影,如不需要则返回。画兼容阴影有两种方式:1.直接在原来的canvas上画;2.先将阴影画在一个Bitmap上,然后再将该Bitmap华仔原来的canvas上。这两种方式是通过字段shadowBitmapDrawingEnable来区分的。画兼容阴影是依靠drawCompatShadow()函数来实现的:
private void drawCompatShadow(@NonNull Canvas canvas) {
//1.先将path画在canvas上
if (drawableState.shadowCompatOffset != 0) {
canvas.drawPath(path, shadowRenderer.getShadowPaint());
}
// Draw the fake shadow for each of the corners and edges.
//2.画四条边及四个角的阴影
for (int index = 0; index < 4; index++) {
cornerShadowOperation[index].draw(shadowRenderer, drawableState.shadowCompatRadius, canvas);
edgeShadowOperation[index].draw(shadowRenderer, drawableState.shadowCompatRadius, canvas);
}
//3.如果是通过Bitmap方式画阴影的话还要清除部分路径
if (shadowBitmapDrawingEnable) {
int shadowOffsetX = getShadowOffsetX();
int shadowOffsetY = getShadowOffsetY();
canvas.translate(-shadowOffsetX, -shadowOffsetY);
canvas.drawPath(path, clearPaint);
canvas.translate(shadowOffsetX, shadowOffsetY);
}
}
绘制的流程注释已经很清楚了,下面来具体看一绘制边和角的阴影的过程,首先看一下边阴影的绘制,是ShadowRenderer类的drawEdgeShadow()方法来实现的:
//class ShadowRenderer
public void drawEdgeShadow(
@NonNull Canvas canvas, @Nullable Matrix transform, @NonNull RectF bounds, int elevation) {
bounds.bottom += elevation;
bounds.offset(0, -elevation);
edgeColors[0] = shadowEndColor;
edgeColors[1] = shadowMiddleColor;
edgeColors[2] = shadowStartColor;
edgeShadowPaint.setShader(
new LinearGradient(
bounds.left,
bounds.top,
bounds.left,
bounds.bottom,
edgeColors,
edgePositions,
Shader.TileMode.CLAMP));
canvas.save();
canvas.concat(transform);
canvas.drawRect(bounds, edgeShadowPaint);
canvas.restore();
}
画边阴影的过程相对简单,即是将bounds表示的阴影区域以线性渐变的方式画在canvas上。再看一下画圆角阴影的过程:
public void drawCornerShadow(
@NonNull Canvas canvas,
@Nullable Matrix matrix,
@NonNull RectF bounds,
int elevation,
float startAngle,
float sweepAngle) {
boolean drawShadowInsideBounds = sweepAngle < 0;
Path arcBounds = scratch;
if (drawShadowInsideBounds) {
cornerColors[0] = 0;
cornerColors[1] = shadowEndColor;
cornerColors[2] = shadowMiddleColor;
cornerColors[3] = shadowStartColor;
} else {
// Calculate the arc bounds to prevent drawing shadow in the same part of the arc.
arcBounds.rewind();
arcBounds.moveTo(bounds.centerX(), bounds.centerY());
arcBounds.arcTo(bounds, startAngle, sweepAngle);
arcBounds.close();
bounds.inset(-elevation, -elevation);
cornerColors[0] = 0;
cornerColors[1] = shadowStartColor;
cornerColors[2] = shadowMiddleColor;
cornerColors[3] = shadowEndColor;
}
float radius = bounds.width() / 2f;
// The shadow is not big enough to draw.
if (radius <= 0) {
return;
}
float startRatio = 1f - (elevation / radius);
float midRatio = startRatio + ((1f - startRatio) / 2f);
cornerPositions[1] = startRatio;
cornerPositions[2] = midRatio;
cornerShadowPaint.setShader(
new RadialGradient(
bounds.centerX(),
bounds.centerY(),
radius,
cornerColors,
cornerPositions,
Shader.TileMode.CLAMP));
// TODO(b/117606382): handle oval bounds by scaling the canvas.
canvas.save();
canvas.concat(matrix);
if (!drawShadowInsideBounds) {
canvas.clipPath(arcBounds, Op.DIFFERENCE);
// This line is required for the next drawArc to work correctly, I think.
canvas.drawPath(arcBounds, transparentPaint);
}
canvas.drawArc(bounds, startAngle, sweepAngle, true, cornerShadowPaint);
canvas.restore();
}
这段代码主要是在画一个径向渐变的圆弧阴影,但是分两种情况:1.包含内部阴影;2.不包含内部阴影。这是通过sweepAngle < 0来判断的,当条件成立时则在画圆弧时会多画一个圆周的路径,因此需要计算多画的路径arcBounds并在最后清除这一部分。在不包含内部阴影的情况下,则所画的即是外部圆周阴影。