玖叶教程网

前端编程开发入门

Flutter实现圆形文本,带动画的那种

是那种比较好看的那种,欢迎欣赏。

本头条核心宗旨

欢迎来到「技术刚刚好」作者,「技术刚刚好」是个人维护,每天至少更新一篇Flutter技术文章,实时为大家播报Flutter最新消息。如果你刚好也在关注Flutter这门技术,那就跟我一起学习进步吧,你的赞,收藏,转发是对我个人最大的支持,维护不易,欢迎关注。

技术刚刚好经历

近几年,移动端跨平台开发技术层出不穷,从Facebook家的ReactNative,到阿里家WEEX,前端技术在移动端跨平台开发中大展身手,技术刚刚好作为一名Android开发,经历了从Reactjs到Vuejs的不断学习。而在2018年,我们的主角变成了Flutter,这是Goolge开源的一个移动端跨平台解决方案,可以快速开发精美的移动App。希望跟大家一起学习,一起进步!

本文核心要点

做Flutter开发这么久,要学习的东西还是挺多的,一个文本组件的实现应该很简单,但是配合一个圆形的组件应该怎么实现了?今天就带大家看看圆形文本组件怎么实行,而且还有动画的实现。


圆形组件带文本

CircularText代码

CircularText是一个核心的实现代码,全部贴出来。

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

enum CircularTextDirection { clockwise, anticlockwise }

enum CircularTextPosition { outside, inside }

class CircularText extends StatelessWidget {
 //Text
 final String text;

 //Text style
 final TextStyle textStyle;

 //Circle radius
 final double radius;

 //Spacing between characters
 final double spacing;

 //Text starting position
 final double startAngle;

 //Background shape paint
 final Paint backgroundPaint;

 //Text position either outside or inside circle
 final CircularTextPosition position;

 //Text direction either clockwise or anticlockwise
 final CircularTextDirection direction;

 CircularText(
 {Key key,
 @required this.text,
 this.textStyle = const TextStyle(),
 this.radius = 125,
 this.spacing = 10,
 this.startAngle = 0,
 Paint backgroundPaint,
 this.position = CircularTextPosition.inside,
 this.direction = CircularTextDirection.clockwise})
 : assert(text != null),
 assert(textStyle != null),
 assert(radius != null && radius >= 0),
 assert(spacing != null && spacing >= 0),
 assert(startAngle != null && startAngle >= 0),
 this.backgroundPaint = backgroundPaint ??
 (backgroundPaint = Paint()..color = Colors.transparent),
 assert(position != null),
 assert(direction != null),
 super(key: key);

 @override
 Widget build(BuildContext context) {
 return FittedBox(
 child: SizedBox.fromSize(
 size: Size(2 * radius, 2 * radius),
 child: CustomPaint(
 painter: CircularTextPainter(
 text: text,
 textStyle: textStyle,
 spacing: spacing,
 startAngle: startAngle,
 backgroundPaint: backgroundPaint,
 position: position,
 direction: direction),
 ),
 ),
 );
 }
}

//要实现自定义画图器,可以子类化或实现此接口来定义您的自定义画图委托。
//CustomPaint子类必须实现paint和shouldRepaint方法,
//还可以选择实现hitTest和shouldRebuildSemantics方法以及 语义Builder获取器。
class CircularTextPainter extends CustomPainter {
 final String text;
 final TextStyle textStyle;
 final double spacing;
 final double startAngle;
 final Paint backgroundPaint;
 final CircularTextPosition position;
 final CircularTextDirection direction;

 double _radius = 0.0;
 List<TextPainter> _charPainters = [];

 CircularTextPainter(
 {this.text,
 this.textStyle,
 this.spacing,
 this.startAngle,
 this.backgroundPaint,
 this.position,
 this.direction}) {
 for (final char in text.toUpperCase().split("")) {
 final tp = TextPainter(
 text: TextSpan(text: char, style: textStyle),
 textDirection: TextDirection.ltr);
 tp.layout();
 _charPainters.add(tp);
 }
 }

 @override
 void paint(Canvas canvas, Size size) {
 _radius = min(size.width / 2, size.height / 2);
 canvas.translate(size.width / 2, size.height / 2);
 canvas.drawCircle(Offset.zero, _radius, backgroundPaint);

 if (direction == CircularTextDirection.clockwise) {
 _paintTextClockwise(canvas, size);
 } else {
 _paintTextAntiClockwise(canvas, size);
 }
 }

