A flutter package which will help you to generate pin code fields with beautiful design and animations

Overview

A flutter package which will help you to generate pin code fields with beautiful design and animations. Can be useful for OTP or pin code inputs 🤓 🤓

Features 💚

  • Automatically focuses the next field on typing and focuses previous field on deletation
  • Cursor support ⚡️
  • Can be set to any length. (3-6 fields recommended)
  • 3 different shapes for text fields
  • Highly customizable
  • 3 different types of animation for input texts
  • Animated active, inactive, selected and disabled field color switching
  • Autofocus option
  • Otp-code pasting from clipboard
  • iOS autofill support
  • Error animation. Currently have shake animation only. Watch the example app for how to integrate.
  • Works with Flutter's Form. You can use Form validator right off the bat.
  • Get currently typed text and use your condition to validate it. (for example: if (currentText.length != 6 || currentText != "your desired code"))
  • Haptic Feedback support
  • Animated obscure widget support
  • Single placeholder text

Getting Started ⚡️

Demo

Different Shapes

Notes

  • To enable "Fill color" for each cells, enableActiveFill must be set to true. The default value is false.
  • To change the keyboard type, for example to use only number keyboard, or only for email use keyboardType parameter, default is [TextInputType.visiblePassword]
  • FocosNode and TextEditingController will get disposed automatically. Use autoDisposeControllers = false to disable it.
  • to use v5.0.0 or above, developers must have Flutter SDK 1.20.0 or above.
  • to use v6.0.0 or above, developers must have Flutter SDK 1.22.0 or above.

Properties 🔖

/// The [BuildContext] of the application
  final BuildContext appContext;

  ///Box Shadow for Pincode
  final List<BoxShadow>? boxShadows;

  /// length of how many cells there should be. 3-8 is recommended by me
  final int length;

  /// you already know what it does i guess :P default is false
  final bool obscureText;

  /// Character used for obscuring text if obscureText is true.
  ///
  /// Must not be empty. Single character is recommended.
  ///
  /// Default is ● - 'Black Circle' (U+25CF)
  final String obscuringCharacter;

  /// Widget used to obscure text
  ///
  /// it overrides the obscuringCharacter
  final Widget? obscuringWidget;

  /// Whether to use haptic feedback or not
  ///
  ///
  final bool useHapticFeedback;

  /// Haptic Feedback Types
  ///
  /// heavy, medium, light links to respective impacts
  /// selection - selectionClick, vibrate - vibrate
  /// check [HapticFeedback] for more
  final HapticFeedbackTypes hapticFeedbackTypes;

  /// Decides whether typed character should be
  /// briefly shown before being obscured
  final bool blinkWhenObscuring;

  /// Blink Duration if blinkWhenObscuring is set to true
  final Duration blinkDuration;

  /// returns the current typed text in the fields
  final ValueChanged<String> onChanged;

  /// returns the typed text when all pins are set
  final ValueChanged<String>? onCompleted;

  /// returns the typed text when user presses done/next action on the keyboard
  final ValueChanged<String>? onSubmitted;

  /// the style of the text, default is [ fontSize: 20, fontWeight: FontWeight.bold]
  final TextStyle? textStyle;

  /// the style of the pasted text, default is [fontWeight: FontWeight.bold] while
  /// [TextStyle.color] is [ThemeData.accentColor]
  final TextStyle? pastedTextStyle;

  /// background color for the whole row of pin code fields.
  final Color? backgroundColor;

  /// This defines how the elements in the pin code field align. Default to [MainAxisAlignment.spaceBetween]
  final MainAxisAlignment mainAxisAlignment;

  /// [AnimationType] for the text to appear in the pin code field. Default is [AnimationType.slide]
  final AnimationType animationType;

  /// Duration for the animation. Default is [Duration(milliseconds: 150)]
  final Duration animationDuration;

  /// [Curve] for the animation. Default is [Curves.easeInOut]
  final Curve animationCurve;

  /// [TextInputType] for the pin code fields. default is [TextInputType.visiblePassword]
  final TextInputType keyboardType;

  /// If the pin code field should be autofocused or not. Default is [false]
  final bool autoFocus;

  /// Should pass a [FocusNode] to manage it from the parent
  final FocusNode? focusNode;

  /// A list of [TextInputFormatter] that goes to the TextField
  final List<TextInputFormatter> inputFormatters;

  /// Enable or disable the Field. Default is [true]
  final bool enabled;

  /// [TextEditingController] to control the text manually. Sets a default [TextEditingController()] object if none given
  final TextEditingController? controller;

  /// Enabled Color fill for individual pin fields, default is [false]
  final bool enableActiveFill;

  /// Auto dismiss the keyboard upon inputting the value for the last field. Default is [true]
  final bool autoDismissKeyboard;

  /// Auto dispose the [controller] and [FocusNode] upon the destruction of widget from the widget tree. Default is [true]
  final bool autoDisposeControllers;

  /// Configures how the platform keyboard will select an uppercase or lowercase keyboard.
  /// Only supports text keyboards, other keyboard types will ignore this configuration. Capitalization is locale-aware.
  /// - Copied from 'https://api.flutter.dev/flutter/services/TextCapitalization-class.html'
  /// Default is [TextCapitalization.none]
  final TextCapitalization textCapitalization;

  final TextInputAction textInputAction;

  /// Triggers the error animation
  final StreamController<ErrorAnimationType>? errorAnimationController;

  /// Callback method to validate if text can be pasted. This is helpful when we need to validate text before pasting.
  /// e.g. validate if text is number. Default will be pasted as received.
  final bool Function(String? text)? beforeTextPaste;

  /// Method for detecting a pin_code form tap
  /// work with all form windows
  final Function? onTap;

  /// Configuration for paste dialog. Read more [DialogConfig]
  final DialogConfig? dialogConfig;

  /// Theme for the pin cells. Read more [PinTheme]
  final PinTheme pinTheme;

  /// Brightness dark or light choices for iOS keyboard.
  final Brightness? keyboardAppearance;

  /// Validator for the [TextFormField]
  final FormFieldValidator<String>? validator;

  /// An optional method to call with the final value when the form is saved via
  /// [FormState.save].
  final FormFieldSetter<String>? onSaved;

  /// enables auto validation for the [TextFormField]
  /// Default is false
  final AutovalidateMode autovalidateMode;

  /// The vertical padding from the [PinCodeTextField] to the error text
  /// Default is 16.
  final double errorTextSpace;

  /// Enables pin autofill for TextFormField.
  /// Default is true
  final bool enablePinAutofill;

  /// Error animation duration
  final int errorAnimationDuration;

  /// Whether to show cursor or not
  final bool showCursor;

  /// The color of the cursor, default to Theme.of(context).accentColor
  final Color? cursorColor;

  /// width of the cursor, default to 2
  final double cursorWidth;

  /// Height of the cursor, default to FontSize + 8;
  final double? cursorHeight;

  /// Autofill cleanup action
  final AutofillContextAction onAutoFillDisposeAction;

  /// Use external [AutoFillGroup]
  final bool useExternalAutoFillGroup;

  /// Displays a hint or placeholder in the field if it's value is empty.
  /// It only appears if it's not null. Single character is recommended.
  final String? hintCharacter;

  /// the style of the [hintCharacter], default is [fontSize: 20, fontWeight: FontWeight.bold]
  /// and it also uses the [textStyle]'s properties
  /// [TextStyle.color] is [Colors.grey]
  final TextStyle? hintStyle;

<<<<<<< HEAD
  /// ScrollPadding for the text field. Same as [TextFormField]'s scrollPadding
  final EdgeInsets scrollPadding;

=======
  /// ScrollPadding follows the same property as TextField's ScrollPadding, default to
  /// const EdgeInsets.all(20),
  final EdgeInsets scrollPadding;
>>>>>>> 8272cbfd8a1dab43b2b4f4f1107752dda1d9d230

PinTheme

/// Colors of the input fields which have inputs. Default is [Colors.green]
  final Color activeColor;

  /// Color of the input field which is currently selected. Default is [Colors.blue]
  final Color selectedColor;

  /// Colors of the input fields which don't have inputs. Default is [Colors.red]
  final Color inactiveColor;

  /// Colors of the input fields if the [PinCodeTextField] is disabled. Default is [Colors.grey]
  final Color disabledColor;

  /// Colors of the input fields which have inputs. Default is [Colors.green]
  final Color activeFillColor;

  /// Color of the input field which is currently selected. Default is [Colors.blue]
  final Color selectedFillColor;

  /// Colors of the input fields which don't have inputs. Default is [Colors.red]
  final Color inactiveFillColor;

  /// Color of the input field when in error mode. Default is [Colors.redAccent]
  final Color errorBorderColor;

  /// Border radius of each pin code field
  final BorderRadius borderRadius;

  /// [height] for the pin code field. default is [50.0]
  final double fieldHeight;

  /// [width] for the pin code field. default is [40.0]
  final double fieldWidth;

  /// Border width for the each input fields. Default is [2.0]
  final double borderWidth;

  /// this defines the shape of the input fields. Default is underlined
  final PinCodeFieldShape shape;

DialogConfig

/// title of the [AlertDialog] while pasting the code. Default to [Paste Code]
  final String dialogTitle;

  /// content of the [AlertDialog] while pasting the code. Default to ["Do you want to paste this code "]
  final String dialogContent;

  /// Affirmative action text for the [AlertDialog]. Default to "Paste"
  final String affirmativeText;

  /// Negative action text for the [AlertDialog]. Default to "Cancel"
  final String negativeText;

  /// The default dialog theme, should it be iOS or other(including web and Android)
  final Platform platform; //enum Platform { iOS, other } other indicates for web and android

Contributors

Thanks to everyone whoever suggested their thoughts to improve this package. And special thanks goes to these people:

<<<<<<< HEAD ======= >>>>>>> 8272cbfd8a1dab43b2b4f4f1107752dda1d9d230
Emmanuel Vlad
Emmanuel Vlad

📖 💻
Atiq
Atiqur Rahaman

🎨
Milind Mevada
Milind Mevada

📖 💻
Reme Le Hane
Reme Le Hane

📖 💻
TabooSun
TabooSun

💻
Thalles Santos
Thalles Santos

💻
ItamarMu
ItamarMu

💻
Jonathan White
ThinkDigitalSoftware

💻
Jeffry Hermanto
Jeffry Hermanto

💻
ItamarMu
ItamarMu

💻
Sebastian Roth
Sebastian Roth

💻
Dango Mango
Dango Mango

💻
Stanislav Ilin
Stanislav Ilin

💻
Varun Barad
Varun Barad

💻
Mohak Shrivastava
Mohak Shrivastava

💻
ItamarMu
ItamarMu

💻
Kim Minju
Kim Minju

💻
Dmitry Vakhnin
Dmitry Vakhnin

💻
serendipity1004
Jiho Choi

💻
jobfeikens
Job

💻
BrunoEleodoro
Bruno Eleodoro Roza

💻
tgbarker
tgbarker

💻
karabanovbs
karabanovbs

💻
adarsh-technocrat
Adarsh kumar singh

💻
adrianFarkas
Farkas Adrián

💻
grafovdenis
Denis Grafov

💻
ItzNotABug
DarShan

💻
dhruvanbhalara
Dhruvan Bhalara

💻
rodion-m
Rodion Mostovoy

💻

The pin code text field widget example

