前端悬浮窗效果_Flutter自绘组件:微信悬浮窗(一)

  看微信公众号的时候时常会想退出去回复消息,但又不想放弃已经阅读一半的文章,因为回复信息后再从公众号找到该篇文章之间有不必要的时间花费,微信悬浮窗的出现解决了这个烦恼,回复完消息之后只需要点击悬浮窗就可以回到之前在阅读的文章中。在对比多篇公众号文章的时候悬浮窗也使得在不同文章之间的切换更方便。悬浮窗的出现主要是为了省去用户在不同页面之间频繁切换时不必要的时间和精力的开销,这个组件小细节比较多,完整复刻估计需要分成三或四部分来讲,这篇文章主要讲述的是悬浮窗在点击前的按钮形态的实现,下一篇文章讲解按钮在不同形态之间切换,动起来。

先上最终效果对比图:

d1eeccefbce85912be9a73ebcb7b038e.gif

da66b28d4d03d2e5e3aa6cf78c45d91c.gif

实现思路

通过观察可得,悬浮按钮处于边缘的时候是属于不规则形状。属于官方的UI库中是不存在的不规则图形,也无法通过组合已有组件实现。这时候就需要用到Flutter提供的CustomPaintCustomPainter来实现自绘组件,但具体的绘制工作是由Canvas类和Paint类进行的。

按钮图解

对微信的悬浮按钮进行分析后,发现微信悬浮按钮主要有三种形态:左边缘按钮形态,中心按钮形态,和右边缘按钮形态,而每一个形态在按下的时候会有一个阴影,表示已选中,松开的时候阴影消失,表示未选中。具体形态图解如下(按画布大小为50x50设计):

617fe07bed38f8703fe5d5f674ef2d47.png

0194588c3e488351744845aa0d856f50.png

2d0b9716a3c12037b62ddb34563e00cf.png

4a87beeadd55afd042d81b807e176ccf.png

图解非一步到位,只是对原组件进行分析后大概画一下,在绘图的过程中再进行一些细微的调整达到较好的视觉效果(并非专业UI设计师,随便画画)。进行分析图解后,便可以着手对每一个形态进行绘制了。

使用到的类

在着手开始绘制前,我们需要了解我们绘制使用到的几个类:

CustomPaint

构造函数如下:

CustomPaint({
Key key,
this.painter, //背景画笔
this.foregroundPainter, //前景画笔
this.size = Size.zero, //画布即绘制区域的大小
this.isComplex = false, //是否为复杂绘制,若是Flutter则会启用一些缓存策略减少绘制的开销
this.willChange = false, //和isComplex配合使用,当启用缓存,表示下一帧中绘制是否会改变
Widget child, //子节点,可以为空
})

CustomPainter

CustomPainter中定义的虚函数paint,主要的绘制工作都是在这个函数中完成,主要定义如下:

void paint(Canvas canvas, Size size);

size: 表示绘制区域的大小,传递自CustomPaint中的size

canvas: 画布,其内封装了大量的绘制方法,此处列举本文中用到方法: 

API名称功能
drawCircle绘制圆形
drawPath绘制路径
drawImageRect根据给出的图片及原矩形(src)和目标矩形(dst)绘制图片
clipRRect根据给出的圆角矩形对画布进行剪裁(超出区域不绘制)
drawShadow绘制阴影
drawColor根据模式绘制颜色(本文用于绘制图片背景填充颜色)

Paint

如果说Canvas是画布,那么Paint就是画笔。Canvas中封装的很多绘制方法都需要一个画笔参数去进行绘制。画笔Paint中定义了一些画笔的基本属性,如画笔宽度,画笔颜色,笔触类型等,例子如下:

Paint _paint = Paint()
..color = Colors.blue //画笔颜色,此处为蓝色
..strokeCap = StrokeCap.round //画笔笔触类型
..isAntiAlias = true //是否启动抗锯齿
..blendMode = BlendMode.exclusion //颜色混合模式
..style = PaintingStyle.fill //绘画风格,默认为填充
..colorFilter = ColorFilter.mode(Colors.blueAccent,
BlendMode.exclusion) //颜色渲染模式
..maskFilter = MaskFilter.blur(BlurStyle.inner, 3.0) //模糊遮罩效果
..filterQuality = FilterQuality.high //颜色渲染模式的质量
..strokeWidth = 15.0; //画笔的宽度

