Skip to content
This repository was archived by the owner on Aug 21, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.gravatar.app.homeUi.presentation.home.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
internal fun BlurredHeaderBackground(
avatarUrl: String,
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
Box(modifier) {
AsyncImageWithCachePlaceholder(
avatarUrl,
modifier = Modifier
.matchParentSize()
.blur(radius = 40.dp, edgeTreatment = BlurredEdgeTreatment.Rectangle)
.alpha(0.7f)
)
Box(
modifier = Modifier
.matchParentSize()
.background(Color.Black.copy(alpha = 0.2f))
)
content()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.blur
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
Expand All @@ -32,7 +29,7 @@ import androidx.constraintlayout.compose.MotionLayout
import androidx.constraintlayout.compose.MotionScene
import androidx.constraintlayout.compose.layoutId
import com.gravatar.app.homeUi.R
import com.gravatar.app.homeUi.presentation.home.components.AsyncImageWithCachePlaceholder
import com.gravatar.app.homeUi.presentation.home.components.BlurredHeaderBackground
import com.gravatar.app.homeUi.presentation.home.components.GravatarAvatarWithShadow
import com.gravatar.app.homeUi.presentation.home.components.topbar.TopBarPickerPopup

Expand Down Expand Up @@ -61,17 +58,10 @@ internal fun GravatarHeader(
.decodeToString()
}

Box(
modifier = modifier
.fillMaxWidth()
BlurredHeaderBackground(
avatarUrl = avatarUrl,
modifier = modifier.fillMaxWidth(),
) {
AsyncImageWithCachePlaceholder(
avatarUrl,
modifier = Modifier
.matchParentSize()
.blur(radius = 40.dp, edgeTreatment = BlurredEdgeTreatment.Rectangle)
.alpha(0.7f)
)
MotionLayout(
motionScene = MotionScene(content = motionScene),
progress = progress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
Expand All @@ -60,7 +58,7 @@ import androidx.compose.ui.unit.lerp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.lerp
import com.gravatar.app.homeUi.R
import com.gravatar.app.homeUi.presentation.home.components.AsyncImageWithCachePlaceholder
import com.gravatar.app.homeUi.presentation.home.components.BlurredHeaderBackground
import com.gravatar.app.homeUi.presentation.home.components.GravatarAvatarWithShadow
import com.gravatar.app.homeUi.presentation.home.components.topbar.TopBarPickerPopup
import com.gravatar.restapi.models.Profile
Expand All @@ -72,7 +70,7 @@ private val PROFILE_INFO_START_PADDING = 16.dp
private val PROFILE_INFO_TOP_PADDING = 16.dp
private val LINK_TOP_PADDING = 16.dp
private val LINK_INTERNAL_PADDING = 8.dp
private val MENU_BUTTON_SIZE = 44.dp
internal val MENU_BUTTON_SIZE = 44.dp
private const val HEADER_STATE_TRANSITION_DURATION = 300

@Composable
Expand Down Expand Up @@ -258,19 +256,10 @@ private fun AnimatedProfileHeaderSavedState(
label = "linkAlpha"
)

Box(
modifier
.fillMaxWidth()
BlurredHeaderBackground(
avatarUrl = avatarUrl.orEmpty(),
modifier = modifier.fillMaxWidth(),
) {
// Background image with blur
AsyncImageWithCachePlaceholder(
avatarUrl.orEmpty(),
modifier = Modifier
.matchParentSize()
.blur(radius = 40.dp, edgeTreatment = BlurredEdgeTreatment.Rectangle)
.alpha(0.7f)
)

// Content container with animated layout
Box(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package com.gravatar.app.homeUi.presentation.home.share

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModelStoreOwner
import com.gravatar.app.design.theme.GravatarAppTheme
import com.gravatar.app.homeUi.presentation.home.share.components.ShareHeader
import org.koin.androidx.compose.koinViewModel

Expand Down Expand Up @@ -38,6 +41,21 @@ internal fun ShareScreen(uiState: ShareUiState, onEvent: (ShareEvent) -> Unit) {
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
ShareHeader()
ShareHeader(
avatarUrl = uiState.avatarUrl.orEmpty(),
modifier = Modifier
.fillMaxWidth()
)
}
}

@Preview
@Composable
private fun ShareScreenPreview() {
GravatarAppTheme {
ShareScreen(
uiState = ShareUiState(),
onEvent = { }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import com.gravatar.restapi.models.Profile

internal data class ShareUiState(
val profile: Profile? = null,
val avatarUrl: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.gravatar.app.homeUi.presentation.home.share
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.gravatar.app.usercomponent.domain.repository.UserRepository
import com.gravatar.app.usercomponent.domain.usecase.GetAvatarUrl
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -12,18 +13,32 @@ import kotlinx.coroutines.flow.update

internal class ShareViewModel(
private val userRepository: UserRepository,
private val getAvatarUrl: GetAvatarUrl,
) : ViewModel() {

private val _uiState = MutableStateFlow(ShareUiState())
internal val uiState: StateFlow<ShareUiState> = _uiState.asStateFlow()

init {
collectProfile()
collectAvatarUrl()
}

@Suppress("UnusedParameter")
fun onEvent(shareEvent: ShareEvent) = Unit

private fun collectAvatarUrl() {
getAvatarUrl()
.onEach { avatarUrl ->
_uiState.update { currentState ->
currentState.copy(
avatarUrl = avatarUrl?.toString(),
)
}
}
.launchIn(viewModelScope)
}

private fun collectProfile() {
userRepository.getProfile()
.onEach { profile ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,113 @@
package com.gravatar.app.homeUi.presentation.home.share.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.gravatar.app.design.theme.GravatarAppTheme
import com.gravatar.app.homeUi.R
import com.gravatar.app.homeUi.presentation.home.components.BlurredHeaderBackground
import com.gravatar.app.homeUi.presentation.home.components.topbar.TopBarPickerPopup
import com.gravatar.app.homeUi.presentation.home.profile.header.MENU_BUTTON_SIZE

@Composable
internal fun ShareHeader() {
// Empty for now as per requirements
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
internal fun ShareHeader(
avatarUrl: String,
modifier: Modifier = Modifier,
) {
var topBarMenuVisible by remember { mutableStateOf(false) }

BlurredHeaderBackground(
avatarUrl = avatarUrl,
modifier = modifier.fillMaxWidth()
) {
// Content will be added later
Row(
modifier = Modifier
.statusBarsPadding()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(22.dp),
verticalAlignment = Alignment.Top
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.padding(top = 6.dp)
.weight(1f),
) {
Box(
modifier = Modifier
.background(Color.White, RoundedCornerShape(4.dp))
.fillMaxWidth()
.aspectRatio(
ratio = 1f,
matchHeightConstraintsFirst = false,
)
)
Text(
text = stringResource(R.string.share_tab_scan_qr_code),
style = MaterialTheme.typography.bodyMedium,
color = Color.White,
)
}
Column {
Box {
IconButton(
onClick = {
topBarMenuVisible = true
},
modifier = Modifier
.size(MENU_BUTTON_SIZE)
) {
Image(
painter = painterResource(id = R.drawable.more_button),
contentDescription = stringResource(R.string.gravatar_tab_header_more_options),
)
}
if (topBarMenuVisible) {
TopBarPickerPopup(
anchorAlignment = Alignment.End,
offset = DpOffset(0.dp, 6.dp),
onDismissRequest = { topBarMenuVisible = false },
)
}
}
}
}
}
}

@Preview
@Composable
private fun ShareHeaderPreview() {
GravatarAppTheme {
ShareHeader(
avatarUrl = "url",
modifier = Modifier
.fillMaxWidth()
)
}
}
1 change: 1 addition & 0 deletions homeUi/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@
<string name="gravatar_tab_avatar_download_started">Image download has been queued. You\'ll receive a notification when it\'s complete.</string>
<string name="gravatar_tab_no_avatar_selected_info">No avatar selected. Showing the default avatar.</string>
<string name="home_no_internet_available">No internet connection</string>
<string name="share_tab_scan_qr_code">Let others scan this QR code to share your contact information.</string>

</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.gravatar.app.homeUi.presentation.home.share.components

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.ui.Modifier
import com.gravatar.app.design.theme.GravatarAppTheme
import com.gravatar.app.testUtils.roborazzi.RoborazziTest
import org.junit.Test

class ShareHeaderTest : RoborazziTest() {

@Test
fun shareHeader() = screenshotTest {
GravatarAppTheme {
ShareHeader(
avatarUrl = "url",
modifier = Modifier.fillMaxWidth()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import coil.Coil
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.test.FakeImageLoaderEngine
import com.dropbox.differ.SimpleImageComparator
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.RoborazziOptions
import com.github.takahirom.roborazzi.RoborazziRule
import com.github.takahirom.roborazzi.captureRoboImage
import org.junit.Before
Expand All @@ -37,6 +39,12 @@ abstract class RoborazziTest {
val roborazziRule = RoborazziRule(
options = RoborazziRule.Options(
outputDirectoryPath = SCREENSHOTS_PATH,
roborazziOptions = RoborazziOptions(
compareOptions = RoborazziOptions.CompareOptions(
imageComparator = SimpleImageComparator(maxDistance = 0.007F, hShift = 1),
changeThreshold = 0.005f,
),
),
),
)

Expand Down