 void _paintTextClockwise(Canvas canvas, Size size) {
 bool hasStrokeStyle = backgroundPaint.style == PaintingStyle.stroke &&
 backgroundPaint.strokeWidth > 0.0;

 canvas.rotate((startAngle - 90) * pi / 180);
 for (int i = 0; i < _charPainters.length; i++) {
 final tp = _charPainters[i];
 final x = -tp.width / 2;
 final y = position == CircularTextPosition.outside
 ? (-_radius - tp.height) -
 (hasStrokeStyle ? backgroundPaint.strokeWidth / 2 : 0.0)
 : -_radius - (hasStrokeStyle ? tp.height / 2 : 0.0);

 tp.paint(canvas, Offset(x, y));
 canvas.rotate(spacing * pi / 180);
 }
 }

 void _paintTextAntiClockwise(Canvas canvas, Size size) {
 bool hasStrokeStyle = backgroundPaint.style == PaintingStyle.stroke &&
 backgroundPaint.strokeWidth > 0.0;

 canvas.rotate((-startAngle - 270) * pi / 180);
 for (int i = 0; i < _charPainters.length; i++) {
 final tp = _charPainters[i];
 final x = -tp.width / 2;
 final y = position == CircularTextPosition.outside
 ? _radius + (hasStrokeStyle ? backgroundPaint.strokeWidth / 2 : 0.0)
 : (_radius - tp.height) + (hasStrokeStyle ? tp.height / 2 : 0.0);

 tp.paint(canvas, Offset(x, y));
 canvas.rotate(-spacing * pi / 180);
 }
 }

 @override
 bool shouldRepaint(CircularTextPainter oldDelegate) =>
 oldDelegate.text != text ||
 oldDelegate.textStyle != textStyle ||
 oldDelegate.spacing != spacing ||
 oldDelegate.startAngle != startAngle ||
 oldDelegate.backgroundPaint != backgroundPaint ||
 oldDelegate.position != position ||
 oldDelegate.direction != direction;
}

启动类main

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_circular_text/circular_text.dart';

void main() {
 runApp(MaterialApp(home: CircularTextDemo()));
 SystemChrome.setEnabledSystemUIOverlays([]);
}

class CircularTextDemo extends StatefulWidget {
 @override
 _CircularTextDemoState createState() => _CircularTextDemoState();
}

class _CircularTextDemoState extends State<CircularTextDemo> {
 double _spacing = 10;
 double _startAngle = 0;
 double _strokeWidth = 0.0;
 bool _showStroke = false;
 bool _showBackground = true;
 CircularTextPosition _position = CircularTextPosition.inside;
 CircularTextDirection _direction = CircularTextDirection.clockwise;

 @override
 Widget build(BuildContext context) {
 return Material(
 child: Container(
 color: Colors.white,
 alignment: Alignment.center,
 padding: EdgeInsets.only(left: 10, right: 10, top: 50, bottom: 10),
 child: SizedBox.fromSize(
 size: Size(360, double.infinity),
 child: Column(
 mainAxisAlignment: MainAxisAlignment.center,
 crossAxisAlignment: CrossAxisAlignment.center,
 children: <Widget>[
 buildCircularTextWidget(),
 SizedBox.fromSize(size: Size(40, 40)),
 Expanded(
 flex: 1,
 child: CustomScrollView(
 slivers: <Widget>[
 buildSpacingPanel(),
 buildStartAnglePanel(),
 buildStrokeWidthPanel(),
 buildShowBackgroundShapePanel(),
 buildCircularTextPositionPanel(),
 buildCircularTextDirectionPanel(),
 ],
 ),
 ),
 ],
 ),
 ),
 ),
 );
 }

 Widget buildCircularTextWidget() {
 final backgroundPaint = Paint();
 if (_showBackground) {
 backgroundPaint..color = Colors.grey.shade200;
 if (_showStroke) {
 backgroundPaint
 ..color = Colors.grey.shade200
 ..style = PaintingStyle.stroke
 ..strokeWidth = _strokeWidth;
 }
 } else {
 backgroundPaint.color = Colors.transparent;
 }

 return CircularText(
 text: "circular text widget",
 textStyle: TextStyle(
 fontSize: 25, color: Colors.blue, fontWeight: FontWeight.bold),
 radius: 125,
 spacing: _spacing,
 startAngle: _startAngle,
 backgroundPaint: backgroundPaint,
 position: _position,
 direction: _direction,
 );
 }

 Widget buildSpacingPanel() {
 return SliverToBoxAdapter(
 child: Row(
 mainAxisAlignment: MainAxisAlignment.spaceBetween,
 children: <Widget>[
 Text("SPACING", style: TextStyle(fontWeight: FontWeight.bold)),
 Slider(
 value: _spacing,
 min: 0,
 max: 30,
 onChanged: (value) {
 setState(() => _spacing = value);
 },
 )
 ],
 ),
 );
 }

 Widget buildStartAnglePanel() {
 return SliverToBoxAdapter(
 child: Row(
 mainAxisAlignment: MainAxisAlignment.spaceBetween,
 children: <Widget>[
 Text("START ANGLE", style: TextStyle(fontWeight: FontWeight.bold)),
 Slider(
 value: _startAngle,
 min: 0,
 max: 360,
 onChanged: (value) {
 setState(() => _startAngle = value);
 },
 )
 ],
 ),
 );
 }

 Widget buildStrokeWidthPanel() {
 return SliverToBoxAdapter(
 child: Row(
 mainAxisAlignment: MainAxisAlignment.spaceBetween,
 children: <Widget>[
 Text("STROKE WIDTH", style: TextStyle(fontWeight: FontWeight.bold)),
 Slider(
 value: _strokeWidth,
 min: 0,
 max: 100,
 onChanged: _showStroke
 ? (value) {
 setState(() => _strokeWidth = value);
 }
 : null,
 ),
 Checkbox(
 value: _showStroke,
 onChanged: (value) {
 setState(() => _showStroke = value);
 },
 )
 ],
 ),
 );
 }

 Widget buildShowBackgroundShapePanel() {
 return SliverToBoxAdapter(
 child: Row(
 mainAxisAlignment: MainAxisAlignment.spaceBetween,
 children: <Widget>[
 Text("SHOW BACKGROUND",
 style: TextStyle(fontWeight: FontWeight.bold)),
 Checkbox(
 value: _showBackground,
 onChanged: (value) {
 setState(() => _showBackground = value);
 },
 )
 ],
 ),
 );
 }

 Widget buildCircularTextPositionPanel() {
 return SliverToBoxAdapter(
 child: Row(
 mainAxisAlignment: MainAxisAlignment.spaceBetween,
 children: <Widget>[
 Text("POSITION", style: TextStyle(fontWeight: FontWeight.bold)),
 DropdownButton<CircularTextPosition>(
 value: _position,
 items: [
 DropdownMenuItem(
 child: Text("INSIDE"),
 value: CircularTextPosition.inside,
 ),
 DropdownMenuItem(
 child: Text("OUTSIDE"),
 value: CircularTextPosition.outside,
 )
 ],
 onChanged: (value) {
 setState(() => _position = value);
 },
 )
 ],
 ),
 );
 }

 Widget buildCircularTextDirectionPanel() {
 return SliverToBoxAdapter(
 child: Row(
 mainAxisAlignment: MainAxisAlignment.spaceBetween,
 children: <Widget>[
 Text("DIRECTION", style: TextStyle(fontWeight: FontWeight.bold)),
 DropdownButton<CircularTextDirection>(
 value: _direction,
 items: [
 DropdownMenuItem(
 child: Text("CLOCKWISE"),
 value: CircularTextDirection.clockwise,
 ),
 DropdownMenuItem(
 child: Text("ANTI CLOCKWISE"),
 value: CircularTextDirection.anticlockwise,
 )
 ],
 onChanged: (value) {
 setState(() => _direction = value);
 },
 )
 ],
 ),
 );
 }
}

总结

代码实现比较简单,逻辑也比较清楚。没啥说的,点赞,转发,收藏吧。

谢谢观看技术刚刚好的文章技术刚刚好是个人维护,每天至少更新一篇Flutter技术文章,实时为大家播报Flutter最新消息。如果你刚好也在关注Flutter这门技术,那就跟我一起学习进步吧,你的赞,收藏,转发是对我个人最大的支持,维护不易,欢迎关注。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言