我们在实际使用中根据需要去选择相应的属性,并不需要全部初始化。

Path

路径,使用drawPath中绘制不规则图形可以通过函数或者点间连线、曲线等表示。本文用到的方法如下:

API名称功能
moveTo将路径起点移动到指定位置
lineTo从当前位置连线到指定位置
arcTo曲线

开始绘制

对于工程其他的文件和布局不做讨论,主要讨论如何实现继承CustomPainter实现paint方法的绘制,对于如何使用这个Painter,可以参考《Flutter实战》(https://book.flutterchina.club/chapter10/custom_paint.html)。

边缘按钮的绘制

由于左右边缘按钮的图形绘制方法类似,因此我们主要讨论 左边缘按钮 的实现。由图解可知左边缘按钮是不规则的形状。我们的画布的尺寸为50x50,我们可以把这个形状看作一个圆形和一个正方形的重合,这是一种思路。

821adb469d610508d59de097d8ae7989.png

但我写的是另一种思路:看作是三条直线和一段圆弧所组成路径,以边缘按钮内层的具体代码实现为例:

//edgePath: 按钮外边缘路径,黑色线条
var edgePath = new Path() ..moveTo(size.width / 2, size.height); //移动去x轴中点(25,0)
edgePath.lineTo(0.0, size.height); //第一条直线
edgePath.lineTo(0.0, 0.0);//第二条直线
edgePath.lineTo(size.width / 2,0.0);//第三条直线
//圆弧在圆心在(25,25),半径为25的圆上
Rect rect1 = Rect.fromCircle(center:Offset(size.width / 2,size.height / 2),radius: 25);
edgePath.arcTo(rect1,pi * 1.5,pi,true); //右半圆,从 3/2 Π处起步 经过Π 个角度。

var paint = new Paint()
..isAntiAlias = true //画曲线时抗锯齿看起来更圆润
..strokeWidth = 0.75 //
..strokeCap = StrokeCap.round
..maskFilter = MaskFilter.blur(BlurStyle.normal,0.25) //线条模糊
..style = PaintingStyle.stroke //线条,不填充
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1);

canvas.drawPath(edgePath, paint);//绘制路径

效果如下:

30b5a9144481b538a8904b7e6abf4d02.png

同理绘制内层阴影部分为:

 //绘制按钮内层
var paint = Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 0.9);
//..color = Color.fromRGBO(0xDA,0xDA,0xDA,0.9);

//path : 按钮内边缘路径
var path = new Path() ..moveTo(size.width / 2 , size.height - 1.5);
path.lineTo(0.0, size.height - 1.5);
path.lineTo(0.0, 1.5);
path.lineTo(size.width / 2 ,1.5);
Rect rect = Rect.fromCircle(center:Offset(size.width / 2,size.height / 2),radius: 23.5);
path.arcTo(rect,pi * 1.5,pi,true);
canvas.drawPath(path, paint);

合起来效果为:

b492c8572678b4d1e8aeb670dfd50d00.png

最重点是中间图标logo的绘制。 

此处有踩坑点,canvas中绘制图片的方法

void drawImageRect(Image image, Rect src, Rect dst, Paint paint) 

参数image的类型Image并不是我们日常使用到的Image类型,而是封装在ui库中的一个官方私有类,只能通过监听图片流返回一个Future,再通过FutureBuilderimage传递给CustomPainter的子类。详情看 ui.Image加载探索(https://cloud.tencent.com/developer/article/1622733)这篇博客。具体内容在下一篇文章中再解释,本文暂时不涉及,不需要理解。

  先讲绘制。对于中间logo的绘制,我们将原图片的大小输出为目标区域的大小。其次,区域为矩形,而我们需要输出的logo是圆形区域,因此需要对画布进行剪裁,示意图如下:

72d8423ad533e68589eaaade334ace38.png

具体实现代码如下:

//绘制中间图标
paint = new Paint();
canvas.save(); //剪裁前保存图层
RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - 17.5,size.width / 2 - 17.5, 35, 35),Radius.circular(17.5));
canvas.clipRRect(imageRRect);//图片为圆形,圆形剪裁
canvas.drawColor(Colors.white, BlendMode.srcOver); //设置填充颜色为白色
Rect srcRect = Rect.fromLTWH(0.0, 0.0, buttonImage.width.toDouble(), buttonImage.height.toDouble());
Rect dstRect = Rect.fromLTWH(size.width / 2 - 17.5, size.height / 2 - 17.5, 35, 35);
canvas.drawImageRect(buttonImage, srcRect, dstRect, paint);
canvas.restore();//图片绘制完毕恢复图层

完整的左边缘按钮绘制代码:

 //绘制左边界悬浮按钮
void paintLeftEdgeButton(Canvas canvas,Size size,bool isPress)
{
//绘制按钮内层
var paint = Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 0.9);
//..color = Color.fromRGBO(0xDA,0xDA,0xDA,0.9);

//path : 按钮内边缘路径
var path = new Path() ..moveTo(size.width / 2 , size.height - 1.5);
path.lineTo(0.0, size.height - 1.5);
path.lineTo(0.0, 1.5);
path.lineTo(size.width / 2 ,1.5);
Rect rect = Rect.fromCircle(center:Offset(size.width / 2,size.height / 2),radius: 23.5);
path.arcTo(rect,pi * 1.5,pi,true);
canvas.drawPath(path, paint);


//edgePath: 按钮外边缘路径,黑色线条
var edgePath = new Path() ..moveTo(size.width / 2, size.height);
edgePath.lineTo(0.0, size.height);
edgePath.lineTo(0.0, 0.0);
edgePath.lineTo(size.width / 2,0.0);
Rect rect1 = Rect.fromCircle(center:Offset(size.width / 2,size.height / 2),radius: 25);
edgePath.arcTo(rect1,pi * 1.5,pi,true);

paint
..isAntiAlias = true
..strokeWidth = 0.75
..strokeCap = StrokeCap.round
..maskFilter = MaskFilter.blur(BlurStyle.normal,0.25) //线条模糊
..style = PaintingStyle.stroke
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1);
canvas.drawPath(edgePath, paint);

//按下则画阴影,表示选中
if(isPress) canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, false);

//绘制中间图标
paint = new Paint();
canvas.save(); //剪裁前保存图层
RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - 17.5,size.width / 2 - 17.5, 35, 35),Radius.circular(17.5));
canvas.clipRRect(imageRRect);//图片为圆形,圆形剪裁
canvas.drawColor(Colors.white, BlendMode.srcOver); //设置填充颜色为白色
Rect srcRect = Rect.fromLTWH(0.0, 0.0, buttonImage.width.toDouble(), buttonImage.height.toDouble());
Rect dstRect = Rect.fromLTWH(size.width / 2 - 17.5, size.height / 2 - 17.5, 35, 35);
canvas.drawImageRect(buttonImage, srcRect, dstRect, paint);
canvas.restore();//图片绘制完毕恢复图层
}

效果图:

c5e6aeb86b2dfad27f3aa68d2075862d.png

同理,右边缘按钮的绘制为:

//绘制右边界按钮
void paintRightEdgeButton(Canvas canvas,Size size){

var paint = Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 0.9);

var path = Path() ..moveTo(size.width / 2, 1.5);
path.lineTo(size.width,1.5);
path.lineTo(size.width, size.height - 1.5);
path.lineTo(size.width / 2, size.height - 1.5);

Rect rect = Rect.fromCircle(center: Offset(size.width / 2,size.height / 2),radius: 23.5);
path.arcTo(rect, pi * 0.5, pi, true);

canvas.drawPath(path, paint);//绘制


//edgePath: 按钮外边缘路径
var edgePath = Path() ..moveTo(size.width / 2,0.0);
edgePath.lineTo(size.width,0.0);
edgePath.lineTo(size.width, size.height);
edgePath.lineTo(size.width / 2, size.height);
Rect edgeRect = Rect.fromCircle(center: Offset(size.width / 2,size.height / 2),radius: 25);
edgePath.arcTo(edgeRect, pi * 0.5, pi, true);

paint
..isAntiAlias = true
..strokeWidth = 0.75
..strokeCap = StrokeCap.round
..maskFilter = MaskFilter.blur(BlurStyle.normal,0.25)
..style = PaintingStyle.stroke
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1);
canvas.drawPath(edgePath, paint);

