Skip to content

Commit d0222ec

Browse files
authored
Merge pull request #890 from synonymdev/fix/button-bg-blur
fix: use haze for secondary button bg blur
2 parents 6477e12 + c9893a8 commit d0222ec

File tree

7 files changed

+155
-159
lines changed

7 files changed

+155
-159
lines changed

app/src/main/java/to/bitkit/di/HttpModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import io.ktor.http.contentType
1818
import io.ktor.http.isSuccess
1919
import io.ktor.serialization.kotlinx.json.json
2020
import kotlinx.serialization.json.Json
21-
import to.bitkit.utils.UrlValidator
2221
import to.bitkit.utils.AppError
2322
import to.bitkit.utils.Logger
23+
import to.bitkit.utils.UrlValidator
2424
import javax.inject.Singleton
2525
import io.ktor.client.plugins.logging.Logger as KtorLogger
2626

app/src/main/java/to/bitkit/ui/Locals.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.material3.DrawerState
44
import androidx.compose.runtime.Composable
55
import androidx.compose.runtime.compositionLocalOf
66
import androidx.compose.runtime.staticCompositionLocalOf
7+
import dev.chrisbanes.haze.HazeState
78
import to.bitkit.models.BalanceState
89
import to.bitkit.repositories.CurrencyState
910
import to.bitkit.viewmodels.ActivityListViewModel
@@ -29,6 +30,7 @@ val LocalActivityListViewModel = staticCompositionLocalOf<ActivityListViewModel?
2930
val LocalTransferViewModel = staticCompositionLocalOf<TransferViewModel?> { null }
3031
val LocalSettingsViewModel = staticCompositionLocalOf<SettingsViewModel?> { null }
3132
val LocalBackupsViewModel = staticCompositionLocalOf<BackupsViewModel?> { null }
33+
val LocalHazeState = staticCompositionLocalOf<HazeState?> { null }
3234

3335
val appViewModel: AppViewModel?
3436
@Composable get() = LocalAppViewModel.current
@@ -56,3 +58,6 @@ val backupsViewModel: BackupsViewModel?
5658

5759
val drawerState: DrawerState?
5860
@Composable get() = LocalDrawerState.current
61+
62+
val hazeState: HazeState?
63+
@Composable get() = LocalHazeState.current

app/src/main/java/to/bitkit/ui/components/Button.kt

Lines changed: 88 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Box
99
import androidx.compose.foundation.layout.Column
1010
import androidx.compose.foundation.layout.PaddingValues
1111
import androidx.compose.foundation.layout.Row
12+
import androidx.compose.foundation.layout.fillMaxSize
1213
import androidx.compose.foundation.layout.fillMaxWidth
1314
import androidx.compose.foundation.layout.padding
1415
import androidx.compose.foundation.layout.requiredHeight
@@ -24,6 +25,7 @@ import androidx.compose.material3.Text
2425
import androidx.compose.runtime.Composable
2526
import androidx.compose.ui.Alignment
2627
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.draw.clip
2729
import androidx.compose.ui.graphics.Color
2830
import androidx.compose.ui.graphics.ColorFilter
2931
import androidx.compose.ui.graphics.graphicsLayer
@@ -33,11 +35,16 @@ import androidx.compose.ui.text.style.TextOverflow
3335
import androidx.compose.ui.tooling.preview.Preview
3436
import androidx.compose.ui.unit.Dp
3537
import androidx.compose.ui.unit.dp
38+
import dev.chrisbanes.haze.HazeState
39+
import dev.chrisbanes.haze.HazeStyle
40+
import dev.chrisbanes.haze.HazeTint
41+
import dev.chrisbanes.haze.hazeEffect
42+
import dev.chrisbanes.haze.hazeSource
43+
import dev.chrisbanes.haze.rememberHazeState
3644
import to.bitkit.R
3745
import to.bitkit.ui.shared.modifiers.alphaFeedback
3846
import to.bitkit.ui.shared.modifiers.clickableAlpha
3947
import to.bitkit.ui.shared.modifiers.rememberDebouncedClick
40-
import to.bitkit.ui.shared.util.glassBlur
4148
import to.bitkit.ui.shared.util.primaryButtonStyle
4249
import to.bitkit.ui.theme.AppButtonDefaults
4350
import to.bitkit.ui.theme.AppThemeSurface
@@ -76,12 +83,6 @@ enum class ButtonSize {
7683
Small -> 8.dp
7784
Large -> 6.dp
7885
}
79-
val secondaryBlurRadius: Dp
80-
get() = when (this) {
81-
Small -> 5.dp
82-
Large -> 8.dp
83-
}
84-
8586
fun secondaryBorder(enabled: Boolean): BorderStroke = when (this) {
8687
Large -> BorderStroke(2.dp, if (enabled) Colors.Gray4 else Color.Transparent)
8788
Small -> BorderStroke(1.dp, if (enabled) Colors.White16 else Color.Transparent)
@@ -170,53 +171,75 @@ fun SecondaryButton(
170171
size: ButtonSize = ButtonSize.Large,
171172
enabled: Boolean = true,
172173
fullWidth: Boolean = true,
174+
hazeState: HazeState? = null,
173175
) {
174176
val contentPadding = PaddingValues(horizontal = size.secondaryHorizontalPadding.takeIf { text != null } ?: 0.dp)
175177
val border = size.secondaryBorder(enabled)
176178
val contentColor = when (size) {
177179
ButtonSize.Large -> Colors.White80
178180
ButtonSize.Small -> Colors.White64
179181
}
180-
OutlinedButton(
181-
onClick = rememberDebouncedClick(onClick = onClick),
182-
enabled = enabled && !isLoading,
183-
colors = AppButtonDefaults.secondaryColors.copy(contentColor = contentColor),
184-
contentPadding = contentPadding,
185-
border = border,
182+
// hazeEffect must be on a Box wrapper (not OutlinedButton — Material's Surface draws over it)
183+
// and AFTER size modifiers (Haze needs to know dimensions)
184+
val buttonShape = MaterialTheme.shapes.extraLarge
185+
Box(
186186
modifier = modifier
187187
.then(if (fullWidth) Modifier.fillMaxWidth() else Modifier)
188188
.requiredHeight(size.height)
189-
.glassBlur(blurRadius = size.secondaryBlurRadius)
190-
) {
191-
if (isLoading) {
192-
GradientCircularProgressIndicator(
193-
strokeWidth = 2.dp,
194-
modifier = Modifier.size(size.height / 2)
189+
.clip(buttonShape)
190+
.then(
191+
if (hazeState != null) {
192+
Modifier.hazeEffect(
193+
state = hazeState,
194+
style = HazeStyle(
195+
blurRadius = 12.dp,
196+
backgroundColor = Color.Black,
197+
tint = HazeTint(Color.Black.copy(alpha = 0.2f)),
198+
),
199+
)
200+
} else {
201+
Modifier
202+
}
195203
)
196-
} else {
197-
Row(
198-
verticalAlignment = Alignment.CenterVertically,
199-
horizontalArrangement = Arrangement.spacedBy(size.secondaryGap),
200-
) {
201-
if (icon != null) {
202-
Box(
203-
modifier = if (enabled) {
204-
Modifier
205-
} else {
206-
Modifier.graphicsLayer {
207-
colorFilter = ColorFilter.tint(Colors.White32)
204+
) {
205+
OutlinedButton(
206+
onClick = rememberDebouncedClick(onClick = onClick),
207+
enabled = enabled && !isLoading,
208+
colors = AppButtonDefaults.secondaryColors.copy(contentColor = contentColor),
209+
contentPadding = contentPadding,
210+
border = border,
211+
modifier = if (fullWidth) Modifier.fillMaxSize() else Modifier,
212+
) {
213+
if (isLoading) {
214+
GradientCircularProgressIndicator(
215+
strokeWidth = 2.dp,
216+
modifier = Modifier.size(size.height / 2)
217+
)
218+
} else {
219+
Row(
220+
verticalAlignment = Alignment.CenterVertically,
221+
horizontalArrangement = Arrangement.spacedBy(size.secondaryGap),
222+
) {
223+
if (icon != null) {
224+
Box(
225+
modifier = if (enabled) {
226+
Modifier
227+
} else {
228+
Modifier.graphicsLayer {
229+
colorFilter = ColorFilter.tint(Colors.White32)
230+
}
208231
}
232+
) {
233+
icon()
209234
}
210-
) {
211-
icon()
212235
}
213-
}
214-
text?.let {
215-
Text(
216-
text = text,
217-
maxLines = 1,
218-
overflow = TextOverflow.Ellipsis,
219-
)
236+
text?.let {
237+
Text(
238+
text = text,
239+
maxLines = 1,
240+
overflow = TextOverflow.Ellipsis,
241+
)
242+
}
220243
}
221244
}
222245
}
@@ -278,7 +301,7 @@ fun TertiaryButton(
278301
}
279302
}
280303

281-
@Preview(showBackground = true)
304+
@Preview
282305
@Composable
283306
private fun PrimaryButtonPreview() {
284307
AppThemeSurface {
@@ -405,33 +428,39 @@ private fun PrimaryButtonPreview() {
405428
}
406429
}
407430

408-
@Preview(showBackground = true)
431+
@Preview
409432
@Composable
410433
private fun SecondaryButtonPreview() {
434+
val hazeState = rememberHazeState()
411435
AppThemeSurface {
412436
Box {
413-
Image(
414-
painter = painterResource(R.drawable.lightning),
415-
contentDescription = null,
416-
contentScale = ContentScale.Crop,
417-
modifier = Modifier.matchParentSize()
418-
)
437+
Box(
438+
modifier = Modifier
439+
.matchParentSize()
440+
.hazeSource(hazeState)
441+
) {
442+
Image(
443+
painter = painterResource(R.drawable.lightning),
444+
contentDescription = null,
445+
contentScale = ContentScale.Crop,
446+
modifier = Modifier.matchParentSize()
447+
)
448+
}
419449
Column(
420450
verticalArrangement = Arrangement.spacedBy(8.dp),
421451
modifier = Modifier.padding(16.dp)
422452
) {
423-
SecondaryButton(
424-
text = "Secondary",
425-
onClick = {},
426-
)
453+
SecondaryButton(text = "Secondary", hazeState = hazeState, onClick = {})
427454
SecondaryButton(
428455
text = "Secondary With padding",
429-
modifier = Modifier.padding(horizontal = 32.dp),
456+
hazeState = hazeState,
430457
onClick = {},
458+
modifier = Modifier.padding(horizontal = 32.dp)
431459
)
432460
SecondaryButton(
433461
text = "Secondary With Icon",
434462
onClick = {},
463+
hazeState = hazeState,
435464
icon = {
436465
Icon(
437466
imageVector = Icons.Filled.Favorite,
@@ -443,12 +472,14 @@ private fun SecondaryButtonPreview() {
443472
SecondaryButton(
444473
text = "Secondary Loading",
445474
isLoading = true,
475+
hazeState = hazeState,
446476
onClick = {},
447477
)
448478
SecondaryButton(
449479
text = "Secondary Disabled",
450480
onClick = {},
451481
enabled = false,
482+
hazeState = hazeState,
452483
icon = {
453484
Icon(
454485
imageVector = Icons.Filled.Favorite,
@@ -461,12 +492,14 @@ private fun SecondaryButtonPreview() {
461492
text = "Secondary Small",
462493
size = ButtonSize.Small,
463494
fullWidth = false,
495+
hazeState = hazeState,
464496
onClick = {},
465497
)
466498
SecondaryButton(
467499
text = "Secondary Small Loading",
468500
size = ButtonSize.Small,
469501
isLoading = true,
502+
hazeState = hazeState,
470503
onClick = {},
471504
)
472505
SecondaryButton(
@@ -485,6 +518,7 @@ private fun SecondaryButtonPreview() {
485518
onClick = {},
486519
fullWidth = false,
487520
size = ButtonSize.Large,
521+
hazeState = hazeState,
488522
icon = {
489523
Icon(
490524
imageVector = Icons.Filled.Favorite,
@@ -513,7 +547,7 @@ private fun SecondaryButtonPreview() {
513547
}
514548
}
515549

516-
@Preview(showBackground = true)
550+
@Preview
517551
@Composable
518552
private fun TertiaryButtonPreview() {
519553
AppThemeSurface {

app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ import androidx.compose.foundation.background
55
import androidx.compose.foundation.layout.Box
66
import androidx.compose.foundation.layout.Column
77
import androidx.compose.foundation.layout.Spacer
8+
import androidx.compose.foundation.layout.WindowInsets
9+
import androidx.compose.foundation.layout.WindowInsetsSides
810
import androidx.compose.foundation.layout.fillMaxSize
911
import androidx.compose.foundation.layout.fillMaxWidth
1012
import androidx.compose.foundation.layout.height
1113
import androidx.compose.foundation.layout.offset
14+
import androidx.compose.foundation.layout.only
1215
import androidx.compose.foundation.layout.padding
1316
import androidx.compose.foundation.layout.size
17+
import androidx.compose.foundation.layout.statusBars
1418
import androidx.compose.foundation.layout.systemBarsPadding
19+
import androidx.compose.foundation.layout.windowInsetsPadding
1520
import androidx.compose.material3.Icon
1621
import androidx.compose.runtime.Composable
1722
import androidx.compose.runtime.getValue
@@ -26,6 +31,8 @@ import androidx.compose.ui.res.stringResource
2631
import androidx.compose.ui.tooling.preview.Preview
2732
import androidx.compose.ui.unit.dp
2833
import com.synonym.bitkitcore.Activity
34+
import dev.chrisbanes.haze.hazeSource
35+
import dev.chrisbanes.haze.rememberHazeState
2936
import kotlinx.collections.immutable.ImmutableList
3037
import kotlinx.collections.immutable.persistentListOf
3138
import to.bitkit.R
@@ -68,22 +75,32 @@ fun SavingsWalletScreen(
6875
mutableStateOf(hasFunds && !isGeoBlocked)
6976
}
7077

78+
val hazeState = rememberHazeState()
7179
Box(
7280
modifier = Modifier
7381
.fillMaxSize()
74-
.background(Colors.Black)
7582
.blockPointerInputPassthrough()
7683
) {
77-
Image(
78-
painter = painterResource(id = R.drawable.piggybank),
79-
contentDescription = null,
80-
contentScale = ContentScale.Fit,
84+
// Background layer: hazeSource must be a sibling of hazeEffect, not a parent.
85+
// Haze can't blur an ancestor — source and effect must be at the same level.
86+
Box(
8187
modifier = Modifier
82-
.align(Alignment.TopEnd)
83-
.padding(top = 32.dp)
84-
.offset(x = (120).dp)
85-
.size(268.dp)
86-
)
88+
.matchParentSize()
89+
.background(Colors.Black)
90+
.hazeSource(hazeState)
91+
) {
92+
Image(
93+
painter = painterResource(id = R.drawable.piggybank),
94+
contentDescription = null,
95+
contentScale = ContentScale.Fit,
96+
modifier = Modifier
97+
.align(Alignment.TopEnd)
98+
.padding(top = 0.dp)
99+
.offset(x = (160).dp)
100+
.size(360.dp)
101+
.windowInsetsPadding(WindowInsets.statusBars.only(WindowInsetsSides.Vertical))
102+
)
103+
}
87104
ScreenColumn(noBackground = true) {
88105
AppTopBar(
89106
titleText = stringResource(R.string.wallet__savings__title),
@@ -108,7 +125,7 @@ fun SavingsWalletScreen(
108125
IncomingTransfer(
109126
amount = balances.balanceInTransferToSavings,
110127
remainingDuration = forceCloseRemainingDuration,
111-
modifier = Modifier.padding(vertical = 8.dp),
128+
modifier = Modifier.padding(vertical = 8.dp)
112129
)
113130
}
114131

@@ -123,9 +140,10 @@ fun SavingsWalletScreen(
123140
Icon(
124141
painter = painterResource(R.drawable.ic_transfer),
125142
contentDescription = null,
126-
modifier = Modifier.size(16.dp),
143+
modifier = Modifier.size(16.dp)
127144
)
128145
},
146+
hazeState = hazeState,
129147
modifier = Modifier.testTag("TransferToSpending")
130148
)
131149
}

0 commit comments

Comments
 (0)