Updated on 2026-08-14
This commit is contained in:
parent
ac8436460c
commit
459db3d0df
17 changed files with 263 additions and 102 deletions
|
|
@ -2,14 +2,14 @@ package com.tangem.core.ui
|
|||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.message.EventMessageHandler
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
|
||||
@Stable
|
||||
interface UiDependencies {
|
||||
|
||||
val hapticManager: HapticManager
|
||||
val vibratorHapticManager: VibratorHapticManager
|
||||
|
||||
val appThemeModeHolder: AppThemeModeHolder
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.core.ui.haptic
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
|
||||
internal class DefaultHapticManager(
|
||||
private val view: View,
|
||||
private val vibratorHapticManager: VibratorHapticManager?,
|
||||
) : HapticManager {
|
||||
|
||||
override fun perform(effect: TangemHapticEffect) {
|
||||
when (effect) {
|
||||
is TangemHapticEffect.View -> {
|
||||
effect.androidHapticFeedbackCode?.let {
|
||||
ViewCompat.performHapticFeedback(view, it)
|
||||
}
|
||||
}
|
||||
is TangemHapticEffect.OneTime -> {
|
||||
if (vibratorHapticManager != null) {
|
||||
vibratorHapticManager.performOneTime(effect)
|
||||
} else {
|
||||
when (effect) {
|
||||
TangemHapticEffect.OneTime.Tick -> perform(TangemHapticEffect.View.SegmentTick)
|
||||
TangemHapticEffect.OneTime.Click -> perform(TangemHapticEffect.View.ContextClick)
|
||||
TangemHapticEffect.OneTime.DoubleClick -> perform(TangemHapticEffect.View.ContextClick)
|
||||
TangemHapticEffect.OneTime.HeavyClick -> perform(TangemHapticEffect.View.LongPress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@ package com.tangem.core.ui.haptic
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
/**
|
||||
* Haptic feedback.
|
||||
* @see [TangemHapticEffect.OneTime] for one-time effects.
|
||||
* @see [TangemHapticEffect.View] for view effects.
|
||||
*/
|
||||
@Stable
|
||||
interface HapticManager {
|
||||
|
||||
fun vibrateShort()
|
||||
|
||||
fun vibrateMeduim()
|
||||
|
||||
fun vibrateLong()
|
||||
fun perform(effect: TangemHapticEffect)
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package com.tangem.core.ui.haptic
|
||||
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun MockHapticManager(mockHapticFeedback: HapticFeedback? = null): HapticManager =
|
||||
if (mockHapticFeedback == null) MockHapticManager else MockHapticManagerImpl(mockHapticFeedback)
|
||||
|
||||
val MockHapticManager: HapticManager = MockHapticManagerImpl()
|
||||
|
||||
private class MockHapticManagerImpl(
|
||||
private val mockHapticFeedback: HapticFeedback? = null,
|
||||
) : HapticManager {
|
||||
|
||||
override fun vibrateShort() {
|
||||
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
/** Intentionally do nothing */
|
||||
}
|
||||
|
||||
override fun vibrateMeduim() {
|
||||
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
/** Intentionally do nothing */
|
||||
}
|
||||
|
||||
override fun vibrateLong() {
|
||||
mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
/** Intentionally do nothing */
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.tangem.core.ui.haptic
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
sealed interface TangemHapticEffect {
|
||||
|
||||
/**
|
||||
* For cases when view could not be visible on screen (ex. Activity is not in foreground)
|
||||
* or there is no view context (ex. background service, Model, ViewModel)
|
||||
*/
|
||||
enum class OneTime : TangemHapticEffect {
|
||||
Click,
|
||||
DoubleClick,
|
||||
HeavyClick,
|
||||
Tick,
|
||||
;
|
||||
|
||||
val code: Int
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
get() = when (this) {
|
||||
Click -> VibrationEffect.EFFECT_CLICK
|
||||
DoubleClick -> VibrationEffect.EFFECT_DOUBLE_CLICK
|
||||
HeavyClick -> VibrationEffect.EFFECT_HEAVY_CLICK
|
||||
Tick -> VibrationEffect.EFFECT_TICK
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preferred way to provide haptic feedback for UI components
|
||||
* @see [androidx.core.view.HapticFeedbackConstantsCompat]
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
enum class View(internal val androidHapticFeedbackCode: Int? = null) : TangemHapticEffect {
|
||||
/**
|
||||
* The user has performed a long press on an object that is resulting in an action being
|
||||
* performed
|
||||
*/
|
||||
LongPress(0),
|
||||
/**
|
||||
* The user has pressed on a virtual on-screen key
|
||||
*/
|
||||
VirtualKey(1),
|
||||
/**
|
||||
* The user has pressed either an hour or minute tick of a Clock
|
||||
*/
|
||||
ClockTick(4),
|
||||
/**
|
||||
* The user has performed a context click on an object
|
||||
*/
|
||||
ContextClick(6),
|
||||
/**
|
||||
* The user has pressed a virtual or software keyboard key
|
||||
*/
|
||||
KeyboardPress(3),
|
||||
/**
|
||||
* The user has released a virtual keyboard key
|
||||
*/
|
||||
KeyboardRelease(7),
|
||||
/**
|
||||
* The user has released a virtual key
|
||||
*/
|
||||
VirtualKeyRelease(8),
|
||||
/**
|
||||
* The user has performed a selection/insertion handle move on text field
|
||||
*/
|
||||
TextHandleMove(9),
|
||||
/**
|
||||
* The user has started a gesture (e.g. on the soft keyboard)
|
||||
*/
|
||||
GestureStart(12),
|
||||
/**
|
||||
* The user has finished a gesture (e.g. on the soft keyboard)
|
||||
*/
|
||||
GestureEnd(13),
|
||||
/**
|
||||
* A haptic effect to signal the confirmation or successful completion of a user interaction
|
||||
*/
|
||||
Confirm(16),
|
||||
/**
|
||||
* A haptic effect to signal the rejection or failure of a user interaction
|
||||
*/
|
||||
Reject(17),
|
||||
/**
|
||||
* The user has toggled a switch or button into the on position
|
||||
*/
|
||||
ToggleOn(21),
|
||||
/**
|
||||
* The user has toggled a switch or button into the off position
|
||||
*/
|
||||
ToggleOff(22),
|
||||
/**
|
||||
* The user is executing a swipe/drag-style gesture, such as pull-to-refresh, where the
|
||||
* gesture action is “eligible” at a certain threshold of movement, and can be cancelled by
|
||||
* moving back past the threshold. This constant indicates that the user's motion has just
|
||||
* passed the threshold for the action to be activated on release
|
||||
*/
|
||||
GestureThresholdActivate(23),
|
||||
/**
|
||||
* The user is executing a swipe/drag-style gesture, such as pull-to-refresh, where the
|
||||
* gesture action is “eligible” at a certain threshold of movement, and can be cancelled by
|
||||
* moving back past the threshold. This constant indicates that the user's motion has just
|
||||
* re-crossed back "under" the threshold for the action to be activated, meaning the gesture is
|
||||
* currently in a cancelled state
|
||||
*/
|
||||
GestureThresholdDeactivate(24),
|
||||
/**
|
||||
* The user has started a drag-and-drop gesture. The drag target has just been "picked up"
|
||||
*/
|
||||
DragStart(25),
|
||||
/**
|
||||
* The user is switching between a series of potential choices, for example items in a list
|
||||
* or discrete points on a slider
|
||||
*/
|
||||
SegmentTick(26),
|
||||
/**
|
||||
* The user is switching between a series of many potential choices, for example minutes on a
|
||||
* clock face, or individual percentages. This constant is expected to be very soft, so as
|
||||
* not to be uncomfortable when performed a lot in quick succession. If the device can’t make
|
||||
* a suitably soft vibration, then it may not make any vibration
|
||||
*/
|
||||
SegmentFrequentTick(27),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.ui.haptic
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
/**
|
||||
* Haptic feedback
|
||||
* For cases when view could not be visible on screen (ex. Activity is not in foreground)
|
||||
* or there is no view context (ex. background service, Model, ViewModel)
|
||||
* @see [HapticManager] for view effects.
|
||||
*/
|
||||
@Stable
|
||||
interface VibratorHapticManager {
|
||||
|
||||
fun performOneTime(effect: TangemHapticEffect.OneTime)
|
||||
}
|
||||
|
|
@ -8,10 +8,12 @@ import androidx.compose.material.ProvideTextStyle
|
|||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.tangem.core.ui.components.TangemShimmer
|
||||
import com.tangem.core.ui.haptic.DefaultHapticManager
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
import com.tangem.core.ui.haptic.MockHapticManager
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.windowsize.WindowSize
|
||||
import com.valentinilk.shimmer.Shimmer
|
||||
|
||||
|
|
@ -21,7 +23,7 @@ fun TangemTheme(
|
|||
windowSize: WindowSize,
|
||||
typography: TangemTypography = TangemTheme.typography,
|
||||
dimens: TangemDimens = TangemTheme.dimens,
|
||||
hapticManager: HapticManager = MockHapticManager,
|
||||
vibratorHapticManager: VibratorHapticManager? = null,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
|
|
@ -40,6 +42,12 @@ fun TangemTheme(
|
|||
)
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
|
||||
val hapticManager = remember(view) {
|
||||
DefaultHapticManager(view = view, vibratorHapticManager = vibratorHapticManager)
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colors = materialThemeColors(colors = themeColors, isDark = isDark),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import com.tangem.core.ui.haptic.MockHapticManager
|
||||
import com.tangem.core.ui.windowsize.rememberWindowSizePreview
|
||||
|
||||
@Composable
|
||||
|
|
@ -29,7 +27,6 @@ fun TangemThemePreview(
|
|||
typography = typography,
|
||||
dimens = dimens,
|
||||
windowSize = rememberWindowSizePreview(maxWidth, maxHeight),
|
||||
hapticManager = MockHapticManager(LocalHapticFeedback.current),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ internal fun ComposeScreen.createComposeView(context: Context, activity: Activit
|
|||
TangemTheme(
|
||||
isDark = shouldUseDarkTheme(appThemeMode),
|
||||
windowSize = windowSize,
|
||||
hapticManager = uiDependencies.hapticManager,
|
||||
vibratorHapticManager = uiDependencies.vibratorHapticManager,
|
||||
snackbarHostState = uiDependencies.globalSnackbarHostState,
|
||||
) {
|
||||
ScreenContent(modifier = screenModifier)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue