diff --git a/.gitignore b/.gitignore index fe60d12..33f9329 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ build/ # Android related -**/android/**/gradle-wrapper.jar +**/android/**/gradle.wrapper/ **/android/.gradle **/android/captures/ **/android/gradlew @@ -76,4 +76,4 @@ ios/.generated/ .idea/instapk.xml instapk.log* -pubspec.lock \ No newline at end of file +pubspec.lock diff --git a/README.md b/README.md index 6f6f2b9..f9bfbf6 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,17 @@ import 'package:flutter_credit_card/flutter_credit_card.dart'; ), ``` +*Floating Card* + +```dart + CreditCardWidget( + shouldFloat: true, + shouldAddShadow: true, + shouldAddGlare: true, + ); +``` +> NOTE: Currently the floating card animation is not supported on mobile platform browsers. + 4. Adding CreditCardForm ```dart diff --git a/android/build.gradle b/android/build.gradle index 61a6a15..45820db 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ group 'com.simform.flutter_credit_card' -version '1.0-SNAPSHOT' +version '3.0.7' buildscript { ext.kotlin_version = '1.7.10' diff --git a/android/src/main/kotlin/com/simform/flutter_credit_card/FlutterCreditCardPlugin.kt b/android/src/main/kotlin/com/simform/flutter_credit_card/FlutterCreditCardPlugin.kt index 878915f..cf88773 100644 --- a/android/src/main/kotlin/com/simform/flutter_credit_card/FlutterCreditCardPlugin.kt +++ b/android/src/main/kotlin/com/simform/flutter_credit_card/FlutterCreditCardPlugin.kt @@ -2,7 +2,6 @@ package com.simform.flutter_credit_card import android.content.Context import android.content.pm.PackageManager -import android.hardware.Sensor import android.hardware.SensorManager import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -10,59 +9,55 @@ import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodChannel -/** FlutterCreditCardPlugin */ -class FlutterCreditCardPlugin: FlutterPlugin { - private lateinit var gyroscopeChannel: EventChannel - - private lateinit var methodChannel: MethodChannel - - private lateinit var gyroScopeStreamHandler: StreamHandlerImpl - - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - setupEventChannels(binding.applicationContext, binding.binaryMessenger) - setupMethodChannel(binding.applicationContext, binding.binaryMessenger) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - teardownEventChannels() - teardownMethodChannel() - } +private const val GYROSCOPE_CHANNEL_NAME = "com.simform.flutter_credit_card/gyroscope" +private const val METHOD_CHANNEL_NAME = "com.simform.flutter_credit_card" - private fun setupEventChannels(context: Context, messenger: BinaryMessenger) { - val sensorsManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager +/** FlutterCreditCardPlugin */ +class FlutterCreditCardPlugin : FlutterPlugin { + private lateinit var gyroscopeChannel: EventChannel + private lateinit var methodChannel: MethodChannel + private lateinit var gyroscopeStreamHandler: GyroscopeStreamHandler + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + initEventChannels(binding.applicationContext, binding.binaryMessenger) + initMethodChannel(binding.applicationContext, binding.binaryMessenger) + } - gyroscopeChannel = EventChannel(messenger, GYROSCOPE_CHANNEL_NAME) - gyroScopeStreamHandler = com.simform.flutter_credit_card.StreamHandlerImpl( - sensorsManager, - Sensor.TYPE_GYROSCOPE, - ) - gyroscopeChannel.setStreamHandler(gyroScopeStreamHandler) - } + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + disposeChannels() + } - private fun teardownEventChannels() { - gyroscopeChannel.setStreamHandler(null) - gyroScopeStreamHandler.onCancel(null) - } + private fun initEventChannels(context: Context, messenger: BinaryMessenger) { + val sensorsManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager - private fun setupMethodChannel(context: Context, messenger: BinaryMessenger) { - methodChannel = MethodChannel(messenger, METHOD_CHANNEL_NAME) - methodChannel.setMethodCallHandler { call, result -> - if (call.method == "isGyroscopeAvailable") { - val packageManager: PackageManager = context.packageManager - val gyroExists = - packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_GYROSCOPE) - result.success(gyroExists) - } + gyroscopeChannel = EventChannel(messenger, GYROSCOPE_CHANNEL_NAME) + gyroscopeStreamHandler = GyroscopeStreamHandler(sensorsManager) + gyroscopeChannel.setStreamHandler(gyroscopeStreamHandler) } - } - private fun teardownMethodChannel() { - methodChannel.setMethodCallHandler(null) - } + private fun initMethodChannel(context: Context, messenger: BinaryMessenger) { + methodChannel = MethodChannel(messenger, METHOD_CHANNEL_NAME) + methodChannel.setMethodCallHandler { call, result -> + when (call.method) { + "isGyroscopeAvailable" -> { + val isAvailable = + context.packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_GYROSCOPE) + result.success(isAvailable) + } + "dispose" -> { + disposeChannels() + result.success(null) + } + else -> { + result.notImplemented() + } + } + } + } - companion object { - private const val GYROSCOPE_CHANNEL_NAME = "com.simform.flutter_credit_card/gyroscope" - private const val METHOD_CHANNEL_NAME = "com.simform.flutter_credit_card" - private const val DEFAULT_UPDATE_INTERVAL = SensorManager.SENSOR_DELAY_NORMAL - } + private fun disposeChannels() { + methodChannel.setMethodCallHandler(null) + gyroscopeStreamHandler.onCancel(null) + gyroscopeChannel.setStreamHandler(null) + } } diff --git a/android/src/main/kotlin/com/simform/flutter_credit_card/StreamHandlerImpl.kt b/android/src/main/kotlin/com/simform/flutter_credit_card/GyroscopeStreamHandler.kt similarity index 74% rename from android/src/main/kotlin/com/simform/flutter_credit_card/StreamHandlerImpl.kt rename to android/src/main/kotlin/com/simform/flutter_credit_card/GyroscopeStreamHandler.kt index cea6f0c..56e56b4 100644 --- a/android/src/main/kotlin/com/simform/flutter_credit_card/StreamHandlerImpl.kt +++ b/android/src/main/kotlin/com/simform/flutter_credit_card/GyroscopeStreamHandler.kt @@ -7,19 +7,20 @@ import android.hardware.SensorManager import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink -internal class StreamHandlerImpl( - private val sensorManager: SensorManager, - sensorType: Int, +private const val FPS_60 = 16666 + +internal class GyroscopeStreamHandler( + private val sensorManager: SensorManager, ) : EventChannel.StreamHandler { private var sensorEventListener: SensorEventListener? = null private val sensor: Sensor by lazy { - sensorManager.getDefaultSensor(sensorType) + sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) } override fun onListen(arguments: Any?, events: EventSink) { sensorEventListener = createSensorEventListener(events) - sensorManager.registerListener(sensorEventListener, sensor, 16666) + sensorManager.registerListener(sensorEventListener, sensor, FPS_60) } override fun onCancel(arguments: Any?) { @@ -31,10 +32,7 @@ internal class StreamHandlerImpl( override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} override fun onSensorChanged(event: SensorEvent) { - val sensorValues = DoubleArray(event.values.size) - event.values.forEachIndexed { index, value -> - sensorValues[index] = value.toDouble() - } + val sensorValues = event.values.map { it.toDouble() }.toDoubleArray() events.success(sensorValues) } } diff --git a/android/src/test/kotlin/com/simform/flutter_credit_card/FlutterCreditCardPluginTest.kt b/android/src/test/kotlin/com/simform/flutter_credit_card/FlutterCreditCardPluginTest.kt deleted file mode 100644 index b5bdc78..0000000 --- a/android/src/test/kotlin/com/simform/flutter_credit_card/FlutterCreditCardPluginTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.simform.flutter_credit_card - -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import kotlin.test.Test -import org.mockito.Mockito - -/* - * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. - * - * Once you have built the plugin's example app, you can run these tests from the command - * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or - * you can run them directly from IDEs that support JUnit such as Android Studio. - */ - -internal class FlutterCreditCardPluginTest { - @Test - fun onMethodCall_getPlatformVersion_returnsExpectedValue() { - val plugin = FlutterCreditCardPlugin() - - val call = MethodCall("getPlatformVersion", null) - val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) - plugin.onMethodCall(call, mockResult) - - Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) - } -} diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies deleted file mode 100644 index 9198981..0000000 --- a/example/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_credit_card","path":"/Users/kavan/Simform Projects/flutter_credit_card/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_credit_card","path":"/Users/kavan/Simform Projects/flutter_credit_card/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"flutter_credit_card","dependencies":[]}],"date_created":"2023-08-11 14:22:00.698558","version":"3.10.4"} \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore index 07488ba..065b534 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -22,6 +22,7 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ @@ -35,6 +36,8 @@ **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java +**/android/**/build/ +**/android/**/local.properties # iOS/XCode related **/ios/**/*.mode1v3 @@ -49,6 +52,7 @@ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ +**/ios/**/Podfile.lock **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock deleted file mode 100644 index de1491e..0000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,22 +0,0 @@ -PODS: - - Flutter (1.0.0) - - flutter_credit_card (0.0.1): - - Flutter - -DEPENDENCIES: - - Flutter (from `Flutter`) - - flutter_credit_card (from `.symlinks/plugins/flutter_credit_card/ios`) - -EXTERNAL SOURCES: - Flutter: - :path: Flutter - flutter_credit_card: - :path: ".symlinks/plugins/flutter_credit_card/ios" - -SPEC CHECKSUMS: - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_credit_card: 7aca3f6603dedd2d489aa87e23f8c72e375f50f1 - -PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 - -COCOAPODS: 1.11.3 diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index d085545..0000000 --- a/example/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Flutter -import UIKit -import XCTest - -@testable import flutter_credit_card - -// This demonstrates a simple unit test of the Swift portion of this plugin's implementation. -// -// See https://developer.apple.com/documentation/xctest for more information about using XCTest. - -class RunnerTests: XCTestCase { - - func testGetPlatformVersion() { - let plugin = FlutterCreditCardPlugin() - - let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) - - let resultExpectation = expectation(description: "result block must be called.") - plugin.handle(call) { result in - XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion) - resultExpectation.fulfill() - } - waitForExpectations(timeout: 1) - } - -} diff --git a/example/lib/main.dart b/example/lib/main.dart index f78c18e..3b13908 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,15 +6,14 @@ import 'package:flutter_credit_card/flutter_credit_card.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await CreditCardWidget.instance.initialize(); - runApp(MySample()); + runApp(const MySample()); } class MySample extends StatefulWidget { + const MySample({Key? key}) : super(key: key); + @override - State createState() { - return MySampleState(); - } + State createState() => MySampleState(); } class MySampleState extends State { @@ -25,20 +24,15 @@ class MySampleState extends State { bool isCvvFocused = false; bool useGlassMorphism = false; bool useBackgroundImage = false; - OutlineInputBorder? border; + bool useFloatingAnimation = true; + final OutlineInputBorder border = OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey.withOpacity(0.7), + width: 2.0, + ), + ); final GlobalKey formKey = GlobalKey(); - @override - void initState() { - border = OutlineInputBorder( - borderSide: BorderSide( - color: Colors.grey.withOpacity(0.7), - width: 2.0, - ), - ); - super.initState(); - } - @override Widget build(BuildContext context) { return MaterialApp( @@ -64,9 +58,9 @@ class MySampleState extends State { height: 30, ), CreditCardWidget( - isFloatingAnimationEnabled: true, - isGlareAnimationEnabled: true, - isShadowAnimationEnabled: true, + shouldFloat: useFloatingAnimation, + shouldAddGlare: true, + shouldAddShadow: true, glassmorphismConfig: useGlassMorphism ? Glassmorphism.defaultConfig() : null, cardNumber: cardNumber, @@ -202,6 +196,31 @@ class MySampleState extends State { ], ), ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Floating Card', + style: TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + const Spacer(), + Switch( + value: useFloatingAnimation, + inactiveTrackColor: Colors.grey, + activeColor: Colors.white, + activeTrackColor: AppColors.colorE5D1B2, + onChanged: (bool value) => setState(() { + useFloatingAnimation = value; + }), + ), + ], + ), + ), const SizedBox( height: 20, ), @@ -253,7 +272,7 @@ class MySampleState extends State { } void _onValidate() { - if (formKey.currentState!.validate()) { + if (formKey.currentState?.validate() ?? false) { print('valid!'); } else { print('invalid!'); @@ -261,8 +280,9 @@ class MySampleState extends State { } void onCreditCardModelChange(CreditCardModel? creditCardModel) { + if (creditCardModel == null) return; setState(() { - cardNumber = creditCardModel!.cardNumber; + cardNumber = creditCardModel.cardNumber; expiryDate = creditCardModel.expiryDate; cardHolderName = creditCardModel.cardHolderName; cvvCode = creditCardModel.cvvCode; diff --git a/ios/Classes/FlutterCreditCardPlugin.swift b/ios/Classes/FlutterCreditCardPlugin.swift index 2fd7566..9cd91a7 100644 --- a/ios/Classes/FlutterCreditCardPlugin.swift +++ b/ios/Classes/FlutterCreditCardPlugin.swift @@ -2,67 +2,61 @@ import Flutter import UIKit import CoreMotion -private var motionManager: CMMotionManager? -private var eventChannels = [String: FlutterEventChannel]() -private var streamHandlers = [String: FlutterStreamHandler]() -private func initMotionManager() { - if motionManager == nil { - motionManager = CMMotionManager() - } -} - -private func isGyroscopeAvailable() -> Bool { - initMotionManager() - let gyroAvailable = motionManager?.isGyroAvailable ?? false - return gyroAvailable -} - +private let gyroscopeStreamHandlerName = "com.simform.flutter_credit_card/gyroscope" +private let methodChannelName = "com.simform.flutter_credit_card" public class FlutterCreditCardPlugin: NSObject, FlutterPlugin { + private var motionManager: CMMotionManager? + private var gyroscopeChannel: FlutterEventChannel? + private var gyroscopeStreamHandler: GyroscopeStreamHandler? + public static func register(with registrar: FlutterPluginRegistrar) { - - let gyroscopeStreamHandlerName = "com.simform.flutter_credit_card/gyroscope" - let gyroscopeStreamHandler = MTGyroscopeStreamHandler() - streamHandlers[gyroscopeStreamHandlerName] = gyroscopeStreamHandler - - let gyroscopeChannel = FlutterEventChannel(name: gyroscopeStreamHandlerName, binaryMessenger: registrar.messenger()) - gyroscopeChannel.setStreamHandler(gyroscopeStreamHandler) - eventChannels[gyroscopeStreamHandlerName] = gyroscopeChannel - - - let channel = FlutterMethodChannel(name: "com.simform.flutter_credit_card", binaryMessenger: registrar.messenger()) let instance = FlutterCreditCardPlugin() + instance.motionManager = CMMotionManager() + instance.gyroscopeStreamHandler = GyroscopeStreamHandler(motionManager: instance.motionManager!) + instance.gyroscopeChannel = FlutterEventChannel(name: gyroscopeStreamHandlerName, binaryMessenger: registrar.messenger()) + instance.gyroscopeChannel?.setStreamHandler(instance.gyroscopeStreamHandler) + + let channel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: registrar.messenger()) registrar.addMethodCallDelegate(instance, channel: channel) } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "isGyroscopeAvailable": - let avaialble = isGyroscopeAvailable() - result(avaialble) + result(motionManager?.isGyroAvailable ?? false) + case "dispose": + motionManager = nil + gyroscopeStreamHandler?.onCancel(withArguments: nil) + gyroscopeChannel?.setStreamHandler(nil) + result(nil) default: result(FlutterMethodNotImplemented) } } } -public class MTGyroscopeStreamHandler: NSObject, FlutterStreamHandler { - +public class GyroscopeStreamHandler: NSObject, FlutterStreamHandler { + private var motionManager: CMMotionManager? + + init(motionManager: CMMotionManager) { + self.motionManager = motionManager + super.init() + } + public func onListen(withArguments arguments: Any?, eventSink sink: @escaping FlutterEventSink) -> FlutterError? { - initMotionManager() motionManager?.startGyroUpdates(to: OperationQueue()){ (gyroData, error) in if let rotationRate = gyroData?.rotationRate { - sink([rotationRate.x,rotationRate.y,rotationRate.z]) + sink([rotationRate.x, rotationRate.y, rotationRate.z]) } } - return nil } + // Add the timer to the current run loop. public func onCancel(withArguments arguments: Any?) -> FlutterError? { motionManager?.stopGyroUpdates() - return FlutterError() + motionManager = nil + return nil } - } - diff --git a/ios/LICENSE b/ios/LICENSE new file mode 100644 index 0000000..556689c --- /dev/null +++ b/ios/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Simform Solutions + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/ios/flutter_credit_card.podspec b/ios/flutter_credit_card.podspec index 29d928c..433afc5 100644 --- a/ios/flutter_credit_card.podspec +++ b/ios/flutter_credit_card.podspec @@ -4,14 +4,14 @@ # Pod::Spec.new do |s| s.name = 'flutter_credit_card' - s.version = '0.0.1' + s.version = '3.0.7' s.summary = 'A new Flutter plugin project.' s.description = <<-DESC A new Flutter plugin project. DESC - s.homepage = 'http://example.com' + s.homepage = 'https://github.com/simformsolutions/flutter_credit_card' s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } + s.author = { 'Your Company' => 'developer@simform.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' diff --git a/lib/constants.dart b/lib/constants.dart index 1576cbd..082e7cf 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -2,21 +2,22 @@ import 'dart:math'; import 'dart:ui'; class AppConstants { - static const double webBreakPoint = 800; + static const double floatWebBreakPoint = 650; 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; + static const double minRestBackVel = 0.01; + static const double maxRestBackVel = 0.05; + static const double defaultRestBackVel = 0.8; - /// Color constants - static const Color defaultGlareColor = Color(0xffFFFFFF), - defaultShadowColor = Color(0xff000000); - - static const double defaultMaxAngle = pi / 10, + static const Duration fps60 = Duration(microseconds: 16666); + static const Duration fps60Offset = Duration(microseconds: 16667); - minBlurRadius = 10, - minShadowOpacity = 0.3; + /// Color constants + static const Color defaultGlareColor = Color(0xffFFFFFF); + static const Color defaultShadowColor = Color(0xff000000); + static const double defaultMaximumAngle = pi / 10; + static const double minBlurRadius = 10; + static const double minShadowOpacity = 0.3; } diff --git a/lib/credit_card_background.dart b/lib/credit_card_background.dart index 9e4a115..eb0c140 100644 --- a/lib/credit_card_background.dart +++ b/lib/credit_card_background.dart @@ -1,33 +1,34 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; -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_animation/floating_controller.dart'; +import 'floating_animation/glare_effect_widget.dart'; import 'glassmorphism_config.dart'; class CardBackground extends StatelessWidget { const CardBackground({ - Key? key, required this.backgroundGradientColor, + required this.child, + required this.padding, this.backgroundImage, this.backgroundNetworkImage, - required this.child, this.width, this.height, this.glassmorphismConfig, - required this.padding, this.border, this.floatingController, this.glarePosition, this.shadowEnabled = false, - }) : assert( - (backgroundImage == null && backgroundNetworkImage == null) || - (backgroundImage == null && backgroundNetworkImage != null) || - (backgroundImage != null && backgroundNetworkImage == null), - "You can't use network image & asset image at same time for card background"), - super(key: key); + super.key, + }) : assert( + (backgroundImage == null && backgroundNetworkImage == null) || + (backgroundImage == null && backgroundNetworkImage != null) || + (backgroundImage != null && backgroundNetworkImage == null), + 'You can\'t use network image & asset image at same time as card' + ' background', + ); final String? backgroundImage; final String? backgroundNetworkImage; @@ -54,46 +55,34 @@ class CardBackground extends StatelessWidget { return Stack( alignment: Alignment.center, children: [ - 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( borderRadius: BorderRadius.circular(8), + boxShadow: shadowEnabled && floatingController != null + ? [ + BoxShadow( + blurRadius: AppConstants.minBlurRadius, + color: AppConstants.defaultShadowColor + .withOpacity(AppConstants.minShadowOpacity), + offset: Offset( + floatingController!.y * 100, + -floatingController!.x * 100 + 8, + ), + ), + ] + : null, border: border, - gradient: glassmorphismConfig != null - ? glassmorphismConfig!.gradient - : backgroundGradientColor, - image: backgroundImage != null && backgroundImage!.isNotEmpty + gradient: + glassmorphismConfig?.gradient ?? backgroundGradientColor, + image: backgroundImage?.isNotEmpty ?? false ? DecorationImage( - image: ExactAssetImage( - backgroundImage!, - ), + image: ExactAssetImage(backgroundImage!), fit: BoxFit.fill, ) - : backgroundNetworkImage != null && - backgroundNetworkImage!.isNotEmpty + : backgroundNetworkImage?.isNotEmpty ?? false ? DecorationImage( - image: NetworkImage( - backgroundNetworkImage!, - ), + image: NetworkImage(backgroundNetworkImage!), fit: BoxFit.fill, ) : null, @@ -108,20 +97,17 @@ class CardBackground extends StatelessWidget { clipBehavior: Clip.hardEdge, borderRadius: BorderRadius.circular(8), child: GlareEffectWidget( - glarePosition: glarePosition, - controller: floatingController, border: border, - child: Container( - child: glassmorphismConfig != null - ? BackdropFilter( - filter: ui.ImageFilter.blur( - sigmaX: glassmorphismConfig?.blurX ?? 0.0, - sigmaY: glassmorphismConfig?.blurY ?? 0.0, - ), - child: child, - ) - : child, - ), + glarePosition: glarePosition, + child: glassmorphismConfig == null + ? child + : BackdropFilter( + filter: ui.ImageFilter.blur( + sigmaX: glassmorphismConfig!.blurX, + sigmaY: glassmorphismConfig!.blurY, + ), + child: child, + ), ), ), ), diff --git a/lib/credit_card_widget.dart b/lib/credit_card_widget.dart index 3461a0c..d0b19e9 100644 --- a/lib/credit_card_widget.dart +++ b/lib/credit_card_widget.dart @@ -2,17 +2,17 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_credit_card/constants.dart'; -import 'package:flutter_credit_card/extension.dart'; -import 'package:flutter_credit_card/flutter_credit_card_platform_interface.dart'; +import 'constants.dart'; import 'credit_card_animation.dart'; import 'credit_card_background.dart'; 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 'extension.dart'; +import 'floating_animation/cursor_listener.dart'; +import 'floating_animation/floating_controller.dart'; +import 'floating_animation/floating_event.dart'; +import 'flutter_credit_card_platform_interface.dart'; import 'glassmorphism_config.dart'; const Map CardTypeIconAsset = { @@ -29,12 +29,12 @@ const Map CardTypeIconAsset = { class CreditCardWidget extends StatefulWidget { /// A widget showcasing credit card UI. const CreditCardWidget({ - Key? key, required this.cardNumber, required this.expiryDate, required this.cardHolderName, required this.cvvCode, required this.showBackView, + required this.onCreditCardWidgetChange, this.bankName, this.animationDuration = const Duration(milliseconds: 500), this.height, @@ -54,16 +54,16 @@ class CreditCardWidget extends StatefulWidget { this.isChipVisible = true, this.isSwipeGestureEnabled = true, this.customCardTypeIcons = const [], - required this.onCreditCardWidgetChange, this.padding = AppConstants.creditCardPadding, this.chipColor, this.frontCardBorder, this.backCardBorder, this.obscureInitialCardNumber = false, - this.isFloatingAnimationEnabled = false, - this.isShadowAnimationEnabled = false, - this.isGlareAnimationEnabled = false, - }) : super(key: key); + this.shouldFloat = false, + this.shouldAddShadow = false, + this.shouldAddGlare = false, + super.key, + }); /// A string indicating number on the card. final String cardNumber; @@ -168,14 +168,20 @@ class CreditCardWidget extends StatefulWidget { /// Provides border at back of credit card widget. final BoxBorder? backCardBorder; - final bool isFloatingAnimationEnabled; - - static final FlutterCreditCardPlatform instance = - FlutterCreditCardPlatform.instance; + /// Denotes whether card floating animation is enabled. + /// Defaults to false. + /// + /// Enabling this would float the card as per the movement of device or mouse + /// pointer. + final bool shouldFloat; - final bool isShadowAnimationEnabled; + /// Denotes whether to add a shadow beneath the card. + /// This takes effect only when [shouldFloat] is true. + final bool shouldAddShadow; - final bool isGlareAnimationEnabled; + /// Denotes whether to add a glare - a shinning effect - over the card. + /// This takes effect only when [shouldFloat] is true. + final bool shouldAddGlare; /// floating animation enabled/disabled @override @@ -188,29 +194,30 @@ class _CreditCardWidgetState extends State late Animation _frontRotation; late Animation _backRotation; late Gradient backgroundGradientColor; - late bool isFrontVisible = true; - late bool isGestureUpdate = false; + bool isFrontVisible = true; + bool isGestureUpdate = false; bool isAmex = false; - FloatingController get frontFloatingController => - FloatingController.defaultController; - - FloatingController get backFloatingController => - FloatingController.defaultController; + final FloatingController floatingAnimController = + FloatingController.predefined( + isGyroscopeAvailable: + FlutterCreditCardPlatform.instance.isGyroscopeAvailable, + ); - StreamController frontCardStreamController = + final StreamController frontCardStreamController = StreamController.broadcast(); - StreamController backCardStreamController = + final StreamController backCardStreamController = StreamController.broadcast(); Orientation? orientation; + Size? screenSize; - bool isAnimation = false; + bool isAnimating = false; double get glarePosition => - pi / 4 + (frontFloatingController.y / (pi / 10) * (2 * pi)); + pi / 4 + (floatingAnimController.y / (pi / 10) * (2 * pi)); @override void initState() { @@ -224,26 +231,17 @@ class _CreditCardWidgetState extends State controller.addStatusListener((AnimationStatus status) { if (status == AnimationStatus.forward) { - isAnimation = true; + isAnimating = true; } else if (status == AnimationStatus.reverse) { - isAnimation = true; + isAnimating = true; } else if (status == AnimationStatus.completed) { - isAnimation = false; + isAnimating = false; } else { - isAnimation = false; + isAnimating = false; } }); - if (widget.isFloatingAnimationEnabled) { - CreditCardWidget.instance.floatingStream?.listen((FloatingEvent event) { - if (isFrontVisible) { - frontCardStreamController.add(event); - } else { - backCardStreamController.add(event); - } - }); - } - + _initializeFloatingAnimation(); _gradientSetup(); _updateRotations(false); } @@ -251,6 +249,7 @@ class _CreditCardWidgetState extends State @override void didChangeDependencies() { orientation = MediaQuery.of(context).orientation; + screenSize = MediaQuery.of(context).size; super.didChangeDependencies(); } @@ -259,73 +258,10 @@ class _CreditCardWidgetState extends State if (widget.cardBgColor != oldWidget.cardBgColor) { _gradientSetup(); } - super.didUpdateWidget(oldWidget); - } - - Matrix4 computeBackTransformForEvent(FloatingEvent? event) { - final Matrix4 matrix = Matrix4.identity()..setEntry(3, 2, 0.001); - - if (isAnimation) { - return matrix; - } - if (event != null) { - 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); - matrix.translate( - backFloatingController.y * -((45) * 2.0), - backFloatingController.x * (45), - ); - } - - return matrix; - } - - Matrix4 computeTransformForEvent(FloatingEvent? event) { - final Matrix4 matrix = Matrix4.identity()..setEntry(3, 2, 0.001); - - if (isAnimation) { - return matrix; + if (oldWidget.shouldFloat != widget.shouldFloat) { + _initializeFloatingAnimation(); } - if (event != null) { - 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); - matrix.translate( - frontFloatingController.y * -((45) * 2.0), - frontFloatingController.x * (45), - ); - } - - return matrix; + super.didUpdateWidget(oldWidget); } void _gradientSetup() { @@ -346,6 +282,7 @@ class _CreditCardWidgetState extends State @override void dispose() { + FlutterCreditCardPlatform.instance.dispose(); controller.dispose(); backCardStreamController.close(); frontCardStreamController.close(); @@ -355,31 +292,25 @@ class _CreditCardWidgetState extends State @override Widget build(BuildContext context) { /// - /// If uer adds CVV then toggle the card from front to back.. + /// If user adds CVV then toggle the card from front to back. /// controller forward starts animation and shows back layout. /// controller reverse starts animation and shows front layout. /// - if (!isGestureUpdate) { - _updateRotations(false); - if (widget.showBackView) { - isFrontVisible = false; - controller.forward(); - } else { - isFrontVisible = true; - controller.reverse(); - } - } else { + if (isGestureUpdate) { isGestureUpdate = false; + } else { + _toggleSide(flipFromRight: false, showBackSide: widget.showBackView); } - final CardType? cardType = widget.cardType != null - ? widget.cardType - : detectCCType(widget.cardNumber); - widget.onCreditCardWidgetChange(CreditCardBrand(cardType)); + final CreditCardBrand cardBrand = CreditCardBrand( + widget.cardType ?? detectCCType(widget.cardNumber), + ); + widget.onCreditCardWidgetChange(cardBrand); - DateTime? lastPointerEventTime; + final StreamController floatingStream = + isFrontVisible ? frontCardStreamController : backCardStreamController; - final Widget child = Stack( + return Stack( children: [ _cardGesture( child: AnimationCard( @@ -393,75 +324,80 @@ class _CreditCardWidgetState extends State child: _buildBackContainer(), ), ), + if (!FlutterCreditCardPlatform.instance.isGyroscopeAvailable && + widget.shouldFloat) + Positioned.fill( + child: LayoutBuilder( + builder: (_, BoxConstraints constraints) { + final double parentHeight = constraints.maxHeight; + final double parentWidth = constraints.maxWidth; + final double outerPadding = + (screenSize?.width ?? parentWidth) - parentWidth; + final double padding = outerPadding != 0 && widget.padding == 0 + ? AppConstants.creditCardPadding + : widget.padding; + + return CursorListener( + onPositionChange: floatingStream.add, + height: parentHeight - padding, + width: parentWidth - padding, + padding: padding, + ); + }, + ), + ) ], ); - - 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() { - _toggleSide(false); - } + FloatingEvent? _processEventForOrientation(FloatingEvent? event) { + if (event == null || event.type == FloatingType.pointer) { + return event; + } + + final bool isLandscape = orientation == Orientation.landscape; + final double xValue = isLandscape ? -event.y : event.x; + final double yValue = isLandscape ? event.x : event.y; - void _rightRotation() { - _toggleSide(true); + return FloatingEvent( + type: event.type, + x: xValue, + y: yValue, + z: event.z, + ); } - void _toggleSide(bool isRightTap) { - _updateRotations(!isRightTap); - if (isFrontVisible) { - controller.forward(); + void _toggleSide({ + required bool flipFromRight, + bool? showBackSide, + }) { + _updateRotations(flipFromRight); + if (showBackSide ?? isFrontVisible) { isFrontVisible = false; + controller.forward(); } else { - controller.reverse(); isFrontVisible = true; + controller.reverse(); } } void _updateRotations(bool isRightSwipe) { setState(() { - final bool rotateToLeft = - (isFrontVisible && !isRightSwipe) || !isFrontVisible && isRightSwipe; + final bool rotateToLeft = (isFrontVisible && !isRightSwipe) || + (!isFrontVisible && isRightSwipe); + final double start = rotateToLeft ? (pi / 2) : (-pi / 2); + final double end = rotateToLeft ? (-pi / 2) : (pi / 2); ///Initialize the Front to back rotation tween sequence. _frontRotation = TweenSequence( >[ TweenSequenceItem( - tween: Tween( - begin: 0.0, end: rotateToLeft ? (pi / 2) : (-pi / 2)) + tween: Tween(begin: 0.0, end: start) .chain(CurveTween(curve: Curves.linear)), weight: 50.0, ), TweenSequenceItem( - tween: ConstantTween(rotateToLeft ? (-pi / 2) : (pi / 2)), + tween: ConstantTween(end), weight: 50.0, ), ], @@ -471,15 +407,12 @@ class _CreditCardWidgetState extends State _backRotation = TweenSequence( >[ TweenSequenceItem( - tween: ConstantTween(rotateToLeft ? (pi / 2) : (-pi / 2)), + tween: ConstantTween(start), weight: 50.0, ), TweenSequenceItem( - tween: Tween( - begin: rotateToLeft ? (-pi / 2) : (pi / 2), end: 0.0) - .chain( - CurveTween(curve: Curves.linear), - ), + tween: Tween(begin: end, end: 0.0) + .chain(CurveTween(curve: Curves.linear)), weight: 50.0, ), ], @@ -523,13 +456,16 @@ class _CreditCardWidgetState extends State stripped.substring(stripped.length - 4); } } - if (widget.isFloatingAnimationEnabled && isFrontVisible) + + if (widget.shouldFloat && isFrontVisible) { return StreamBuilder( stream: frontCardStreamController.stream, builder: (BuildContext context, AsyncSnapshot snapshot) { return Transform( - transform: computeTransformForEvent(snapshot.data), - filterQuality: FilterQuality.high, + transform: floatingAnimController.transform( + _processEventForOrientation(snapshot.data), + shouldAvoid: isAnimating, + ), alignment: FractionalOffset.center, child: _frontCardBackground( defaultTextStyle: defaultTextStyle, @@ -538,11 +474,12 @@ class _CreditCardWidgetState extends State ); }, ); - else + } else { return _frontCardBackground( defaultTextStyle: defaultTextStyle, number: number, ); + } } Widget _frontCardBackground({ @@ -551,11 +488,8 @@ class _CreditCardWidgetState extends State }) { return CardBackground( glarePosition: - widget.isGlareAnimationEnabled && widget.isFloatingAnimationEnabled - ? glarePosition - : null, - floatingController: - widget.isFloatingAnimationEnabled ? frontFloatingController : null, + widget.shouldAddGlare && widget.shouldFloat ? glarePosition : null, + floatingController: widget.shouldFloat ? floatingAnimController : null, backgroundImage: widget.backgroundImage, backgroundNetworkImage: widget.backgroundNetworkImage, backgroundGradientColor: backgroundGradientColor, @@ -564,6 +498,7 @@ class _CreditCardWidgetState extends State width: widget.width, padding: widget.padding, border: widget.frontCardBorder, + shadowEnabled: widget.shouldAddShadow && widget.shouldFloat, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -681,20 +616,24 @@ class _CreditCardWidgetState extends State ? widget.cvvCode.replaceAll(RegExp(r'\d'), '*') : widget.cvvCode; - return widget.isFloatingAnimationEnabled + return widget.shouldFloat && !isFrontVisible ? StreamBuilder( stream: backCardStreamController.stream, builder: - (BuildContext context, AsyncSnapshot snapshot) => - Transform( - transform: computeBackTransformForEvent(snapshot.data), - filterQuality: FilterQuality.high, - alignment: FractionalOffset.center, - child: _backCardBackground( - cvv: cvv, - defaultTextStyle: defaultTextStyle, - ), - )) + (BuildContext context, AsyncSnapshot snapshot) { + return Transform( + transform: floatingAnimController.transform( + _processEventForOrientation(snapshot.data), + shouldAvoid: isAnimating, + ), + alignment: FractionalOffset.center, + child: _backCardBackground( + cvv: cvv, + defaultTextStyle: defaultTextStyle, + ), + ); + }, + ) : _backCardBackground( cvv: cvv, defaultTextStyle: defaultTextStyle, @@ -707,11 +646,8 @@ class _CreditCardWidgetState extends State }) { return CardBackground( glarePosition: - widget.isGlareAnimationEnabled && widget.isFloatingAnimationEnabled - ? glarePosition - : null, - floatingController: - widget.isFloatingAnimationEnabled ? backFloatingController : null, + widget.shouldAddGlare && widget.shouldFloat ? glarePosition : null, + floatingController: widget.shouldFloat ? floatingAnimController : null, backgroundImage: widget.backgroundImage, backgroundNetworkImage: widget.backgroundNetworkImage, backgroundGradientColor: backgroundGradientColor, @@ -720,6 +656,7 @@ class _CreditCardWidgetState extends State width: widget.width, padding: widget.padding, border: widget.backCardBorder, + shadowEnabled: widget.shouldAddShadow && widget.shouldFloat, child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, @@ -791,11 +728,7 @@ class _CreditCardWidgetState extends State ? GestureDetector( onPanEnd: (_) { isGestureUpdate = true; - if (isRightSwipe) { - _leftRotation(); - } else { - _rightRotation(); - } + _toggleSide(flipFromRight: isRightSwipe); }, onPanUpdate: (DragUpdateDetails details) { // Swiping in right direction. @@ -813,6 +746,25 @@ class _CreditCardWidgetState extends State : child; } + void _initializeFloatingAnimation() { + if (widget.shouldFloat) { + FlutterCreditCardPlatform.instance.initialize().then((_) { + FlutterCreditCardPlatform.instance.floatingStream + ?.listen((FloatingEvent event) { + if (isAnimating) { + return; + } + + if (isFrontVisible) { + frontCardStreamController.add(event); + } else { + backCardStreamController.add(event); + } + }); + }); + } + } + /// Credit Card prefix patterns as of March 2019 /// A [List] represents a range. /// i.e. ['51', '55'] represents the range of cards starting with '51' to those starting with '55' diff --git a/lib/floating_animation/cursor_listener.dart b/lib/floating_animation/cursor_listener.dart new file mode 100644 index 0000000..17b1f76 --- /dev/null +++ b/lib/floating_animation/cursor_listener.dart @@ -0,0 +1,164 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import '../constants.dart'; +import 'floating_event.dart'; + +class CursorListener extends StatefulWidget { + /// This widget listens cursor entry and exit while hovering on the card + const CursorListener({ + required this.onPositionChange, + required this.height, + required this.width, + required this.padding, + super.key, + }); + + /// Any padding applied to the area where the cursor movement is to be + /// detected. + final double padding; + + /// The height of the area where the cursor movement is to be detected. + final double height; + + /// The width of the area where the cursor movement is to be detected. + final double width; + + ///This called when a pointer event is received. + final ValueChanged onPositionChange; + + @override + State createState() => _CursorListenerState(); +} + +class _CursorListenerState extends State { + /// 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; + + /// A timer that progressively increases or decreases the intensity factor. + Timer? velocityTimer; + + late double surroundedPadding = widget.padding * 2; + + @override + Widget build(BuildContext context) { + return MouseRegion( + hitTestBehavior: HitTestBehavior.translucent, + onEnter: (_) => _onCursorEnter(), + onExit: (_) => _onCursorExit(), + onHover: (PointerHoverEvent details) { + _onCursorMove(details.localPosition); + }, + ); + } + + @override + void dispose() { + velocityTimer?.cancel(); + super.dispose(); + } + + void _onCursorMove(Offset position) { + if (DateTime.now().difference(lastPointerEvent) < AppConstants.fps60) { + /// Drop event since it occurs too early. + return; + } + + double x = 0.0; + double y = 0.0; + + // Compute the fractional offset. + x = (position.dy - (widget.height / 2)) / widget.height; + y = -(position.dx - (widget.width / 2)) / widget.width; + + // Apply the intensity factor. + x *= intensityFactor; + y *= intensityFactor; + + // Calculate the maximum allowable offset while staying within the + // screen bounds when the card widget is larger than + // [AppConstants.webBreakPoint]. + if (widget.width > AppConstants.floatWebBreakPoint) { + try { + final double clampingFactor = surroundedPadding / widget.height; + + if (!clampingFactor.isNaN && !clampingFactor.isInfinite) { + // Clamp the x and y values to stay within screen bounds. + x = x.clamp(-clampingFactor, clampingFactor); + y = y.clamp(-clampingFactor, clampingFactor); + } + } catch (_) { + // Ignore clamping if it causes an error. + } + } + + // Notify the position change. + widget.onPositionChange( + FloatingEvent( + type: FloatingType.pointer, + x: x, + y: y, + ), + ); + + // Store the previous values. + lastPointerEvent = DateTime.now(); + lastOffset = Offset(position.dx, position.dy); + } + + /// Animate the intensity factor to 1, to smoothly get to the pointer's + /// position. + Future _onCursorEnter() async { + _cancelVelocityTimer(); + + velocityTimer = Timer.periodic( + AppConstants.fps60Offset, + (_) { + if (intensityFactor < 1) { + if (intensityFactor <= 0.05) { + intensityFactor = 0.05; + } + intensityFactor = min(1, intensityFactor * 1.2); + _onCursorMove(lastOffset); + } else { + _cancelVelocityTimer(); + } + }, + ); + } + + /// Animate the intensity factor to 0, to smoothly get back to the initial + /// position. + Future _onCursorExit() async { + _cancelVelocityTimer(); + + velocityTimer = Timer.periodic( + AppConstants.fps60Offset, + (_) { + if (intensityFactor > 0.05) { + intensityFactor = max(0, intensityFactor * 0.95); + _onCursorMove(lastOffset); + } else { + _cancelVelocityTimer(); + } + }, + ); + } + + /// Cancels the velocity timer. + void _cancelVelocityTimer() { + velocityTimer?.cancel(); + velocityTimer = null; + } +} diff --git a/lib/floating_animation/floating_controller.dart b/lib/floating_animation/floating_controller.dart new file mode 100644 index 0000000..2233383 --- /dev/null +++ b/lib/floating_animation/floating_controller.dart @@ -0,0 +1,93 @@ +import 'dart:math'; + +import 'package:flutter/rendering.dart'; +import 'package:flutter_credit_card/constants.dart'; + +import 'floating_event.dart'; + +class FloatingController { + /// Houses [x] and [y] angles, and the transformation logic for the + /// floating effect. + FloatingController({ + required this.maximumAngle, + this.restBackVelocity, + this.isGyroscopeAvailable = false, + }); + + FloatingController.predefined({this.isGyroscopeAvailable = false}) + : restBackVelocity = AppConstants.defaultRestBackVel, + maximumAngle = AppConstants.defaultMaximumAngle; + + final bool isGyroscopeAvailable; + + /// The maximum floating animation moving angle. + double maximumAngle; + + /// Represents the x value for gyroscope and mouse pointer data. + double x = 0; + + /// Represents the y value for gyroscope and mouse pointer data. + double y = 0; + + /// Determines the velocity when the card rests back to default position. + double? restBackVelocity; + + /// The actual resting back factor used by the widget. + /// + /// Computed from the [restBackVelocity] value which lerps from 0 to 1 between + /// [minRestBackVel] and [maxRestBackVel]. + double get restBackFactor { + if (restBackVelocity == null) { + return 1; + } else { + const double restBackVelRange = + AppConstants.maxRestBackVel - AppConstants.minRestBackVel; + final double adjusted = + AppConstants.minRestBackVel + (restBackVelocity! * restBackVelRange); + return 1 - adjusted; + } + } + + /// Restricts [x] and [y] values to extend within the limit of the + /// [maximumAngle] only. + void boundAngle() { + x = min(maximumAngle / 2, max(-maximumAngle / 2, x)); + y = min(maximumAngle / 2, max(-maximumAngle / 2, y)); + } + + /// Transforms the [x] and [y] angles by performing operations on the angles + /// received from the [event]. + Matrix4 transform( + FloatingEvent? event, { + /// Denotes whether to avoid applying any transformation. + bool shouldAvoid = false, + }) { + final Matrix4 matrix = Matrix4.identity()..setEntry(3, 2, 0.001); + + if (shouldAvoid || event == null) { + return matrix; + } + + if (isGyroscopeAvailable) { + x += event.x * 0.016; + y -= event.y * 0.016; + + boundAngle(); + + // Apply the velocity to float the card. + x *= restBackFactor; + y *= restBackFactor; + } else { + x = event.x * 0.2; + y = event.y * 0.2; + } + + // Rotate the matrix by the resulting x and y values. + matrix + ..rotateX(x) + ..rotateY(y) + ..translate(y * -90, x * 45); + + return matrix; + } +} diff --git a/lib/floating_card_setup/floating_event.dart b/lib/floating_animation/floating_event.dart similarity index 81% rename from lib/floating_card_setup/floating_event.dart rename to lib/floating_animation/floating_event.dart index 1aab82f..f6ebb9d 100644 --- a/lib/floating_card_setup/floating_event.dart +++ b/lib/floating_animation/floating_event.dart @@ -2,12 +2,12 @@ enum FloatingType { pointer, gyroscope } class FloatingEvent { - const FloatingEvent({required this.type, this.x = 0, this.y = 0, this.z = 0}); - - const FloatingEvent.zero({required this.type}) - : x = 0, - y = 0, - z = 0; + const FloatingEvent({ + required this.type, + this.x = 0, + this.y = 0, + this.z = 0, + }); /// The event's [x], [y] and [z] values. /// diff --git a/lib/floating_card_setup/glare_effect_widget.dart b/lib/floating_animation/glare_effect_widget.dart similarity index 56% rename from lib/floating_card_setup/glare_effect_widget.dart rename to lib/floating_animation/glare_effect_widget.dart index da1301d..cc5b386 100644 --- a/lib/floating_card_setup/glare_effect_widget.dart +++ b/lib/floating_animation/glare_effect_widget.dart @@ -1,21 +1,26 @@ 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({ - super.key, required this.child, this.glarePosition, - this.controller, this.border, + super.key, }); final Widget child; - final double? glarePosition; final BoxBorder? border; - final FloatingController? controller; + final double? glarePosition; + + static final List _glareGradientColors = [ + AppConstants.defaultGlareColor.withOpacity(0.1), + AppConstants.defaultGlareColor.withOpacity(0.07), + AppConstants.defaultGlareColor.withOpacity(0.05), + ]; + + static const List _gradientStop = [0.1, 0.3, 0.6]; @override Widget build(BuildContext context) { @@ -23,7 +28,7 @@ class GlareEffectWidget extends StatelessWidget { clipBehavior: Clip.none, children: [ child, - if (controller != null && glarePosition != null) + if (glarePosition != null) Positioned.fill( child: Container( clipBehavior: Clip.hardEdge, @@ -31,16 +36,8 @@ class GlareEffectWidget extends StatelessWidget { border: border, gradient: LinearGradient( tileMode: TileMode.clamp, - colors: [ - AppConstants.defaultGlareColor.withOpacity(0.1), - AppConstants.defaultGlareColor.withOpacity(0.07), - AppConstants.defaultGlareColor.withOpacity(0.05), - ], - stops: const [ - 0.1, - 0.3, - 0.6, - ], + colors: _glareGradientColors, + stops: _gradientStop, transform: GradientRotation(glarePosition!), ), ), diff --git a/lib/floating_card_setup/floating_controller.dart b/lib/floating_card_setup/floating_controller.dart deleted file mode 100644 index 893a391..0000000 --- a/lib/floating_card_setup/floating_controller.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:math'; - -import 'package:flutter_credit_card/constants.dart'; - -class FloatingController { - /// A controller that holds the [Motion] widget's X and Y angles. - FloatingController({ - this.floatingBack = 0.8, - this.maximumAngle = pi / 10, - }); - - /// Floating back will be use when card comes back to default position - /// the velocity will be determine by [floatingBack] value - double? floatingBack; - - /// The actual floating back factor used by the widget. - /// - /// Computed from the [floatingBack] value which lerps from 0 to 1 between [minfloatingBack] and [maxfloatingBack]. - double get floatingBackFactor => floatingBack != null - ? 1 - - (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 - double x = 0, y = 0; - - static final FloatingController defaultController = FloatingController(); - - /// let x and y value can only extend to specified angle - void limitTheAngle() { - x = min(maximumAngle / 2, max(-maximumAngle / 2, x)); - y = min(maximumAngle / 2, max(-maximumAngle / 2, y)); - } -} diff --git a/lib/floating_card_setup/mouse_pointer_listener.dart b/lib/floating_card_setup/mouse_pointer_listener.dart deleted file mode 100644 index 5cbb095..0000000 --- a/lib/floating_card_setup/mouse_pointer_listener.dart +++ /dev/null @@ -1,146 +0,0 @@ -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_method_channel.dart b/lib/flutter_credit_card_method_channel.dart index 8557698..7a38eed 100644 --- a/lib/flutter_credit_card_method_channel.dart +++ b/lib/flutter_credit_card_method_channel.dart @@ -2,9 +2,12 @@ import 'dart:io'; import 'package:flutter/services.dart'; -import 'floating_card_setup/floating_event.dart'; +import 'floating_animation/floating_event.dart'; import 'flutter_credit_card_platform_interface.dart'; +const String _methodChannelName = 'com.simform.flutter_credit_card'; +const String _eventChannelName = 'com.simform.flutter_credit_card/gyroscope'; + /// An implementation of [FlutterCreditCardPlatform] that uses method channels. class MethodChannelFlutterCreditCard extends FlutterCreditCardPlatform { static EventChannel? _gyroscopeEventChannel; @@ -13,20 +16,11 @@ class MethodChannelFlutterCreditCard extends FlutterCreditCardPlatform { static Stream? _gyroscopeStream; - @override - bool get isSafariMobile => false; - static bool _isGyroscopeAvailable = true; @override bool get isGyroscopeAvailable => _isGyroscopeAvailable; - @override - bool get isPermissionGranted => false; - - @override - bool get isPermissionRequired => false; - @override Stream? get floatingStream { try { @@ -35,12 +29,16 @@ class MethodChannelFlutterCreditCard extends FlutterCreditCardPlatform { .map((dynamic event) { final List list = event.cast(); return FloatingEvent( - type: FloatingType.gyroscope, x: list[0], y: list[1], z: list[2]); + type: FloatingType.gyroscope, + x: list.first, + y: list[1], + z: list[2], + ); }); - _gyroscopeStream?.listen((FloatingEvent event) {}); return _gyroscopeStream as Stream; } catch (e) { - // If a PlatformException is thrown, the plugin is not available on the device. + // If a PlatformException is thrown, the plugin is not available on the + // device. _isGyroscopeAvailable = false; return null; } @@ -49,24 +47,24 @@ class MethodChannelFlutterCreditCard extends FlutterCreditCardPlatform { @override Future initialize() async { if (Platform.isIOS || Platform.isAndroid) { - _methodChannel ??= const MethodChannel('com.simform.flutter_credit_card'); + _methodChannel ??= const MethodChannel(_methodChannelName); _isGyroscopeAvailable = await _methodChannel!.invokeMethod('isGyroscopeAvailable') ?? false; - _gyroscopeEventChannel ??= - const EventChannel('com.simform.flutter_credit_card/gyroscope'); - - } else if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) { - // Desktop platforms should not use the gyroscope events. + _gyroscopeEventChannel ??= const EventChannel(_eventChannelName); + } else { + // Other platforms should not use the gyroscope events. _isGyroscopeAvailable = false; } - - return; } - @override - Future requestPermission() async => true; + Future dispose() async { + _isGyroscopeAvailable = false; + _gyroscopeEventChannel = null; + await _methodChannel?.invokeMethod('dispose'); + _methodChannel = null; + } } diff --git a/lib/flutter_credit_card_platform_interface.dart b/lib/flutter_credit_card_platform_interface.dart index 7c621c4..416f72e 100644 --- a/lib/flutter_credit_card_platform_interface.dart +++ b/lib/flutter_credit_card_platform_interface.dart @@ -1,6 +1,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'floating_card_setup/floating_event.dart'; +import 'floating_animation/floating_event.dart'; import 'flutter_credit_card_method_channel.dart'; abstract class FlutterCreditCardPlatform extends PlatformInterface { @@ -24,37 +24,13 @@ abstract class FlutterCreditCardPlatform extends PlatformInterface { _instance = instance; } - Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } - - - - /// Platform features declaration - - /// Detects if the platform is Safari Mobile (iOS or iPad). - bool get isSafariMobile => false; - - /// Indicates whether the gradient is available. - bool get isGradientOverlayAvailable => !isSafariMobile; - /// Indicates whether the gyroscope is available. bool get isGyroscopeAvailable => false; - /// Indicates whether a permission is required to access gyroscope data. - bool get isPermissionRequired => false; - - /// Indicates whether the permission is granted. - bool get isPermissionGranted => false; - /// The gyroscope stream, if available. Stream? get floatingStream => null; - Future initialize() async { - throw UnimplementedError(); - } + Future initialize() async => throw UnimplementedError(); - Future requestPermission() async { - throw UnimplementedError(); - } + Future dispose() async => throw UnimplementedError(); } diff --git a/lib/flutter_credit_card_web.dart b/lib/flutter_credit_card_web.dart index 83dc67c..5c25c14 100644 --- a/lib/flutter_credit_card_web.dart +++ b/lib/flutter_credit_card_web.dart @@ -1,18 +1,12 @@ 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 'floating_animation/floating_event.dart'; import 'flutter_credit_card_platform_interface.dart'; -@JS() -external dynamic get evaluatePermission; - -/// A web implementation of the FlutterCreditCardPlatform of the FlutterCreditCard plugin. +/// A web implementation of the FlutterCreditCardPlatform of the +/// FlutterCreditCard plugin. class FlutterCreditCardWeb extends FlutterCreditCardPlatform { /// Constructs a FlutterCreditCardWeb FlutterCreditCardWeb(); @@ -21,119 +15,15 @@ class FlutterCreditCardWeb extends FlutterCreditCardPlatform { 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; + bool get isGyroscopeAvailable => false; @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; - } + Stream? get floatingStream => null; @override - Future initialize() async { - return; - } + Future initialize() async {} @override - Future requestPermission() async { - // TODO(kavan): Add request permission for IOS Web - return false; - } + Future dispose() async {} } diff --git a/pubspec.yaml b/pubspec.yaml index 0d7726f..5e6db6e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,12 +5,13 @@ homepage: https://github.com/simformsolutions/flutter_credit_card issue_tracker: https://github.com/simformsolutions/flutter_credit_card/issues environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=2.17.0 <4.0.0' + flutter: '>=3.0.0' dependencies: flutter: sdk: flutter - plugin_platform_interface: + plugin_platform_interface: ^2.1.6 flutter_web_plugins: sdk: flutter dev_dependencies: