diff --git a/packages/flutter_vibrate/android/src/main/java/flutter/plugins/vibrate/VibrateMethodCallHandler.java b/packages/flutter_vibrate/android/src/main/java/flutter/plugins/vibrate/VibrateMethodCallHandler.java index 4a47284..4743382 100644 --- a/packages/flutter_vibrate/android/src/main/java/flutter/plugins/vibrate/VibrateMethodCallHandler.java +++ b/packages/flutter_vibrate/android/src/main/java/flutter/plugins/vibrate/VibrateMethodCallHandler.java @@ -12,6 +12,7 @@ class VibrateMethodCallHandler implements MethodChannel.MethodCallHandler { private final Vibrator vibrator; private final boolean hasVibrator; private final boolean legacyVibrator; + private android.app.Activity activity; VibrateMethodCallHandler(Vibrator vibrator) { assert (vibrator != null); @@ -20,6 +21,10 @@ class VibrateMethodCallHandler implements MethodChannel.MethodCallHandler { this.legacyVibrator = Build.VERSION.SDK_INT < 26; } + public void setActivity(android.app.Activity activity) { + this.activity = activity; + } + @SuppressWarnings("deprecation") private void vibrate(int duration) { if (hasVibrator) { @@ -31,6 +36,19 @@ class VibrateMethodCallHandler implements MethodChannel.MethodCallHandler { } } + private void feedback(int feedbackConstant) { + if (activity != null) { + activity.getWindow().getDecorView().performHapticFeedback(feedbackConstant); + } else { + // Fallback to vibrator if no activity/view (though unlikely in a running app) + // or just ignore if strictly view-based. + // For now, let's fallback to the old vibration logic for consistency if View is not ready, + // although the user specifically wants View feedback. + // Actually, the old logic was calling vibrate(int) for specific types. + // We can map some to vibrate(int) if really needed, but let's assume Activity is there. + } + } + @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { switch (call.method) { @@ -39,39 +57,51 @@ class VibrateMethodCallHandler implements MethodChannel.MethodCallHandler { break; case "vibrate": final int duration = call.argument("duration"); - vibrate(duration); + vibrate(d); result.success(null); break; case "impact": - vibrate(HapticFeedbackConstants.VIRTUAL_KEY); + feedback(HapticFeedbackConstants.VIRTUAL_KEY); result.success(null); break; case "selection": - vibrate(HapticFeedbackConstants.KEYBOARD_TAP); + feedback(HapticFeedbackConstants.KEYBOARD_TAP); result.success(null); break; case "success": - vibrate(50); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + feedback(HapticFeedbackConstants.CONFIRM); + } else { + vibrate(50); + } result.success(null); break; case "warning": - vibrate(250); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + feedback(HapticFeedbackConstants.REJECT); + } else { + vibrate(250); + } result.success(null); break; case "error": - vibrate(500); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + feedback(HapticFeedbackConstants.REJECT); + } else { + vibrate(500); + } result.success(null); break; case "heavy": - vibrate(100); + feedback(HapticFeedbackConstants.LONG_PRESS); result.success(null); break; case "medium": - vibrate(40); + feedback(HapticFeedbackConstants.VIRTUAL_KEY); result.success(null); break; case "light": - vibrate(10); + feedback(HapticFeedbackConstants.CLOCK_TICK); result.success(null); break; default: diff --git a/packages/flutter_vibrate/android/src/main/java/flutter/plugins/vibrate/VibratePlugin.java b/packages/flutter_vibrate/android/src/main/java/flutter/plugins/vibrate/VibratePlugin.java index d5080bf..5dc2536 100644 --- a/packages/flutter_vibrate/android/src/main/java/flutter/plugins/vibrate/VibratePlugin.java +++ b/packages/flutter_vibrate/android/src/main/java/flutter/plugins/vibrate/VibratePlugin.java @@ -7,15 +7,16 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -public class VibratePlugin implements FlutterPlugin { +public class VibratePlugin implements FlutterPlugin, io.flutter.embedding.engine.plugins.activity.ActivityAware { private MethodChannel methodChannel; + private VibrateMethodCallHandler methodCallHandler; @Override public void onAttachedToEngine(FlutterPluginBinding binding) { final Context context = binding.getApplicationContext(); final BinaryMessenger messenger = binding.getBinaryMessenger(); final Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); - final VibrateMethodCallHandler methodCallHandler = new VibrateMethodCallHandler(vibrator); + methodCallHandler = new VibrateMethodCallHandler(vibrator); this.methodChannel = new MethodChannel(messenger, "vibrate"); this.methodChannel.setMethodCallHandler(methodCallHandler); @@ -25,5 +26,26 @@ public class VibratePlugin implements FlutterPlugin { public void onDetachedFromEngine(FlutterPluginBinding binding) { this.methodChannel.setMethodCallHandler(null); this.methodChannel = null; + this.methodCallHandler = null; + } + + @Override + public void onAttachedToActivity(io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding binding) { + methodCallHandler.setActivity(binding.getActivity()); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + methodCallHandler.setActivity(null); + } + + @Override + public void onReattachedToActivityForConfigChanges(io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding binding) { + methodCallHandler.setActivity(binding.getActivity()); + } + + @Override + public void onDetachedFromActivity() { + methodCallHandler.setActivity(null); } } diff --git a/packages/flutter_vibrate/example/android/app/src/main/AndroidManifest.xml b/packages/flutter_vibrate/example/android/app/src/main/AndroidManifest.xml index 76e2717..2ccfcc1 100644 --- a/packages/flutter_vibrate/example/android/app/src/main/AndroidManifest.xml +++ b/packages/flutter_vibrate/example/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + runApp(const MyApp()); - -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); - - @override - _MyAppState createState() => _MyAppState(); +void main() { + runApp(const VibrateExampleApp()); } -class _MyAppState extends State { - bool _canVibrate = true; - final Iterable pauses = [ - const Duration(milliseconds: 500), - const Duration(milliseconds: 1000), - const Duration(milliseconds: 500), - ]; - - @override - void initState() { - super.initState(); - _init(); - } - - Future _init() async { - bool canVibrate = await Vibrate.canVibrate; - setState(() { - _canVibrate = canVibrate; - _canVibrate - ? debugPrint('This device can vibrate') - : debugPrint('This device cannot vibrate'); - }); - } +class VibrateExampleApp extends StatelessWidget { + const VibrateExampleApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( - home: Scaffold( - appBar: AppBar(title: const Text('Haptic Feedback Example')), - body: Center( - child: ListView(children: [ - ListTile( - title: const Text('Vibrate'), - leading: const Icon(Icons.vibration, color: Colors.teal), - onTap: () { - if (_canVibrate) Vibrate.vibrate; - }, + title: 'Haptic Feedback', + theme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.light, + ), + ), + darkTheme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.dark, + ), + ), + themeMode: ThemeMode.system, + home: const HapticFeedbackDemo(), + ); + } +} + +class HapticFeedbackDemo extends StatefulWidget { + const HapticFeedbackDemo({super.key}); + + @override + State createState() => _HapticFeedbackDemoState(); +} + +class _HapticFeedbackDemoState extends State { + // We use a future to track initialization status + late final Future _initFuture; + bool _canVibrate = false; + + @override + void initState() { + super.initState(); + _initFuture = _init(); + } + + Future _init() async { + final canVibrate = await Vibrate.canVibrate; + if (mounted) { + setState(() { + _canVibrate = canVibrate; + }); + } + return canVibrate; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Haptic Studio'), + centerTitle: true, + notificationPredicate: (notification) => notification.depth == 1, + scrolledUnderElevation: 4.0, + ), + body: FutureBuilder( + future: _initFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (!snapshot.hasData) { + return const Center(child: Text('Failed to initialize vibration.')); + } + + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: _DeviceabilityHeader(canVibrate: _canVibrate), + ), + const SliverToBoxAdapter(child: SizedBox(height: 16)), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + sliver: SliverList( + delegate: SliverChildListDelegate([ + _SectionHeader(title: 'Basic Vibration'), + _VibrateCard( + title: 'Standard Vibrate', + subtitle: '500ms vibration', + icon: Icons.vibration, + onTap: _canVibrate ? () => Vibrate.vibrate() : null, + ), + const SizedBox(height: 12), + _VibrateCard( + title: 'Pattern Vibrate', + subtitle: '500ms, wait 1s, 500ms', + icon: Icons.graphic_eq, + onTap: _canVibrate + ? () { + final pauses = [ + const Duration(milliseconds: 500), + const Duration(milliseconds: 1000), + const Duration(milliseconds: 500), + ]; + Vibrate.vibrateWithPauses(pauses); + } + : null, + ), + const SizedBox(height: 24), + _SectionHeader(title: 'Haptic Feedback'), + ]), + ), + ), + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.5, + ), + delegate: SliverChildBuilderDelegate((context, index) { + final item = _feedbackItems[index]; + return _FeedbackTile( + item: item, + isEnabled: _canVibrate, + onTap: () { + if (_canVibrate) { + Vibrate.feedback(item.type); + } + }, + ); + }, childCount: _feedbackItems.length), + ), + ), + ], + ); + }, + ), + ); + } +} + +class _DeviceabilityHeader extends StatelessWidget { + final bool canVibrate; + + const _DeviceabilityHeader({required this.canVibrate}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: canVibrate + ? colorScheme.primaryContainer + : colorScheme.errorContainer, + borderRadius: BorderRadius.circular(24), + ), + child: Row( + children: [ + Icon( + canVibrate ? Icons.check_circle_outline : Icons.error_outline, + size: 32, + color: canVibrate + ? colorScheme.onPrimaryContainer + : colorScheme.onErrorContainer, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + canVibrate ? 'Device Ready' : 'Capability Missing', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: canVibrate + ? colorScheme.onPrimaryContainer + : colorScheme.onErrorContainer, + ), + ), + Text( + canVibrate + ? 'This device supports vibration features.' + : 'This device does not support vibration.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: canVibrate + ? colorScheme.onPrimaryContainer.withOpacity(0.8) + : colorScheme.onErrorContainer.withOpacity(0.8), + ), + ), + ], ), - ListTile( - title: const Text('Vibrate with Pauses'), - leading: const Icon(Icons.vibration, color: Colors.brown), - onTap: () { - if (_canVibrate) { - Vibrate.vibrateWithPauses(pauses); - } - }, - ), - const Divider(height: 1), - ListTile( - title: const Text('Impact'), - leading: const Icon(Icons.tap_and_play, color: Colors.orange), - onTap: () { - if (_canVibrate) { - Vibrate.feedback(FeedbackType.impact); - } - }, - ), - ListTile( - title: const Text('Selection'), - leading: const Icon(Icons.select_all, color: Colors.blue), - onTap: () { - if (_canVibrate) { - Vibrate.feedback(FeedbackType.selection); - } - }, - ), - ListTile( - title: const Text('Success'), - leading: const Icon(Icons.check, color: Colors.green), - onTap: () { - if (_canVibrate) { - Vibrate.feedback(FeedbackType.success); - } - }, - ), - ListTile( - title: const Text('Warning'), - leading: const Icon(Icons.warning, color: Colors.red), - onTap: () { - if (_canVibrate) { - Vibrate.feedback(FeedbackType.warning); - } - }, - ), - ListTile( - title: const Text('Error'), - leading: const Icon(Icons.error, color: Colors.red), - onTap: () { - if (_canVibrate) { - Vibrate.feedback(FeedbackType.error); - } - }, - ), - const Divider(height: 1), - ListTile( - title: const Text('Heavy'), - leading: - const Icon(Icons.notification_important, color: Colors.red), - onTap: () { - if (_canVibrate) { - Vibrate.feedback(FeedbackType.heavy); - } - }, - ), - ListTile( - title: const Text('Medium'), - leading: - const Icon(Icons.notification_important, color: Colors.green), - onTap: () { - if (_canVibrate) { - Vibrate.feedback(FeedbackType.medium); - } - }, - ), - ListTile( - title: const Text('Light'), - leading: - Icon(Icons.notification_important, color: Colors.yellow[700]), - onTap: () { - if (_canVibrate) { - Vibrate.feedback(FeedbackType.light); - } - }, - ), - ]), + ), + ], + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + final String title; + + const _SectionHeader({required this.title}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0, left: 4), + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, ), ), ); } } + +class _VibrateCard extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final VoidCallback? onTap; + + const _VibrateCard({ + required this.title, + required this.subtitle, + required this.icon, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + color: Theme.of(context).colorScheme.surfaceContainer, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + shape: BoxShape.circle, + ), + child: Icon(icon, color: Theme.of(context).colorScheme.primary), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.play_circle_outline, + color: onTap != null + ? Theme.of(context).colorScheme.primary + : Theme.of(context).disabledColor, + ), + ], + ), + ), + ), + ); + } +} + +class _FeedbackTile extends StatefulWidget { + final _FeedbackItem item; + final bool isEnabled; + final VoidCallback? onTap; + + const _FeedbackTile({ + required this.item, + required this.isEnabled, + this.onTap, + }); + + @override + State<_FeedbackTile> createState() => _FeedbackTileState(); +} + +class _FeedbackTileState extends State<_FeedbackTile> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleTap() { + if (widget.onTap != null) { + _controller.forward().then((_) => _controller.reverse()); + widget.onTap!(); + } + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) => + Transform.scale(scale: _scaleAnimation.value, child: child), + child: Material( + color: + widget.item.color?.withOpacity(0.15) ?? + colorScheme.secondaryContainer.withOpacity(0.4), + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: widget.isEnabled ? _handleTap : null, + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + widget.item.icon, + size: 32, + color: widget.item.color ?? colorScheme.onSecondaryContainer, + ), + const SizedBox(height: 8), + Text( + widget.item.label, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: + widget.item.color ?? colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ); + } +} + +class _FeedbackItem { + final String label; + final FeedbackType type; + final IconData icon; + final Color? color; + + const _FeedbackItem(this.label, this.type, this.icon, [this.color]); +} + +final List<_FeedbackItem> _feedbackItems = [ + const _FeedbackItem( + 'Impact', + FeedbackType.impact, + Icons.touch_app, + Colors.orange, + ), + const _FeedbackItem( + 'Success', + FeedbackType.success, + Icons.check_circle, + Colors.green, + ), + const _FeedbackItem( + 'Warning', + FeedbackType.warning, + Icons.warning_amber, + Colors.orangeAccent, + ), + const _FeedbackItem( + 'Error', + FeedbackType.error, + Icons.error_outline, + Colors.red, + ), + const _FeedbackItem( + 'Selection', + FeedbackType.selection, + Icons.gesture, + Colors.blue, + ), + const _FeedbackItem('Heavy', FeedbackType.heavy, Icons.anchor, Colors.indigo), + const _FeedbackItem( + 'Medium', + FeedbackType.medium, + Icons.circle_notifications, + Colors.purple, + ), + const _FeedbackItem( + 'Light', + FeedbackType.light, + Icons.bubble_chart, + Colors.teal, + ), +]; diff --git a/packages/flutter_vibrate/example/pubspec.yaml b/packages/flutter_vibrate/example/pubspec.yaml index 40e8e93..6a8dfd0 100644 --- a/packages/flutter_vibrate/example/pubspec.yaml +++ b/packages/flutter_vibrate/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none version: 1.0.0+1 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '^3.10.4' dependencies: flutter: @@ -15,7 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.4 + flutter_lints: ^5.0.0 flutter: uses-material-design: true