//如果按下则绘制阴影
if(isPress)
canvas.drawShadow(path, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, false);

//绘制中间图标
paint = new Paint();
canvas.save(); //剪裁前保存图层
RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - 17.5,size.width / 2 - 17.5, 35, 35),Radius.circular(17.5));
canvas.clipRRect(imageRRect);//图片为圆形,圆形剪裁
canvas.drawColor(Colors.white, BlendMode.srcOver); //设置填充颜色为白色
Rect srcRect = Rect.fromLTWH(0.0, 0.0, buttonImage.width.toDouble(), buttonImage.height.toDouble());
Rect dstRect = Rect.fromLTWH(size.width / 2 - 17.5, size.height / 2 - 17.5, 35, 35);
canvas.drawImageRect(buttonImage, srcRect, dstRect, paint);
canvas.restore();//图片绘制完毕恢复图层

}

中心按钮的绘制

中心按钮为规则的两个圆型+中间的logo,因此绘制很简单,具体代码如下:

 //绘制中心按钮
void paintCenterButton(Canvas canvas,Size size)
{
//绘制按钮内层
var paint = new Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 0.9);
canvas.drawCircle(Offset(size.width / 2,size.height / 2), 23.5, paint);

//绘制按钮外层边线
paint
..isAntiAlias = true
..style = PaintingStyle.stroke
..strokeWidth = 0.75
..maskFilter = MaskFilter.blur(BlurStyle.normal,0.25)
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1);
canvas.drawCircle(Offset(size.width / 2,size.height / 2), 25, paint);

//如果按下则绘制阴影
if(isPress){
var circleRect = Rect.fromCircle(center: Offset(size.width / 2,size.height / 2),radius: 25);
var circlePath = new Path() ..moveTo(size.width / 2, size.height / 2);
circlePath.arcTo(circleRect, 0, 2 * 3.14, true);
canvas.drawShadow(circlePath, Color.fromRGBO(0xCF, 0xCF, 0xCF, 0.3), 0.5, false);
}

//绘制中间图标
paint = new Paint();
canvas.save(); //图片剪裁前保存图层
RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - 17.5,size.width / 2 - 17.5, 35, 35),Radius.circular(35));
canvas.clipRRect(imageRRect);//图片为圆形,圆形剪裁
canvas.drawColor(Colors.white, BlendMode.srcOver); //设置填充颜色为白色
Rect srcRect = Rect.fromLTWH(0.0, 0.0, buttonImage.width.toDouble(), buttonImage.height.toDouble());
Rect dstRect = Rect.fromLTWH(size.width / 2 - 17.5, size.height / 2 - 17.5, 35, 35);
canvas.drawImageRect(buttonImage, srcRect, dstRect, paint);
canvas.restore();//恢复剪裁前的图层

}

如何选择绘制

细心的你可能发现了一个变量isPress,他代表着什么呢,代表着的是否手指按下悬浮按钮,若按下则绘制表示选中的阴影部分。当你有了边缘按钮和中心按钮,组件的绘制是一帧一帧的画面,paint(Canvas canvas,Size size)绘制的是一帧的画面,而如何判断这一帧时选择绘制左边缘按钮还是右边缘按钮,中心按钮?因此,在FloatingButtonPainter中还应该定义几个判断的变量。

FloatingButtonPainter({
Key key,
@required this.isLeft,
@required this.isEdge,
@required this.isPress,
@required this.buttonImage
});

//按钮是否在屏幕左侧,屏幕宽度中线为准
final bool isLeft;
//按钮是否在屏幕边界,左/右边界
final bool isEdge;
//按钮是否被按下
final bool isPress;
//内按钮logo ui.image
final ui.Image buttonImage;

paint方法中进行判断

@overridevoid paint(Canvas canvas, Size size) {// TODO: implement paint  //按钮是否在边缘  if(isEdge){//按钮在屏幕左边或右边    if(isLeft)
paintLeftEdgeButton(canvas, size);//绘制左边缘按钮 else paintRightEdgeButton(canvas, size);//绘制右边缘按钮 }else{
paintCenterButton(canvas, size);//绘制中心按钮 }
}

完整代码

FloatingButtonPainter完整代码:

