文章目录

  • 1、与shape相关类的功能及关系概述
  • 2、边角绘制流程
  • 3、兼容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@Overridepublic 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并在最后清除这一部分。在不包含内部阴影的情况下,则所画的即是外部圆周阴影。

Material Desion之Shape与shadow原理实现相关推荐

  1. Material delta download的deletion处理原理

    Created by Jerry Wang, last modified on Apr 15, 2014 在ERP里删除某一个Material的德语版本的description: 在ERP端点save ...

  2. Threejs中的Shadow Mapping(阴影贴图)

    简而言之,步骤如下: 1.从灯光位置视点(阴影相机)创建深度图. 2.从相机的位置角度进行屏幕渲染,在每个像素点,比较由阴影相机的MVP矩阵计算的深度值和深度图的值的大小,如果深度图值小的话,则表示该 ...

  3. 打造 Material 颜色主题 | 实现篇

    作者 / Nick Rout, Material Developer Advocate 使用 Material 主题自定义 Material 组件,目的是让组件观感与品牌保持一致.Material 主 ...

  4. Android shape定义背景带阴影

    <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android=" ...

  5. XGBoost 原理介绍

    1.简介 XGBoost的全称是eXtreme Gradient Boosting,它是经过优化的分布式梯度提升库,旨在高效.灵活且可移植.XGBoost是大规模并行boosting tree的工具, ...

  6. 打造 Material 形状主题 | 设计篇

    作者 / Liam Spradlin,Material Design 推广工程师 形状,就像颜色和排版一样,对于提升界面的美感和实用性至关重要.但与颜色和排版不同,形状的作用通常颇为微妙.虽然界面的所 ...

  7. ECCV2022论文列表(中英对照)

    Paper ID Paper Title 论文标题 8 Learning Uncoupled-Modulation CVAE for 3D Action-Conditioned Human Motio ...

  8. App为了漂亮脸蛋也要美颜,Theme 与 Style 的使用,附一键变装 demo

    前言 作为 Android 开发者,不知你是否也有这样的体验,随着项目变得越来越大,各种不同圆角的 shape,不同透明度的 color,不同大小的阴影效果,它们使资源文件越来越多 我认为造成这种问题 ...

  9. 【航线运输驾驶员理论考试】气象学

    1.TAF is the weather forcast information applied to an area within ( ) from the center of an airport ...

  10. rcm认证_这款小型rcm可以更精确地执行外科手术

    rcm认证 Harvard researchers drew upon inspiration from the Japanese art of Origami to develop this inn ...

最新文章

  1. VMware三种上网模型
  2. python程序如何做界面_python是如何写界面程序的?
  3. 布隆过滤器速度_布隆过滤器的分析和实现
  4. 服务器和客户端之间的变量交互
  5. 前端学习(585):查看和编辑css
  6. java学习(121):treeset排序集合
  7. 小程序 | 云函数获取用户openid
  8. ⼩程序中⽀持es7的async语法
  9. 有时候,一个人也挺好
  10. 跨域两种解决方案CORS以及JSONP
  11. python os模块大全
  12. java实现5 4 3 2 1递归_递归及递归的使用
  13. 游戏必备组件_没有网络也可以肝的单机小游戏!玩一局就停不下来
  14. rufus linux 教程,图文回复rufus使用教程【操作步骤】
  15. 【转载】最全的计算广告资料,广告算法工程师入门
  16. PLC控制系统接地要求
  17. github修改语言设置
  18. GSM与GPRS区别介绍
  19. 【读书笔记】《全域营销:付费增长与流量变现实战讲义》——我的公域私域运营教科书
  20. 21个有用的python工具

热门文章

  1. java实现将word转换pdf
  2. 常见互联网公司职级和薪资一览,有条件的一定要进大厂,薪水是真高
  3. Tomcat下载安装及配置Https教程
  4. 在线html调试,debugger调试
  5. JS 案例 个人所得税计算器
  6. 虚拟机全屏后隐藏vmware菜单栏的问题
  7. Python笔记之Django网页模板的继承block(挖坑填坑、HTML转义)
  8. 中国网页游戏行业调研与分析
  9. 各代iphone尺寸_iPhone12大小尺寸是多少?四款iPhone12系列尺寸对比长宽高
  10. 看完这篇,轻松解决FastReport合并单元格!