pin_ui 0.1.0+1 copy "pin_ui: ^0.1.0+1" to clipboard
pin_ui: ^0.1.0+1 copied to clipboard

Widgets for implementing PIN code related screens in Flutter applications

The pin_ui package is responsible for fast layout of advanced PIN code related screens in Flutter applications. It provides 2 core widgets:

  1. Pinpad. Highly customizable numeric keyboard for entering PIN code.
  2. Pin Indicator. Obscured (or not) field for visualizing entered PIN code status with lots of pre-made animations to choose from.

If you are also interested in fast implementation of backend part of PIN code feature, then check out pin package.

Pub Star on Github

Pinpad #

Pinpad is a numeric keyboard with 2 extra key slots. Usually they place "Forgot PIN" and Biometrics buttons there.

drawing

Usage #

  • 0-9 keys can be decorated with Flutter's BoxDecoration class to change its appearance: background color, border radius, border, shadows, etc. There are 3 states: default, pressed and disabled. Each is customizable on its own, so you can make them look different. Provide keyDefaultDecoration, keyPressedDecoration and keyDisabledDecoration to set it in a way you prefer.
  • 0-9 keys has a TextStyle property. There are also 3 states: default, pressed and disabled. Set keyDefaultTextStyle, keyPressedTextStyle and keyDisabledTextStyle to style digits inside the keys.
  • Resize keys with keyHeight and keyWidth.
  • Spacing between keys can be changed with horizontalSpacing and verticalSpacing properties. By default, these values will be calculated depending on the screen size.
  • You can disable or make pinpad invisible by setting enabled and isVisible. Making it invisible will not change actual size of the keyboard.
  • Vibration can be enabled with vibrationEnabled property. It will make a slight vibration feedback when a key is pressed.
  • To add extra keys to the left and right of 0 key, provide leftExtraKey and rightExtraKey. These parameters have PinpadExtraKey type. It is a wrapper above your child widget where you add onTap callback. Child can be any widget, but also you can use PinpadKey or PinpadTextKey provided by this package to make all buttons look the same.
Pinpad(
  onKeyTap: myKeyTapHandler,
  keyDefaultDecoration: myDecoration,
  keyPressedDecoration: myDecoratino.copyWith(color: blue),
  keyDisabledDecoration: myDecoration,
  keyDefaultTextStyle: myTextStyle,
  keyPressedTextStyle: myTextStyle.copyWith(fontWeight: FontWeight.w700),
  keyDisabledTextStyle: myTextStyle.copyWith(color: grey),
  leftExtraKey: PinpadExtraKey(
    onTap: handleForgotPinTap,
    child: myForgotPinButton,
  ),
  rightExtraKey: PinpadExtraKey(
    onTap: handleEraseTap,
    child: myEraseButton,
  ),
  horizontalSpacing: x,
  verticalSpacing: y,
  keyWidth: m,
  keyHeight: n,
  enabled: myEnableCondition,
  isVisible: myVisibleCondition,
  vibrationEnabled: true,
)


Pin Indicator #

Pin Indicator is a widget that provides visual representation of PIN code: how many digits are entered, is there an error, was an attempt successful and so on.
The simplest variants of Pin Indicator is a line of colored dots or obscured stars.

Usage #

PinIndicator widget has 2 constructors: default one and PinIndicator.builder. They have mostly the same set of parameters, the main difference is that by using .builder version you can provide any widgets as items. So it more customizable. If you don't need this customization level, just use default one. It provides simple items that can be decorated with Flutter'sBoxDecoration.

  • Items inside PinIndicator have 4 states: Default represents not entered PIN code digits, Input represents entered PIN code digits, Error indicates that there is something wrong happened (wrong PIN entered), Success indicates that user entered correct PIN code.
    So there are 4 different parameters to customize your Pin Indicator.
    For .builder constructor there are 4 required parameters: defaultItemBuilder, inputItemBuilder, errorItemBuilder and successItemBuilder.
    For default constructor there are 4 optional parameters: defaultDecoration, inputDecoration, errorDecoration and successDecoration. These parameters are not required! If not provided, pre-made decorations will be used instead.
  • controller – your instance of PinIndicatorAnimationController class for managing animations if needed.
  • length is total number of digits in PIN code. It can be anything starting from 3, but usually it is 4, 5 or 6.
  • currentLength represents number of already entered digits by user.
  • isError – error state enabler.
  • isSuccess – success state enabler.
  • spacing – distance between items.
  • size – size of item. It resizes items for PinIndicator. There is no such parameter for .builder constructor, but take in mind that all items better be the same fixed size. Even if they must look different, make them take same amount of space by wrapping with SizedBox or adding invisible margins. This will make Indicator static and will not break animations that depends on child size in their calculations.
  • loadingCollapseAnimationChild – widget used in Loading Collapse animation.
  • successCollapseAnimationChild – widget used in Success Collapse animation.
PinIndicator(
  errorDecoration = myErrorDecoration,
  successDecoration = mySuccessDecoration,
  inputDecoration = myInputDecoration,
  defaultDecoration = myDefaultDecoration,
  length = 4,
  currentLength = pin.length,
  isError = isPinError,
  isSuccess = isPinSuccess,
  controller = myController,
  spacing = 24,
  size = 14,
  loadingCollapseAnimationChild = myLoadingCollapsedWidget,
  successCollapseAnimationChild = mySuccessCollapsedWidget,
)

PinIndicator.builder(
  errorItemBuilder: (i) => myErrorItemBuilder(i),
  successItemBuilder: (i) => mySuccessItemBuilder(i),
  inputItemBuilder: (i) => myInputItemBuilder(i),
  defaultItemBuilder: (i) => myDefaultItemBuilder(i),
  length = 4,
  currentLength = pin.length,
  isError = isPinError,
  isSuccess = isPinSuccess,
  controller = myController,
  spacing = 24,
  loadingCollapseAnimationChild = myLoadingCollapsedWidget,
  successCollapseAnimationChild = mySuccessCollapsedWidget,
)

NOTE: builders for PinIndicator.builder constructor must return not normal Widget type but PreferredSizeWidget. To do so just add implements PreferredSizeWidget to a StatefulWidget or StatelessWidget and provide a size with preferredSize overridden getter. It is required for some animations, because they use size of Pin Indicator items in their calculations.

There is also a type defined for these 4 builders named PinIndicatorItemBuilder. Use it if you need to pass them through layers of widgets or store somewhere:

typedef PinIndicatorItemBuilder = PreferredSizeWidget Function(int index);

Animations #

Animations are core features of pin_ui package. It contains lots of pre-made animations for several scenarios when user interact with your app.
Why use animations? First of all, it looks much nicer. Second thing is that they can take user's attention while some async backed requests or other initialization processes will happen in background. Usually it's done with boring loading indicators or shimmers. But a good sequences of cool animations can handle this task much better!

Pin Indicator can be animated in such ways:

  • Input. Animate input when user enters a digit of PIN code.
  • Loading. Animate loading when you need to hide long-lasting async operation or to make animations flow more smooth and obvious to user.
  • Success. Animate success when user enters correct PIN code to also hide long-lasting async operation after loading animation. And to demonstrate user that they entered correct PIN code and no more actions required from their side.
  • Error. Animate error when user entered wrong PIN code to show them that entered PIN code is incorrect.
  • Clear. Animate clear when user clears entire PIN code at once if there are such function or when other logic triggers clearing of entire PIN code, such as tapping "Forget PIN" button or requesting biometrics.
  • Erase. Animate erase when user erases a digit from PIN code.
  • Idle. Animate idle when user was inactive for a while to instigate them for an action and show that app is still alive and waits for an action from user's side.

Each of these animation types has a set of already implemented animations to chose from. Here is the table with all of them plus useful information and recommendations.