import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:ui' as ui;

class FloatingButtonPainter extends CustomPainter
{
FloatingButtonPainter({
Key key,
@required this.isLeft,
@required this.isEdge,
@required this.isPress,
@required this.buttonImage
});

//按钮是否在屏幕左侧,屏幕宽度 / 2
final bool isLeft;
//按钮是否在屏幕边界,左/右边界
final bool isEdge;
//按钮是否被按下
final bool isPress;
//内按钮图片 ui.image
final ui.Image buttonImage;
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
if(isEdge){
if(isLeft)
paintLeftEdgeButton(canvas, size);//绘制左边缘按钮
else
paintRightEdgeButton(canvas, size);//绘制右边缘按钮
}
else{
paintCenterButton(canvas, size); //绘制中心按钮
}
}

//绘制左边界悬浮按钮
void paintLeftEdgeButton(Canvas canvas,Size size)
{
//绘制按钮内层
var paint = Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 0.9);
//..color = Color.fromRGBO(0xDA,0xDA,0xDA,0.9);

//path : 按钮内边缘路径
var path = new Path() ..moveTo(size.width / 2 , size.height - 1.5);
path.lineTo(0.0, size.height - 1.5);
path.lineTo(0.0, 1.5);
path.lineTo(size.width / 2 ,1.5);
Rect rect = Rect.fromCircle(center:Offset(size.width / 2,size.height / 2),radius: 23.5);
path.arcTo(rect,pi * 1.5,pi,true);
canvas.drawPath(path, paint);


//edgePath: 按钮外边缘路径,黑色线条
var edgePath = new Path() ..moveTo(size.width / 2, size.height);
edgePath.lineTo(0.0, size.height);
edgePath.lineTo(0.0, 0.0);
edgePath.lineTo(size.width / 2,0.0);
Rect rect1 = Rect.fromCircle(center:Offset(size.width / 2,size.height / 2),radius: 25);
edgePath.arcTo(rect1,pi * 1.5,pi,true);

paint
..isAntiAlias = true
..strokeWidth = 0.75
..strokeCap = StrokeCap.round
..maskFilter = MaskFilter.blur(BlurStyle.normal,0.25) //线条模糊
..style = PaintingStyle.stroke
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1);
canvas.drawPath(edgePath, paint);

//按下则画阴影,表示选中
if(isPress) canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, false);

//绘制中间图标
paint = new Paint();
canvas.save(); //剪裁前保存图层
RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - 17.5,size.width / 2 - 17.5, 35, 35),Radius.circular(17.5));
canvas.clipRRect(imageRRect);//图片为圆形,圆形剪裁
canvas.drawColor(Colors.white, BlendMode.srcOver); //设置填充颜色为白色
Rect srcRect = Rect.fromLTWH(0.0, 0.0, buttonImage.width.toDouble(), buttonImage.height.toDouble());
Rect dstRect = Rect.fromLTWH(size.width / 2 - 17.5, size.height / 2 - 17.5, 35, 35);
canvas.drawImageRect(buttonImage, srcRect, dstRect, paint);
canvas.restore();//图片绘制完毕恢复图层
}

//绘制右边界按钮
void paintRightEdgeButton(Canvas canvas,Size size){

var paint = Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 0.9);

var path = Path() ..moveTo(size.width / 2, 1.5);
path.lineTo(size.width,1.5);
path.lineTo(size.width, size.height - 1.5);
path.lineTo(size.width / 2, size.height - 1.5);

Rect rect = Rect.fromCircle(center: Offset(size.width / 2,size.height / 2),radius: 23.5);
path.arcTo(rect, pi * 0.5, pi, true);

canvas.drawPath(path, paint);//绘制


//edgePath: 按钮外边缘路径
var edgePath = Path() ..moveTo(size.width / 2,0.0);
edgePath.lineTo(size.width,0.0);
edgePath.lineTo(size.width, size.height);
edgePath.lineTo(size.width / 2, size.height);
Rect edgeRect = Rect.fromCircle(center: Offset(size.width / 2,size.height / 2),radius: 25);
edgePath.arcTo(edgeRect, pi * 0.5, pi, true);