PinCodeTextField(
  length: 6,
  obscureText: false,
  animationType: AnimationType.fade,
  pinTheme: PinTheme(
    shape: PinCodeFieldShape.box,
    borderRadius: BorderRadius.circular(5),
    fieldHeight: 50,
    fieldWidth: 40,
    activeFillColor: Colors.white,
  ),
  animationDuration: Duration(milliseconds: 300),
  backgroundColor: Colors.blue.shade50,
  enableActiveFill: true,
  errorAnimationController: errorController,
  controller: textEditingController,
  onCompleted: (v) {
    print("Completed");
  },
  onChanged: (value) {
    print(value);
    setState(() {
      currentText = value;
    });
  },
  beforeTextPaste: (text) {
    print("Allowing to paste $text");
    //if you return true then it will show the paste confirmation dialog. Otherwise if false, then nothing will happen.
    //but you can show anything you want here, like your pop up saying wrong paste format or etc
    return true;
  },
)

Shape can be among these 3 types

enum PinCodeFieldShape { box, underline, circle }

Animations can be among these 3 types

enum AnimationType { scale, slide, fade, none }

Haptic Feedbacks can be among these 5 types

enum HapticFeedbackTypes {
  heavy,
  light,
  medium,
  selection,
  vibrate,
}

Trigger Error animation

  1. Create a StreamController
StreamController<ErrorAnimationType> errorController = StreamController<ErrorAnimationType>();
  1. And pass the controller like this.
PinCodeTextField(
  length: 6,
  obscureText: false,
  animationType: AnimationType.fade,
  animationDuration: Duration(milliseconds: 300),
  errorAnimationController: errorController, // Pass it here
  onChanged: (value) {
    setState(() {
      currentText = value;
    });
  },
)
  1. Then you can trigger the animation just by writing this:
errorController.add(ErrorAnimationType.shake); // This will shake the pin code field

This full code is from the example folder. You can run the example to see.

_PinCodeVerificationScreenState(); } class _PinCodeVerificationScreenState extends State { var onTapRecognizer; TextEditingController textEditingController = TextEditingController(); // ..text = "123456"; StreamController errorController; bool hasError = false; String currentText = ""; final GlobalKey scaffoldKey = GlobalKey (); final formKey = GlobalKey (); @override void initState() { onTapRecognizer = TapGestureRecognizer() ..onTap = () { Navigator.pop(context); }; errorController = StreamController (); super.initState(); } @override void dispose() { errorController.close(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.blue.shade50, key: scaffoldKey, body: GestureDetector( onTap: () {}, child: Container( height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width, child: ListView( children: [ SizedBox(height: 30), Container( height: MediaQuery.of(context).size.height / 3, child: FlareActor( "assets/otp.flr", animation: "otp", fit: BoxFit.fitHeight, alignment: Alignment.center, ), ), SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text( 'Phone Number Verification', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22), textAlign: TextAlign.center, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 8), child: RichText( text: TextSpan( text: "Enter the code sent to ", children: [ TextSpan( text: widget.phoneNumber, style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 15)), ], style: TextStyle(color: Colors.black54, fontSize: 15)), textAlign: TextAlign.center, ), ), SizedBox( height: 20, ), Form( key: formKey, child: Padding( padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 30), child: PinCodeTextField( appContext: context, pastedTextStyle: TextStyle( color: Colors.green.shade600, fontWeight: FontWeight.bold, ), length: 6, obscureText: false, obscuringCharacter: '*', animationType: AnimationType.fade, validator: (v) { if (v.length < 3) { return "I'm from validator"; } else { return null; } }, pinTheme: PinTheme( shape: PinCodeFieldShape.box, borderRadius: BorderRadius.circular(5), fieldHeight: 60, fieldWidth: 50, activeFillColor: hasError ? Colors.orange : Colors.white, ), cursorColor: Colors.black, animationDuration: Duration(milliseconds: 300), textStyle: TextStyle(fontSize: 20, height: 1.6), backgroundColor: Colors.blue.shade50, enableActiveFill: true, errorAnimationController: errorController, controller: textEditingController, keyboardType: TextInputType.number, boxShadows: [ BoxShadow( offset: Offset(0, 1), color: Colors.black12, blurRadius: 10, ) ], onCompleted: (v) { print("Completed"); }, // onTap: () { // print("Pressed"); // }, onChanged: (value) { print(value); setState(() { currentText = value; }); }, beforeTextPaste: (text) { print("Allowing to paste $text"); //if you return true then it will show the paste confirmation dialog. Otherwise if false, then nothing will happen. //but you can show anything you want here, like your pop up saying wrong paste format or etc return true; }, )), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 30.0), child: Text( hasError ? "*Please fill up all the cells properly" : "", style: TextStyle( color: Colors.red, fontSize: 12, fontWeight: FontWeight.w400), ), ), SizedBox( height: 20, ), RichText( textAlign: TextAlign.center, text: TextSpan( text: "Didn't receive the code? ", style: TextStyle(color: Colors.black54, fontSize: 15), children: [ TextSpan( text: " RESEND", recognizer: onTapRecognizer, style: TextStyle( color: Color(0xFF91D3B3), fontWeight: FontWeight.bold, fontSize: 16)) ]), ), SizedBox( height: 14, ), Container( margin: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 30), child: ButtonTheme( height: 50, child: FlatButton( onPressed: () { formKey.currentState.validate(); // conditions for validating if (currentText.length != 6 || currentText != "towtow") { errorController.add(ErrorAnimationType .shake); // Triggering error shake animation setState(() { hasError = true; }); } else { setState(() { hasError = false; scaffoldKey.currentState.showSnackBar(SnackBar( content: Text("Aye!!"), duration: Duration(seconds: 2), )); }); } }, child: Center( child: Text( "VERIFY".toUpperCase(), style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), )), ), ), decoration: BoxDecoration( color: Colors.green.shade300, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( color: Colors.green.shade200, offset: Offset(1, -2), blurRadius: 5), BoxShadow( color: Colors.green.shade200, offset: Offset(-1, 2), blurRadius: 5) ]), ), SizedBox( height: 16, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ FlatButton( child: Text("Clear"), onPressed: () { textEditingController.clear(); }, ), FlatButton( child: Text("Set Text"), onPressed: () { textEditingController.text = "123456"; }, ), ], ) ], ), ), ), ); } } ">
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PinCodeVerificationScreen(
          "+8801376221100"), // a random number, please don't call xD
    );
  }
}

