Material Desion之Shape与shadow原理实现

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并在最后清除这一部分。在不包含内部阴影的情况下,则所画的即是外部圆周阴影。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值