Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-07 15:42:58 +03:00
parent ac8436460c
commit 459db3d0df
17 changed files with 263 additions and 102 deletions

View file

@ -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

View file

@ -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)
}
}
}
}
}
}

View file

@ -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)
}

View file

@ -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 */
}
}

View file

@ -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 cant make
* a suitably soft vibration, then it may not make any vibration
*/
SegmentFrequentTick(27),
}
}

View file

@ -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)
}

View file

@ -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),
) {

View file

@ -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,
)
}

View file

@ -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)