Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,19 @@ class HapticFeedbackPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
}

override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "canVibrate") {
canVibrate(result)
} else {
val pattern = Pattern.values().find { it.name == call.method }
if (pattern != null) {
val args = call.arguments as? Map<*, *>
val usage = Usage.fromArguments(args)
val useAndroidHapticConstants = (args?.get("useAndroidHapticConstants") as? Boolean) ?: false
vibratePattern(pattern, usage, useAndroidHapticConstants, result)
} else {
result.notImplemented()
when (call.method) {
"canVibrate" -> canVibrate(result)
"prepare" -> result.success(null)
else -> {
val pattern = Pattern.values().find { it.name == call.method }
if (pattern != null) {
val args = call.arguments as? Map<*, *>
val usage = Usage.fromArguments(args)
val useAndroidHapticConstants = (args?.get("useAndroidHapticConstants") as? Boolean) ?: false
vibratePattern(pattern, usage, useAndroidHapticConstants, result)
} else {
result.notImplemented()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
import CoreHaptics
import Flutter
import UIKit

public class HapticFeedbackPlugin: NSObject, FlutterPlugin {
private let notificationGenerator = UINotificationFeedbackGenerator()
private let lightImpactGenerator = UIImpactFeedbackGenerator(style: .light)
private let mediumImpactGenerator = UIImpactFeedbackGenerator(style: .medium)
private let heavyImpactGenerator = UIImpactFeedbackGenerator(style: .heavy)
private let rigidImpactGenerator: UIImpactFeedbackGenerator
private let softImpactGenerator: UIImpactFeedbackGenerator
private let selectionGenerator = UISelectionFeedbackGenerator()

override init() {
if #available(iOS 13.0, *) {
rigidImpactGenerator = UIImpactFeedbackGenerator(style: .rigid)
softImpactGenerator = UIImpactFeedbackGenerator(style: .soft)
} else {
rigidImpactGenerator = UIImpactFeedbackGenerator(style: .medium)
softImpactGenerator = UIImpactFeedbackGenerator(style: .light)
}
super.init()
}

public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "haptic_feedback", binaryMessenger: registrar.messenger())
let instance = HapticFeedbackPlugin()
Expand All @@ -12,32 +32,35 @@ public class HapticFeedbackPlugin: NSObject, FlutterPlugin {
switch call.method {
case "canVibrate":
canVibrate(result: result)
case "prepare":
prepare(type: call.arguments as? String, result: result)
case "success":
notification(type: .success, result: result)
notificationGenerator.notificationOccurred(.success)
result(nil)
case "warning":
notification(type: .warning, result: result)
notificationGenerator.notificationOccurred(.warning)
result(nil)
case "error":
notification(type: .error, result: result)
notificationGenerator.notificationOccurred(.error)
result(nil)
case "light":
impact(style: .light, result: result)
lightImpactGenerator.impactOccurred()
result(nil)
case "medium":
impact(style: .medium, result: result)
mediumImpactGenerator.impactOccurred()
result(nil)
case "heavy":
impact(style: .heavy, result: result)
heavyImpactGenerator.impactOccurred()
result(nil)
case "rigid":
if #available(iOS 13.0, *) {
impact(style: .rigid, result: result)
} else {
impact(style: .medium, result: result)
}
rigidImpactGenerator.impactOccurred()
result(nil)
case "soft":
if #available(iOS 13.0, *) {
impact(style: .soft, result: result)
} else {
impact(style: .light, result: result)
}
softImpactGenerator.impactOccurred()
result(nil)
case "selection":
selection(result: result)
selectionGenerator.selectionChanged()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
Expand All @@ -57,18 +80,25 @@ public class HapticFeedbackPlugin: NSObject, FlutterPlugin {
result(false)
}

private func notification(type: UINotificationFeedbackGenerator.FeedbackType, result: @escaping FlutterResult) {
UINotificationFeedbackGenerator().notificationOccurred(type)
result(nil)
}

private func impact(style: UIImpactFeedbackGenerator.FeedbackStyle, result: @escaping FlutterResult) {
UIImpactFeedbackGenerator(style: style).impactOccurred()
result(nil)
}

private func selection(result: @escaping FlutterResult) {
UISelectionFeedbackGenerator().selectionChanged()
private func prepare(type: String?, result: @escaping FlutterResult) {
switch type {
case "success", "warning", "error":
notificationGenerator.prepare()
case "light":
lightImpactGenerator.prepare()
case "medium":
mediumImpactGenerator.prepare()
case "heavy":
heavyImpactGenerator.prepare()
case "rigid":
rigidImpactGenerator.prepare()
case "soft":
softImpactGenerator.prepare()
case "selection":
selectionGenerator.prepare()
default:
break
}
result(nil)
}
}
5 changes: 5 additions & 0 deletions lib/src/haptic_feedback_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ class MethodChannelHapticFeedback extends HapticFeedbackPlatform {

return await methodChannel.invokeMethod(type.name, arguments);
}

@override
Future<void> prepare(HapticsType type) async {
return await methodChannel.invokeMethod('prepare', type.name);
}
}
8 changes: 8 additions & 0 deletions lib/src/haptic_feedback_platform_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,12 @@ abstract class HapticFeedbackPlatform extends PlatformInterface {
'Use the implementation method of MethodChannelHapticFeedback.',
);
}

/// Prepares the haptic engine for the given [type] on iOS, reducing latency
/// for the next haptic event. This is a no-op on Android.
Future<void> prepare(HapticsType type) {
throw UnsupportedError(
'Use the implementation method of MethodChannelHapticFeedback.',
);
}
}
18 changes: 18 additions & 0 deletions lib/src/haptics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,22 @@ class Haptics {
useAndroidHapticConstants: useAndroidHapticConstants,
);
}

/// Prepares the haptic engine for a haptic event of [type], reducing the
/// latency of the first haptic in a session on iOS.
///
/// On iOS, this calls `prepare()` on the underlying `UIFeedbackGenerator`,
/// warming up the Taptic Engine ahead of the actual feedback event. The
/// engine stays warm for a few seconds. Call this method when you know
/// a haptic event is imminent (e.g., when a screen loads or a gesture
/// begins).
///
/// This method is a no-op on Android and unsupported platforms.
static Future<void> prepare(HapticsType type) async {
if (!isPlatformSupported) {
return;
}

return HapticFeedbackPlatform.instance.prepare(type);
}
}
16 changes: 16 additions & 0 deletions test/haptic_feedback_method_channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,20 @@ void main() {
expect(lastMethodCall?.method, 'heavy');
expect(lastMethodCall?.arguments, {'useAndroidHapticConstants': true});
});

test('prepare invokes prepare method with type name', () async {
await platform.prepare(HapticsType.light);

expect(lastMethodCall?.method, 'prepare');
expect(lastMethodCall?.arguments, 'light');
});

test('prepare forwards each haptics type correctly', () async {
for (final type in HapticsType.values) {
await platform.prepare(type);

expect(lastMethodCall?.method, 'prepare');
expect(lastMethodCall?.arguments, type.name);
}
});
}
24 changes: 22 additions & 2 deletions test/haptic_feedback_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ void main() {
expect(recordingPlatform.lastUseAndroidHapticConstants, false);
});

test(
'vibrate forwards useAndroidHapticConstants to the platform implementation',
test('vibrate forwards useAndroidHapticConstants to the platform implementation',
() async {
final recordingPlatform = RecordingHapticFeedbackPlatform();
HapticFeedbackPlatform.instance = recordingPlatform;
Expand All @@ -63,4 +62,25 @@ void main() {
expect(recordingPlatform.lastType, HapticsType.success);
expect(recordingPlatform.lastUseAndroidHapticConstants, false);
});

test('prepare forwards type to the platform implementation', () async {
final recordingPlatform = RecordingHapticFeedbackPlatform();
HapticFeedbackPlatform.instance = recordingPlatform;

await Haptics.prepare(HapticsType.light);

expect(recordingPlatform.lastPreparedType, HapticsType.light);
});

test('prepare performs nothing on unsupported platforms', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.windows;
addTearDown(() => debugDefaultTargetPlatformOverride = null);

final recordingPlatform = RecordingHapticFeedbackPlatform();
HapticFeedbackPlatform.instance = recordingPlatform;

await Haptics.prepare(HapticsType.heavy);

expect(recordingPlatform.lastPreparedType, isNull);
});
}
9 changes: 9 additions & 0 deletions test/support/mock_platforms.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class MockHapticFeedbackPlatform
HapticsUsage? usage,
bool useAndroidHapticConstants = false,
}) async {}

@override
Future<void> prepare(HapticsType type) async {}
}

class RecordingHapticFeedbackPlatform
Expand All @@ -27,6 +30,7 @@ class RecordingHapticFeedbackPlatform
HapticsType? lastType;
HapticsUsage? lastUsage;
bool? lastUseAndroidHapticConstants;
HapticsType? lastPreparedType;

@override
Future<bool> canVibrate() async {
Expand All @@ -43,4 +47,9 @@ class RecordingHapticFeedbackPlatform
lastUsage = usage;
lastUseAndroidHapticConstants = useAndroidHapticConstants;
}

@override
Future<void> prepare(HapticsType type) async {
lastPreparedType = type;
}
}