pin_ui 0.1.0+1
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:
- Pinpad. Highly customizable numeric keyboard for entering PIN code.
- 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.
Pinpad #
Pinpad is a numeric keyboard with 2 extra key slots. Usually they place "Forgot PIN" and Biometrics buttons there.

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. ProvidekeyDefaultDecoration
,keyPressedDecoration
andkeyDisabledDecoration
to set it in a way you prefer. - 0-9 keys has a
TextStyle
property. There are also 3 states: default, pressed and disabled. SetkeyDefaultTextStyle
,keyPressedTextStyle
andkeyDisabledTextStyle
to style digits inside the keys. - Resize keys with
keyHeight
andkeyWidth
. - Spacing between keys can be changed with
horizontalSpacing
andverticalSpacing
properties. By default, these values will be calculated depending on the screen size. - You can disable or make pinpad invisible by setting
enabled
andisVisible
. 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
andrightExtraKey
. These parameters havePinpadExtraKey
type. It is a wrapper above your child widget where you add onTap callback. Child can be any widget, but also you can usePinpadKey
orPinpadTextKey
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
andsuccessItemBuilder
.
For default constructor there are 4 optional parameters:defaultDecoration
,inputDecoration
,errorDecoration
andsuccessDecoration
. These parameters are not required! If not provided, pre-made decorations will be used instead. controller
– your instance ofPinIndicatorAnimationController
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 forPinIndicator
. 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 normalWidget
type butPreferredSizeWidget
. To do so just addimplements PreferredSizeWidget
to aStatefulWidget
orStatelessWidget
and provide a size withpreferredSize
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
anddelayAfter
– 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
andonInterrupt
– 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.
📱 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!