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 登陸按鈕提示 
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章