安卓写的多了,见过的那些比较好的控件都想用Flutter写出来,前一阵模仿了个登陆按钮,就在昨天完善了一番,感觉可以拿出手了,听说没图都没兴趣看的!那还是先上图吧(注意:录制的GIF看起来比较卡,实际上丝滑流畅)
用户只需要关注显示异常信息即可。登陆成功则直接跳转(因为登陆成功没有动画)、
老规矩,先看看我写的控件怎么使用,后面再讲源码哈
第一步:添加以下代码到你的pubspec.yaml文件
dependencies:
animatedloginbutton: "^0.1.1"
第二步:导包,添加以下代码到你要使用的文件下
import 'package:animatedloginbutton/animatedloginbutton.dart';
第三步:写你的业务代码(下面是我写的示例代码)
import 'dart:io';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:animatedloginbutton/animatedloginbutton.dart';
class LoginAnimationDemo extends StatefulWidget{
@override
State<StatefulWidget> createState() {
return new LoginAnimationDemoState();
}
}
class LoginAnimationDemoState extends State<LoginAnimationDemo>{
//使用系统的请求
var httpClient = new HttpClient();
var url = "https://github.com/";
var _result="";
final LoginErrorMessageController loginErrorMessageController=LoginErrorMessageController();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("登陆按钮动画"),
),
body: new Center(
child:new Container(
child:new AnimatedLoginButton(
loginErrorMessageController:loginErrorMessageController,
onTap: () async {
try {
var request = await httpClient.getUrl(Uri.parse(url));
var response = await request.close();
if (response.statusCode == HttpStatus.ok) {
_result = await response.transform(utf8.decoder).join();
//拿到数据后,对数据进行梳理
loginErrorMessageController.showErrorMessage("网络异常");
} else {
_result = 'ERROR CODE: ${response.statusCode}';
loginErrorMessageController.showErrorMessage("网络异常 $_result");
}
} catch (exception) {
_result = '网络异常';
loginErrorMessageController.showErrorMessage("网络异常");
}
print(_result);
},
),
),
),
);
}
}
使用就需要三步,使用还是很简单的,经过测试,性能也挺好,再附上参数使用表就跟完美了,一起来看看吧
loginTip | 登陆按钮提示 |
width | button的宽度 |
height | button的高度 |
indicatorStarRadian | 弧形指标器的起始角度(旋转的那个带箭头的东西) |
indicatorWidth | 指标器的宽度 |
indicatorColor | 指标器的颜色 |
buttonColorNormal | 未登陆时Button的颜色 |
buttonColorError | 登陆异常Button的颜色 |
textStyleNormal | 未登录时Button文字的样式 |
textStyleError | 登陆异常时Button文字的样式 |
onTap | 点击事件,在此方法中执行登陆操作 |
loginErrorMessageController | 用来显示异常信息 |
showErrorTime | 异常信息显示时间 |
源码:
到了将源码的环节了,我们带着问题去想怎么实现吧! 假如我们用自带的button,他也能收缩(改变宽度),收缩完成后要用层布局套一个Iamge去旋转,后面再伸长,这些都可以做到,但是,后面的异常状态恢复到登陆状态的动画怎么做?想来想去还是得画出button来
说画就画,肯定得先画出button得样子吧,感觉就是两个圆中间夹了一个矩形,中间写上文字而已,想让button变短,那我将中间矩形的宽度通过动画减小不就行了,想想还是蛮有道理的,于是我就写出了如下的代码
///画圆角的button draw corner Button
void drawCornerButton(Canvas canvas,Paint paint,double halfHeight,String txt,TextStyle txtStyle){
canvas.drawCircle(new Offset(halfHeight, halfHeight), halfHeight, paint);
double rectRight = width - halfHeight;
canvas.drawRect(new Rect.fromLTRB(halfHeight, 0.0, rectRight, height), paint);
canvas.drawCircle(new Offset(rectRight, halfHeight), halfHeight, paint);
TextPainter textPainter = new TextPainter();
textPainter.textDirection = TextDirection.ltr;
textPainter.text = new TextSpan(text: txt, style: txtStyle);
textPainter.layout();
double textStarPositionX = (width - textPainter.size.width) / 2;
double textStarPositionY = (height - textPainter.size.height) / 2;
textPainter.paint(canvas, new Offset(textStarPositionX, textStarPositionY));
}
↓↓↓这Button的收缩是没问题了,但是那个旋转的指标器怎么弄呀,这指标器就是个圆里面画个弯弯的箭头么,画圆谁还不会了,这箭头最最简单的方法就是你找个图片画上去,然后开启动画,将图片旋转就行了;但是我这人能画就画,绝对不用图片,一个Path能搞定的事,就不用麻烦美工了,而且我这个还是自适应的屏幕的;闲话少说,画好Pth也要旋转,不过是旋转CustomPaint 控件,看怎么利用path画出效果
void drawCircleButton(Canvas canvas,Paint paint,double halfHeight){
canvas.drawCircle(new Offset(halfHeight, halfHeight), halfHeight, paint);
paint.color=indicatorColor;
paint.style=PaintingStyle.stroke;
paint.strokeWidth=indicatorWidth;
double smallCircleRadius=halfHeight/2;
canvas.drawArc(new Rect.fromLTRB(smallCircleRadius, smallCircleRadius, height-smallCircleRadius, height-smallCircleRadius), indicatorStarRadian, indicatorRadian, false, paint);
double radian=indicatorStarRadian+indicatorRadian;
Path path=getPath(smallCircleRadius,radian);
canvas.save();
canvas.translate(halfHeight, halfHeight);
canvas.drawPath(path, paint);
canvas.restore();
}
///画等腰三角形
Path getPath(double radius,double radian){
Path path=new Path();
double yPoint=sin(radian)*radius;
double xPoint=cos(radian)*radius;
double halfSide=isoscelesTriangle/2;
path.moveTo(xPoint, yPoint+halfSide);
path.lineTo(xPoint, yPoint-halfSide);
double xVertex=xPoint+sqrt(pow(isoscelesTriangle, 2)-pow(halfSide,2));
path.lineTo(xVertex, yPoint);
path.close();
return path;
}
↓↓↓现在能伸缩,能旋转,就缺最后一步了,就是从异常状态恢复到登陆状态的动画,这个动画实在异常状态的Button上再绘制一层,而这一层是由2个Layer叠加出来的,叠加是利用Flutter的BlendMode和canvas.saveLayer完成的,和原生安卓的一样哈,叠加出来的这一层显示多大的面积,是由动画来控制的,看代码
paint.color=recoverButtonColor;
Paint layerPaint=Paint();
canvas.saveLayer(new Rect.fromLTWH(0.0, 0.0, recoverCircleRadius, size.height), layerPaint);
drawCornerButton(canvas,paint,halfHeight,recoverText,recoverTextStyle);
layerPaint.blendMode=BlendMode.dstIn;
canvas.saveLayer(new Rect.fromLTWH(0.0, 0.0, recoverCircleRadius, size.height), layerPaint);
canvas.drawCircle(new Offset(0.0, 0.0), recoverCircleRadius, paint);
canvas.restore();
canvas.restore();
↓↓↓该画的画完了,该叠加的也叠加了,但是我们还得控制什么时候画圆角的Button,什么时候画圆形旋转的Button,什么时候绘制叠加层,整个逻辑在paint方法里面通过一些状态的判断完成,看下paint方法吧
@override
void paint(Canvas canvas, Size size) {
Paint paint=new Paint();
paint.color=buttonColorShow;
double halfHeight = height / 2;
if(height<width) {
drawCornerButton(canvas,paint,halfHeight,showText,showTextStyle);
}else{
drawCircleButton(canvas,paint,halfHeight);
}
if(isNeedRecoverLoginState){
paint.color=recoverButtonColor;
Paint layerPaint=Paint();
canvas.saveLayer(new Rect.fromLTWH(0.0, 0.0, recoverCircleRadius, size.height), layerPaint);
drawCornerButton(canvas,paint,halfHeight,recoverText,recoverTextStyle);
layerPaint.blendMode=BlendMode.dstIn;
canvas.saveLayer(new Rect.fromLTWH(0.0, 0.0, recoverCircleRadius, size.height), layerPaint);
canvas.drawCircle(new Offset(0.0, 0.0), recoverCircleRadius, paint);
canvas.restore();
canvas.restore();
}
}
↓↓↓绘制的类全写完了,我们只只要在build里面引用,传好参数就就行,不过我们的控件还得写个点击方法,(用户写的话话不好控制,我们写好点击让用户写回调更好),看下build方法
@override
Widget build(BuildContext context) {
return new GestureDetector(
child: new RotationTransition( //布局中加载时动画的weight
child: new CustomPaint(
size: new Size(widgetWidth, widgetHeight),
painter: new LoginPainter(
width:widgetWidth,
height:widgetHeight,
recoverText:widget.loginTip,
indicatorStarRadian: widget.indicatorStarRadian,
indicatorWidth:widget.indicatorWidth,
buttonColorShow:_buttonColor,
indicatorColor:widget.indicatorColor,
recoverTextStyle: widget.textStyleNormal,
recoverButtonColor: widget.buttonColorNormal,
isNeedRecoverLoginState: isNeedRecoverLoginState,
recoverCircleRadius: _recoverCircleRadius,
showText: _loginTextTip,
showTextStyle: _textStyle,
),
),
turns: new Tween(begin: 0.0, end: 150.0).animate(
animationControllerWait)
..addStatusListener((animationStatus) {
if (animationStatus == AnimationStatus.completed) {
animationControllerWait.repeat();
}
}),
),
onTap: (){
if(!_isInAnimation) {
_isInAnimation=true;
controllerStart.forward();
}
},
);
}
剩下的就是一些动画初始化,和动画的监听,动画和动画之间逻辑的协调,这些都很简单的,写一个,剩下的复制就行,不过复制的时候要小心,我就是在复制的时候变量名没改完,结果动画变得很诡异,注意:dispose的时候,要注销AnimationController,dispose的时候,要注销AnimationController,dispose的时候,要注销AnimationController;看一下Start动画变量的初始化和监听
void initAnimationStart(){
animationStart = new Tween(begin: widgetWidth, end: widgetHeight).animate(controllerStart)
..addListener(() {
if(isReset){
return;
}
if(mounted){
setState(() {
widgetWidth=animationStart.value;
});
}
});
animationStart.addStatusListener((animationStatus){
if(animationStatus==AnimationStatus.completed){
//动画结束时首先将animationController重置
isReset=true;
controllerStart.reset();
isReset=false;
animationControllerWait.forward();
handleAnimationEnd();
}else if(animationStatus==AnimationStatus.forward){
}
});
}
到这里我们的代码就完了,使用起来简单,源码稍微有那么一点点长,要是有不理解的地方,你可以联系我,如果在使用的过程中有什么问题,也请留言,随时为你解答,
最后附上源码地址:https://github.com/OpenFlutter/PullToRefresh;
里面有很多更酷的控件,欢迎Star;如果喜欢Flutter,可以加入我们哦,我们的QQ群是 :892398530