pin_pad.dart 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import '../../../core/theme/app_colors.dart';
  4. import '../../../core/widgets/pressable.dart';
  5. /// Four small status dots for the PIN entry.
  6. class PinDots extends StatelessWidget {
  7. const PinDots({
  8. super.key,
  9. required this.length,
  10. required this.filled,
  11. this.error = false,
  12. });
  13. final int length;
  14. final int filled;
  15. final bool error;
  16. @override
  17. Widget build(BuildContext context) {
  18. final colors = context.colors;
  19. return Row(
  20. mainAxisSize: MainAxisSize.min,
  21. children: [
  22. for (var index = 0; index < length; index++)
  23. AnimatedContainer(
  24. duration: const Duration(milliseconds: 140),
  25. curve: Curves.easeOutCubic,
  26. width: 16,
  27. height: 16,
  28. margin: const EdgeInsets.symmetric(horizontal: 9),
  29. decoration: BoxDecoration(
  30. shape: BoxShape.circle,
  31. color: index < filled
  32. ? (error ? colors.danger : colors.accent)
  33. : Colors.transparent,
  34. border: Border.all(
  35. color: index < filled
  36. ? (error ? colors.danger : colors.accent)
  37. : colors.inkFaint.withValues(alpha: 0.5),
  38. width: 1.4,
  39. ),
  40. ),
  41. ),
  42. ],
  43. );
  44. }
  45. }
  46. /// The numeric keypad shared by the lock and PIN setup screens.
  47. class PinKeypad extends StatelessWidget {
  48. const PinKeypad({
  49. super.key,
  50. required this.onDigit,
  51. required this.onBackspace,
  52. this.enabled = true,
  53. });
  54. final ValueChanged<String> onDigit;
  55. final VoidCallback onBackspace;
  56. final bool enabled;
  57. @override
  58. Widget build(BuildContext context) {
  59. const rows = [
  60. ['1', '2', '3'],
  61. ['4', '5', '6'],
  62. ['7', '8', '9'],
  63. ];
  64. return AnimatedOpacity(
  65. duration: const Duration(milliseconds: 140),
  66. opacity: enabled ? 1 : 0.42,
  67. child: IgnorePointer(
  68. ignoring: !enabled,
  69. child: Column(
  70. mainAxisSize: MainAxisSize.min,
  71. children: [
  72. for (final row in rows) ...[
  73. Row(
  74. mainAxisAlignment: MainAxisAlignment.center,
  75. children: [
  76. for (final digit in row) ...[
  77. _DigitButton(
  78. label: digit,
  79. onTap: () {
  80. HapticFeedback.selectionClick();
  81. onDigit(digit);
  82. },
  83. ),
  84. if (digit != row.last) const SizedBox(width: 16),
  85. ],
  86. ],
  87. ),
  88. const SizedBox(height: 14),
  89. ],
  90. Row(
  91. mainAxisAlignment: MainAxisAlignment.center,
  92. children: [
  93. const SizedBox(width: 76, height: 68),
  94. const SizedBox(width: 16),
  95. _DigitButton(
  96. label: '0',
  97. onTap: () {
  98. HapticFeedback.selectionClick();
  99. onDigit('0');
  100. },
  101. ),
  102. const SizedBox(width: 16),
  103. _BackspaceButton(
  104. onTap: () {
  105. HapticFeedback.selectionClick();
  106. onBackspace();
  107. },
  108. ),
  109. ],
  110. ),
  111. ],
  112. ),
  113. ),
  114. );
  115. }
  116. }
  117. class _DigitButton extends StatelessWidget {
  118. const _DigitButton({required this.label, required this.onTap});
  119. final String label;
  120. final VoidCallback onTap;
  121. @override
  122. Widget build(BuildContext context) {
  123. final colors = context.colors;
  124. return Pressable(
  125. onTap: onTap,
  126. semanticLabel: 'Digit $label',
  127. pressedScale: 0.93,
  128. child: Container(
  129. width: 76,
  130. height: 68,
  131. alignment: Alignment.center,
  132. decoration: BoxDecoration(
  133. color: colors.surface,
  134. borderRadius: BorderRadius.circular(22),
  135. border: Border.all(color: colors.border),
  136. ),
  137. child: Text(
  138. label,
  139. style: Theme.of(context).textTheme.headlineMedium
  140. ?.copyWith(fontWeight: FontWeight.w500, fontSize: 26),
  141. ),
  142. ),
  143. );
  144. }
  145. }
  146. class _BackspaceButton extends StatelessWidget {
  147. const _BackspaceButton({required this.onTap});
  148. final VoidCallback onTap;
  149. @override
  150. Widget build(BuildContext context) {
  151. final colors = context.colors;
  152. return Pressable(
  153. onTap: onTap,
  154. semanticLabel: 'Delete digit',
  155. pressedScale: 0.93,
  156. child: Container(
  157. width: 76,
  158. height: 68,
  159. alignment: Alignment.center,
  160. decoration: BoxDecoration(
  161. color: Colors.transparent,
  162. borderRadius: BorderRadius.circular(22),
  163. border: Border.all(color: colors.border.withValues(alpha: 0.7)),
  164. ),
  165. child: Icon(Icons.backspace_outlined, size: 23, color: colors.inkMuted),
  166. ),
  167. );
  168. }
  169. }
  170. /// The three-by-three dot mark used on the lock screen.
  171. class BrandMark extends StatelessWidget {
  172. const BrandMark({super.key, this.size = 8, this.gap = 7});
  173. final double size;
  174. final double gap;
  175. @override
  176. Widget build(BuildContext context) {
  177. final colors = context.colors;
  178. return Column(
  179. mainAxisSize: MainAxisSize.min,
  180. children: [
  181. for (var row = 0; row < 3; row++) ...[
  182. Row(
  183. mainAxisSize: MainAxisSize.min,
  184. children: [
  185. for (var column = 0; column < 3; column++) ...[
  186. Container(
  187. width: size,
  188. height: size,
  189. decoration: BoxDecoration(
  190. color: colors.accent,
  191. shape: BoxShape.circle,
  192. ),
  193. ),
  194. if (column < 2) SizedBox(width: gap),
  195. ],
  196. ],
  197. ),
  198. if (row < 2) SizedBox(height: gap),
  199. ],
  200. ],
  201. );
  202. }
  203. }