系列文章
效果图
标题有些拗口,一开始的标题好像触发敏感词了,所以只能这样了
分析
很明显这是由两种颜色组成滴~
运动轨迹分别是里面一个圈、外面一个圈。具体表现为,外层两个四分之一弧,内层两个四分之一弧,且中心对称。
内外相对运动,内圈速度要略慢一些,毕竟周长比较短(速度全凭个人喜好)~
按内外分为两个widget,并对他们进行旋转,就可以实现了。
弧度表
来源网络,这个下面会用到
绘制代码
绘制中心对称的两个90度弧,使用CustomPaint即可实现。
以外部widget(黑色)为例,代码如下:
CustomPaint(
painter: OuterPainter(),///传入我们自己的 painter
)
class OuterPainter extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
///canvas 是我们作画的画布,Size则是父widget给我们的画布大小
///也可以通过CustomPaint的size属性指定,
///其次如果CustomPaint的child不为空,这个size的值会与child大小有关
///CustomPaint的size将被忽略
///初始化一个画笔
Paint paint = Paint();
paint.color = Color.fromRGBO(51, 51, 51, 1);
paint.strokeWidth = 6;///画笔粗细
paint.isAntiAlias = true;///抗锯齿
paint.style = PaintingStyle.stroke;///默认是fill,我们不需要填充,选stroke
///首先我们需要一个矩形,画布好根据这个矩形来确定圆的位置(这个圆是抽象的)
Rect rect = Rect.fromCircle(center: Offset(size.width/2,size.height/2),radius: size.width/2);
/// drawArc 绘制一条弧线
/// 参数1 确定圆的矩形,
/// 参数2和3,分别是起始位置和扫过的角度,它原名是叫startAngle 和 sweepAngle,
/// 0.0从中心点到右侧0度 , 扫过pi/2 度 (90度),
/// 参数4(userCenter)false 只绘制一个弧线、如果是true,则会绘制一个扇形
/// 参数5 画笔
canvas.drawArc(rect, 0.0, pi/2, false, paint);
///因为是中心对称,所以我们将位置移动180度
canvas.drawArc(rect, pi, pi/2, false, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false; ///如果你的painter和一个动画绑定,理论上这里应是true(或者根据自己需要的值进行判断)表示需要重绘
}
}
这样我们就可以绘制出外面的widget了,同理可以绘制出里面的,下面我们就开始让他们转了。
动画代码
我们可以直接使用 RotationTransition包裹我们的widget,并传入对应的animation,通过animationController来控制。首先我们先定义animation,代码如下:
AnimationController outerController,innerController;
Animation outerAnim,innerAnim;
@override
void initState() {
super.initState();
///使用动画的页面 要混入TickerProviderStateMixin,否则 vsync传入参数this 会报错,
/// 简单的讲,每次vsync信号回来的回调,会计算并更新动画的值,在随后刷新widget 便会看到改变
///(前提你的widget有值与动画相关)
///参数2:动画持续时间
outerController = AnimationController(vsync: this,duration: Duration(milliseconds:3000));
innerController = AnimationController(vsync: this,duration: Duration(milliseconds: 2000));
///补间动画,begin和end的差可以理解为动画的距离,可以看到外部要比内部的值,不管是从时间还是距离都要大一些
///,如果一致,内部的旋转速度就会比外部的快了。
outerAnim = Tween(begin: 0.0,end: 2.0).animate(outerController);
innerAnim = Tween(begin: 1.0,end: 0.0).animate(innerController);
///分别为动画的执行状态,这个见名知意,不做赘述了。
innerController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print("completed");
innerController.reset();
innerController.forward();
} else if (status == AnimationStatus.dismissed) {
print("dismissed");
innerController.forward();
} else if (status == AnimationStatus.forward) {
print("forward");
} else if (status == AnimationStatus.reverse) {
print("reverse");
}
});
outerController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print("completed");
outerController.reset();
outerController.forward();
} else if (status == AnimationStatus.dismissed) {
print("dismissed");
outerController.forward();
} else if (status == AnimationStatus.forward) {
print("forward");
} else if (status == AnimationStatus.reverse) {
print("reverse");
}
});
}
这里我们就完成了animation的定义,之后我们将outerAnim,innerAnim;分别传给RotationTransition的turns参数即可。
完整代码
class DemoPageState extends State<DemoPage> with TickerProviderStateMixin {
AnimationController outerController,innerController;
Animation outerAnim,innerAnim;
@override
void dispose() {
outerController?.dispose();
innerController?.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
outerController = AnimationController(vsync: this,duration: Duration(milliseconds:3000));
innerController = AnimationController(vsync: this,duration: Duration(milliseconds: 2000));
outerAnim = Tween(begin: 0.0,end: 2.0).animate(outerController);
innerAnim = Tween(begin: 1.0,end: 0.0).animate(innerController);
innerController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print("completed");
innerController.reset();
innerController.forward();
} else if (status == AnimationStatus.dismissed) {
print("dismissed");
innerController.forward();
} else if (status == AnimationStatus.forward) {
print("forward");
} else if (status == AnimationStatus.reverse) {
print("reverse");
}
});
outerController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print("completed");
outerController.reset();
outerController.forward();
} else if (status == AnimationStatus.dismissed) {
print("dismissed");
outerController.forward();
} else if (status == AnimationStatus.forward) {
print("forward");
} else if (status == AnimationStatus.reverse) {
print("reverse");
}
});
}
@override
Widget build(BuildContext context) {
if(!outerController.isAnimating)outerController.forward();
if(!innerController.isAnimating)innerController.forward();
return Container(
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Stack(
alignment: Alignment.center,
children: <Widget>[
RotationTransition(
turns:outerAnim ,
child: Container(
width: 100,
height: 100,
child: CustomPaint(
painter: OuterPainter(),
),
),
),
RotationTransition(
turns: innerAnim,
child: Container(
width: 86,
height: 86,
child: CustomPaint(
painter: InnerPainter(),
),
),
),
],
),
],
),
);
}
}
DEMO
https://github.com/bladeofgod/flutter_loading