衡阳市建设学校官方网站,做网站赚钱还是做app赚钱,wordpress主题博客,wordpress m3u8播放器#x1f4da; 目录
介绍组合多个组件自绘组件 Custompaint绘制边界RepaintBoundaryCustomPainter与Canvas画笔Paint绘制组件例子 本文学习和引用自《Flutter实战第二版》#xff1a;作者#xff1a;杜文 1. 介绍
当Flutter提供的现有组件无法满足我们的需求#xff0c;或… 目录
介绍组合多个组件自绘组件 Custompaint绘制边界RepaintBoundaryCustomPainter与Canvas画笔Paint绘制组件例子 本文学习和引用自《Flutter实战·第二版》作者杜文 1. 介绍
当Flutter提供的现有组件无法满足我们的需求或者我们为了共享代码需要封装一些通用组件这时我们就需要自定义组件。在Flutter中自定义组件有三种方式通过组合其他组件、自绘和实现RenderObject。
组合多个Widget通过已有组件来拼装组合成一个新的组件。通过CustomPaint自绘通过Flutter中提供的CustomPaint和Canvas来实现UI自绘。通过RenderObject自绘RenderObject中最终也是通过Canvas API来绘制的。而CustomPaint只是为了方便开发者封装的一个代理类。
2. 组合多个组件
实现一个渐变色背景支持圆角按下有涟漪效果的按钮并且实现一个带动画的可以旋转的容器每次点击按钮容器旋转一点。
import package:flutter/material.dart;/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});overrideStateHomePage createState() HomePageState();
}/// 实现
class HomePageState extends StateHomePage {double myTurns 0.0;overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text(Flutter Home),),body: Container(alignment: Alignment.center,child: Column(children: [TurnBox(turns: myTurns,speed: 500,child: const Icon(Icons.refresh,size: 50,)),MyButton(colors: const [Colors.green, Colors.orangeAccent],height: 60.0,width: 300.0,borderRadius: const BorderRadius.all(Radius.circular(8)),onPressed: () {setState(() {myTurns 0.2;});},child: const Text(放手一搏),)],),));}
}
/// 自定义按钮
class MyButton extends StatelessWidget {const MyButton({Key? key,this.colors,this.width,this.height,this.onPressed,this.borderRadius,required this.child}): super(key: key);// 渐变色数组final ListColor? colors;// 按钮属性final double? width;final double? height;final BorderRadius? borderRadius;// 点击回调事件final GestureTapCallback? onPressed;final Widget child;overrideWidget build(BuildContext context) {ThemeData theme Theme.of(context);ListColor myColors colors ?? [theme.primaryColor, theme.primaryColorDark];return DecoratedBox(decoration: BoxDecoration(gradient: LinearGradient(colors: myColors),borderRadius: borderRadius),child: Material(type: MaterialType.transparency,child: InkWell(splashColor: Colors.white70,highlightColor: Colors.transparent,borderRadius: borderRadius ?? BorderRadius.circular(8),onTap: onPressed,child: ConstrainedBox(constraints: BoxConstraints.tightFor(height: height, width: width),child: Center(child: Padding(padding: const EdgeInsets.all(8.0),child: DefaultTextStyle(style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18.0),child: child,),),),),),),);}
}/// 自定义旋转
class TurnBox extends StatefulWidget {const TurnBox({Key? key,this.turns .0, // 旋转的“圈”数,一圈为360度如0.25圈即90度this.speed 200, // 过渡动画执行的总时长required this.child}) :super(key: key);final double turns;final int speed;final Widget child;overrideTurnBoxState createState() TurnBoxState();
}class TurnBoxState extends StateTurnBox with SingleTickerProviderStateMixin {AnimationController? myController;overridevoid initState() {super.initState();myController AnimationController(vsync: this,lowerBound: -double.infinity,upperBound: double.infinity);myController!.value widget.turns;}overridevoid dispose() {myController!.dispose();super.dispose();}overrideWidget build(BuildContext context) {return RotationTransition(turns: myController!,child: widget.child,);}overridevoid didUpdateWidget(TurnBox oldWidget) {super.didUpdateWidget(oldWidget);// 旋转角度发生变化时执行过渡动画if (oldWidget.turns ! widget.turns) {myController!.animateTo(widget.turns,duration: Duration(milliseconds: widget.speed??200),curve: Curves.easeOut,);}}
}3. 自绘组件
对于一些复杂或不规则的UI我们可能无法通过组合其他组件的方式来实现。比如一个正六边形、一个渐变的圆形进度条、一个棋盘等。在Flutter中提供了一个CustomPaint 组件它可以结合画笔CustomPainter来实现自定义图形绘制。
3-1. CustomPaint
画笔CustomPainter绘制时我们需要提供前景或背景画笔两者也可以同时提供。我们的画笔需要继承CustomPainter类我们在画笔类中实现真正的绘制逻辑。
属性描述painter背景画笔会显示在子节点后面foregroundPainter前景画笔会显示在子节点前面size当child为null时代表默认绘制区域大小如果有child则忽略此参数画布尺寸则为child尺寸。如果有child但是想指定画布为特定大小可以使用SizeBox包裹CustomPaint实现。isComplex是否复杂的绘制如果是Flutter会应用一些缓存策略来减少重复渲染的开销。willChange和isComplex配合使用当启用缓存时该属性代表在下一帧中绘制是否会改变。
3-2. 绘制边界RepaintBoundary
如果CustomPaint有子节点为了避免子节点不必要的重绘并提高性能通常情况下都会将子节点包裹在RepaintBoundary组件中这样会在绘制时就会创建一个新的绘制层Layer其子组件将在新的Layer上绘制而父组件将在原来Layer上绘制也就是说RepaintBoundary 子组件的绘制将独立于父组件的绘制RepaintBoundary会隔离其子节点和CustomPaint本身的绘制边界。
CustomPaint(// 指定画布大小size: Size(300, 300),painter: MyPainter(),child: RepaintBoundary(child:...),
)3-3. CustomPainter与Canvas
CustomPainter中提定义了一个虚函数paint。它有两个参数Canvas和Size。Canvas是Flutter中绘制UI的底层组件它是一个画布包括各种绘制方法。Size是当前绘制区域大小。Canvas常用API如下
API描述drawLine画线drawPoint画点drawPath画路径drawImage画图像drawRect画矩形drawCircle画圆drawOval画椭圆drawArc画圆弧
3-4. 画笔Paint
Flutter提供了Paint类来实现画笔。在Paint中我们可以配置画笔的各种属性如粗细、颜色、样式等。如下例子
// 创建一个画笔并配置其属性
var paint Paint()..isAntiAlias true // 是否抗锯齿..style PaintingStyle.fill // 画笔样式填充..color Color(0x77cdb175); // 画笔颜色3-5. 绘制组件例子
如下是一个自定义绘制的饼状图。
饼图完整代码
import dart:math;
import package:flutter/material.dart;typedef PieChartViewTap Function(int index);
typedef OutsideText Text Function(PieChartModel model, String scale);class PieChartView extends ImplicitlyAnimatedWidget {final ListPieChartModel models;/// 是否显示内部圆final bool isShowHole;/// 内部圆的半径final double holeRadius;/// 内部圆的颜色final Color holeColor;/// 扇形分割线宽度final double spaceWidth;/// 溢出上方文字final OutsideText? outsideTopText;/// 溢出下方文字final OutsideText? outsideBottomText;/// 扇形点击事件final PieChartViewTap? onTap;const PieChartView(this.models, {Key? key,this.holeRadius 55.0,this.isShowHole true,this.holeColor Colors.white,this.spaceWidth 2.0,this.outsideTopText,this.outsideBottomText,this.onTap,Curve curve Curves.linear,Duration duration const Duration(milliseconds: 150),}) : super(key: key,curve: curve,duration: duration,);overrideCustomPieViewState createState() CustomPieViewState();
}class CustomPieViewState extends AnimatedWidgetBaseStatePieChartView {CustomPieTween? customPieTween;ListPieChartModel get end widget.models.map((e) PieChartModel(value: e.value, color: e.color, name: e.name, radius: e.radius)).toList();overrideWidget build(BuildContext context) {return CustomPaint(size: Size.infinite,painter: PieChartPainter(context,customPieTween!.evaluate(animation),holeRadius: widget.holeRadius,isShowHole: widget.isShowHole,holeColor: widget.holeColor,spaceWidth: widget.spaceWidth,outsideTopText: widget.outsideTopText,outsideBottomText: widget.outsideBottomText,onTap: widget.onTap,),);}overridevoid forEachTween(TweenVisitordynamic visitor) {customPieTween visitor(customPieTween, end, (dynamic value) {return CustomPieTween(begin: value, end: end);}) as CustomPieTween;}
}class CustomPieTween extends TweenListPieChartModel {CustomPieTween({ListPieChartModel? begin, ListPieChartModel? end}): super(begin: begin, end: end);overrideListPieChartModel lerp(double t) {ListPieChartModel list [];begin?.asMap().forEach((index, model) {list.add(model..radius lerpDouble(model.radius, end?[index].radius ?? 100.0, t));});return list;}double lerpDouble(double radius, double radius2, double t) {if (radius radius2) {return radius;}var d (radius2 - radius) * t;var value radius d;return value;}
}class PieChartPaint extends CustomPaint {const PieChartPaint({Key? key}) : super(key: key);
}class PieChartPainter extends CustomPainter {final BuildContext context;final ListPieChartModel models;final bool isShowHole;final double holeRadius;final Color holeColor;final double spaceWidth;final OutsideText? outsideTopText;final OutsideText? outsideBottomText;final PieChartViewTap? onTap;final ListPath paths [];final Path holePath Path();Offset oldTapOffset Offset.zero;PieChartPainter(this.context,this.models, {this.holeRadius 60.0,this.isShowHole true,this.holeColor Colors.white,this.spaceWidth 2.0,this.outsideTopText,this.outsideBottomText,this.onTap,});overridevoid paint(Canvas canvas, Size size) {//移动到中心点canvas.translate(size.width / 2, size.height / 2);//绘制饼状图_drawPie(canvas, size);//绘制分割线_drawSpaceLine(canvas);// 绘制中心圆_drawHole(canvas, size);// drawLineAndText(canvas);}overridebool shouldRepaint(CustomPainter oldDelegate) oldDelegate ! this;overridebool? hitTest(Offset position) {return _interceptTouchEvent(position);}bool _interceptTouchEvent(Offset offset) {if (oldTapOffset.dx offset.dx oldTapOffset.dy offset.dy) {return false;}oldTapOffset offset;for (int i 0; i paths.length; i) {if (paths[i].contains(offset) !holePath.contains(offset)) {onTap?.call(i);oldTapOffset offset;return true;}}onTap?.call(-1);return false;}/// 绘制分割线void _drawSpaceLine(Canvas canvas) {var sumValue models.folddouble(0.0, (sum, model) sum model.value);var startAngle 0.0;for (var model in models) {_drawLine(canvas, startAngle, model.radius);startAngle model.value / sumValue * 360;_drawLine(canvas, startAngle, model.radius);}}void _drawLine(Canvas canvas, double angle, double radius) {var endX cos(angle * pi / 180) * radius;var endY sin(angle * pi / 180) * radius;Paint paint Paint()..style PaintingStyle.fill..color Colors.white..strokeWidth spaceWidth;canvas.drawLine(Offset.zero, Offset(endX, endY), paint);}/// 绘制饼状图void _drawPie(Canvas canvas, Size size) {var startAngle 0.0;var sumValue models.folddouble(0.0, (sum, model) sum model.value);for (var model in models) {Paint paint Paint()..style PaintingStyle.fill..color model.color;var sweepAngle model.value / sumValue * 360;canvas.drawArc(Rect.fromCircle(radius: model.radius, center: Offset.zero),startAngle * pi / 180, sweepAngle * pi / 180, true, paint);Path path Path();var centerX size.width / 2;var centerY size.height / 2;path.addArc(Rect.fromCircle(radius: model.radius, center: Offset(centerX, centerY)),startAngle * pi / 180,sweepAngle * pi / 180);path.moveTo(centerX, centerY);path.lineTo(centerX cos(startAngle * pi / 180) * model.radius,centerY sin(startAngle * pi / 180) * model.radius);path.lineTo(centerX cos((sweepAngle startAngle) * pi / 180) * model.radius,centerY sin((sweepAngle startAngle) * pi / 180) * model.radius);paths.add(path);// 为每一个区域绘制延长线和文字_drawLineAndText(canvas, size, model.radius, startAngle, sweepAngle, model);startAngle sweepAngle;}}/// 绘制延长线和文字void _drawLineAndText(Canvas canvas, Size size, double radius,double startAngle, double sweepAngle, PieChartModel model) {var ratio (sweepAngle / 360.0 * 100).toStringAsFixed(2);var top outsideTopText?.call(model, ratio) ??Text(model.name,style: const TextStyle(color: Colors.black38),);var topTextPainter getTextPainter(top);var bottom outsideBottomText?.call(model, ratio) ??Text($ratio%,style: const TextStyle(color: Colors.black38),);var bottomTextPainter getTextPainter(bottom);// 绘制横线// 计算开始坐标以及转折点的坐标var startX radius * (cos((startAngle (sweepAngle / 2)) * (pi / 180)));var startY radius * (sin((startAngle (sweepAngle / 2)) * (pi / 180)));var firstLine radius / 5;var secondLine max(bottomTextPainter.width, topTextPainter.width) radius / 4;var pointX (radius firstLine) *(cos((startAngle (sweepAngle / 2)) * (pi / 180)));var pointY (radius firstLine) *(sin((startAngle (sweepAngle / 2)) * (pi / 180)));// 计算坐标在左边还是在右边// 并计算横线结束坐标// 如果结束坐标超过了绘制区域则改变结束坐标的值var endX 0.0;// 距离绘制边界的偏移量var marginOffset 20.0;if (pointX - startX 0) {endX min(pointX secondLine, size.width / 2 - marginOffset);secondLine endX - pointX;} else {endX max(pointX - secondLine, -size.width / 2 marginOffset);secondLine pointX - endX;}Paint paint Paint()..style PaintingStyle.fill..strokeWidth 1..color Colors.grey;// 绘制延长线canvas.drawLine(Offset(startX, startY), Offset(pointX, pointY), paint);canvas.drawLine(Offset(pointX, pointY), Offset(endX, pointY), paint);// 文字距离中间横线上下间距偏移量var offset 4;var textWidth bottomTextPainter.width;var textStartX 0.0;textStartX _calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);bottomTextPainter.paint(canvas, Offset(textStartX, pointY offset));textWidth topTextPainter.width;var textHeight topTextPainter.height;textStartX _calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);topTextPainter.paint(canvas, Offset(textStartX, pointY - offset - textHeight));// 绘制文字前面的小圆点paint.color model.color;canvas.drawCircle(Offset(textStartX - 8, pointY - 4 - topTextPainter.height / 2),4,paint);}double _calculateTextStartX(double stopX, double startX, double w,double line2, double textStartX, int offset) {if (stopX - startX 0) {if (w line2) {textStartX (stopX offset);} else {textStartX (stopX (line2 - w));}} else {if (w line2) {textStartX (stopX - offset - w);} else {textStartX (stopX - (line2 - w) - w);}}return textStartX;}TextPainter getTextPainter(Text text) {TextPainter painter TextPainter(locale: Localizations.localeOf(context),maxLines: text.maxLines,textDirection: TextDirection.ltr,text: TextSpan(text: text.data,style: text.style,),);painter.layout();return painter;}/// 绘制中心圆void _drawHole(Canvas canvas, Size size) {if (isShowHole) {holePath.reset();Paint paint Paint()..style PaintingStyle.fill..color Colors.white;canvas.drawCircle(Offset.zero, holeRadius, paint);var centerX size.width / 2;var centerY size.height / 2;holePath.addArc(Rect.fromCircle(radius: holeRadius, center: Offset(centerX, centerY)),0,360 * pi / 180);}}
}/// 数据
class PieChartModel {double value;Color color;String name;double radius;PieChartModel({required this.value,required this.color,required this.name,this.radius 100,});
}使用
import package:flutter/material.dart;
import package:demo1/widget/MyPieChart.dart;/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});overrideStateHomePage createState() HomePageState();
}/// 实现
class HomePageState extends StateHomePage {double myTurns 0.0;overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text(Flutter Home),),body: Container(alignment: Alignment.center,// 饼图child: PieChartView([PieChartModel(value: 35,name: A,color: Colors.blue,radius: 100,),PieChartModel(value: 15,name: B,color: Colors.red,radius: 100,),PieChartModel(value: 22,name: C,color: Colors.yellow,radius: 100,),PieChartModel(value: 18,name: D,color: Colors.orange,radius: 100,),PieChartModel(value: 39,name: F,color: Colors.green,radius: 100,),],),));}
}本次分享就到这儿啦我是鹏多多如果您看了觉得有帮助欢迎评论关注点赞转发我们下次见~
往期文章
手把手教你搭建规范的团队vue项目包含commitlinteslintprettierhuskycommitizen等等Web Woeker和Shared Worker的使用以及案例Vue2全家桶Element搭建的PC端在线音乐网站vue3element-plus配置cdn助你上手Vue3全家桶之Vue3教程助你上手Vue3全家桶之VueX4教程助你上手Vue3全家桶之Vue-Router4教程超详细Vue的九种通信方式超详细Vuex手把手教程使用nvm管理node.js版本以及更换npm淘宝镜像源vue中利用.env文件存储全局环境变量以及配置vue启动和打包命令超详细Vue-Router手把手教程
个人主页
CSDNGitHub简书博客园掘金