diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 305fce5..541be3a 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -14,6 +14,7 @@ + diff --git a/app/src/main/java/com/techlads/composetv/features/wiw/WhoIsWatching.kt b/app/src/main/java/com/techlads/composetv/features/wiw/WhoIsWatching.kt new file mode 100644 index 0000000..8cca173 --- /dev/null +++ b/app/src/main/java/com/techlads/composetv/features/wiw/WhoIsWatching.kt @@ -0,0 +1,9 @@ +package com.techlads.composetv.features.wiw + +import androidx.compose.runtime.Composable +import com.techlads.composetv.features.wiw.data.Avatar + +@Composable +fun WhoIsWatchingScreen(onProfileSelection: (avatar: Avatar) -> Unit) { + WhoIsWatchingContent(onProfileSelection) +} \ No newline at end of file diff --git a/app/src/main/java/com/techlads/composetv/features/wiw/WhoIsWatchingContent.kt b/app/src/main/java/com/techlads/composetv/features/wiw/WhoIsWatchingContent.kt new file mode 100644 index 0000000..a31609a --- /dev/null +++ b/app/src/main/java/com/techlads/composetv/features/wiw/WhoIsWatchingContent.kt @@ -0,0 +1,241 @@ +@file:OptIn(ExperimentalTvMaterial3Api::class) + +package com.techlads.composetv.features.wiw + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.with +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.tv.foundation.PivotOffsets +import androidx.tv.foundation.lazy.list.TvLazyRow +import androidx.tv.material3.Border +import androidx.tv.material3.ClickableSurfaceDefaults +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.Icon +import androidx.tv.material3.IconButton +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Text +import com.techlads.composetv.R +import com.techlads.composetv.features.wiw.data.Avatar +import kotlinx.coroutines.delay + +private val avatarList = listOf( + Avatar( + title = "Jack", + image = R.drawable.boy + ), Avatar( + title = "Alice", + image = R.drawable.girl + ), Avatar( + title = "Archit", + image = R.drawable.old + ) +) + +@Composable +fun WhoIsWatchingContent(onProfileSelection: (avatar: Avatar) -> Unit) { + + //initial height set at 0.dp + var containerWidth by remember { mutableStateOf(0.dp) } + // get local density from composable + val density = LocalDensity.current + + + Box( + modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { + containerWidth = with(density) { + it.size.width.toDp() + } + }, contentAlignment = Alignment.Center + ) { + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + + var selectedAvatar by remember { + mutableStateOf("") + } + + var isLeft by remember { + mutableStateOf(false) + } + + var lastPosition by remember { + mutableStateOf(0) + } + + val requester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + delay(10) + requester.requestFocus() + } + + Text( + text = "Who's Watching?", + style = MaterialTheme.typography.titleLarge.copy(fontSize = 38.sp), + modifier = Modifier.padding(top = 32.dp) + ) + + Spacer(modifier = Modifier.size(68.dp)) + + TvLazyRow( + contentPadding = PaddingValues(horizontal = containerWidth / 2), + horizontalArrangement = Arrangement.Center, + pivotOffsets = PivotOffsets(0.5f, 0.5f), + modifier = Modifier.fillMaxWidth() + ) { + items(avatarList.size) { + val item = avatarList[it] + val itemIndex = it + ScaleAbleAvatar( + avatarRes = item.image, + modifier = Modifier + .then(if (it == 1) Modifier.focusRequester(requester) else Modifier) + .onFocusChanged { + if (it.isFocused) { + isLeft = lastPosition > itemIndex + lastPosition = itemIndex + } + selectedAvatar = item.title + }, + onProfileSelection = onProfileSelection + ) + } + } + + if (selectedAvatar.isNotEmpty()) { + Spacer(modifier = Modifier.padding(top = 48.dp)) + ProfileName(name = selectedAvatar, isLeft) + } + + Spacer(modifier = Modifier.size(38.dp)) + + IconButton(onClick = { /*TODO*/ }) { + Icon( + painter = painterResource(id = R.drawable.ic_settings), + contentDescription = "Settings" + ) + } + } + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun ProfileName(name: String, scaleUp: Boolean) { + AnimatedContent( + targetState = name, + transitionSpec = { + // Compare the incoming number with the previous number. + if (scaleUp) { + // If the target number is larger, it slides up and fades in + // while the initial (smaller) number slides up and fades out. + slideInVertically { height -> height } + fadeIn() with + slideOutVertically { height -> -height } + fadeOut() + } else { + // If the target number is smaller, it slides down and fades in + // while the initial number slides down and fades out. + slideInVertically { height -> -height } + fadeIn() with + slideOutVertically { height -> height } + fadeOut() + }.using( + // Disable clipping since the faded slide-in/out should + // be displayed out of bounds. + SizeTransform(clip = false) + ) + }, label = "" + ) { text -> + Text( + text = text, + style = MaterialTheme.typography.titleSmall.copy(fontSize = 24.sp), + modifier = Modifier.padding(top = 32.dp) + ) + } +} + +@Composable +fun ScaleAbleAvatar( + modifier: Modifier, + avatarRes: Int, + onProfileSelection: (avatar: Avatar) -> Unit +) { + Surface( + onClick = { + onProfileSelection(avatarList[avatarRes]) + }, + modifier = modifier.padding(horizontal = 32.dp), + border = ClickableSurfaceDefaults.border( + border = Border( + border = BorderStroke( + 4.dp, Color.Transparent + ), shape = CircleShape + ), focusedBorder = Border( + border = BorderStroke( + 4.dp, Color.White + ), shape = CircleShape + ) + ), + shape = ClickableSurfaceDefaults.shape(shape = CircleShape), + scale = ClickableSurfaceDefaults.scale(focusedScale = 1.5f) + ) { + AvatarIcon(avatarRes = avatarRes, modifier = Modifier.size(120.dp)) + } +} + +@Composable +fun AvatarIcon(modifier: Modifier, @DrawableRes avatarRes: Int, description: String? = null) { + Image( + painter = painterResource(id = avatarRes), + contentDescription = description, + contentScale = ContentScale.Crop, + modifier = modifier.aspectRatio(1f) + ) +} + +@Preview(device = Devices.TV_1080p, showBackground = true) +@Composable +private fun WhoIsWatchingPreview() { + WhoIsWatchingContent { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/techlads/composetv/features/wiw/data/Avatar.kt b/app/src/main/java/com/techlads/composetv/features/wiw/data/Avatar.kt new file mode 100644 index 0000000..6a332e4 --- /dev/null +++ b/app/src/main/java/com/techlads/composetv/features/wiw/data/Avatar.kt @@ -0,0 +1,5 @@ +package com.techlads.composetv.features.wiw.data + +import androidx.annotation.DrawableRes + +data class Avatar(val title: String, @DrawableRes val image: Int) diff --git a/app/src/main/java/com/techlads/composetv/navigation/AppNavigation.kt b/app/src/main/java/com/techlads/composetv/navigation/AppNavigation.kt index 6153c1e..4ff01c9 100644 --- a/app/src/main/java/com/techlads/composetv/navigation/AppNavigation.kt +++ b/app/src/main/java/com/techlads/composetv/navigation/AppNavigation.kt @@ -8,14 +8,15 @@ import androidx.compose.animation.fadeOut import androidx.compose.runtime.Composable import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController -import com.google.accompanist.navigation.animation.composable import com.google.accompanist.navigation.animation.AnimatedNavHost +import com.google.accompanist.navigation.animation.composable import com.techlads.composetv.features.details.ProductDetailsScreen import com.techlads.composetv.features.home.HomeScreen import com.techlads.composetv.features.home.HomeViewModel -import com.techlads.composetv.features.login.withToken.DeviceTokenAuthenticationScreen import com.techlads.composetv.features.login.withEmailPassword.LoginScreen +import com.techlads.composetv.features.login.withToken.DeviceTokenAuthenticationScreen import com.techlads.composetv.features.player.PlayerScreen +import com.techlads.composetv.features.wiw.WhoIsWatchingScreen @OptIn(ExperimentalAnimationApi::class) @Composable @@ -27,7 +28,7 @@ fun AppNavigation(navController: NavHostController, viewModel: HomeViewModel) { enterTransition = { tabEnterTransition() }, exitTransition = { tabExitTransition() }) { LoginScreen { - navController.navigateSingleTopTo(Screens.Home.title) + navController.navigateSingleTopTo(Screens.WhoIsWatching.title) } } @@ -40,6 +41,15 @@ fun AppNavigation(navController: NavHostController, viewModel: HomeViewModel) { } } + composable( + Screens.WhoIsWatching.title, + enterTransition = { tabEnterTransition() }, + exitTransition = { tabExitTransition() }) { + WhoIsWatchingScreen { + navController.navigateSingleTopTo(Screens.Home.title) + } + } + composable( Screens.Home.title, enterTransition = { tabEnterTransition() }, diff --git a/app/src/main/java/com/techlads/composetv/navigation/Screens.kt b/app/src/main/java/com/techlads/composetv/navigation/Screens.kt index 6f2cf5b..93ef284 100644 --- a/app/src/main/java/com/techlads/composetv/navigation/Screens.kt +++ b/app/src/main/java/com/techlads/composetv/navigation/Screens.kt @@ -6,4 +6,5 @@ sealed class Screens(val title: String) { object Home : Screens("home_screen") object Player : Screens("player_screen") object ProductDetail : Screens("product_detail") + object WhoIsWatching : Screens("who_is_watching") } \ No newline at end of file diff --git a/app/src/main/res/drawable/boy.png b/app/src/main/res/drawable/boy.png new file mode 100644 index 0000000..aa134ff Binary files /dev/null and b/app/src/main/res/drawable/boy.png differ diff --git a/app/src/main/res/drawable/boy3.png b/app/src/main/res/drawable/boy3.png new file mode 100644 index 0000000..cd6f26f Binary files /dev/null and b/app/src/main/res/drawable/boy3.png differ diff --git a/app/src/main/res/drawable/girl.png b/app/src/main/res/drawable/girl.png new file mode 100644 index 0000000..7accc02 Binary files /dev/null and b/app/src/main/res/drawable/girl.png differ diff --git a/app/src/main/res/drawable/men.jpeg b/app/src/main/res/drawable/men.jpeg new file mode 100644 index 0000000..34f3c55 Binary files /dev/null and b/app/src/main/res/drawable/men.jpeg differ diff --git a/app/src/main/res/drawable/old.png b/app/src/main/res/drawable/old.png new file mode 100644 index 0000000..12bd832 Binary files /dev/null and b/app/src/main/res/drawable/old.png differ