paint
..isAntiAlias = true
..strokeWidth = 0.75
..strokeCap = StrokeCap.round
..maskFilter = MaskFilter.blur(BlurStyle.normal,0.25)
..style = PaintingStyle.stroke
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1);
canvas.drawPath(edgePath, paint);

//如果按下则绘制阴影
if(isPress)
canvas.drawShadow(path, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, false);

//绘制中间图标
paint = new Paint();
canvas.save(); //剪裁前保存图层
RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - 17.5,size.width / 2 - 17.5, 35, 35),Radius.circular(17.5));
canvas.clipRRect(imageRRect);//图片为圆形,圆形剪裁
canvas.drawColor(Colors.white, BlendMode.srcOver); //设置填充颜色为白色
Rect srcRect = Rect.fromLTWH(0.0, 0.0, buttonImage.width.toDouble(), buttonImage.height.toDouble());
Rect dstRect = Rect.fromLTWH(size.width / 2 - 17.5, size.height / 2 - 17.5, 35, 35);
canvas.drawImageRect(buttonImage, srcRect, dstRect, paint);
canvas.restore();//图片绘制完毕恢复图层

}

//绘制中心按钮
void paintCenterButton(Canvas canvas,Size size)
{
//绘制按钮内层
var paint = new Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 0.9);
canvas.drawCircle(Offset(size.width / 2,size.height / 2), 23.5, paint);

//绘制按钮外层边线
paint
..isAntiAlias = true
..style = PaintingStyle.stroke
..strokeWidth = 0.75
..maskFilter = MaskFilter.blur(BlurStyle.normal,0.25)
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1);
canvas.drawCircle(Offset(size.width / 2,size.height / 2), 25, paint);

//如果按下则绘制阴影
if(isPress){
var circleRect = Rect.fromCircle(center: Offset(size.width / 2,size.height / 2),radius: 25);
var circlePath = new Path() ..moveTo(size.width / 2, size.height / 2);
circlePath.arcTo(circleRect, 0, 2 * 3.14, true); //使用pi会出错。
canvas.drawShadow(circlePath, Color.fromRGBO(0xCF, 0xCF, 0xCF, 0.3), 0.5, false);
}

//绘制中间图标
paint = new Paint();
canvas.save(); //图片剪裁前保存图层
RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - 17.5,size.width / 2 - 17.5, 35, 35),Radius.circular(35));
canvas.clipRRect(imageRRect);//图片为圆形,圆形剪裁
canvas.drawColor(Colors.white, BlendMode.srcOver); //设置填充颜色为白色
Rect srcRect = Rect.fromLTWH(0.0, 0.0, buttonImage.width.toDouble(), buttonImage.height.toDouble());
Rect dstRect = Rect.fromLTWH(size.width / 2 - 17.5, size.height / 2 - 17.5, 35, 35);
canvas.drawImageRect(buttonImage, srcRect, dstRect, paint);
canvas.restore();//恢复剪裁前的图层

}
//测试绘制
void paintTest(Canvas canvas,Size size){
var paint = Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 0.9);

var path = Path() ..moveTo(size.width / 2, 1.5);
path.lineTo(size.width,1.5);
path.lineTo(size.width, size.height - 1.5);
path.lineTo(size.width / 2, size.height - 1.5);

Rect rect = Rect.fromCircle(center: Offset(size.width / 2,size.height / 2),radius: 23.5);
path.arcTo(rect, pi * 0.5, pi, true);

canvas.drawPath(path, paint);//绘制


//edgePath: 按钮外边缘路径
var edgePath = Path() ..moveTo(size.width / 2,0.0);
edgePath.lineTo(size.width,0.0);
edgePath.lineTo(size.width, size.height);
edgePath.lineTo(size.width / 2, size.height);
Rect edgeRect = Rect.fromCircle(center: Offset(size.width / 2,size.height / 2),radius: 23.5);
edgePath.arcTo(edgeRect, pi * 0.5, pi, true);

paint
..isAntiAlias = true
..strokeWidth = 0.75
..strokeCap = StrokeCap.round
..maskFilter = MaskFilter.blur(BlurStyle.normal,0.25)
..style = PaintingStyle.stroke
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1);
canvas.drawPath(edgePath, paint);

//绘制中间图标
paint = new Paint();
RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2 - 20,size.width / 2 - 20, 40, 40),Radius.circular(20));
canvas.clipRRect(imageRRect);//图片为圆形,圆形剪裁
canvas.drawColor(Colors.white, BlendMode.srcOver); //设置填充颜色为白色
Rect srcRect = Rect.fromLTWH(0.0, 0.0, buttonImage.width.toDouble(), buttonImage.height.toDouble());
Rect dstRect = Rect.fromLTWH(size.width / 2 - 17.5, size.height / 2 - 17.5, 35, 35);
canvas.drawImageRect(buttonImage, srcRect, dstRect, paint);

//如果按下则绘制阴影
if(isPress)
canvas.drawShadow(path, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, false);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}

总结

写这个的篇幅比我料想中需要的篇幅还多,目前只实现各种形态绘制,实现FloatingButtonPainter类。但如何使用这个类,让它在各个形态之间切换,让它动起来,实现最终效果,在下一篇文章中继续,有兴趣的可以关注,点赞,点个在看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/504349.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

date类型_Quartz与Date---cron的相互转换

产生原因:因为项目最近设计到了一个Quartz相关的模块,前端需要传递时间参数到后台, 然后后台设置一个新的定时任务, 所以后台需要一个可以实现Date与cron之间的相互转换(因为Quartz需要的Cron格式的数据),所以就借助java的SimpleDateFormat的格式化,然后…

vbs if 不等于_6、if语句和关系表达式

示例3.1:星星公司致力于信件快递业务,收费标准是:500g以内6元,超过500g9元。应该就是输入重量,显示钱,那可以用cout和cin,如果w小于500,c是6,否则,c是9。//pr…

python文件是怎么写_python头文件怎么写

本文主要以python2为例。首先介绍一下Python头文件的编程风格,然后再给大家详细介绍import部分的基本用法。这两个部分就是Python中头文件的组成模块。编程风格#!/usr/bin/env python #在文件头部 ( 第一行 ) 加上 设置 Python 解释器 # -*- coding: utf-8 -*- #在文…

【学习笔记】第二章——处理机调度的概念、层次、时机、切换过程 调度方式、调度算法的指标

文章目录一. 概念 & 层次1. 高级调度(作业调度)2. 中级调度(内存调度)挂起态 & 七状态模型3. 低级调度(进程调度)4. 三种调度的对比联系 && 总结二. 时机、切换过程 & 调度方式1. 进程…

formdata上传文件_关于multipart/formdata上传文件

最近在做一个文件上传的开放接口,用到Content-Type: multipart/form-data这种请求类型,特地做了一些研究和记录。在最初的 http协议中,并没有上传文件方面的功能。RFC1867为 http协议添加了这个能力。常见的浏览器,如 Microsoft I…

【学习笔记】第二章——调度算法:先来先服务FCFS、短作业优先SJF、高响应比HRRN

文章目录一. 先来先服务(FCFS)二. 短作业优先(SJF)三. 高响应比优先1. 对前面两种算法的思考2. 描述四. 一、二、三总结例子都要手动写一遍哦~这三个是供早期的批处理系统使用的算法 一. 先来先服务(FCFS&a…

【学习笔记】第二章——时间片轮转RR、优先级调度、多级反馈队列调度算法

文章目录一. 时间片轮转二. 优先级调度三. 多级反馈队列调度算法四. 总结一. 时间片轮转 公平,轮流给进程提供时间片只用于进程调度(只有进程才能被分配时间片)抢占式,由时钟装置发出时钟中断来通知**缺点:**高频的进…

laravel 分词搜索匹配度_elasticsearch基础笔记9-elasticsearch 词项全文搜索

es的核心功能就是搜索和分析。那么我们看看搜索相关内容1、搜索机制在进入搜索之前,会对查询体根据情况进行分析和处理。2、有哪些常用搜索类型全文查询 词项查询 复合查询 嵌套查询 位置查询 特殊查询等。我们常用到的就是前三种,学起来简单&#xff0c…

【学习笔记】第二章——进程同步、进程互斥、进程互斥的硬件/软件实现方法

文章目录一. 进程同步 && 进程互斥四个区域四个原则总结:二. 进程互斥的软件实现方法1)单标志法2)双标志先检查法3)双标志后检查法4)Peterson 算法总结三. 进程互斥的硬件实现方法1)中断屏蔽方法2&…

python的数据结构包括那些_python算法与数据结构-什么是数据结构

一、什么是数据结构 数据结构,直白地理解,就是研究数据的存储方式。 我们知道,数据存储只有一个目的,即为了方便后期对数据的再利用,就如同我们使用数组存储 {1,2,3,4,5} 是为了后期取得它们的值,因此&…

seo自动发外链_一套节约成本全网营销方案-小小课堂SEO培训教程

很多公司都是由一些小型工作室或个人工作室慢慢发展而来的,在这过程中,人员、资金链、资源、项目等因素一旦出现问题,可能会导致项目失败,那么在互联网上投入的资金如果过多,可能都是白白打了水漂。今天,小…

python 支付宝个人账单_金融支付财务融合业务-实践分享1:订单、账单、交易流水、账套知识解构、原理解析...

本文作者从实际工作实践出发,结合案例等分享了电商金融支付财务融合中的基本概念和相关原理解析,包括:订单、账单、交易流水和账知识解构,供大家一同参考和学习。从事电商、进销存、金融、支付、财务的产品同学,是否对…

【学习笔记】第二章——信号量机制 用信号量实现互斥、同步

文章目录一. 信号量机制整型信号量记录型信号量例子:总结二. 用信号量实现互斥、同步互斥同步前驱总结一. 信号量机制 信号量:一个用于表示系统中某种资源的数量的变量(整数 or 记录型变量)一对原语:wait(S) 和 signa…

opengl 安装_一步步学OpenGL(34) -《GLFX,一个OpenGL效果库》

教程 34GLFX,一个OpenGL效果库原文: http://ogldev.atspace.co.uk/www/tutorial34/tutorial34.htmlCSDN完整版专栏: https://blog.csdn.net/cordova/article/category/9266966理论介绍这篇教程暂时先不继续探索OpenGL的特性和3D技术了,来介绍…

【LeetCode笔记】剑指 Offer 60. n个骰子的点数(Java、动态规划)

文章目录题目描述代码 & 思路1. 二维数组(方便理解)2. 一维数组(节约空间)二刷鸽了好久的打题博客~要继续补起来了! 今天不打题,明天变垃圾 QAQ 题目描述 一眼就想先暴力枚举、或者递归呀&…

java 根据圆心计算圆弧上点的经纬度_【控制测量学】-高斯投影正算公式以及java代码

正算公式(将经纬度转化为坐标):java代码(附有源代码和修改后的代码):源代码:/** * 由经纬度反算成高斯投影坐标 * * param longitude * param latitude * return */ public static double[] GaussToBLToGauss( double longitude, double latitude) { …

【LeetCode笔记】剑指 Offer 59 - II. 队列的最大值(Java、辅助队列)

文章目录题目描述思路 && 代码二刷题目描述 恢复打题的第二天打卡~ 可以说是这道最小栈的兄弟题目了,很相似总体思路还是一样,靠空间换时间~也就是借助辅助队列 思路 && 代码 这篇题解的动图做得很好&#xff…

redis setnx原子性_不支持原子性的 Redis 事务也叫事务吗?

文章收录在 GitHub JavaKeeper ,N线互联网开发必备技能兵器谱 假设现在有这样一个业务,用户获取的某些数据来自第三方接口信息,为避免频繁请求第三方接口,我们往往会加一层缓存,缓存肯定要有时效性,假设我们…

【LeetCode笔记】剑指 Offer 67. 把字符串转换成整数 (Java、字符串)

文章目录题目描述思路 && 代码二刷题目描述 老阅读题了,感觉这道题有点类似写业务接口。 思路 && 代码 首先,把情况划分一下: 非正常值,直接给 0(这个实际上可以划分到2的代码中)可取…

详细描述三个适于瀑布模型的项目_信息系统项目管理师-第二三章:信息系统项目管理基础与立项管理2...

三、信息系统项目的生命周期1、项目生命周期的定义—项目从启动、组织与准备、执行到结束所经历的一系列阶段2、★项目生命周期的特征①成本与人力投入在开始时较低,在工作执行期间达到最高,并在项目快要结束时迅速回落②风险与不确定性在项目开始时最大,并在项目的整个生命周期…