class PinCodeVerificationScreen extends StatefulWidget {
  final String phoneNumber;

  PinCodeVerificationScreen(this.phoneNumber);

  @override
  _PinCodeVerificationScreenState createState() =>
      _PinCodeVerificationScreenState();
}

class _PinCodeVerificationScreenState extends State<PinCodeVerificationScreen> {
  var onTapRecognizer;

  TextEditingController textEditingController = TextEditingController();
  // ..text = "123456";

  StreamController<ErrorAnimationType> errorController;

  bool hasError = false;
  String currentText = "";
  final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
  final formKey = GlobalKey<FormState>();

  @override
  void initState() {
    onTapRecognizer = TapGestureRecognizer()
      ..onTap = () {
        Navigator.pop(context);
      };
    errorController = StreamController<ErrorAnimationType>();
    super.initState();
  }

  @override
  void dispose() {
    errorController.close();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blue.shade50,
      key: scaffoldKey,
      body: GestureDetector(
        onTap: () {},
        child: Container(
          height: MediaQuery.of(context).size.height,
          width: MediaQuery.of(context).size.width,
          child: ListView(
            children: <Widget>[
              SizedBox(height: 30),
              Container(
                height: MediaQuery.of(context).size.height / 3,
                child: FlareActor(
                  "assets/otp.flr",
                  animation: "otp",
                  fit: BoxFit.fitHeight,
                  alignment: Alignment.center,
                ),
              ),
              SizedBox(height: 8),
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 8.0),
                child: Text(
                  'Phone Number Verification',
                  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22),
                  textAlign: TextAlign.center,
                ),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 30.0, vertical: 8),
                child: RichText(
                  text: TextSpan(
                      text: "Enter the code sent to ",
                      children: [
                        TextSpan(
                            text: widget.phoneNumber,
                            style: TextStyle(
                                color: Colors.black,
                                fontWeight: FontWeight.bold,
                                fontSize: 15)),
                      ],
                      style: TextStyle(color: Colors.black54, fontSize: 15)),
                  textAlign: TextAlign.center,
                ),
              ),
              SizedBox(
                height: 20,
              ),
              Form(
                key: formKey,
                child: Padding(
                    padding: const EdgeInsets.symmetric(
                        vertical: 8.0, horizontal: 30),
                    child: PinCodeTextField(
                      appContext: context,
                      pastedTextStyle: TextStyle(
                        color: Colors.green.shade600,
                        fontWeight: FontWeight.bold,
                      ),
                      length: 6,
                      obscureText: false,
                      obscuringCharacter: '*',
                      animationType: AnimationType.fade,
                      validator: (v) {
                        if (v.length < 3) {
                          return "I'm from validator";
                        } else {
                          return null;
                        }
                      },
                      pinTheme: PinTheme(
                        shape: PinCodeFieldShape.box,
                        borderRadius: BorderRadius.circular(5),
                        fieldHeight: 60,
                        fieldWidth: 50,
                        activeFillColor:
                            hasError ? Colors.orange : Colors.white,
                      ),
                      cursorColor: Colors.black,
                      animationDuration: Duration(milliseconds: 300),
                      textStyle: TextStyle(fontSize: 20, height: 1.6),
                      backgroundColor: Colors.blue.shade50,
                      enableActiveFill: true,
                      errorAnimationController: errorController,
                      controller: textEditingController,
                      keyboardType: TextInputType.number,
                      boxShadows: [
                        BoxShadow(
                          offset: Offset(0, 1),
                          color: Colors.black12,
                          blurRadius: 10,
                        )
                      ],
                      onCompleted: (v) {
                        print("Completed");
                      },
                      // onTap: () {
                      //   print("Pressed");
                      // },
                      onChanged: (value) {
                        print(value);
                        setState(() {
                          currentText = value;
                        });
                      },
                      beforeTextPaste: (text) {
                        print("Allowing to paste $text");
                        //if you return true then it will show the paste confirmation dialog. Otherwise if false, then nothing will happen.
                        //but you can show anything you want here, like your pop up saying wrong paste format or etc
                        return true;
                      },
                    )),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30.0),
                child: Text(
                  hasError ? "*Please fill up all the cells properly" : "",
                  style: TextStyle(
                      color: Colors.red,
                      fontSize: 12,
                      fontWeight: FontWeight.w400),
                ),
              ),
              SizedBox(
                height: 20,
              ),
              RichText(
                textAlign: TextAlign.center,
                text: TextSpan(
                    text: "Didn't receive the code? ",
                    style: TextStyle(color: Colors.black54, fontSize: 15),
                    children: [
                      TextSpan(
                          text: " RESEND",
                          recognizer: onTapRecognizer,
                          style: TextStyle(
                              color: Color(0xFF91D3B3),
                              fontWeight: FontWeight.bold,
                              fontSize: 16))
                    ]),
              ),
              SizedBox(
                height: 14,
              ),
              Container(
                margin:
                    const EdgeInsets.symmetric(vertical: 16.0, horizontal: 30),
                child: ButtonTheme(
                  height: 50,
                  child: FlatButton(
                    onPressed: () {
                      formKey.currentState.validate();
                      // conditions for validating
                      if (currentText.length != 6 || currentText != "towtow") {
                        errorController.add(ErrorAnimationType
                            .shake); // Triggering error shake animation
                        setState(() {
                          hasError = true;
                        });
                      } else {
                        setState(() {
                          hasError = false;
                          scaffoldKey.currentState.showSnackBar(SnackBar(
                            content: Text("Aye!!"),
                            duration: Duration(seconds: 2),
                          ));
                        });
                      }
                    },
                    child: Center(
                        child: Text(
                      "VERIFY".toUpperCase(),
                      style: TextStyle(
                          color: Colors.white,
                          fontSize: 18,
                          fontWeight: FontWeight.bold),
                    )),
                  ),
                ),
                decoration: BoxDecoration(
                    color: Colors.green.shade300,
                    borderRadius: BorderRadius.circular(5),
                    boxShadow: [
                      BoxShadow(
                          color: Colors.green.shade200,
                          offset: Offset(1, -2),
                          blurRadius: 5),
                      BoxShadow(
                          color: Colors.green.shade200,
                          offset: Offset(-1, 2),
                          blurRadius: 5)
                    ]),
              ),
              SizedBox(
                height: 16,
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  FlatButton(
                    child: Text("Clear"),
                    onPressed: () {
                      textEditingController.clear();
                    },
                  ),
                  FlatButton(
                    child: Text("Set Text"),
                    onPressed: () {
                      textEditingController.text = "123456";
                    },
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}
Comments
  • HOWTO: show keyboard on resume

    HOWTO: show keyboard on resume

    May I kindly have some assistance how to show a keyboard on resume?

    Here's my code:

    
    class _PinCodeVerificationScreenState extends State<PinCodeVerificationScreen> with WidgetsBindingObserver {
    
      TextEditingController controller = TextEditingController();
      FocusNode _focusNode;
      bool hasError = false;
      String currentText = "";
      bool _isButtonTapped = false;
    
      @override
      void initState() {
        super.initState();
        WidgetsBinding.instance.addObserver(this);
        _focusNode = FocusNode();
      }
    
      void didChangeAppLifecycleState(AppLifecycleState state) {
        if(state == AppLifecycleState.resumed){
          print('RESUME');
          _focusNode.requestFocus();
        }
      }
    
      @override
      void dispose() {
        super.dispose();
        WidgetsBinding.instance.addObserver(this);
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: GestureDetector(
            onTap: () => FocusScope.of(context).requestFocus(new FocusNode()),
            child: Container(
              height: MediaQuery.of(context).size.height,
              width: MediaQuery.of(context).size.width,
              child: ListView(
                children: <Widget>[
                  SizedBox(height: 8),
                  Padding(
                    padding: const EdgeInsets.symmetric(vertical: 8.0),
                    child: Text('Codice di verifica', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), textAlign: TextAlign.center),
                  ),
                  Padding(
                    padding:const EdgeInsets.symmetric(vertical: 0.0, horizontal: 5),
                    child: PinCodeTextField(
                      controller: controller,
                      backgroundColor: Colors.transparent, // remove this line to have white background
                      textInputType: TextInputType.number,
                      autoFocus: true,
                      focusNode: _focusNode,
                      
    
    
    
    wontfix 
    opened by angelocapone 23
  • Cannot tap while running integration tests

    Cannot tap while running integration tests

    I'm using it for a project and trying to write an integration on it. Following lines are used to enter the value in it.

    await driver.waitFor(otpTextField); await driver.tap(otpTextField); await driver.enterText("1"); Expected: The field takes the tap and gets the text. Actual The flutter drive times out saying tap is taking too long. I can manually tap and enter values though.

    Version: 5.0.1 Flutter Version: 1.20.1 Dart: 2.9.0

    wontfix 
    opened by istriman29022020 10
  • IOS - misplaced cursor

    IOS - misplaced cursor

    Hi, my QA team found that there's a misplaced cursor on the iOS iPhone 7 device running ios 13.

    6b53c2c4-beee-4517-a37f-f0a84c4876ba

    Here's my implementation

    return new Scaffold(
          key: _otpKey,
          backgroundColor: Colors.white,
          appBar: CustomAppBar(title: AppLocalization.of(context).verificationNumber, showBackButton: false),
          body: _isResendLoading
              ? LoadingIndicator()
              : Container(
                  padding: EdgeInsets.fromLTRB(40.0, 20.0, 40.0, 20.0),
                  child: new Column(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: <Widget>[
                      Container(
                        margin: EdgeInsets.only(top: 15.0),
                      ),
                      RichText(
                        text: TextSpan(
                          style: TextStyle(color: CustomColors.mainColor()),
                          children: <TextSpan>[
                            TextSpan(text: AppLocalization.of(context).enterTheCode + ' '),
                            TextSpan(
                                text: phoneNumber + '.',
                                style: TextStyle(fontWeight: FontWeight.bold)),
                            TextSpan(text: ' ' + AppLocalization.of(context).itsOnlyValid),
                          ],
                        ),
                      ),
                      _buildForm(context),
                      Expanded(
                        child: Align(
                          alignment: Alignment.bottomCenter,
                          child: _buildBottom(context),
                        ),
                      ),
                    ],
                  ),
                ),
        );
      }
    
      Widget _buildForm(context) {
        return new Form(
          key: otpFormKey,
          child: new Container(
            width: 250.0,
            child: new Column(
              children: <Widget>[
                PinCodeTextField(
                  length: 4,
                  obsecureText: false,
                  shape: PinCodeFieldShape.circle,
                  borderRadius: BorderRadius.circular(20.0),
                  backgroundColor: Colors.white,
                  textInputType: TextInputType.number,
                  currentText: (value) {
                    setState(() {
                      _otpCode = value;
                    });
    
                    if (value.length == 4) {
                      verifyOtp();
                      return;
                    }
                  },
                  borderWidth: 2.0,
                  activeColor: Color.fromRGBO(90, 96, 117, 1),
                  inactiveColor: Color.fromRGBO(205, 205, 205, 1),
                  fieldHeight: 100.0,
                  fieldWidth: 50.0,
                )
              ],
            ),
          ),
        );
      }
    
    opened by padejar 10
  •  I'm not able to edit the otp value entered by tapping on the particular block. Auto focus on last block

    I'm not able to edit the otp value entered by tapping on the particular block. Auto focus on last block

    Hey, firstly thank you for the package I have been able to use it well for my use cases so far.

    I am facing one issue, it's that I am not able to focus on a particular block. For example, I have entered an incorrect value in box 2 then I have to delete all the values from block 4 till block 2 to re-enter the value. The cursor seems to focus on the last block always.

    Is there any property that I can pass to enable this? I looked into the package code but couldn't find the solution.

    Hoping for a response, thanks for your time & effort.

    wontfix 
    opened by sakina1403 9
  • Make dialog more flexible.

    Make dialog more flexible.

    I like the paste function of components very much. But I think she can change.

    The existing methods of parameter control will bring many problems, such as user interface and localization. In fact, it can make it more flexible. I've removed the getactionbuttons and showpastedialog functions. Then I give the ownership to the developers themselves.

    I added the following code:

    final String Function(String pasteValue, String fieldValue) pasteAction;

                onLongPress: widget.enabled
                    ? () async {
                        var data = await Clipboard.getData("text/plain");
                        if (data.text.isNotEmpty) {
                          var str = widget.pasteAction(data.text, _textEditingController.text);
                          if (str != null && str.isNotEmpty == true){
                            _textEditingController.text = str.substring(0, widget.length - 1);
                          }
                          else if (widget.pasteAction == null){ // Default paste
                            _textEditingController.text = data.text.substring(0, widget.length - 1);
                          }
                        }
                      }
                    : null,
    

    In this way, developers can customize the response mode of pasteaction. You can adjust it according to your UI, and you can consider the need to paste without asking.

    Because multiple parameters will be deleted. So I didn't try to pull the request.

    wontfix 
    opened by SF-Simon 9
  • [Feature]: Add LastPin

    [Feature]: Add LastPin

    Hi 👋,

    this PR will integrate "last pin" functionality to the actual state.

    I added "lastPin" as a parameter and initialized the controller and the initialValues with the String. If lastPin is not provided, the standard use case as always will appear.

    The actual use case why I am requesting this PR is: Bildschirmfoto 2019-12-26 um 17 11 02

    opened by Ahmadre 9
  • How Proper code for shape underline

    How Proper code for shape underline

    pinTheme: PinTheme(
                                shape: PinCodeFieldShape.underline,
                                fieldHeight: 60,
                                fieldWidth: 50,
                                inactiveColor: kChacaYellowColor,
                                activeColor: kChacaYellowColor,
                                inactiveFillColor: kChacaYellowColor,
                                selectedColor: kChacaYellowColor,
                                selectedFillColor: kChacaYellowColor,
                                activeFillColor: hasError ? Colors.orange : Colors.white,
                              )
    

    its showing eror like this

    • A borderRadius can only be given for a uniform Border.
    bug wontfix 
    opened by echomartha 8
  • Error When change pin code not focus next input

    Error When change pin code not focus next input

    Thank for your package. I have an error when enter pin code then cursor not focus on next input and only fill all when enter max length. Can you help me?

    wontfix 
    opened by ngocdtph03070 8
  • Autofill for android

    Autofill for android

    Nice work by the team!

    I'm not sure if the package supports autofill, it seems not included in this current version. Is there any way to get it work?

    Moreover android supports SMS verify, but that comes with bit restriction like, <#> Your otp code is 987879, If the sender doesn't contain # in front of the message, it won't get detected.

    I'm using Firebase OTP login that doesn't include # In front of every message, so no way of autofill that sms to my field.

    Is there any solution to this? Thanks.

    wontfix 
    opened by Puspharaj 8
  • Not responding to taps when I choose to autofill from SMS

    Not responding to taps when I choose to autofill from SMS

    My Android offers me to "automatically put codes from SMS". If I agree, your pin code widget completely stops responding to manual input. I tap on it, and keyboard doesn't appear. Seems like it endlessly waits for SMS, blocking the input. If I press the "back" button, a pin form begins working again. The issue appeared only on the real device.

    bug 
    opened by subzero911 8
  • show keyboard after on back press

    show keyboard after on back press

    It is not possible to show keyboard again , after pressing keyboard's on back button. Please help to solve this issue. I already used autoDisposeControllers: false ,but it doesn't work.And problem is only for android devices.

    wontfix 
    opened by konuleminova 8
  • Keyboard with (autofocus: true) hides after navigation to a new screen

    Keyboard with (autofocus: true) hides after navigation to a new screen

    A simple code snippet to reproduce this issue is below. Using the go_router package as a navigation. When I open the 2nd screen which has the field autofocus: true, - the keyboard hides anyway! Expected behavior - the keyboard is not hidden as if we were using the commented-out TextFormField widget. P.S.: The same issue using Navigator with "replacement" methods (Navigator.pushReplacement...)

    
    import 'package:flutter/material.dart';
    import 'package:go_router/go_router.dart';
    import 'package:pin_code_fields/pin_code_fields.dart';
    
    void main() => runApp(const MyApp());
    
    final GoRouter _router = GoRouter(
      routes: <RouteBase>[
        GoRoute(
          path: '/',
          name: 'base',
          builder: (BuildContext context, GoRouterState state) {
            return const HomeScreen();
          },
        ),
        GoRoute(
          path: '/details',
          name: 'details',
          builder: (BuildContext context, GoRouterState state) {
            return const DetailsScreen();
          },
        ),
      ],
    );
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp.router(
          routerConfig: _router,
        );
      }
    }
    
    class HomeScreen extends StatelessWidget {
      const HomeScreen({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Home Screen')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                // TextFormField(
                //   autofocus: true,
                // ),
    
                PinCodeTextField(
                  appContext: context,
                  length: 4,
                  autoFocus: true,
                  onChanged: (String value) {},
                ),
                ElevatedButton(
                  onPressed: () => context.goNamed('details'),
                  child: const Text('Go to the Details screen'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class DetailsScreen extends StatelessWidget {
      const DetailsScreen({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Details Screen')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                TextFormField(
                  autofocus: true,
                ),
                ElevatedButton(
                  onPressed: () => context.goNamed('base'),
                  child: const Text('Go back to the Home screen'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    
    opened by nickolight 1
  • Text Color

    Text Color

    how can i change text color of typed characters? its white and i can't change that

    PinCodeTextField(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      pinTheme: PinTheme(
        borderWidth: 1,
        fieldWidth: 30,
        shape: PinCodeFieldShape.underline,
        activeColor: Colors.black,
        inactiveColor: Colors.black,
        activeFillColor: Colors.transparent,
        selectedColor: Colors.black,
        selectedFillColor: Colors.transparent,
        inactiveFillColor: Colors.transparent,
        fieldOuterPadding: EdgeInsets.zero,
      ),
      appContext: context,
      length: 6,
      obscureText: true,
      obscuringCharacter: '*',
      blinkWhenObscuring: true,
      hintStyle: AppTheme(context).color(Colors.black).make(),
      animationType: AnimationType.fade,
      cursorColor: Colors.black,
      animationDuration: const Duration(milliseconds: 300),
      enableActiveFill: true,
      //errorAnimationController: errorController,
      controller: model.$verifyCode.adaptee,
      keyboardType: TextInputType.number,
      onCompleted: (v) {},
      onChanged: (value) {},
      pastedTextStyle: TextStyle(
        color: Colors.green.shade600,
        fontWeight: FontWeight.bold,
      ),
      beforeTextPaste: (text) {
        return true;
      },
    ),
    
    opened by pishguy 0
  • Question: Why is the GestureDetector used in the example?

    Question: Why is the GestureDetector used in the example?

    I am setting up the library, so I am not familiar with it.

    I found there is a GestureDetector wrapping the full body of the Scaffold in the example.

    However, it doesn't have any apparent functionality since the onTap "does nothing" and the only other parameter is "child" (where the actual body of the example resides).

    Am I missing something? Is it only a way to suggest that the focus of the "text flieds" could be granted by catching a tap anywhere on the page? It is confusing...

    opened by guplem 0
  • Add toolbarOptions field, that TextFormField supports

    Add toolbarOptions field, that TextFormField supports

    Hi, There is an issue with copy/paste to the PinCodeTextField.

    inputFormatters: [FilteringTextInputFormatter.digitsOnly], - with this field we can allow users, to input only digits from the keyboard. But still exist an opportunity to paste text to the PinCodeTextField.

    It would be great to be able to control such kind of behavior.

    opened by nickolight 0
  • Set State is being called after disposed from Animation/Timer callback

    Set State is being called after disposed from Animation/Timer callback

    On changing the state of the screen with PinCodeTextField, this error is being thrown:

    ======== Exception caught by foundation library ====================================================
    The following assertion was thrown while dispatching notifications for FocusNode:
    setState() called after dispose(): _PinCodeTextFieldState#28fbb(lifecycle state: defunct, not mounted, tickers: tracking 0 tickers)
    
    This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
    
    The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
    This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
    
    opened by shaurya-src 0
Releases(V7.3.0)
  • V7.3.0(Jul 28, 2021)

    AutovalidateMode.disabled will not add extra space below the pin cells Added new attributes errorBorderColor, readOnly, textGradient and scrollPadding

    Source code(tar.gz)
    Source code(zip)
  • V7.2.0(May 26, 2021)

  • v7.0.0(Mar 11, 2021)

    Features ✨

    • Added AutofillContextAction, default is AutofillContextAction.commit

    Breaking changes ⚠️

    • Migrated to null-safety.
    • Minimum Flutter version is set to 1.22.0

    Fixes 🐛

    • Fixed default text value not showing, #153
    Source code(tar.gz)
    Source code(zip)
  • v6.1.0(Jan 20, 2021)

    Features ✨

    • Added haptic feedback enum HapticFeedbackTypes { heavy, light, medium, selection, vibrate, }
    • Added animated obscure widget support obscuringWidget and blinkWhenObscuring

    Fixes 🐛

    • Fixed bug related to TextStyle not given.
    • Fixed bug related to setState is being called after disposal.
    Source code(tar.gz)
    Source code(zip)
  • v6.0.2(Dec 29, 2020)

  • v6.0.1(Nov 10, 2020)

  • v6.0.0(Oct 10, 2020)

    Features ✨

    • Added Cursor ` /// Whether to show cursor or not final bool showCursor;

      /// The color of the cursor, default to Theme.of(context).accentColor final Color cursorColor;

      /// width of the cursor, default to 2 final double cursorWidth;

      /// Height of the cursor, default to FontSize + 8; final double cursorHeight; `

    • Added boxhShadows

    Breaking changes ⚠️

    • autoValidate changed according to the new version of Flutter
    • Minimum Flutter version is set to 1.22.0

    Fixes 🐛

    • typo fixes
    Source code(tar.gz)
    Source code(zip)
  • v5.1.0(Sep 16, 2020)

    [5.1.0]

    Features ✨

    • Added errorAnimationDuration to customize Error animation duration.
    • Added enablePinAutofill to enable or disable the auto-fill feature.

    Breaking changes ⚠️

    • Chagned textInputType to more familiar keyboardType to change they keyboard type

    Fixes 🐛

    • Fixed rootNavigator issue for paste dialogs, #101
    Source code(tar.gz)
    Source code(zip)
  • v5.0.0(Aug 15, 2020)

    Features ✨

    • Added onSave, onTap callbacks.
    • Added PastedTextStyle.
    • Added iOS autofill added wtih Flutter version 1.20.0

    Breaking changes ⚠️

    • Must provide context in the appContext parameter.
    • Minimum Flutter version is set to 1.20.0

    Fixes 🐛

    • Reopen keyboard onTap on the cells #92, thanks to https://github.com/YaroslavGS for the suggestion
    Source code(tar.gz)
    Source code(zip)
  • v4.0.0(Jul 15, 2020)

    Features ✨ Added support for flutter-web Breaking changes ⚠️ Replaced internal automatic OS selection for dialog styles, you can select which style you want by configuring with the DialogConfig

    Source code(tar.gz)
    Source code(zip)
  • v3.1.0(Jun 14, 2020)

    Better performance overall

    Features ✨

    • Added new parameter called validator, autoValidate & errorTextSpace.

    Breaking changes ⚠️

    • The internal TextField has been changed to TextFormField to work with Form
    • The debug logs will not be printed in release builds.
    Source code(tar.gz)
    Source code(zip)
  • v3.0.0(May 3, 2020)

    Better performance overall

    Features ✨

    • Added new parameter called bool Function(String text) beforeTextPaste, a callback method to validate if text can be pasted.
    • Introduced PinTheme & DialogConfig
    • Added an optional constructor parameter: void Function(String) onCompleted. which triggers when all fields are filled.

    Fixes 🐛

    • When pressing the back button to close the keyboard, you can't open the keyboard back up again. #51
    • When we long press on the OTP field to paste the code & if clipboard data is NULL, it gets crashed. #45

    Breaking changes ⚠️

    • Removed all the color, cell height, width & dialog configurations and moved them in PinTheme and DialogConfig
    Source code(tar.gz)
    Source code(zip)
  • v2.5.1(Apr 11, 2020)

  • v2.5.0(Apr 11, 2020)

    • Added errorAnimationController to trigger shake animation. It can be used for errors.
    • Added autoDisposeControllers which can be set to true for auto TextEidtingController and FocusNode disposal.
    • Fixed typos & Optimized code
    Source code(tar.gz)
    Source code(zip)
Owner
Adar
Has a very keen interest in Front-end, mobile applications & problem-solving. Currently working with Flutter :) And A Dota freak xD
Adar
🔔 A flutter package to create cool and beautiful text animations. [Flutter Favorite Package]

Animated Text Kit A flutter package which contains a collection of some cool and awesome text animations. Recommended package for text animations in C

Ayush Agarwal 1.4k Jan 6, 2023
A widget for stacking cards, which users can swipe horizontally and vertically with beautiful animations.

A widget for stacking cards, which users can swipe horizontally and vertically with beautiful animations.

HeavenOSK 97 Jan 6, 2023
Create powerful animations in Flutter and use the hero animation for complex animations

Hero Animation - Locations UI - Flutter Create powerful animations in Flutter and use the hero animation for complex animations. ⚡  Social Media  Twit

null 3 Dec 11, 2021
Beautiful-Login-Design - A design challenge made in flutter + custompainter

Login Design This little project was proposed in [Flutter & Dart en Español] gro

Juan Suarez 10 Nov 3, 2022
☀️ A Flutter package for some material design app intro screens with some cool animations.

IntroViews is inspired by Paper Onboarding and developed with love from scratch. Checkout our fully responsive live example app. Table of contents Fea

Ayush Agarwal 652 Dec 30, 2022
A catalog of beautiful, reusable and elegant animations

Animations Catalog The goal of this project is to catalog and build multiple animation patterns with flutter. Budget App Animation Harley Swipe To Enl

null 3 Sep 6, 2021
Generate Flutter vector code from a subset of SVG files.

built_vector Generates Flutter vector code from minimalist SVG-like files. Usage > pub global activate built_vector > pub global run built_vector -i <

Aloïs Deniel 33 Dec 29, 2020
A flutter package which contains a collection of some cool and beautiful effects; support android and ios

flutter effects A flutter package which contains a collection of some cool and beautiful effects; support android and ios . Screenshot type support ch

大海豚 462 Jan 3, 2023
A beautiful animated flutter widget package library. The tab bar will attempt to use your current theme out of the box, however you may want to theme it.

Motion Tab Bar A beautiful animated widget for your Flutter apps Preview: | | Getting Started Add the plugin: dependencies: motion_tab_bar: ^0.1.5 B

Rezaul Islam 237 Nov 15, 2022
A package to create nice and smooth animations for flutter

animation_director A package to create nice and smooth animations for flutter Introduction A simple package to build beautiful and smooth animations f

null 10 Nov 28, 2022
Flutter package for creating awesome animations.

?? Simple Animations Simple Animations is a powerful package to create beautiful custom animations in no time. ?? fully tested ?? well documented ?? e

Felix Blaschke 879 Dec 31, 2022
A flutter package that adds support for vector data based animations.

animated_vector Description and inspiration A package that adds support for vector data based animations. The data format is heavily inspired from the

Potato Open Sauce Project 6 Apr 26, 2022
A Flutter package with a selection of simple yet very customizable set of loading animations.

Flutter Loading Animations A simple yet very customizable set of loading animations for Flutter projects. Installation Add the following to your pubsp

Andre Cytryn 171 Sep 23, 2022
Render After Effects animations natively on Flutter. This package is a pure Dart implementation of a Lottie player.

Lottie for Flutter Lottie is a mobile library for Android and iOS that parses Adobe After Effects animations exported as json with Bodymovin and rende

Xavier H. 894 Jan 2, 2023
A set of transition patterns within the animations package using flutter.

Flutter Motion Transitions A fultter app to demonstrate Material motion system. Material Motion System The four main Material transition patterns are

Rafsan Ahmad 17 Oct 13, 2022
Help you to build pull-down refresh and pull-up loading in the simplest way.

frefresh Help you to build pull-down refresh and pull-up loading in the simplest way. Although unprecedented simplicity, but the effect is amazing. It

Fliggy Mobile 427 Nov 26, 2022
Domain-Driven Design + Parse Server For FlutterDomain-Driven Design + Parse Server For Flutter

Domain-Driven Design + Parse Server Install Parse SDK on Flutter project and then update .env file PARSE_APPLICATION_ID=YOUR_APP_ID_HERE PARSE_CLIENT_

Long Hoàng 19 Nov 18, 2022
Generate a timeline for a list

Timeline A flutter package that allows you to create basic timelines on your flutter application. This is customizable and easy to plugin to your appl

Rejish Radhakrishnan 65 Nov 10, 2022
Loading widget based on a Flare animation, allow you to create beautiful custom loading widgets or dialogs

flare_loading Loading widget based on a Flare animation, allow you to create custom loading widgets or dialogs If you're using Rive instead of Flare p

Jimmy Aumard 25 Apr 16, 2021