Type Name Demo Notes and Recommendations Vibration
Input Inflate • Default Input animation
• Recommended to be used in pair with Erase Deflate animation
+
Input Fall • Recommended to be used in pair with Erase Take Off animation -
Input Fade • Recommended to be used in pair with Erase Fade animation -
Loading Jump • Default Loading animation
• Good at combining with Success Fill Last animation
• Add delays if you want to repeat this animation
+
Loading Wave Inflate -
Loading Wave Deflate -
Loading Collapse • You can provide own animation indicator in PinIndicator via loadingCollapseAnimationChild parameter
• Animation will end right after collapsing, so you have to configure time to show the loader by setting delayAfter
• Recommended not to use any Success animation right after this one as it will require to Pin Indicator items appear imminently from nowhere and only then start animating
-
Loading Travel -
Success Collapse • Default Success animation
• You can provide own child which is displayed after collapse in PinIndicator via successCollapseAnimationChild parameter
-
Success Fill • This one is great when you make navigation without animation (in onComplete callback) to a screen which has a background color same as fill color of animation, so it looks like seconds splash screen for your app
• This animation will have the same play time for any screen size or any start position (position of Pin Indicator on the screen) because it dynamically calculates fill speed when built
-
Success Fill last • Same as Success Fill animation, but this one is perfect for combining with Loading Jump animation -
Success Kick +
Error Shake • Default Error animation +
Error Jiggle • Jiggle effect will only be visible for non-circle Pin Indicator items +
Error Brownian • Items randomly move around and then returns to the start point -
Error Blink -
Clear Drop -
Clear Fade • Default Clear animation
• Recommended to be used after Error animations if you need so (like in the example), to not overload it with unnecessary moving staff on screen
-
Erase Deflate • Default Erase animation
• Recommended to be used in pair with Input Inflate animation
+
Erase Take off • Recommended to be used in pair with Input Fall animation -
Erase Fade • Recommended to be used in pair with Input Fade animation -
Idle Wave • Default Idle animation -
Idle Pulse -
Idle Flash • Items randomly inflate and deflate -

Seeing alone, some animations may look raw at first, but by combining them together and adding delays before and after, good sequences can be created!

You can try it out in the example project and use it as a playground to test your ideas. Also, it can be a great start point to begin with where you can copy some code for your application.

Controller #

Animations are called via PinIndicatorAnimationController provided by this package. Associate a controller with PinIndicator by passing it in controller parameter. After that you can call animation methods and Indicator will be animated in a way you said it to.

final controller = PinIndicatorAnimationController();
        
PinIndicator(
  controller: controller,
  ...
)

Controller is responsible for managing animations. It is done with Queue, so all animation calls are synchronous operations. In queue animations can go one by one, or they can interrupt other not important animations. More on this in Animations priority section.

To start playing a desired animation just call appropriate method from controller.

controller.animateLoading();

Simple call does not contain any parameters. In this case default animation will be played without any other modifications. But you can slightly modify animation call by passing necessary parameters. List of available parameters differs from one method to another. Here is description for them:

  • animation – is a variant of animation you want to play (check table above).
  • delayBefore and delayAfter – delays to be set before and after this animation. They are useful for making a sequences of animations or adding extra time for animation to last longer in some cases.
  • onComplete and onInterrupt – useful callbacks for any cases: staring one animation after another, reacting to user input during interruptible animations, navigating or triggering other needed logic.
  • animationSpeed – animation speed multiplier. Useful in some cases because all animation have preconfigured duration.
  • vibration – enables vibration for animation (not all animation will vibrate!). More in Vibration section.
  • repeatCount – a way to avoid calling an animation multiple times.


You may want to update your UI when animations starts or ends. This can be done by listening to controller via ValueListenableBuilder. It will return a value with PinIndicatorAnimation type containing service information about current animation or null if no animation is playing. You can avoid using this and in case you need to check what animation is now playing appeal to getters in controller.
Listening to controller may be useful in some cases. For example, if you want to disable Pinpad or make it invisible when loading or success animation are in progress to show user that something is happening in background and no more actions required. Or if you want to update Scaffold background color depending on current animation.
In this code snippet it listens to the controller and disables or hides Pinpad when needed:

return ValueListenableBuilder(
  valueListenable: controller,
  builder: (context, value, child) {
    return Column(
      children: [
        PinIndicator(
          contoller: controller,
          ...
        ),
        Pinpad(
          // Disable keyboard when important animation is playing
          enabled: !controller.isAnimatingNonInterruptible,
          // Hide keyboard when Success animation is playing
          isVisible: !controller.isAnimatingSuccess,
          ...
        ),
      ],
    );  
  },
);


All the animations works with any allowed number of items (> 3). But if you have too many items animations will look fast because they have fixed play time. So animation speed can be adjusted to make any of them a bit faster of slower via animationSpeed parameter when calling the animation:

controller.animateSuccess(
  animation: PinSuccessAnimation.fillLast,
  animationSpeed: 2, // <-- animation will be played at 2x speed
  // animationSpeed: 0.33, // <-- this will make animation 3 times slower

  // Both delays won't be affected with animationSpeed value
  delayBefore: Duration(milliseconds: 240),
  delayAfter: Duration(seconds: 1),
);


When starting a new animation you may want to replace items of Pin Indicator with different widget or at least change their colors, so animation looks more natural and easily understandable for user. To do so you don't need to add any extra conditions or update your builders' code. Pin Indicator widget will handle it all by reading isError and isSuccess values for these two states. And for handling default and input states it will depend on length and currentLength to decide what builder or decoration apply for each item.

Managing Error and Success states is easy via onComplete and onInterruct callbacks. onComplete triggers when animation and delay after (if set) is over, onInterrupt triggers when animation is stopped (by user with stop()or by other more important animation). These callbacks can be set when calling animation in controller:

// Your Pin Indicator widget
PinIndicator(
  controller: controller,
  length: 4,
  currentLength: pin.length,
  isError: isPinError,
  isSuccess: isPinSuccess,
)

________________________________________________________________________________


// In case user entered correct PIN code
controller.animateLoading(
  onComplete: () => setState(() => isPinSuccess = true),
);
controller.animateSuccess(
  onComplete: () { /* Perform navigation or any other necessary logic here */ },
);

________________________________________________________________________________


// In case user entered wrong PIN code     
setState(() => isPinError = true); // Set Error state
controller.animateError(
  onInterrupt: clear,
);
controller.animateClear(
  onComplete: clear,
  onInterrupt: clear,
);

void clear() => setState(() {
  pin = ''; // Clean current entered pin variable
  isPinError = false; // Go back to Default state for a new attempt
});

NOTE: As any other controller, this one also should be disposed if possible!

Animations priority #

Animations queue designed that way so animation can interrupt other one if it is possible. Interruptibility of every animation is preconfigured. As long as role to interrupt other animations. Here is the list of all animation types with their properties:

Type Can interrupt Is interruptible
Input True True
Loading False False
Success False False
Error False True
Clear False True
Erase True True
Idle False True

So take that in mind when designing logic and animation sequences.
Normally animations goes like this:

  • First there are Inputs and Erases
  • If PIN is correct, one or a few Loadings and one Success
  • If PIN in wrong, one Error and one Clear
  • Idles can start anytime

Follow these rules and you won't meet any troubles.

Vibration #

Some of the animation have a vibration feature! You can check if it is available in the table.

To enable vibration feature you have to add this to your Android Manifest:

<uses-permission android:name="android.permission.VIBRATE"/>

Then initialize controller with initializeVibration() async method. Otherwise, it will throw an exception when animation with vibration called!

After preparation, you can call animations and enable vibration feature:

controller.animateLoading(
  animation: PinLoadingAnimation.jump,
  vibration: true,
);

If animation was called with vibration enabled, but it is not implemented for some reason, nothing bad will happen.

Additional information #

👀 See also: pin #

pin package focused on PIN code's backend part. It fully covers all the necessary logic of storing, updating, validating PIN, setting and handling timeouts, calling biometrics, and some other options for better user experience.
pin_ui + pin are perfect to work together in pair. Combining these two may save you days of development and the result will be already perfect even out of the box.

Pub Star on Github

📱 Examples #

This package has a brief but complex enough example. Feel free to use it as a playground or a template for your applications!

Also, there is a more complete example project that uses both pin and pin_ui packages in it.

You can share your own examples for this section.

✨ Adding new animations or customizing existing ones #

Before adding a new animation read an instruction on how to do it in a way it is intended to.

pin_ui package is designed to be easily extendable in terms of adding new animations for Pin Indicator, but currently there are no such API provided. You can still add own animations by forking source code repository of this package or contribute by suggesting something useful for others.

🛠 Contributing #

You have an interesting open source example to share with community? Found a bug, have a great ready to go new animation or want to suggest an idea for new animation? You're always welcome! Fell free to open an issue or pull request in GitHub repository!


5
likes
130
points
16
downloads

Publisher

unverified uploader

Weekly Downloads

Widgets for implementing PIN code related screens in Flutter applications

Repository (GitHub)
View/report issues

Topics

#pin #pin-code #widget #ui #animations

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, vibration

More

Packages that depend on pin_ui