Skip to content

Commit 22e93a0

Browse files
committed
Additional values plus demo
1 parent 9774db8 commit 22e93a0

6 files changed

Lines changed: 108 additions & 86 deletions

File tree

.idea/vcs.xml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.mpp.demo
18+
19+
import androidx.compose.foundation.layout.PaddingValues
20+
import androidx.compose.foundation.layout.Spacer
21+
import androidx.compose.foundation.layout.height
22+
import androidx.compose.foundation.lazy.LazyColumn
23+
import androidx.compose.foundation.lazy.items
24+
import androidx.compose.material.Button
25+
import androidx.compose.material.Text
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
28+
import androidx.compose.ui.platform.LocalHapticFeedback
29+
import androidx.compose.ui.unit.dp
30+
31+
private val haptics = listOf(
32+
"Confirm" to HapticFeedbackType.Confirm,
33+
"Reject" to HapticFeedbackType.Reject,
34+
35+
"LongPress" to HapticFeedbackType.LongPress,
36+
"ContextClick" to HapticFeedbackType.ContextClick,
37+
38+
"TextHandleMove" to HapticFeedbackType.TextHandleMove,
39+
"SegmentTick" to HapticFeedbackType.SegmentTick,
40+
"SegmentFrequentTick" to HapticFeedbackType.SegmentFrequentTick,
41+
42+
"GestureEnd" to HapticFeedbackType.GestureEnd,
43+
"GestureThresholdActivate" to HapticFeedbackType.GestureThresholdActivate,
44+
"ToggleOff" to HapticFeedbackType.ToggleOff,
45+
"ToggleOn" to HapticFeedbackType.ToggleOn,
46+
"VirtualKey" to HapticFeedbackType.VirtualKey,
47+
)
48+
49+
val HapticFeedbackExample = Screen.Example("Haptic feedback") {
50+
51+
val feedback = LocalHapticFeedback.current
52+
53+
LazyColumn(
54+
contentPadding = PaddingValues(16.dp)
55+
) {
56+
items(haptics) {
57+
Button(onClick = {
58+
feedback.performHapticFeedback(it.second)
59+
}) {
60+
Text(it.first)
61+
}
62+
Spacer(Modifier.height(16.dp))
63+
}
64+
}
65+
}

compose/mpp/demo/src/webMain/kotlin/androidx/compose/mpp/demo/Main.web.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ fun main() {
5050
Screen.Example("Web Clipboard API example") {
5151
WebClipboardDemo()
5252
},
53-
HtmlInteropDemos
53+
HtmlInteropDemos,
54+
HapticFeedbackExample,
5455
)
5556
) }
5657

compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.web.kt

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,58 +27,50 @@ import kotlin.js.toJsNumber
2727

2828
@OptIn(ExperimentalWasmJsInterop::class)
2929
internal class WebHapticFeedback : HapticFeedback {
30-
// Check if API is supported before doing anything
31-
private val isVibrationSupported = isVibrationSupported()
3230

33-
// Declare these hardcoded patterns to avoid js-interop on every call
34-
private val ConfirmVibrationPattern: JsArray<JsNumber> = vibrationPatternOf(18, 32, 36)
35-
private val RejectVibrationPattern: JsArray<JsNumber> = vibrationPatternOf(18, 28, 18, 28, 18)
36-
private val SinglePulseVibrationPattern: JsArray<JsNumber> = vibrationPatternOf(12)
37-
private val SoftTickVibrationPattern: JsArray<JsNumber> = vibrationPatternOf(6)
31+
// on Android these values are configured
32+
// see config_longPressVibePattern in https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/res/res/values/config.xml
33+
// We don't have a high-level browser API right now. So we hardcode the patterns here.
34+
// TODO: to eventually avoid the hardcoded values, follow the new browser API proposal https://github.com/WICG/web-haptics
35+
private companion object {
36+
val ConfirmVibrationPattern = vibrationPatternOf(18, 32, 36)
37+
val RejectVibrationPattern = vibrationPatternOf(18, 28, 18, 28, 18)
38+
val SinglePulseVibrationPattern = vibrationPatternOf(12)
39+
val SoftTickVibrationPattern = vibrationPatternOf(6)
40+
val LongPressVibrationPattern = vibrationPatternOf(0, 30)
41+
val VirtualKeyVibrationPattern = vibrationPatternOf(0, 20)
42+
}
3843

3944
override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) {
40-
if (!isVibrationSupported) return
4145
val pattern = vibrationPatternFor(hapticFeedbackType) ?: return
4246
vibrate(pattern)
4347
}
4448

