Fix android and update example

This commit is contained in:
2026-01-13 22:44:49 -08:00
parent 9126b79873
commit b0736478cd
5 changed files with 494 additions and 140 deletions
@@ -12,6 +12,7 @@ class VibrateMethodCallHandler implements MethodChannel.MethodCallHandler {
private final Vibrator vibrator; private final Vibrator vibrator;
private final boolean hasVibrator; private final boolean hasVibrator;
private final boolean legacyVibrator; private final boolean legacyVibrator;
private android.app.Activity activity;
VibrateMethodCallHandler(Vibrator vibrator) { VibrateMethodCallHandler(Vibrator vibrator) {
assert (vibrator != null); assert (vibrator != null);
@@ -20,6 +21,10 @@ class VibrateMethodCallHandler implements MethodChannel.MethodCallHandler {
this.legacyVibrator = Build.VERSION.SDK_INT < 26; this.legacyVibrator = Build.VERSION.SDK_INT < 26;
} }
public void setActivity(android.app.Activity activity) {
this.activity = activity;
}
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void vibrate(int duration) { private void vibrate(int duration) {
if (hasVibrator) { 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 @Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) { public void onMethodCall(MethodCall call, MethodChannel.Result result) {
switch (call.method) { switch (call.method) {
@@ -39,39 +57,51 @@ class VibrateMethodCallHandler implements MethodChannel.MethodCallHandler {
break; break;
case "vibrate": case "vibrate":
final int duration = call.argument("duration"); final int duration = call.argument("duration");
vibrate(duration); vibrate(d);
result.success(null); result.success(null);
break; break;
case "impact": case "impact":
vibrate(HapticFeedbackConstants.VIRTUAL_KEY); feedback(HapticFeedbackConstants.VIRTUAL_KEY);
result.success(null); result.success(null);
break; break;
case "selection": case "selection":
vibrate(HapticFeedbackConstants.KEYBOARD_TAP); feedback(HapticFeedbackConstants.KEYBOARD_TAP);
result.success(null); result.success(null);
break; break;
case "success": case "success":
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
feedback(HapticFeedbackConstants.CONFIRM);
} else {
vibrate(50); vibrate(50);
}
result.success(null); result.success(null);
break; break;
case "warning": case "warning":
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
feedback(HapticFeedbackConstants.REJECT);
} else {
vibrate(250); vibrate(250);
}
result.success(null); result.success(null);
break; break;
case "error": case "error":
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
feedback(HapticFeedbackConstants.REJECT);
} else {
vibrate(500); vibrate(500);
}
result.success(null); result.success(null);
break; break;
case "heavy": case "heavy":
vibrate(100); feedback(HapticFeedbackConstants.LONG_PRESS);
result.success(null); result.success(null);
break; break;
case "medium": case "medium":
vibrate(40); feedback(HapticFeedbackConstants.VIRTUAL_KEY);
result.success(null); result.success(null);
break; break;
case "light": case "light":
vibrate(10); feedback(HapticFeedbackConstants.CLOCK_TICK);
result.success(null); result.success(null);
break; break;
default: default:
@@ -7,15 +7,16 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodChannel; 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 MethodChannel methodChannel;
private VibrateMethodCallHandler methodCallHandler;
@Override @Override
public void onAttachedToEngine(FlutterPluginBinding binding) { public void onAttachedToEngine(FlutterPluginBinding binding) {
final Context context = binding.getApplicationContext(); final Context context = binding.getApplicationContext();
final BinaryMessenger messenger = binding.getBinaryMessenger(); final BinaryMessenger messenger = binding.getBinaryMessenger();
final Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); 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 = new MethodChannel(messenger, "vibrate");
this.methodChannel.setMethodCallHandler(methodCallHandler); this.methodChannel.setMethodCallHandler(methodCallHandler);
@@ -25,5 +26,26 @@ public class VibratePlugin implements FlutterPlugin {
public void onDetachedFromEngine(FlutterPluginBinding binding) { public void onDetachedFromEngine(FlutterPluginBinding binding) {
this.methodChannel.setMethodCallHandler(null); this.methodChannel.setMethodCallHandler(null);
this.methodChannel = 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);
} }
} }
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.VIBRATE" />
<application <application
android:label="vibrate_example" android:label="vibrate_example"
android:name="${applicationName}" android:name="${applicationName}"
+424 -123
View File
@@ -1,142 +1,443 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart';
void main() => runApp(const MyApp()); void main() {
runApp(const VibrateExampleApp());
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
} }
class _MyAppState extends State<MyApp> { class VibrateExampleApp extends StatelessWidget {
bool _canVibrate = true; const VibrateExampleApp({super.key});
final Iterable<Duration> pauses = [
const Duration(milliseconds: 500),
const Duration(milliseconds: 1000),
const Duration(milliseconds: 500),
];
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
bool canVibrate = await Vibrate.canVibrate;
setState(() {
_canVibrate = canVibrate;
_canVibrate
? debugPrint('This device can vibrate')
: debugPrint('This device cannot vibrate');
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
home: Scaffold( title: 'Haptic Feedback',
appBar: AppBar(title: const Text('Haptic Feedback Example')), theme: ThemeData(
body: Center( useMaterial3: true,
child: ListView(children: [ colorScheme: ColorScheme.fromSeed(
ListTile( seedColor: Colors.deepPurple,
title: const Text('Vibrate'), brightness: Brightness.light,
leading: const Icon(Icons.vibration, color: Colors.teal),
onTap: () {
if (_canVibrate) Vibrate.vibrate;
},
), ),
ListTile( ),
title: const Text('Vibrate with Pauses'), darkTheme: ThemeData(
leading: const Icon(Icons.vibration, color: Colors.brown), useMaterial3: true,
onTap: () { colorScheme: ColorScheme.fromSeed(
if (_canVibrate) { seedColor: Colors.deepPurple,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: const HapticFeedbackDemo(),
);
}
}
class HapticFeedbackDemo extends StatefulWidget {
const HapticFeedbackDemo({super.key});
@override
State<HapticFeedbackDemo> createState() => _HapticFeedbackDemoState();
}
class _HapticFeedbackDemoState extends State<HapticFeedbackDemo> {
// We use a future to track initialization status
late final Future<bool> _initFuture;
bool _canVibrate = false;
@override
void initState() {
super.initState();
_initFuture = _init();
}
Future<bool> _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<bool>(
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); Vibrate.vibrateWithPauses(pauses);
} }
}, : null,
),
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);
}
},
), ),
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),
),
),
],
),
),
],
),
);
}
}
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<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
);
_scaleAnimation = Tween<double>(
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,
),
];
@@ -4,7 +4,7 @@ publish_to: none
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '^3.10.4'
dependencies: dependencies:
flutter: flutter:
@@ -15,7 +15,7 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^1.0.4 flutter_lints: ^5.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true