@@ -20,29 +20,32 @@ import androidx.compose.material.icons.Icons
2020import androidx.compose.material.icons.filled.ArrowDownward
2121import androidx.compose.material.icons.filled.ArrowUpward
2222import androidx.compose.material3.Icon
23+ import androidx.compose.material3.MaterialTheme
2324import androidx.compose.runtime.Composable
2425import androidx.compose.ui.Alignment
2526import androidx.compose.ui.Modifier
2627import androidx.compose.ui.draw.clip
28+ import androidx.compose.ui.draw.drawWithContent
29+ import androidx.compose.ui.draw.shadow
30+ import androidx.compose.ui.geometry.Offset
2731import androidx.compose.ui.graphics.Brush
2832import androidx.compose.ui.graphics.Color
33+ import androidx.compose.ui.graphics.drawscope.Stroke
2934import androidx.compose.ui.platform.testTag
3035import androidx.compose.ui.res.painterResource
3136import androidx.compose.ui.res.stringResource
3237import androidx.compose.ui.tooling.preview.Preview
3338import androidx.compose.ui.unit.dp
34- import dev.chrisbanes.haze.HazeState
35- import dev.chrisbanes.haze.hazeEffect
3639import dev.chrisbanes.haze.hazeSource
37- import dev.chrisbanes.haze.materials.CupertinoMaterials
3840import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
3941import dev.chrisbanes.haze.rememberHazeState
4042import to.bitkit.R
43+ import to.bitkit.ui.shared.util.clickableAlpha
44+ import to.bitkit.ui.shared.util.gradientBackground
45+ import to.bitkit.ui.shared.util.primaryButtonStyle
4146import to.bitkit.ui.theme.AppThemeSurface
4247import to.bitkit.ui.theme.Colors
4348
44- private val buttonBg = Color (40 , 40 , 40 ).copy(alpha = 0.95f )
45- private val buttonBgOverlay = Brush .verticalGradient(colors = listOf (buttonBg, Color .Transparent ))
4649
4750private val iconToTextGap = 4 .dp
4851private val iconSize = 20 .dp
@@ -54,7 +57,6 @@ private val buttonRightShape = RoundedCornerShape(topEndPercent = 50, bottomEndP
5457@Composable
5558fun BoxScope.TabBar (
5659 modifier : Modifier = Modifier ,
57- hazeState : HazeState = rememberHazeState(),
5860 onSendClick : () -> Unit = {},
5961 onReceiveClick : () -> Unit = {},
6062 onScanClick : () -> Unit = {},
@@ -64,37 +66,23 @@ fun BoxScope.TabBar(
6466 modifier = modifier
6567 .align(Alignment .BottomCenter )
6668 .fillMaxWidth()
67- .background(
68- Brush .verticalGradient(
69- colorStops = arrayOf(
70- 0.0f to Color .Transparent ,
71- 0.5f to Colors .Black ,
72- ),
73- )
74- )
7569 .padding(horizontal = 16 .dp)
7670 .padding(bottom = 16 .dp)
7771 .navigationBarsPadding()
7872 ) {
79- Row {
80- val buttonMaterial = CupertinoMaterials .ultraThin(containerColor = buttonBg)
73+ Row (
74+ modifier = Modifier .primaryButtonStyle(
75+ isEnabled = true ,
76+ shape = MaterialTheme .shapes.large,
77+ )
78+ ) {
8179 // Send Button
8280 Box (
8381 contentAlignment = Alignment .Center ,
8482 modifier = Modifier
8583 .weight(1f )
8684 .height(60 .dp)
8785 .clip(buttonLeftShape)
88- .background(buttonBg) // fallback if no haze
89- .hazeEffect(
90- state = hazeState,
91- style = buttonMaterial,
92- )
93- .background(
94- Brush .verticalGradient(
95- colors = listOf (buttonBg, Color .Transparent ),
96- )
97- )
9886 .clickable { onSendClick() }
9987 .testTag(" Send" )
10088 ) {
@@ -116,12 +104,6 @@ fun BoxScope.TabBar(
116104 .weight(1f )
117105 .height(60 .dp)
118106 .clip(buttonRightShape)
119- .background(buttonBg) // fallback if no haze
120- .hazeEffect(
121- state = hazeState,
122- style = buttonMaterial,
123- )
124- .background(buttonBgOverlay)
125107 .clickable { onReceiveClick() }
126108 .testTag(" Receive" )
127109 ) {
@@ -137,45 +119,74 @@ fun BoxScope.TabBar(
137119 }
138120 }
139121
140- // Scan Button
141- val scanBg = Colors .Gray6 .copy(alpha = 0.75f )
142- // Outer Border
122+ // Scan button
143123 Box (
144- content = {},
145124 contentAlignment = Alignment .Center ,
146125 modifier = Modifier
147- .size(80 .dp)
148- .clip(CircleShape )
149- .background(buttonBg) // fallback if no haze
150- .hazeEffect(
151- state = hazeState,
152- style = CupertinoMaterials .regular(containerColor = buttonBg),
153- )
154- .background(
155- Brush .verticalGradient(colors = listOf (Colors .White10 , Color .Transparent ))
126+ .size(64 .dp)
127+ // Shadow 1: gray2 shadow with radius 0 at y=-1 (top highlight)
128+ .drawWithContent {
129+ // Draw a prominent top highlight
130+ drawCircle(
131+ color = Colors .Gray2 ,
132+ radius = size.width / 2 ,
133+ center = Offset (size.width / 2 , size.height / 2 - 1.5 .dp.toPx()),
134+ alpha = 0.6f
135+ )
136+ drawContent()
137+ }
138+ // Shadow 2: black 25% opacity, radius 25, y offset 20
139+ .shadow(
140+ elevation = 25 .dp,
141+ shape = CircleShape ,
142+ ambientColor = Colors .Black25 ,
143+ spotColor = Colors .Black25
156144 )
157- .clickable { onScanClick() }
158- .padding(2 .dp)
159- .testTag(" Scan" )
160- )
161- // Inner Content
162- Box (
163- contentAlignment = Alignment .Center ,
164- modifier = Modifier
165- .size(76 .dp)
166145 .clip(CircleShape )
167- .background(buttonBg) // fallback if no haze
168- .hazeEffect(
169- state = hazeState,
170- style = CupertinoMaterials .regular(containerColor = Color .Transparent ),
171- )
172- .background(scanBg)
146+ .background(Colors .Gray7 )
147+ // Overlay: Circle strokeBorder with linear gradient mask (iOS: .mask)
148+ .drawWithContent {
149+ drawContent()
150+
151+ // The mask gradient goes from black (visible) at top to clear (invisible) at bottom
152+ val borderWidth = 2 .dp.toPx()
153+
154+ // Create vertical gradient mask (black to clear)
155+ val maskGradient = Brush .verticalGradient(
156+ colors = listOf (
157+ Color .White , // Top: full opacity (shows border)
158+ Color .Transparent // Bottom: transparent (hides border)
159+ ),
160+ startY = 0f ,
161+ endY = size.height
162+ )
163+
164+ // Draw solid black circular border first, then apply gradient as alpha mask
165+ drawCircle(
166+ color = Color .Black ,
167+ radius = (size.width - borderWidth) / 2 ,
168+ center = Offset (size.width / 2 , size.height / 2 ),
169+ style = Stroke (width = borderWidth),
170+ alpha = 1f
171+ )
172+
173+ // Apply gradient mask by drawing gradient as overlay with BlendMode
174+ drawCircle(
175+ brush = maskGradient,
176+ radius = (size.width - borderWidth) / 2 ,
177+ center = Offset (size.width / 2 , size.height / 2 ),
178+ style = Stroke (width = borderWidth),
179+ blendMode = androidx.compose.ui.graphics.BlendMode .DstIn
180+ )
181+ }
182+ .clickableAlpha { onScanClick() }
183+ .testTag(" Scan" )
173184 ) {
174185 Icon (
175186 painter = painterResource(R .drawable.ic_scan),
176187 contentDescription = stringResource(R .string.wallet__recipient_scan),
177- tint = Colors .Gray2 ,
178- modifier = Modifier .size(32 .dp)
188+ tint = Colors .Gray1 ,
189+ modifier = Modifier .size(22 .dp)
179190 )
180191 }
181192 }
@@ -190,7 +201,8 @@ private fun Preview() {
190201 contentAlignment = Alignment .BottomCenter ,
191202 modifier = Modifier
192203 .fillMaxSize()
193- .background(Colors .Brand )
204+ .background(Colors .Black )
205+ .gradientBackground()
194206 ) {
195207 // Content Behind
196208 Column (
@@ -205,7 +217,6 @@ private fun Preview() {
205217 onSendClick = {},
206218 onReceiveClick = {},
207219 onScanClick = {},
208- hazeState = hazeState,
209220 )
210221 }
211222 }
0 commit comments