45-
// We don't have a high-level browser API right now. So we hardcode the patterns here.
46-
// TODO: to eventually avoid the hardcoded values, follow the new browser API proposal https://github.com/WICG/web-haptics
47-
// and rely on it once it's implemented
49+
4850
@OptIn(ExperimentalWasmJsInterop::class)
49-
internal fun vibrationPatternFor(hapticFeedbackType: HapticFeedbackType): JsArray<JsNumber>? {
51+
private fun vibrationPatternFor(hapticFeedbackType: HapticFeedbackType): JsArray<JsNumber>? {
5052
return when (hapticFeedbackType) {
5153
HapticFeedbackType.Confirm -> ConfirmVibrationPattern
54+
HapticFeedbackType.ContextClick -> SinglePulseVibrationPattern
55+
HapticFeedbackType.GestureEnd -> SinglePulseVibrationPattern
56+
HapticFeedbackType.GestureThresholdActivate -> SinglePulseVibrationPattern
57+
HapticFeedbackType.KeyboardTap -> SoftTickVibrationPattern
58+
HapticFeedbackType.LongPress -> LongPressVibrationPattern
5259
HapticFeedbackType.Reject -> RejectVibrationPattern
53-
HapticFeedbackType.ContextClick,
54-
HapticFeedbackType.GestureEnd,
55-
HapticFeedbackType.GestureThresholdActivate,
56-
HapticFeedbackType.LongPress,
57-
HapticFeedbackType.ToggleOff,
58-
HapticFeedbackType.ToggleOn,
59-
HapticFeedbackType.VirtualKey -> SinglePulseVibrationPattern
60-
HapticFeedbackType.KeyboardTap,
61-
HapticFeedbackType.SegmentFrequentTick,
60+
HapticFeedbackType.SegmentFrequentTick -> SoftTickVibrationPattern
6261
HapticFeedbackType.SegmentTick -> SoftTickVibrationPattern
63-
HapticFeedbackType.TextHandleMove -> null
62+
HapticFeedbackType.TextHandleMove -> ConfirmVibrationPattern
63+
HapticFeedbackType.ToggleOff -> SinglePulseVibrationPattern
64+
HapticFeedbackType.ToggleOn -> SinglePulseVibrationPattern
65+
HapticFeedbackType.VirtualKey -> VirtualKeyVibrationPattern
6466
else -> null
6567
}
6668
}
6769
}
6870

6971
@OptIn(ExperimentalWasmJsInterop::class)
7072
private fun vibrationPatternOf(vararg durations: Int): JsArray<JsNumber> =
71-
durations.map { it.toDouble().toJsNumber() }.toJsArray()
72-
73-
@OptIn(ExperimentalWasmJsInterop::class)
74-
private fun isVibrationSupported(): Boolean = js(
75-
//language=javascript
76-
"""
77-
typeof window !== 'undefined' &&
78-
window.navigator != null &&
79-
typeof window.navigator.vibrate === 'function'
80-
"""
81-
)
73+
durations.map { it.toJsNumber() }.toJsArray()
8274

8375
//language=javascript
8476
@OptIn(ExperimentalWasmJsInterop::class)

compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import androidx.compose.ui.input.pointer.composeButtons
5252
import androidx.compose.ui.internal.focusExt
5353
import androidx.compose.ui.navigationevent.BackNavigationEventInput
5454
import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner
55+
import androidx.compose.ui.platform.DefaultHapticFeedback
5556
import androidx.compose.ui.platform.DefaultInputModeManager
5657
import androidx.compose.ui.platform.LocalHapticFeedback
5758
import androidx.compose.ui.platform.PlatformContext
@@ -206,7 +207,7 @@ internal class ComposeWindow(
206207
@VisibleForTesting
207208
internal val archComponentsOwner = DefaultArchitectureComponentsOwner()
208209

209-
private val hapticFeedback = WebHapticFeedback()
210+
private val hapticFeedback = if (isVibrationSupported()) WebHapticFeedback() else DefaultHapticFeedback()
210211

211212
private val navigationEventInput = BackNavigationEventInput()
212213

@@ -876,4 +877,13 @@ private fun Element.isFocused(): Boolean {
876877

877878
private external interface ShadowRootExt {
878879
val activeElement: Element?
879-
}
880+
}
881+
882+
private fun isVibrationSupported(): Boolean = js(
883+
//language=javascript
884+
"""
885+
typeof window !== 'undefined' &&
886+
window.navigator != null &&
887+
typeof window.navigator.vibrate === 'function'
888+
"""
889+
)

compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/platform/WebHapticFeedbackTest.kt

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)