diff --git a/example/lib/main.dart b/example/lib/main.dart index 7b3e37e..ba97acd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -66,6 +66,8 @@ class MySampleState extends State { ), CreditCardWidget( isFloatingAnimationEnabled: true, + isGlareAnimationEnabled: true, + isShadowAnimationEnabled: true, glassmorphismConfig: useGlassMorphism ? Glassmorphism.defaultConfig() : null, cardNumber: cardNumber, diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..2a7e11c --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + flutter_credit_card_example + + + + + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..42eecac --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "flutter_credit_card_example", + "short_name": "flutter_credit_card_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Demonstrates how to use the flutter_credit_card plugin.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/lib/constants.dart b/lib/constants.dart index 5f28325..1576cbd 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -6,31 +6,17 @@ class AppConstants { static const double creditCardAspectRatio = 0.5714; static const double creditCardPadding = 16; + static const double maxfloatingBack = 0.05; + static const double minfloatingBack = 0.01; + static const double defaultDampingFactor = 0.2; /// Color constants static const Color defaultGlareColor = Color(0xffFFFFFF), defaultShadowColor = Color(0xff000000); - /// Default filter quality - static const FilterQuality defaultFilterQuality = FilterQuality.high; + static const double defaultMaxAngle = pi / 10, - /// Numeric constants - double maxElevation = 100, - - /// Defaults - defaultMaxAngle = pi / 10, - - /// Shadow-specific values - minShadowOffset = 0, - maxShadowOffset = 40, - minShadowTopOffset = 5, - maxShadowTopOffset = 45, minBlurRadius = 10, - maxBlurRadius = 30, - minShadowOpacity = 0.3, - maxShadowOpacity = 0.2, - - /// Translation-specific values - maxDistance = 75.0; + minShadowOpacity = 0.3; } diff --git a/lib/credit_card_background.dart b/lib/credit_card_background.dart index f2a5586..9e4a115 100644 --- a/lib/credit_card_background.dart +++ b/lib/credit_card_background.dart @@ -5,7 +5,6 @@ import 'package:flutter_credit_card/floating_card_setup/floating_controller.dart import 'package:flutter_credit_card/floating_card_setup/glare_effect_widget.dart'; import 'constants.dart'; -import 'floating_card_setup/constants.dart'; import 'glassmorphism_config.dart'; class CardBackground extends StatelessWidget { @@ -22,6 +21,7 @@ class CardBackground extends StatelessWidget { this.border, this.floatingController, this.glarePosition, + this.shadowEnabled = false, }) : assert( (backgroundImage == null && backgroundNetworkImage == null) || (backgroundImage == null && backgroundNetworkImage != null) || @@ -40,6 +40,7 @@ class CardBackground extends StatelessWidget { final BoxBorder? border; final FloatingController? floatingController; final double? glarePosition; + final bool shadowEnabled; @override Widget build(BuildContext context) { @@ -53,24 +54,25 @@ class CardBackground extends StatelessWidget { return Stack( alignment: Alignment.center, children: [ - if(floatingController != null) - Positioned( - left: floatingController!.y * 100 + 16, - right: -floatingController!.y * 100 + 16, - top: -floatingController!.x * 100 + 16, - bottom: floatingController!.x * 100 + 16, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8)), - boxShadow: [ - BoxShadow( - blurRadius: minBlurRadius, - color: defaultShadowColor.withOpacity(minShadowOpacity), - ), - ], + if (floatingController != null && shadowEnabled) + Positioned( + left: floatingController!.y * 100 + 16, + right: -floatingController!.y * 100 + 16, + top: -floatingController!.x * 100 + 16, + bottom: floatingController!.x * 100 + 16, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8)), + boxShadow: [ + BoxShadow( + blurRadius: AppConstants.minBlurRadius, + color: AppConstants.defaultShadowColor + .withOpacity(AppConstants.minShadowOpacity), + ), + ], + ), ), ), - ), Container( margin: EdgeInsets.all(padding), decoration: BoxDecoration( diff --git a/lib/credit_card_widget.dart b/lib/credit_card_widget.dart index 8c5cbc4..3461a0c 100644 --- a/lib/credit_card_widget.dart +++ b/lib/credit_card_widget.dart @@ -12,6 +12,7 @@ import 'credit_card_brand.dart'; import 'custom_card_type_icon.dart'; import 'floating_card_setup/floating_controller.dart'; import 'floating_card_setup/floating_event.dart'; +import 'floating_card_setup/mouse_pointer_listener.dart'; import 'glassmorphism_config.dart'; const Map CardTypeIconAsset = { @@ -60,6 +61,8 @@ class CreditCardWidget extends StatefulWidget { this.backCardBorder, this.obscureInitialCardNumber = false, this.isFloatingAnimationEnabled = false, + this.isShadowAnimationEnabled = false, + this.isGlareAnimationEnabled = false, }) : super(key: key); /// A string indicating number on the card. @@ -170,6 +173,10 @@ class CreditCardWidget extends StatefulWidget { static final FlutterCreditCardPlatform instance = FlutterCreditCardPlatform.instance; + final bool isShadowAnimationEnabled; + + final bool isGlareAnimationEnabled; + /// floating animation enabled/disabled @override _CreditCardWidgetState createState() => _CreditCardWidgetState(); @@ -262,15 +269,20 @@ class _CreditCardWidgetState extends State return matrix; } if (event != null) { - backFloatingController.x += - (orientation == Orientation.landscape ? -event.y : event.x) * 0.015; - backFloatingController.y -= - (orientation == Orientation.landscape ? event.x : event.y) * 0.015; - - backFloatingController.limitTheAngle(); - // Apply the damping factor — which may equal 1 and have no effect, if damping is null. - backFloatingController.x *= backFloatingController.floatingBackFactor; - backFloatingController.y *= backFloatingController.floatingBackFactor; + if (CreditCardWidget.instance.isGyroscopeAvailable) { + frontFloatingController.x += + (orientation == Orientation.landscape ? -event.y : event.x) * 0.016; + frontFloatingController.y -= + (orientation == Orientation.landscape ? event.x : event.y) * 0.016; + + frontFloatingController.limitTheAngle(); + // Apply the damping factor — which may equal 1 and have no effect, if damping is null. + frontFloatingController.x *= frontFloatingController.floatingBackFactor; + frontFloatingController.y *= frontFloatingController.floatingBackFactor; + } else { + frontFloatingController.x = event.x * 0.1; + frontFloatingController.y = event.y * 0.1; + } // Rotate the matrix by the resulting x and y values. matrix.rotateX(backFloatingController.x); matrix.rotateY(backFloatingController.y); @@ -290,16 +302,20 @@ class _CreditCardWidgetState extends State return matrix; } if (event != null) { - frontFloatingController.x += - (orientation == Orientation.landscape ? -event.y : event.x) * 0.016; - frontFloatingController.y -= - (orientation == Orientation.landscape ? event.x : event.y) * 0.016; - - frontFloatingController.limitTheAngle(); - // Apply the damping factor — which may equal 1 and have no effect, if damping is null. - frontFloatingController.x *= frontFloatingController.floatingBackFactor; - frontFloatingController.y *= frontFloatingController.floatingBackFactor; - + if (CreditCardWidget.instance.isGyroscopeAvailable) { + frontFloatingController.x += + (orientation == Orientation.landscape ? -event.y : event.x) * 0.02; + frontFloatingController.y -= + (orientation == Orientation.landscape ? event.x : event.y) * 0.02; + + frontFloatingController.limitTheAngle(); + // Apply the damping factor — which may equal 1 and have no effect, if damping is null. + frontFloatingController.x *= frontFloatingController.floatingBackFactor; + frontFloatingController.y *= frontFloatingController.floatingBackFactor; + } else { + frontFloatingController.x = event.x * 0.1; + frontFloatingController.y = event.y * 0.1; + } // Rotate the matrix by the resulting x and y values. matrix.rotateX(frontFloatingController.x); matrix.rotateY(frontFloatingController.y); @@ -331,6 +347,8 @@ class _CreditCardWidgetState extends State @override void dispose() { controller.dispose(); + backCardStreamController.close(); + frontCardStreamController.close(); super.dispose(); } @@ -359,7 +377,9 @@ class _CreditCardWidgetState extends State : detectCCType(widget.cardNumber); widget.onCreditCardWidgetChange(CreditCardBrand(cardType)); - return Stack( + DateTime? lastPointerEventTime; + + final Widget child = Stack( children: [ _cardGesture( child: AnimationCard( @@ -375,6 +395,36 @@ class _CreditCardWidgetState extends State ), ], ); + + return !CreditCardWidget.instance.isGyroscopeAvailable + ? CursorListener( + child: child, + onPositionChange: (Offset newOffset) { + final now = DateTime.now(); + if (lastPointerEventTime == null) { + lastPointerEventTime = now; + } else if (now.difference(lastPointerEventTime!) < + const Duration(microseconds: 1666)) { + /// Drop events more frequent than [_updateInterval] + return; + } + lastPointerEventTime = now; + if (isFrontVisible) + frontCardStreamController.add( + FloatingEvent( + type: FloatingType.pointer, + x: newOffset.dx * 2, + y: newOffset.dy * 2), + ); + else + backCardStreamController.add( + FloatingEvent( + type: FloatingType.pointer, + x: newOffset.dx * 2, + y: newOffset.dy * 2), + ); + }) + : child; } void _leftRotation() { @@ -481,227 +531,136 @@ class _CreditCardWidgetState extends State transform: computeTransformForEvent(snapshot.data), filterQuality: FilterQuality.high, alignment: FractionalOffset.center, - child: CardBackground( - glarePosition: glarePosition, - floatingController: frontFloatingController, - backgroundImage: widget.backgroundImage, - backgroundNetworkImage: widget.backgroundNetworkImage, - backgroundGradientColor: backgroundGradientColor, - glassmorphismConfig: widget.glassmorphismConfig, - height: widget.height, - width: widget.width, - padding: widget.padding, - border: widget.frontCardBorder, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.bankName.isNotNullAndNotEmpty) - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(right: 16, top: 16), - child: Text( - widget.bankName!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: defaultTextStyle, - ), - ), - ), - Expanded( - flex: widget.isChipVisible ? 1 : 0, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (widget.isChipVisible) - Padding( - padding: const EdgeInsets.only(left: 16), - child: Image.asset( - 'icons/chip.png', - package: 'flutter_credit_card', - color: widget.chipColor, - scale: 1, - ), - ), - ], - ), - ), - const SizedBox(height: 10), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - widget.cardNumber.isEmpty - ? 'XXXX XXXX XXXX XXXX' - : number, - style: widget.textStyle ?? defaultTextStyle, - ), - ), - ), - Expanded( - flex: 1, - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - widget.labelValidThru, - style: widget.textStyle ?? - defaultTextStyle.copyWith(fontSize: 7), - textAlign: TextAlign.center, - ), - const SizedBox(width: 5), - Text( - widget.expiryDate.isEmpty - ? widget.labelExpiredDate - : widget.expiryDate, - style: widget.textStyle ?? defaultTextStyle, - ), - ], - ), - ), - ), - Padding( - padding: - const EdgeInsets.only(left: 16, right: 16, bottom: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Visibility( - visible: widget.isHolderNameVisible, - child: Expanded( - child: Text( - widget.cardHolderName.isEmpty - ? widget.labelCardHolder - : widget.cardHolderName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: widget.textStyle ?? defaultTextStyle, - ), - ), - ), - widget.cardType != null - ? getCardTypeImage(widget.cardType) - : getCardTypeIcon(widget.cardNumber), - ], - ), - ), - ], - ), + child: _frontCardBackground( + defaultTextStyle: defaultTextStyle, + number: number, ), ); }, ); else - return CardBackground( - backgroundImage: widget.backgroundImage, - backgroundNetworkImage: widget.backgroundNetworkImage, - backgroundGradientColor: backgroundGradientColor, - glassmorphismConfig: widget.glassmorphismConfig, - height: widget.height, - width: widget.width, - padding: widget.padding, - border: widget.frontCardBorder, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.bankName.isNotNullAndNotEmpty) - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(right: 16, top: 16), - child: Text( - widget.bankName!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: defaultTextStyle, - ), - ), - ), - Expanded( - flex: widget.isChipVisible ? 1 : 0, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (widget.isChipVisible) - Padding( - padding: const EdgeInsets.only(left: 16), - child: Image.asset( - 'icons/chip.png', - package: 'flutter_credit_card', - color: widget.chipColor, - scale: 1, - ), - ), - ], - ), - ), - const SizedBox(height: 10), - Expanded( + return _frontCardBackground( + defaultTextStyle: defaultTextStyle, + number: number, + ); + } + + Widget _frontCardBackground({ + required String number, + required TextStyle defaultTextStyle, + }) { + return CardBackground( + glarePosition: + widget.isGlareAnimationEnabled && widget.isFloatingAnimationEnabled + ? glarePosition + : null, + floatingController: + widget.isFloatingAnimationEnabled ? frontFloatingController : null, + backgroundImage: widget.backgroundImage, + backgroundNetworkImage: widget.backgroundNetworkImage, + backgroundGradientColor: backgroundGradientColor, + glassmorphismConfig: widget.glassmorphismConfig, + height: widget.height, + width: widget.width, + padding: widget.padding, + border: widget.frontCardBorder, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.bankName.isNotNullAndNotEmpty) + Align( + alignment: Alignment.topRight, child: Padding( - padding: const EdgeInsets.only(left: 16), + padding: const EdgeInsets.only(right: 16, top: 16), child: Text( - widget.cardNumber.isEmpty ? 'XXXX XXXX XXXX XXXX' : number, - style: widget.textStyle ?? defaultTextStyle, + widget.bankName!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: defaultTextStyle, ), ), ), - Expanded( - flex: 1, - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - widget.labelValidThru, - style: widget.textStyle ?? - defaultTextStyle.copyWith(fontSize: 7), - textAlign: TextAlign.center, - ), - const SizedBox(width: 5), - Text( - widget.expiryDate.isEmpty - ? widget.labelExpiredDate - : widget.expiryDate, - style: widget.textStyle ?? defaultTextStyle, + Expanded( + flex: widget.isChipVisible ? 1 : 0, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (widget.isChipVisible) + Padding( + padding: const EdgeInsets.only(left: 16), + child: Image.asset( + 'icons/chip.png', + package: 'flutter_credit_card', + color: widget.chipColor, + scale: 1, ), - ], - ), + ), + ], + ), + ), + const SizedBox(height: 10), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + widget.cardNumber.isEmpty ? 'XXXX XXXX XXXX XXXX' : number, + style: widget.textStyle ?? defaultTextStyle, ), ), - Padding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + ), + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.only(left: 16), child: Row( + mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Visibility( - visible: widget.isHolderNameVisible, - child: Expanded( - child: Text( - widget.cardHolderName.isEmpty - ? widget.labelCardHolder - : widget.cardHolderName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: widget.textStyle ?? defaultTextStyle, - ), - ), + Text( + widget.labelValidThru, + style: widget.textStyle ?? + defaultTextStyle.copyWith(fontSize: 7), + textAlign: TextAlign.center, + ), + const SizedBox(width: 5), + Text( + widget.expiryDate.isEmpty + ? widget.labelExpiredDate + : widget.expiryDate, + style: widget.textStyle ?? defaultTextStyle, ), - widget.cardType != null - ? getCardTypeImage(widget.cardType) - : getCardTypeIcon(widget.cardNumber), ], ), ), - ], - ), - ); + ), + Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Visibility( + visible: widget.isHolderNameVisible, + child: Expanded( + child: Text( + widget.cardHolderName.isEmpty + ? widget.labelCardHolder + : widget.cardHolderName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: widget.textStyle ?? defaultTextStyle, + ), + ), + ), + widget.cardType != null + ? getCardTypeImage(widget.cardType) + : getCardTypeIcon(widget.cardNumber), + ], + ), + ), + ], + ), + ); } /// @@ -722,164 +681,108 @@ class _CreditCardWidgetState extends State ? widget.cvvCode.replaceAll(RegExp(r'\d'), '*') : widget.cvvCode; - return widget.isFloatingAnimationEnabled && !isFrontVisible + return widget.isFloatingAnimationEnabled ? StreamBuilder( stream: backCardStreamController.stream, builder: (BuildContext context, AsyncSnapshot snapshot) => Transform( transform: computeBackTransformForEvent(snapshot.data), + filterQuality: FilterQuality.high, alignment: FractionalOffset.center, - child: CardBackground( - glarePosition: glarePosition, - floatingController: frontFloatingController, - backgroundImage: widget.backgroundImage, - backgroundNetworkImage: widget.backgroundNetworkImage, - backgroundGradientColor: backgroundGradientColor, - glassmorphismConfig: widget.glassmorphismConfig, - height: widget.height, - width: widget.width, - padding: widget.padding, - border: widget.backCardBorder, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: Container( - margin: const EdgeInsets.only(top: 16), - height: 48, - color: Colors.black, - ), - ), - Expanded( - flex: 2, - child: Container( - margin: const EdgeInsets.only(top: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - flex: 9, - child: Container( - height: 48, - color: Colors.white70, - ), - ), - Expanded( - flex: 3, - child: Container( - color: Colors.white, - child: Padding( - padding: const EdgeInsets.all(5), - child: Text( - widget.cvvCode.isEmpty - ? isAmex - ? 'XXXX' - : 'XXX' - : cvv, - maxLines: 1, - style: widget.textStyle ?? - defaultTextStyle, - ), - ), - ), - ) - ], - ), - ), - ), - Expanded( - flex: 2, - child: Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: const EdgeInsets.only( - left: 16, right: 16, bottom: 16), - child: widget.cardType != null - ? getCardTypeImage(widget.cardType) - : getCardTypeIcon(widget.cardNumber), - ), - ), - ), - ], - ), + child: _backCardBackground( + cvv: cvv, + defaultTextStyle: defaultTextStyle, ), )) - : CardBackground( - backgroundImage: widget.backgroundImage, - backgroundNetworkImage: widget.backgroundNetworkImage, - backgroundGradientColor: backgroundGradientColor, - glassmorphismConfig: widget.glassmorphismConfig, - height: widget.height, - width: widget.width, - padding: widget.padding, - border: widget.backCardBorder, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: Container( - margin: const EdgeInsets.only(top: 16), - height: 48, - color: Colors.black, - ), - ), - Expanded( - flex: 2, - child: Container( - margin: const EdgeInsets.only(top: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - flex: 9, - child: Container( - height: 48, - color: Colors.white70, - ), - ), - Expanded( - flex: 3, - child: Container( - color: Colors.white, - child: Padding( - padding: const EdgeInsets.all(5), - child: Text( - widget.cvvCode.isEmpty - ? isAmex - ? 'XXXX' - : 'XXX' - : cvv, - maxLines: 1, - style: widget.textStyle ?? defaultTextStyle, - ), - ), - ), - ) - ], + : _backCardBackground( + cvv: cvv, + defaultTextStyle: defaultTextStyle, + ); + } + + Widget _backCardBackground({ + required String cvv, + required TextStyle defaultTextStyle, + }) { + return CardBackground( + glarePosition: + widget.isGlareAnimationEnabled && widget.isFloatingAnimationEnabled + ? glarePosition + : null, + floatingController: + widget.isFloatingAnimationEnabled ? backFloatingController : null, + backgroundImage: widget.backgroundImage, + backgroundNetworkImage: widget.backgroundNetworkImage, + backgroundGradientColor: backgroundGradientColor, + glassmorphismConfig: widget.glassmorphismConfig, + height: widget.height, + width: widget.width, + padding: widget.padding, + border: widget.backCardBorder, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Container( + margin: const EdgeInsets.only(top: 16), + height: 48, + color: Colors.black, + ), + ), + Expanded( + flex: 2, + child: Container( + margin: const EdgeInsets.only(top: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 9, + child: Container( + height: 48, + color: Colors.white70, ), ), - ), - Expanded( - flex: 2, - child: Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: const EdgeInsets.only( - left: 16, right: 16, bottom: 16), - child: widget.cardType != null - ? getCardTypeImage(widget.cardType) - : getCardTypeIcon(widget.cardNumber), + Expanded( + flex: 3, + child: Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.all(5), + child: Text( + widget.cvvCode.isEmpty + ? isAmex + ? 'XXXX' + : 'XXX' + : cvv, + maxLines: 1, + style: widget.textStyle ?? defaultTextStyle, + ), + ), ), - ), - ), - ], + ) + ], + ), ), - ); + ), + Expanded( + flex: 2, + child: Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + child: widget.cardType != null + ? getCardTypeImage(widget.cardType) + : getCardTypeIcon(widget.cardNumber), + ), + ), + ), + ], + ), + ); } Widget _cardGesture({required Widget child}) { @@ -889,10 +792,8 @@ class _CreditCardWidgetState extends State onPanEnd: (_) { isGestureUpdate = true; if (isRightSwipe) { - debugPrint('left Rotation detected'); _leftRotation(); } else { - debugPrint('Right Rotation detected'); _rightRotation(); } }, @@ -1043,8 +944,8 @@ class _CreditCardWidgetState extends State } } - // This method returns the icon for the visa card type if found - // else will return the empty container +// This method returns the icon for the visa card type if found +// else will return the empty container Widget getCardTypeIcon(String cardNumber) { Widget icon; final CardType ccType = detectCCType(cardNumber); diff --git a/lib/floating_card_setup/constants.dart b/lib/floating_card_setup/constants.dart deleted file mode 100644 index 1383573..0000000 --- a/lib/floating_card_setup/constants.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/widgets.dart'; - -/// Color constants -const Color defaultGlareColor = Color(0xffFFFFFF), - defaultShadowColor = Color(0xff000000); - -/// Default filter quality -const FilterQuality defaultFilterQuality = FilterQuality.high; - -/// Numeric constants -const double maxElevation = 100, - - /// Defaults - defaultMaxAngle = pi / 10, - - /// Shadow-specific values - minShadowOffset = 0, - maxShadowOffset = 40, - minShadowTopOffset = 5, - maxShadowTopOffset = 45, - minBlurRadius = 10, - maxBlurRadius = 30, - minShadowOpacity = 0.3, - maxShadowOpacity = 0.2, - - /// Translation-specific values - maxDistance = 75.0; diff --git a/lib/floating_card_setup/floating_controller.dart b/lib/floating_card_setup/floating_controller.dart index 94f4a9b..893a391 100644 --- a/lib/floating_card_setup/floating_controller.dart +++ b/lib/floating_card_setup/floating_controller.dart @@ -1,8 +1,6 @@ import 'dart:math'; -const double maxfloatingBack = 0.05; -const double minfloatingBack = 0.01; -const double defaultDampingFactor = 0.2; +import 'package:flutter_credit_card/constants.dart'; class FloatingController { /// A controller that holds the [Motion] widget's X and Y angles. @@ -20,16 +18,18 @@ class FloatingController { /// Computed from the [floatingBack] value which lerps from 0 to 1 between [minfloatingBack] and [maxfloatingBack]. double get floatingBackFactor => floatingBack != null ? 1 - - (minfloatingBack + - (floatingBack! * (maxfloatingBack - minfloatingBack))) + (AppConstants.minfloatingBack + + (floatingBack! * + (AppConstants.maxfloatingBack - + AppConstants.minfloatingBack))) : 1; /// maximum angle at which floating animation can move double maximumAngle; - /// x,y value for gyroscope and mouse pointer data + /// x,y value for gyroscope and mouse pointer data double x = 0, y = 0; - + static final FloatingController defaultController = FloatingController(); /// let x and y value can only extend to specified angle diff --git a/lib/floating_card_setup/glare_effect_widget.dart b/lib/floating_card_setup/glare_effect_widget.dart index 90861d2..da1301d 100644 --- a/lib/floating_card_setup/glare_effect_widget.dart +++ b/lib/floating_card_setup/glare_effect_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_credit_card/constants.dart'; import 'package:flutter_credit_card/floating_card_setup/floating_controller.dart'; -import 'constants.dart'; class GlareEffectWidget extends StatelessWidget { const GlareEffectWidget({ @@ -32,9 +32,9 @@ class GlareEffectWidget extends StatelessWidget { gradient: LinearGradient( tileMode: TileMode.clamp, colors: [ - defaultGlareColor.withOpacity(0.1), - defaultGlareColor.withOpacity(0.07), - defaultGlareColor.withOpacity(0.05), + AppConstants.defaultGlareColor.withOpacity(0.1), + AppConstants.defaultGlareColor.withOpacity(0.07), + AppConstants.defaultGlareColor.withOpacity(0.05), ], stops: const [ 0.1, diff --git a/lib/floating_card_setup/mouse_pointer_listener.dart b/lib/floating_card_setup/mouse_pointer_listener.dart new file mode 100644 index 0000000..5cbb095 --- /dev/null +++ b/lib/floating_card_setup/mouse_pointer_listener.dart @@ -0,0 +1,146 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/src/gestures/events.dart'; + +class CursorListener extends StatefulWidget { + /// This widget listens cursor entry and exit while hovering on the card + const CursorListener( + {Key? key, required this.child, required this.onPositionChange}) + : super(key: key); + final Widget child; + + ///This called when a pointer event is received. + final Function(Offset newOffset) onPositionChange; + + @override + State createState() => _CursorListenerState(); +} + +class _CursorListenerState extends State { + final GlobalKey> mouseCursorKey = GlobalKey(); + + /// A track of the latest size returned by the widget's layout builder. + Size? childSize; + + /// A value used for deltas and throttling + Offset lastOffset = Offset.zero; + + /// A value used for deltas and throttling + DateTime lastPointerEvent = DateTime.now(); + + /// When idle, the intensity factor is 0. When the pointer enters, it progressively animates to 1. + double intensityFactor = 0; + + double get width => childSize?.width ?? 1; + + double get height => childSize?.height ?? 1; + + /// A timer that progressively increases or decreases the intensity factor. + Timer? velocityTimer; + + @override + void dispose() { + velocityTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Stack( + children: [ + widget.child, + Positioned.fill( + child: LayoutBuilder( + builder: (BuildContext ctx, BoxConstraints constraints) { + childSize = Size(constraints.maxWidth, constraints.maxHeight); + return Listener( + onPointerHover: (PointerHoverEvent details) { + _onPointerMove(position: details.localPosition); + }, + onPointerMove: (PointerMoveEvent details) { + _onPointerMove(position: details.localPosition); + }, + behavior: HitTestBehavior.translucent, + child: MouseRegion( + hitTestBehavior: HitTestBehavior.translucent, + key: mouseCursorKey, + onExit: (PointerExitEvent details) { + _onPointerExit(); + }, + onEnter: (PointerEnterEvent details) { + _onPointerEnter(); + }, + child: Container(), + ), + ); + }, + ), + ) + ], + ); + + void _onPointerMove({required Offset position}) { + if (DateTime.now().difference(lastPointerEvent) < + const Duration(microseconds: 16666)) { + /// Drop event since it occurs too early. + return; + } + + double x, y; + + // Compute the fractional offset. + x = (position.dy - (height / 2)) / height; + y = -(position.dx - (width / 2)) / width; + + // Apply the intensity factor. + x *= intensityFactor; + y *= intensityFactor; + + // Notify the position change. + widget.onPositionChange(Offset(x, y)); + + // Store the pass informations. + lastPointerEvent = DateTime.now(); + lastOffset = Offset(position.dx, position.dy); + } + + /// Animate the intensity factor to 1, to smoothly get to the pointer's position. + Future _onPointerEnter() async { + _cancelVelocityTimer(); + + velocityTimer = + Timer.periodic(const Duration(microseconds: 1 + 16666), (Timer timer) { + if (intensityFactor < 1) { + if (intensityFactor <= 0.05) { + intensityFactor = 0.05; + } + intensityFactor = min(1, intensityFactor * 1.2); + _onPointerMove(position: lastOffset); + } else { + _cancelVelocityTimer(); + } + }); + } + + /// Animate the intensity factor to 0, to smoothly get back to the initial position. + Future _onPointerExit() async { + _cancelVelocityTimer(); + + velocityTimer = + Timer.periodic(const Duration(microseconds: 1 + 16666), (Timer timer) { + if (intensityFactor > 0.05) { + intensityFactor = max(0, intensityFactor * 0.95); + _onPointerMove(position: lastOffset /*, isVelocity: true*/); + } else { + _cancelVelocityTimer(); + } + }); + } + + /// Cancels the velocity timer. + void _cancelVelocityTimer() { + velocityTimer?.cancel(); + velocityTimer = null; + } +} diff --git a/lib/flutter_credit_card_web.dart b/lib/flutter_credit_card_web.dart new file mode 100644 index 0000000..83dc67c --- /dev/null +++ b/lib/flutter_credit_card_web.dart @@ -0,0 +1,139 @@ +import 'dart:async'; +import 'dart:developer' as developer; +import 'dart:html' as html; +import 'dart:js_interop'; +import 'dart:js_util'; + +import 'package:flutter_credit_card/floating_card_setup/floating_event.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +import 'flutter_credit_card_platform_interface.dart'; + +@JS() +external dynamic get evaluatePermission; + +/// A web implementation of the FlutterCreditCardPlatform of the FlutterCreditCard plugin. +class FlutterCreditCardWeb extends FlutterCreditCardPlatform { + /// Constructs a FlutterCreditCardWeb + FlutterCreditCardWeb(); + + static void registerWith(Registrar registrar) { + FlutterCreditCardPlatform.instance = FlutterCreditCardWeb(); + } + + static bool _isGyroscopeAvailable = false; + + @override + bool get isGyroscopeAvailable => _isGyroscopeAvailable; + + + void _featureDetected( + Function initSensor, { + String? apiName, + String? permissionName, + Function? onError, + }) { + try { + initSensor(); + } catch (error) { + if (onError != null) { + onError(); + } + + /// Handle construction errors. + /// + /// If a feature policy blocks use of a feature it is because your code + /// is inconsistent with the policies set on your server. + /// This is not something that would ever be shown to a user. + /// See Feature-Policy for implementation instructions in the browsers. + if (error.toString().contains('SecurityError')) { + /// See the note above about feature policy. + developer.log('$apiName construction was blocked by a feature policy.', + error: error); + + /// if this feature is not supported or Flag is not enabled yet! + } else if (error.toString().contains('ReferenceError')) { + developer.log('$apiName is not supported by the User Agent.', + error: error); + + /// if this is unknown error, rethrow it + } else { + developer.log('Unknown error happened, rethrowing.'); + rethrow; + } + } + } + + DateTime lastFloatingPoint = DateTime.now(); + + StreamController? _gyroscopeStreamController; + Stream? _gyroscopeStream; + + @override + Stream? get floatingStream { + if (_gyroscopeStreamController == null) { + _gyroscopeStreamController = StreamController(); + + // TODO(Kavan): handle IOS web support + /// We have not added device motion stream for IOS + /// Facing issue while calling native method of Gyroscope to check whether + /// it exists or not : refer Motion Plugin's scripts.dart + _featureDetected( + () { + final html.Gyroscope gyroscope = html.Gyroscope(); + setProperty( + gyroscope, + 'onreading', + allowInterop( + (dynamic data) { + if (gyroscope.x != null || + gyroscope.y != null || + gyroscope.z != null) { + _isGyroscopeAvailable = true; + Timer.periodic(const Duration(microseconds: 16666), + (Timer timer) { + _gyroscopeStreamController!.add( + FloatingEvent( + type: FloatingType.gyroscope, + x: gyroscope.x! * 5 as double, + y: gyroscope.y! * 5 as double, + z: gyroscope.z! * 5 as double, + ), + ); + }); + } else { + _isGyroscopeAvailable = false; + } + }, + ), + ); + + gyroscope.start(); + }, + apiName: 'Gyroscope()', + permissionName: 'gyroscope', + onError: () { + html.window.console + .warn('Error: Gyroscope() is not supported by the User Agent.'); + _gyroscopeStreamController! + .add(const FloatingEvent.zero(type: FloatingType.gyroscope)); + }, + ); + + _gyroscopeStream = _gyroscopeStreamController!.stream.asBroadcastStream(); + } + + return _gyroscopeStream; + } + + @override + Future initialize() async { + return; + } + + @override + Future requestPermission() async { + // TODO(kavan): Add request permission for IOS Web + return false; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 2cdfda1..0d7726f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,8 @@ dependencies: flutter: sdk: flutter plugin_platform_interface: - + flutter_web_plugins: + sdk: flutter dev_dependencies: flutter_test: sdk: flutter @@ -25,6 +26,9 @@ flutter: pluginClass: FlutterCreditCardPlugin ios: pluginClass: FlutterCreditCardPlugin + web: + pluginClass: FlutterCreditCardWeb + fileName: flutter_credit_card_web.dart assets: - icons/ fonts: