Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.packages
.pub/
pubspec.lock
pubspec_overrides.yaml

.idea/
build/
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,38 @@ A plugin for handling Vibration API on iOS, Android, and web. [API docs.](https:
[Android, iOS.](vibration)

[Web.](vibration_web)

---

## Local Development

This is a monorepo. The `vibration` and `vibration_ohos` packages depend on
`vibration_platform_interface` by hosted version. When the latest interface
changes haven't been published yet, you need local path overrides to run
analysis and tests.

### Setup

Copy the example override in each consumer package:

```bash
cp vibration/pubspec_overrides.yaml.example vibration/pubspec_overrides.yaml
cp vibration_ohos/pubspec_overrides.yaml.example vibration_ohos/pubspec_overrides.yaml
```

Then run the standard commands:

```bash
# Platform interface (no override needed — it's the source)
cd vibration_platform_interface && flutter pub get && dart analyze lib/

# Main package
cd vibration && flutter pub get --no-example && dart analyze lib/ test/
cd vibration && flutter test test/vibration_test.dart

# OHOS package
cd vibration_ohos && flutter pub get --no-example && dart analyze lib/
```

Delete the overrides when done — they are gitignored but should not be left
around for publishing.
1 change: 1 addition & 0 deletions vibration/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ migrate_working_dir/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
pubspec_overrides.yaml
**/doc/api/
.dart_tool/
.flutter-plugins
Expand Down
3 changes: 2 additions & 1 deletion vibration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ if (await Vibration.hasCustomVibrationsSupport()) {
- `repeat`: Index in the pattern at which to repeat, or -1 for no repeat. Default is -1.
- `intensities`: List of integers representing the vibration intensities for each segment in the pattern.
- `amplitude`: Amplitude of the vibration. Range is 1 to 255. Default is -1 (use platform default).
- `sharpness`: Sharpness of the vibration. iOS only. Range is 0.0 to 1.0. Default is 0.5.
- `sharpness`: Sharpness of the vibration. iOS only. Range is 0.0 to 1.0. When omitted, iOS uses a native fallback derived from the current intensity (intensity × 0.5). With the default amplitude (255), this results in a sharpness of ~0.5, but the actual value varies with amplitude/intensities.
- `sharpnesses`: Per-segment sharpness values for pattern vibrations. iOS only. Each value must be in the range 0.0 to 1.0. When provided, these take precedence over the scalar `sharpness` for each corresponding pattern segment.
- `preset`: Predefined vibration preset. Overrides other parameters if provided.

#### With specific duration (for example, 1 second):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private Vibrator getLegacyVibrator(@NonNull FlutterPluginBinding flutterPluginBi
final Context context = flutterPluginBinding.getApplicationContext();

Vibrator vibrator = ContextCompat.getSystemService(context, Vibrator.class);

if (vibrator != null) {
return vibrator;
}
Expand Down
43 changes: 43 additions & 0 deletions vibration/example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}

def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end

File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
use_frameworks!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
65 changes: 48 additions & 17 deletions vibration/ios/vibration/Sources/vibration/VibrationPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ public class VibrationPlugin: NSObject, FlutterPlugin {
return pattern.enumerated().map { $0.offset % 2 == 0 ? 0 : amplitude }
}

/// Sanitizes a raw sharpness value for CoreHaptics.
/// Returns a value clamped to [0.0, 1.0], or the fallback if raw is nil/NaN/infinite/negative.
private func sanitizeSharpness(raw: Double?, fallback: Float) -> Float {
guard let raw = raw, raw.isFinite, raw >= 0 else {
return min(max(fallback, 0.0), 1.0)
}
return min(max(Float(raw), 0.0), 1.0)
}

private func rawSharpnessAt(index: Int, from list: [Double]) -> Double? {
guard index >= 0, index < list.count else { return nil }
return list[index]
}

private func getPattern(myArgs: [String: Any]) -> [Int] {
let pattern = myArgs["pattern"] as? [Int] ?? []

Expand All @@ -122,8 +136,9 @@ public class VibrationPlugin: NSObject, FlutterPlugin {
return duration == -1 ? 500 : duration
}

private func getSharpness(myArgs: [String: Any]) -> Float {
return myArgs["sharpness"] as? Float ?? 0.5
private func getSharpness(myArgs: [String: Any], intensity: Float) -> Float {
let raw = myArgs["sharpness"] as? Double
return sanitizeSharpness(raw: raw, fallback: intensity * 0.5)
}

/// Returns the repeat index from the method arguments, defaulting to `-1` (no repeat).
Expand All @@ -135,31 +150,42 @@ public class VibrationPlugin: NSObject, FlutterPlugin {

/// Builds an array of `CHHapticEvent` values from a slice of the pattern.
///
/// Sharpness is resolved per segment: `sharpnesses[i]` wins when present; otherwise
/// the scalar `sharpness` is used, and finally the historical intensity × 0.5 fallback.
///
/// - Parameters:
/// - pattern: Timing values in milliseconds, alternating silence and vibration durations.
/// - intensities: Amplitude values (0–255) corresponding to each element in `pattern`.
/// - sharpness: Haptic sharpness applied to all vibration events.
/// - myArgs: Original method-call arguments, used to derive scalar and per-segment sharpness.
/// - startIndex: Index into `pattern` and `intensities` to begin from. Defaults to `0`
/// (full pattern). Pass `repeatIndex` here when building loop-slice events.
@available(iOS 13.0, *)
private func buildHapticEvents(
pattern: [Int],
intensities: [Int],
sharpness: Float,
myArgs: [String: Any],
startIndex: Int = 0
) -> [CHHapticEvent] {
let rawSharpnesses = myArgs["sharpnesses"] as? [Double] ?? []

var hapticEvents: [CHHapticEvent] = []
var rel: Double = 0.0

for i in startIndex..<pattern.count {
let duration = pattern[i]

if intensities[i] != 0 {
let normalizedIntensity = Float(intensities[i]) / 255.0
let sharpness = sanitizeSharpness(
raw: rawSharpnessAt(index: i, from: rawSharpnesses),
fallback: getSharpness(myArgs: myArgs, intensity: normalizedIntensity)
)

hapticEvents.append(
CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(intensities[i]) / 255.0),
CHHapticEventParameter(parameterID: .hapticIntensity, value: normalizedIntensity),
CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness)
],
relativeTime: rel,
Expand Down Expand Up @@ -195,7 +221,7 @@ public class VibrationPlugin: NSObject, FlutterPlugin {
/// - Parameters:
/// - pattern: Full pattern array (only the slice from `startIndex` onward is played).
/// - intensities: Amplitude values corresponding to each element in `pattern`.
/// - sharpness: Haptic sharpness forwarded to `buildHapticEvents`.
/// - myArgs: Original method-call arguments, used to derive scalar and per-segment sharpness.
/// - startIndex: Index in `pattern` at which each iteration begins.
/// - loopSliceMs: Duration of one loop iteration in milliseconds; reused as the delay
/// before the next iteration.
Expand All @@ -205,7 +231,7 @@ public class VibrationPlugin: NSObject, FlutterPlugin {
private func scheduleRepeat(
pattern: [Int],
intensities: [Int],
sharpness: Float,
myArgs: [String: Any],
startIndex: Int,
loopSliceMs: Int,
generation: Int,
Expand All @@ -218,7 +244,7 @@ public class VibrationPlugin: NSObject, FlutterPlugin {
let loopEvents = self.buildHapticEvents(
pattern: pattern,
intensities: intensities,
sharpness: sharpness,
myArgs: myArgs,
startIndex: startIndex
)
try self.playHapticEvents(loopEvents)
Expand All @@ -230,7 +256,7 @@ public class VibrationPlugin: NSObject, FlutterPlugin {
self.scheduleRepeat(
pattern: pattern,
intensities: intensities,
sharpness: sharpness,
myArgs: myArgs,
startIndex: startIndex,
loopSliceMs: loopSliceMs,
generation: generation,
Expand All @@ -243,11 +269,19 @@ public class VibrationPlugin: NSObject, FlutterPlugin {
private func playPattern(myArgs: [String: Any]) -> Void {
let intensities = getIntensities(myArgs: myArgs)
let patternArray = getPattern(myArgs: myArgs)
let sharpness = getSharpness(myArgs: myArgs)
let repeatIndex = getRepeat(myArgs: myArgs)

// Any new play request replaces an outstanding repeat loop, even if this request
// does not itself repeat.
VibrationPlugin.repeatGeneration += 1
let generation = VibrationPlugin.repeatGeneration

do {
let events = buildHapticEvents(pattern: patternArray, intensities: intensities, sharpness: sharpness)
let events = buildHapticEvents(
pattern: patternArray,
intensities: intensities,
myArgs: myArgs
)

try playHapticEvents(events)
} catch {
Expand All @@ -258,19 +292,16 @@ public class VibrationPlugin: NSObject, FlutterPlugin {

guard repeatIndex >= 0, repeatIndex < patternArray.count else { return }

VibrationPlugin.repeatGeneration += 1

let gen = VibrationPlugin.repeatGeneration
let firstPlayMs = patternArray.reduce(0, +)
let loopSliceMs = patternArray[repeatIndex...].reduce(0, +)

scheduleRepeat(
pattern: patternArray,
intensities: intensities,
sharpness: sharpness,
myArgs: myArgs,
startIndex: repeatIndex,
loopSliceMs: loopSliceMs,
generation: gen,
generation: generation,
afterMs: firstPlayMs
)
}
Expand Down Expand Up @@ -329,7 +360,7 @@ public class VibrationPlugin: NSObject, FlutterPlugin {
return
default:
result(FlutterMethodNotImplemented)

return
}
}
Expand Down
11 changes: 9 additions & 2 deletions vibration/lib/vibration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class Vibration {
/// The default vibration duration is 500ms.
/// Amplitude is a range from 1 to 255, if supported.
///
/// [sharpness] is iOS only. When provided, must be in the range 0.0 to 1.0.
/// When omitted, the iOS native fallback logic applies (intensity × 0.5).
///
/// [sharpnesses] is iOS only. Per-segment sharpness values for pattern
/// vibrations, each in the range 0.0 to 1.0.
///
/// If [preset] is provided, it overrides other parameters and uses the preset configuration.
///
/// ```dart
Expand All @@ -72,8 +78,8 @@ class Vibration {
int repeat = -1,
List<int> intensities = const [],
int amplitude = -1,
// sharpness is iOS only
double sharpness = 0.5,
double? sharpness,
List<double> sharpnesses = const [],
VibrationPreset? preset,
}) async {
if (preset != null) {
Expand All @@ -96,6 +102,7 @@ class Vibration {
intensities: intensities,
amplitude: amplitude,
sharpness: sharpness,
sharpnesses: sharpnesses,
);
}

Expand Down
2 changes: 1 addition & 1 deletion vibration/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
vibration_platform_interface: ^0.1.1
vibration_platform_interface: ^0.2.0

dev_dependencies:
flutter_test:
Expand Down
3 changes: 3 additions & 0 deletions vibration/pubspec_overrides.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependency_overrides:
vibration_platform_interface:
path: ../vibration_platform_interface
Loading