Flutter炫酷动画登陆按钮AnimatedLoginButton

安卓写的多了,见过的那些比较好的控件都想用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登陆按钮提示 
widthbutton的宽度
heightbutton的高度
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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

baoolong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值