Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.test.espresso.Espresso
Expand All @@ -49,13 +53,14 @@ class NavigationTest {
@Test
fun profileScreen_back_conversationScreen() {
val navController = getNavController()
// Navigate to profile \
// Navigate to profile
navigateToProfile("Taylor Brooks")
// Check profile is displayed
assertEquals(navController.currentDestination?.id, R.id.nav_profile)
// Extra UI check
// Extra UI check (scroll to Display name as the new profile header is taller)
composeTestRule
.onNodeWithText(composeTestRule.activity.getString(R.string.display_name))
.performScrollTo()
.assertIsDisplayed()

// Press back
Expand All @@ -79,10 +84,25 @@ class NavigationTest {
assertEquals(getNavController().currentDestination?.id, R.id.nav_home)
}

private fun openDrawer() {
val navDrawerNodes = composeTestRule
.onAllNodesWithContentDescription(
composeTestRule.activity.getString(R.string.navigation_drawer_open),
)
.fetchSemanticsNodes()
if (navDrawerNodes.isNotEmpty()) {
composeTestRule.onNodeWithContentDescription(
composeTestRule.activity.getString(R.string.navigation_drawer_open),
).performClick()
} else {
composeTestRule.runOnUiThread {
ViewModelProvider(composeTestRule.activity)[MainViewModel::class.java].openDrawer()
}
}
}

private fun navigateToProfile(name: String) {
composeTestRule.onNodeWithContentDescription(
composeTestRule.activity.getString(R.string.navigation_drawer_open),
).performClick()
openDrawer()

composeTestRule.onNode(hasText(name) and isInDrawer()).performClick()
}
Expand All @@ -95,9 +115,7 @@ class NavigationTest {
)

private fun navigateToHome() {
composeTestRule.onNodeWithContentDescription(
composeTestRule.activity.getString(R.string.navigation_drawer_open),
).performClick()
openDrawer()

composeTestRule.onNode(hasText("composers") and isInDrawer()).performClick()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ val exampleUiState = ConversationUiState(
*/
val colleagueProfile = ProfileScreenState(
userId = "12345",
photo = R.drawable.someone_else,
photo = R.drawable.android_profile_pic,
name = "Taylor Brooks",
status = "Away",
displayName = "taylor",
Expand All @@ -264,7 +264,7 @@ val colleagueProfile = ProfileScreenState(
*/
val meProfile = ProfileScreenState(
userId = "me",
photo = R.drawable.ali,
photo = R.drawable.android_profile_pic,
name = "Ali Conors",
status = "Online",
displayName = "aliconors",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,20 @@

package com.example.compose.jetchat.profile

import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
Expand All @@ -41,21 +39,18 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.blur.BlurRadiusSpec
import androidx.compose.ui.graphics.blur.BlurStop
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
Expand All @@ -68,58 +63,42 @@ import com.example.compose.jetchat.components.baselineHeight
import com.example.compose.jetchat.data.colleagueProfile
import com.example.compose.jetchat.data.meProfile
import com.example.compose.jetchat.theme.JetchatTheme
import com.example.compose.jetchat.theme.ProfileTheme

@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun ProfileScreen(
userData: ProfileScreenState,
nestedScrollInteropConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(),
) {
fun ProfileScreen(userData: ProfileScreenState) {
var functionalityNotAvailablePopupShown by remember { mutableStateOf(false) }
if (functionalityNotAvailablePopupShown) {
FunctionalityNotAvailablePopup { functionalityNotAvailablePopupShown = false }
}

val scrollState = rememberScrollState()

BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollInteropConnection)
.systemBarsPadding(),
) {
Surface {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
ProfileHeader(
scrollState,
userData,
this@BoxWithConstraints.maxHeight,
)
UserInfoFields(userData, this@BoxWithConstraints.maxHeight)
ProfileTheme {
BoxWithConstraints(
modifier = Modifier
.fillMaxSize(),
) {
Surface {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.surfaceContainer)
.verticalScroll(scrollState),
) {
ProfileHeader(userData)
UserInfoFields(userData, this@BoxWithConstraints.maxHeight)
}
}
}

val fabExtended by remember { derivedStateOf { scrollState.value == 0 } }
ProfileFab(
extended = fabExtended,
userIsMe = userData.isMe(),
modifier = Modifier
.align(Alignment.BottomEnd)
// Offsets the FAB to compensate for CoordinatorLayout collapsing behaviour
.offset(y = ((-100).dp)),
onFabClicked = { functionalityNotAvailablePopupShown = true },
)
}
}

@Composable
private fun UserInfoFields(userData: ProfileScreenState, containerHeight: Dp) {
Column {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(40.dp))

NameAndPosition(userData)

Expand Down Expand Up @@ -160,7 +139,8 @@ private fun Name(userData: ProfileScreenState, modifier: Modifier = Modifier) {
Text(
text = userData.name,
modifier = modifier,
style = MaterialTheme.typography.headlineSmall,
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onSurface,
)
}

Expand All @@ -175,22 +155,23 @@ private fun Position(userData: ProfileScreenState, modifier: Modifier = Modifier
}

@Composable
private fun ProfileHeader(scrollState: ScrollState, data: ProfileScreenState, containerHeight: Dp) {
val offset = (scrollState.value / 2)
val offsetDp = with(LocalDensity.current) { offset.toDp() }
private fun ProfileHeader(data: ProfileScreenState) {

data.photo?.let {
Image(
modifier = Modifier
.heightIn(max = containerHeight / 2)
.fillMaxWidth()
// TODO: Update to use offset to avoid recomposition
.padding(
start = 16.dp,
top = offsetDp,
end = 16.dp,
)
.clip(CircleShape),
.aspectRatio(0.96f)
.blur {
radius = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.0f, 0.dp),
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
),
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
},
painter = painterResource(id = it),
contentScale = ContentScale.Crop,
contentDescription = null,
Expand All @@ -205,7 +186,7 @@ fun ProfileProperty(label: String, value: String, isLink: Boolean = false) {
Text(
text = label,
modifier = Modifier.baselineHeight(24.dp),
style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
val style = if (isLink) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,15 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import com.example.compose.jetchat.FunctionalityNotAvailablePopup
import com.example.compose.jetchat.MainViewModel
import com.example.compose.jetchat.R
import com.example.compose.jetchat.components.JetchatAppBar
import com.example.compose.jetchat.theme.JetchatTheme

class ProfileFragment : Fragment() {
Expand All @@ -62,58 +45,25 @@ class ProfileFragment : Fragment() {
}

@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val rootView: View = inflater.inflate(R.layout.fragment_profile, container, false)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
ComposeView(inflater.context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)

rootView.findViewById<ComposeView>(R.id.toolbar_compose_view).apply {
setContent {
var functionalityNotAvailablePopupShown by remember { mutableStateOf(false) }
if (functionalityNotAvailablePopupShown) {
FunctionalityNotAvailablePopup { functionalityNotAvailablePopupShown = false }
}

JetchatTheme {
JetchatAppBar(
// Reset the minimum bounds that are passed to the root of a compose tree
modifier = Modifier.wrapContentSize(),
onNavIconPressed = { activityViewModel.openDrawer() },
title = { },
actions = {
// More icon
Icon(
painter = painterResource(id = R.drawable.ic_more_vert),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.clickable(onClick = {
functionalityNotAvailablePopupShown = true
})
.padding(horizontal = 12.dp, vertical = 16.dp)
.height(24.dp),
contentDescription = stringResource(id = R.string.more_options),
)
},
)
}
}
}

rootView.findViewById<ComposeView>(R.id.profile_compose_view).apply {
setContent {
val userData by viewModel.userData.observeAsState()
val nestedScrollInteropConnection = rememberNestedScrollInteropConnection()

JetchatTheme {
if (userData == null) {
ProfileError()
} else {
ProfileScreen(
userData = userData!!,
nestedScrollInteropConnection = nestedScrollInteropConnection,
)
}
}
}
}
return rootView
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ val BlueGrey50 = Color(0xFF767680)
val BlueGrey60 = Color(0xFF90909A)
val BlueGrey80 = Color(0xFFC6C5D0)
val BlueGrey90 = Color(0xFFE2E1EC)

val ProfileSurface = Color(0xFFEAE9FC)
val ProfileOnSurface = Color(0xFF1D1B1F)
val ProfileOnSurfaceVariant = Color(0xFF48454F)
Loading
Loading