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..0e8e865 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' + classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdk 31 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -49,16 +49,4 @@ android { testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.0.0' } - - testOptions { - unitTests.all { - useJUnitPlatform() - - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } } 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..3ccc5f4 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 @@ -1,68 +1,116 @@ package com.simform.flutter_credit_card +import android.app.Activity import android.content.Context import android.content.pm.PackageManager -import android.hardware.Sensor import android.hardware.SensorManager +import android.os.Build +import android.view.Display +import android.view.WindowManager +import androidx.annotation.ChecksSdkIntAtLeast import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler + +private const val GYROSCOPE_CHANNEL_NAME = "com.simform.flutter_credit_card/gyroscope" +private const val METHOD_CHANNEL_NAME = "com.simform.flutter_credit_card" + +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R) +private val isAtLeastOsR: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R /** 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 fun setupEventChannels(context: Context, messenger: BinaryMessenger) { - val sensorsManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager - - gyroscopeChannel = EventChannel(messenger, GYROSCOPE_CHANNEL_NAME) - gyroScopeStreamHandler = com.simform.flutter_credit_card.StreamHandlerImpl( - sensorsManager, - Sensor.TYPE_GYROSCOPE, - ) - gyroscopeChannel.setStreamHandler(gyroScopeStreamHandler) - } - - private fun teardownEventChannels() { - gyroscopeChannel.setStreamHandler(null) - gyroScopeStreamHandler.onCancel(null) - } - - 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) - } +class FlutterCreditCardPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { + private var activity: Activity? = null + private lateinit var context: Context + + private lateinit var gyroscopeChannel: EventChannel + private lateinit var methodChannel: MethodChannel + private lateinit var gyroscopeStreamHandler: GyroscopeStreamHandler + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + context = binding.applicationContext + initializeChannels(binding.binaryMessenger) } - } - private fun teardownMethodChannel() { - methodChannel.setMethodCallHandler(null) - } + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) = + disposeChannels() + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = if (isAtLeastOsR) binding.activity else null + } + + override fun onDetachedFromActivity() { + activity = null + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = if (isAtLeastOsR) binding.activity else null + } + + override fun onDetachedFromActivityForConfigChanges() { + activity = null + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "initiateEvents" -> { + gyroEventChannelSetup() + result.success(null) + } + + "isGyroscopeAvailable" -> result.success(hasGyroAvailability()) + + "cancelEvents" -> { + gyroEventChannelDisposal() + result.success(null) + } + + else -> result.notImplemented() + } + } + + private fun initializeChannels(messenger: BinaryMessenger) { + gyroscopeChannel = EventChannel(messenger, GYROSCOPE_CHANNEL_NAME) + gyroEventChannelSetup() + methodChannel = MethodChannel(messenger, METHOD_CHANNEL_NAME) + methodChannel.setMethodCallHandler(this) + } + + private fun gyroEventChannelSetup() { + if (!hasGyroAvailability()) return + + val sensorsManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + + gyroscopeStreamHandler = GyroscopeStreamHandler(getViewDisplay(), sensorsManager) + gyroscopeChannel.setStreamHandler(gyroscopeStreamHandler) + } + + private fun gyroEventChannelDisposal() { + gyroscopeStreamHandler.onCancel(null) + gyroscopeChannel.setStreamHandler(null) + } + + private fun disposeChannels() { + gyroEventChannelDisposal() + methodChannel.setMethodCallHandler(null) + activity = null + } + + private fun getViewDisplay(): Display? { + if (isAtLeastOsR) { + return activity?.display + } else { + @Suppress("DEPRECATION") + return (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + } + } - 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 hasGyroAvailability(): Boolean = + context.packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_GYROSCOPE) } diff --git a/android/src/main/kotlin/com/simform/flutter_credit_card/GyroscopeStreamHandler.kt b/android/src/main/kotlin/com/simform/flutter_credit_card/GyroscopeStreamHandler.kt new file mode 100644 index 0000000..12d99fc --- /dev/null +++ b/android/src/main/kotlin/com/simform/flutter_credit_card/GyroscopeStreamHandler.kt @@ -0,0 +1,73 @@ +package com.simform.flutter_credit_card + +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.view.Display +import android.view.Surface +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink + +internal class GyroscopeStreamHandler( + private val display: Display?, + private val sensorManager: SensorManager, +) : EventChannel.StreamHandler { + private var sensorEventListener: SensorEventListener? = null + + private val sensor: Sensor by lazy { + sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + } + + override fun onListen(arguments: Any?, events: EventSink) { + sensorEventListener = createSensorEventListener(events) + // Gyroscope Event sample period set at 60 fps, specified in microseconds. + sensorManager.registerListener(sensorEventListener, sensor, 16666) + } + + override fun onCancel(arguments: Any?) = sensorManager.unregisterListener(sensorEventListener) + + private fun createSensorEventListener(events: EventSink): SensorEventListener { + return object : SensorEventListener { + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} + + override fun onSensorChanged(event: SensorEvent) = + events.success(processForOrientation(event.values)) + } + } + + private fun processForOrientation(values: FloatArray): DoubleArray { + if (display == null) { + return values.map { it.toDouble() }.toDoubleArray() + } else { + val arr = DoubleArray(3) + + val x = values[0].toDouble() + val y = values[1].toDouble() + val z = values[2].toDouble() + + when (display.rotation) { + Surface.ROTATION_0, + Surface.ROTATION_180 -> { + arr[0] = x + arr[1] = y + arr[2] = z + } + + Surface.ROTATION_270 -> { + arr[0] = y + arr[1] = -x + arr[2] = z + } + + Surface.ROTATION_90 -> { + arr[0] = -y + arr[1] = x + arr[2] = z + } + } + + return arr + } + } +} diff --git a/android/src/main/kotlin/com/simform/flutter_credit_card/StreamHandlerImpl.kt b/android/src/main/kotlin/com/simform/flutter_credit_card/StreamHandlerImpl.kt deleted file mode 100644 index cea6f0c..0000000 --- a/android/src/main/kotlin/com/simform/flutter_credit_card/StreamHandlerImpl.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.simform.flutter_credit_card - -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -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, -) : EventChannel.StreamHandler { - private var sensorEventListener: SensorEventListener? = null - - private val sensor: Sensor by lazy { - sensorManager.getDefaultSensor(sensorType) - } - - override fun onListen(arguments: Any?, events: EventSink) { - sensorEventListener = createSensorEventListener(events) - sensorManager.registerListener(sensorEventListener, sensor, 16666) - } - - override fun onCancel(arguments: Any?) { - sensorManager.unregisterListener(sensorEventListener) - } - - private fun createSensorEventListener(events: EventSink): SensorEventListener { - return object : SensorEventListener { - 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() - } - 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/.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/lib/main.dart b/example/lib/main.dart index ba97acd..bc449b5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,19 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_credit_card/credit_card_brand.dart'; import 'package:flutter_credit_card/flutter_credit_card.dart'; -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - - await CreditCardWidget.instance.initialize(); - - runApp(MySample()); -} +void main() => 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 { @@ -26,20 +20,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( @@ -65,9 +54,12 @@ class MySampleState extends State { height: 30, ), CreditCardWidget( - isFloatingAnimationEnabled: true, - isGlareAnimationEnabled: true, - isShadowAnimationEnabled: true, + floatConfig: !useFloatingAnimation + ? null + : FloatConfig( + isGlareEnabled: true, + shadowConfig: FloatShadowConfig.preset(), + ), glassmorphismConfig: useGlassMorphism ? Glassmorphism.defaultConfig() : null, cardNumber: cardNumber, @@ -203,6 +195,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, ), @@ -254,16 +271,16 @@ class MySampleState extends State { } void _onValidate() { - if (formKey.currentState!.validate()) { + if (formKey.currentState?.validate() ?? false) { print('valid!'); } else { print('invalid!'); } } - void onCreditCardModelChange(CreditCardModel? creditCardModel) { + void onCreditCardModelChange(CreditCardModel creditCardModel) { 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..d8c521f 100644 --- a/ios/Classes/FlutterCreditCardPlugin.swift +++ b/ios/Classes/FlutterCreditCardPlugin.swift @@ -2,67 +2,92 @@ 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.gyroscopeChannel = FlutterEventChannel(name: gyroscopeStreamHandlerName, binaryMessenger: registrar.messenger()) + 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 "initiateEvents": + gyroEventSetup() + result(nil) case "isGyroscopeAvailable": - let avaialble = isGyroscopeAvailable() - result(avaialble) + result(hasGyroAvailability()) + case "cancelEvents": + gyroEventDisposal() + result(nil) default: result(FlutterMethodNotImplemented) } } -} + + private func hasGyroAvailability() -> Bool { + return motionManager?.isGyroAvailable ?? false + } + + private func gyroEventSetup() { + motionManager = CMMotionManager() + if (!hasGyroAvailability()) { + motionManager = nil + return + } -public class MTGyroscopeStreamHandler: NSObject, FlutterStreamHandler { + gyroscopeStreamHandler = gyroscopeStreamHandler ?? GyroscopeStreamHandler(motionManager: motionManager!) + gyroscopeChannel?.setStreamHandler(gyroscopeStreamHandler) + } + private func gyroEventDisposal() { + _ = gyroscopeStreamHandler?.onCancel(withArguments: nil) + gyroscopeChannel?.setStreamHandler(nil) + gyroscopeStreamHandler = nil + motionManager = nil + } +} + +public class GyroscopeStreamHandler: NSObject, FlutterStreamHandler { + private var motionManager: CMMotionManager? + + init(motionManager: CMMotionManager) { + self.motionManager = motionManager + // Gyroscope event interval set to 60 fps, specified in seconds. + self.motionManager!.gyroUpdateInterval = 0.016666 + 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(GyroscopeStreamHandler.processForOrientation(rotationRate)) } } - 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 } + static private func processForOrientation(_ rotation: CMRotationRate) -> [Double] { + switch UIDevice.current.orientation { + case UIDeviceOrientation.landscapeLeft: + return [-rotation.y, rotation.x, rotation.z] + case UIDeviceOrientation.landscapeRight: + return [rotation.y, -rotation.x, rotation.z] + default: + return [rotation.x, rotation.y, rotation.z] + } + } } - diff --git a/ios/flutter_credit_card.podspec b/ios/flutter_credit_card.podspec index 29d928c..074e827 100644 --- a/ios/flutter_credit_card.podspec +++ b/ios/flutter_credit_card.podspec @@ -5,14 +5,15 @@ Pod::Spec.new do |s| s.name = 'flutter_credit_card' s.version = '0.0.1' - s.summary = 'A new Flutter plugin project.' + s.summary = 'Flutter Credit Card Widget Plugin Package' s.description = <<-DESC -A new Flutter plugin project. +A Credit Card widget package with support of entering card details, and animations like card flip +and float. DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } + s.homepage = 'https://github.com/simformsolutions/flutter_credit_card' + s.license = { :type => 'MIT', :file => '../LICENSE' } + s.author = { 'Simform Solutions' => 'developer@simform.com' } + s.source = { :http => 'https://github.com/SimformSolutionsPvtLtd/flutter_credit_card/tree/master' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.platform = :ios, '11.0' 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..3b00b77 100644 --- a/lib/credit_card_background.dart +++ b/lib/credit_card_background.dart @@ -1,33 +1,35 @@ 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/float_config.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); + this.shadowConfig, + 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; @@ -40,60 +42,50 @@ class CardBackground extends StatelessWidget { final BoxBorder? border; final FloatingController? floatingController; final double? glarePosition; - final bool shadowEnabled; + final FloatShadowConfig? shadowConfig; @override Widget build(BuildContext context) { - final Orientation orientation = MediaQuery.of(context).orientation; + final MediaQueryData mediaQueryData = MediaQuery.of(context); + final Orientation orientation = mediaQueryData.orientation; + final Size screenSize = mediaQueryData.size; return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final double screenWidth = constraints.maxWidth.isInfinite - ? MediaQuery.of(context).size.width + ? screenSize.width : constraints.maxWidth; - final double screenHeight = MediaQuery.of(context).size.height; + final double screenHeight = screenSize.height; 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: shadowConfig != null && floatingController != null + ? [ + BoxShadow( + blurRadius: shadowConfig!.blurRadius, + color: shadowConfig!.color, + offset: shadowConfig!.offset + + Offset( + floatingController!.y * 100, + -floatingController!.x * 100, + ), + ), + ] + : 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 +100,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..8c589e2 100644 --- a/lib/credit_card_widget.dart +++ b/lib/credit_card_widget.dart @@ -2,17 +2,18 @@ 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/float_config.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 +30,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 +55,14 @@ 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.floatConfig, + super.key, + }); /// A string indicating number on the card. final String cardNumber; @@ -168,14 +167,9 @@ class CreditCardWidget extends StatefulWidget { /// Provides border at back of credit card widget. final BoxBorder? backCardBorder; - final bool isFloatingAnimationEnabled; - - static final FlutterCreditCardPlatform instance = - FlutterCreditCardPlatform.instance; - - final bool isShadowAnimationEnabled; - - final bool isGlareAnimationEnabled; + /// The config for making the card float as per the movement of device or + /// mouse pointer. + final FloatConfig? floatConfig; /// floating animation enabled/disabled @override @@ -183,38 +177,46 @@ class CreditCardWidget extends StatefulWidget { } class _CreditCardWidgetState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, WidgetsBindingObserver { late AnimationController controller; 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; + late bool isFloatingAnimationEnabled = widget.floatConfig != null; - FloatingController get backFloatingController => - FloatingController.defaultController; + final FloatingController floatController = FloatingController.predefined(); - StreamController frontCardStreamController = + final StreamController frontCardFloatStream = StreamController.broadcast(); - StreamController backCardStreamController = + final StreamController backCardFloatStream = StreamController.broadcast(); Orientation? orientation; + Size? screenSize; - bool isAnimation = false; + /// Gives the radians pivoting opposite to the device movement with a center + /// anchor point. + double? get glarePosition => isFloatingAnimationEnabled && + (widget.floatConfig?.isGlareEnabled ?? false) + ? pi / 4 + (floatController.y / floatController.maximumAngle * (2 * pi)) + : null; - double get glarePosition => - pi / 4 + (frontFloatingController.y / (pi / 10) * (2 * pi)); + /// Represents the current floating card animation stream as per the + /// visibility of the front or back side of the credit card. + /// Determined based on [isFrontVisible]. + StreamController get floatingCardStream => + isFrontVisible ? frontCardFloatStream : backCardFloatStream; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); ///initialize the animation controller controller = AnimationController( @@ -222,35 +224,16 @@ class _CreditCardWidgetState extends State vsync: this, ); - controller.addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.forward) { - isAnimation = true; - } else if (status == AnimationStatus.reverse) { - isAnimation = true; - } else if (status == AnimationStatus.completed) { - isAnimation = false; - } else { - isAnimation = false; - } - }); - - if (widget.isFloatingAnimationEnabled) { - CreditCardWidget.instance.floatingStream?.listen((FloatingEvent event) { - if (isFrontVisible) { - frontCardStreamController.add(event); - } else { - backCardStreamController.add(event); - } - }); - } - + _handleFloatingAnimationSetup(); _gradientSetup(); _updateRotations(false); } @override void didChangeDependencies() { - orientation = MediaQuery.of(context).orientation; + final MediaQueryData mediaQuery = MediaQuery.of(context); + orientation = mediaQuery.orientation; + screenSize = mediaQuery.size; super.didChangeDependencies(); } @@ -259,73 +242,25 @@ 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 (oldWidget.floatConfig != widget.floatConfig) { + _handleFloatingAnimationSetup(); } - 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; + super.didUpdateWidget(oldWidget); } - Matrix4 computeTransformForEvent(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.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), - ); + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.inactive: + _handleFloatingAnimationSetup(shouldCancel: true); + break; + case AppLifecycleState.resumed: + _handleFloatingAnimationSetup(); + break; + default: + break; } - - return matrix; + super.didChangeAppLifecycleState(state); } void _gradientSetup() { @@ -346,40 +281,33 @@ class _CreditCardWidgetState extends State @override void dispose() { + FlutterCreditCardPlatform.instance.dispose(); controller.dispose(); - backCardStreamController.close(); - frontCardStreamController.close(); + backCardFloatStream.close(); + frontCardFloatStream.close(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } @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)); - - DateTime? lastPointerEventTime; + final CreditCardBrand cardBrand = CreditCardBrand( + widget.cardType ?? detectCCType(widget.cardNumber), + ); + widget.onCreditCardWidgetChange(cardBrand); - final Widget child = Stack( + return Stack( children: [ _cardGesture( child: AnimationCard( @@ -393,75 +321,71 @@ class _CreditCardWidgetState extends State child: _buildBackContainer(), ), ), + if (isFloatingAnimationEnabled && + !FlutterCreditCardPlatform.instance.isGyroscopeAvailable) + 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: _processFloatingEvent, + 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); - } + void _processFloatingEvent(FloatingEvent? event) { + if (event == null || controller.isAnimating) { + return; + } - void _rightRotation() { - _toggleSide(true); + floatingCardStream.add(event); } - 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 +395,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 +444,16 @@ class _CreditCardWidgetState extends State stripped.substring(stripped.length - 4); } } - if (widget.isFloatingAnimationEnabled && isFrontVisible) + + if (isFloatingAnimationEnabled && isFrontVisible) { return StreamBuilder( - stream: frontCardStreamController.stream, + stream: frontCardFloatStream.stream, builder: (BuildContext context, AsyncSnapshot snapshot) { return Transform( - transform: computeTransformForEvent(snapshot.data), - filterQuality: FilterQuality.high, + transform: floatController.transform( + snapshot.data, + shouldAvoid: controller.isAnimating, + ), alignment: FractionalOffset.center, child: _frontCardBackground( defaultTextStyle: defaultTextStyle, @@ -538,11 +462,12 @@ class _CreditCardWidgetState extends State ); }, ); - else + } else { return _frontCardBackground( defaultTextStyle: defaultTextStyle, number: number, ); + } } Widget _frontCardBackground({ @@ -550,12 +475,8 @@ class _CreditCardWidgetState extends State required TextStyle defaultTextStyle, }) { return CardBackground( - glarePosition: - widget.isGlareAnimationEnabled && widget.isFloatingAnimationEnabled - ? glarePosition - : null, - floatingController: - widget.isFloatingAnimationEnabled ? frontFloatingController : null, + glarePosition: glarePosition, + floatingController: isFloatingAnimationEnabled ? floatController : null, backgroundImage: widget.backgroundImage, backgroundNetworkImage: widget.backgroundNetworkImage, backgroundGradientColor: backgroundGradientColor, @@ -564,6 +485,8 @@ class _CreditCardWidgetState extends State width: widget.width, padding: widget.padding, border: widget.frontCardBorder, + shadowConfig: + isFloatingAnimationEnabled ? widget.floatConfig?.shadowConfig : null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -681,20 +604,24 @@ class _CreditCardWidgetState extends State ? widget.cvvCode.replaceAll(RegExp(r'\d'), '*') : widget.cvvCode; - return widget.isFloatingAnimationEnabled + return isFloatingAnimationEnabled && !isFrontVisible ? StreamBuilder( - stream: backCardStreamController.stream, + stream: backCardFloatStream.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: floatController.transform( + snapshot.data, + shouldAvoid: controller.isAnimating, + ), + alignment: FractionalOffset.center, + child: _backCardBackground( + cvv: cvv, + defaultTextStyle: defaultTextStyle, + ), + ); + }, + ) : _backCardBackground( cvv: cvv, defaultTextStyle: defaultTextStyle, @@ -706,12 +633,8 @@ class _CreditCardWidgetState extends State required TextStyle defaultTextStyle, }) { return CardBackground( - glarePosition: - widget.isGlareAnimationEnabled && widget.isFloatingAnimationEnabled - ? glarePosition - : null, - floatingController: - widget.isFloatingAnimationEnabled ? backFloatingController : null, + glarePosition: glarePosition, + floatingController: isFloatingAnimationEnabled ? floatController : null, backgroundImage: widget.backgroundImage, backgroundNetworkImage: widget.backgroundNetworkImage, backgroundGradientColor: backgroundGradientColor, @@ -720,6 +643,8 @@ class _CreditCardWidgetState extends State width: widget.width, padding: widget.padding, border: widget.backCardBorder, + shadowConfig: + isFloatingAnimationEnabled ? widget.floatConfig?.shadowConfig : null, child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, @@ -791,11 +716,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 +734,26 @@ class _CreditCardWidgetState extends State : child; } + void _handleFloatingAnimationSetup({bool? shouldCancel}) { + isFloatingAnimationEnabled = widget.floatConfig != null; + + if (shouldCancel ?? !isFloatingAnimationEnabled) { + FlutterCreditCardPlatform.instance.dispose(); + return; + } + + FlutterCreditCardPlatform.instance.initialize().then((_) { + final bool isGyroAvailable = + FlutterCreditCardPlatform.instance.isGyroscopeAvailable; + floatController..isGyroscopeAvailable = isGyroAvailable; + + if (isGyroAvailable) { + FlutterCreditCardPlatform.instance.floatingStream + ?.listen(_processFloatingEvent); + } + }); + } + /// 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/float_config.dart b/lib/floating_animation/float_config.dart new file mode 100644 index 0000000..952cfa3 --- /dev/null +++ b/lib/floating_animation/float_config.dart @@ -0,0 +1,61 @@ +import 'dart:ui'; + +import 'package:flutter_credit_card/constants.dart'; + +class FloatConfig { + /// Configuration for making the card float as per the movement of device or + /// mouse pointer. + const FloatConfig({ + this.isGlareEnabled = true, + this.shadowConfig, + }); + + /// The configuration for adding a shadow beneath the card. + final FloatShadowConfig? shadowConfig; + + /// Denotes whether to add a glare - a shinning effect - over the card. + final bool isGlareEnabled; + + @override + bool operator ==(Object other) { + return other is FloatConfig && + other.isGlareEnabled == isGlareEnabled && + other.shadowConfig == shadowConfig; + } + + @override + int get hashCode => Object.hash(isGlareEnabled, shadowConfig); +} + +class FloatShadowConfig { + /// Configuration for the shadow appearing beneath the card when floating + /// animation is enabled via [FloatConfig]. + const FloatShadowConfig({ + required this.offset, + required this.color, + required this.blurRadius, + }); + + /// Preset configuration for the shadow appearing beneath the card. + FloatShadowConfig.preset() + : offset = const Offset(0, 8), + blurRadius = AppConstants.minBlurRadius, + color = AppConstants.defaultShadowColor + .withOpacity(AppConstants.minShadowOpacity); + + /// The offset of shadow from the card. + final Offset offset; + final double blurRadius; + final Color color; + + @override + bool operator ==(Object other) { + return other is FloatShadowConfig && + other.color == color && + other.blurRadius == blurRadius && + other.offset == offset; + } + + @override + int get hashCode => Object.hash(offset, color, blurRadius); +} diff --git a/lib/floating_animation/floating_controller.dart b/lib/floating_animation/floating_controller.dart new file mode 100644 index 0000000..ce81a34 --- /dev/null +++ b/lib/floating_animation/floating_controller.dart @@ -0,0 +1,94 @@ +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() + : restBackVelocity = AppConstants.defaultRestBackVel, + maximumAngle = AppConstants.defaultMaximumAngle, + isGyroscopeAvailable = false; + + 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 57b2523..607dcbe 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.dart b/lib/flutter_credit_card.dart index ebdd307..e27a21b 100644 --- a/lib/flutter_credit_card.dart +++ b/lib/flutter_credit_card.dart @@ -4,4 +4,5 @@ export 'credit_card_form.dart'; export 'credit_card_model.dart'; export 'credit_card_widget.dart'; export 'custom_card_type_icon.dart'; +export 'floating_animation/float_config.dart'; export 'glassmorphism_config.dart'; diff --git a/lib/flutter_credit_card_method_channel.dart b/lib/flutter_credit_card_method_channel.dart index 8557698..2dd09ff 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; + static bool _isGyroscopeAvailable = false; @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; } @@ -48,25 +46,35 @@ 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); + _gyroscopeEventChannel ??= const EventChannel(_eventChannelName); + + await initiateEvents(); + if (Platform.isIOS || Platform.isAndroid) { _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. + } else { + // Other platforms should not use the gyroscope events. _isGyroscopeAvailable = false; } - - return; } + @override + Future initiateEvents() async => + _methodChannel?.invokeMethod('initiateEvents'); + + @override + Future cancelEvents() async => + _methodChannel?.invokeMethod('cancelEvents'); @override - Future requestPermission() async => true; + Future dispose() async { + _isGyroscopeAvailable = false; + _gyroscopeStream = null; + _gyroscopeEventChannel = null; + await cancelEvents(); + _methodChannel = null; + } } diff --git a/lib/flutter_credit_card_platform_interface.dart b/lib/flutter_credit_card_platform_interface.dart index c1e1014..1920239 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,29 +24,21 @@ abstract class FlutterCreditCardPlatform extends PlatformInterface { _instance = instance; } - /// 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. + /// Denotes gyroscope feature availability. bool get isGyroscopeAvailable => false; - /// Indicates whether a permission is required to access gyroscope data. - bool get isPermissionRequired => false; + /// The stream having gyroscope data events, if available. + Stream? get floatingStream => null; - /// Indicates whether the permission is granted. - bool get isPermissionGranted => false; + /// Initializes the method and event channels. + Future initialize() async => throw UnimplementedError(); - /// The gyroscope stream, if available. - Stream? get floatingStream => null; + /// Initiates the gyroscope data events. + Future initiateEvents() async => throw UnimplementedError(); - Future initialize() async { - throw UnimplementedError(); - } + /// Cancels the gyroscope data events. + Future cancelEvents() async => throw UnimplementedError(); - Future requestPermission() async { - throw UnimplementedError(); - } + /// Disposes the method and event channels. + Future dispose() async => throw UnimplementedError(); } diff --git a/lib/flutter_credit_card_web.dart b/lib/flutter_credit_card_web.dart index 83dc67c..d25cc6d 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,21 @@ 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; - } - }, - ), - ); + Stream? get floatingStream => null; - 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(); - } + @override + Future initialize() async {} - return _gyroscopeStream; - } + @override + Future initiateEvents() async {} @override - Future initialize() async { - return; - } + Future cancelEvents() 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..43a8521 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,18 @@ name: flutter_credit_card -description: A Credit Card widget package, support entering card details, card flip animation. +description: A Credit Card widget package with support of entering card details, and animations like card flip + and float. version: 3.0.7 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: