refact detail screen composables

This commit is contained in:
Owen LeJeune
2022-02-15 23:32:18 -05:00
parent cbb44513b6
commit 11e4f964be
5 changed files with 254 additions and 167 deletions

View File

@@ -9,5 +9,6 @@ abstract class DetailedItem(
@Transient open val overview: String?,
@Transient open val productionCompanies: List<ProductionCompany>,
@Transient open val status: String,
@Transient open val tagline: String?
@Transient open val tagline: String?,
@Transient open val voteAverage: Float
): TmdbItem(id, title, posterPath)

View File

@@ -12,9 +12,10 @@ class DetailedMovie(
@SerializedName("production_companies") override val productionCompanies: List<ProductionCompany>,
@SerializedName("status") override val status: String,
@SerializedName("tagline") override val tagline: String?,
@SerializedName("vote_average") override val voteAverage: Float,
@SerializedName("adult") val isAdult: Boolean,
@SerializedName("budget") val budget: Int,
@SerializedName("release_date") val releaseDate: String,
@SerializedName("revenue") val revenue: Int,
@SerializedName("runtime") val runtime: Int?
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline)
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline, voteAverage)

View File

@@ -12,6 +12,7 @@ class DetailedTv(
@SerializedName("production_companies") override val productionCompanies: List<ProductionCompany>,
@SerializedName("status") override val status: String,
@SerializedName("tagline") override val tagline: String?,
@SerializedName("vote_average") override val voteAverage: Float,
@SerializedName("created_by") val createdBy: List<Person>,
@SerializedName("first_air_date") val firstAirDate: String,
@SerializedName("in_production") val inProduction: Boolean,
@@ -19,4 +20,4 @@ class DetailedTv(
@SerializedName("number_of_episodes") val numberOfEpisodes: Int,
@SerializedName("number_of_seasons") val numberOfSeasons: Int,
@SerializedName("seasons") val seasons: List<Season>
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline)
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline, voteAverage)

View File

@@ -10,10 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@@ -23,10 +20,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -154,4 +161,49 @@ fun SearchFab() {
@Composable
fun SearchFabPreview() {
SearchFab()
}
@Composable
fun MinLinesText(
text: String,
modifier: Modifier = Modifier,
minLines: Int = 1,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
val lineHeight = style.fontSize*4/3
Text(
modifier = modifier
.sizeIn(
minHeight = with(LocalDensity.current) {
(lineHeight * minLines).toDp()
}
),
text = text,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = style
)
}

View File

@@ -14,18 +14,13 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.navigation.NavController
import coil.compose.rememberImagePainter
@@ -35,11 +30,10 @@ import com.owenlejeune.tvtime.api.tmdb.DetailService
import com.owenlejeune.tvtime.api.tmdb.MoviesService
import com.owenlejeune.tvtime.api.tmdb.TmdbUtils
import com.owenlejeune.tvtime.api.tmdb.TvService
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.model.*
import com.owenlejeune.tvtime.extensions.dpToPx
import com.owenlejeune.tvtime.ui.components.BackdropImage
import com.owenlejeune.tvtime.ui.components.MinLinesText
import com.owenlejeune.tvtime.ui.components.PosterItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -65,13 +59,6 @@ fun DetailView(
}
}
val images = remember { mutableStateOf<ImageCollection?>(null) }
itemId?.let {
if (images.value == null) {
fetchImages(itemId, service, images)
}
}
val scrollState = rememberScrollState()
ConstraintLayout(
@@ -81,23 +68,15 @@ fun DetailView(
.verticalScroll(state = scrollState)
) {
val (
backButton,
backdropImage,
posterImage,
titleText,
contentColumn
backButton, backdropImage, posterImage, titleText, contentColumn
) = createRefs()
BackdropImage(
modifier = Modifier
.constrainAs(backdropImage) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
.fillMaxWidth()
.height(280.dp),
imageUrl = TmdbUtils.getFullBackdropPath(mediaItem.value),
// collection = images.value
Backdrop(
modifier = Modifier.constrainAs(backdropImage) {
top.linkTo(parent.top)
start.linkTo(parent.start)
},
mediaItem = mediaItem
)
PosterItem(
@@ -110,151 +89,204 @@ fun DetailView(
}
)
Text(
text = mediaItem.value?.title ?: "",
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.constrainAs(titleText) {
bottom.linkTo(posterImage.bottom)
start.linkTo(posterImage.end, margin = 8.dp)
end.linkTo(parent.end, margin = 16.dp)
}
.padding(start = 16.dp, end = 16.dp)
.fillMaxWidth(.6f),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Start,
softWrap = true
TitleText(
modifier = Modifier.constrainAs(titleText) {
bottom.linkTo(posterImage.bottom)
start.linkTo(posterImage.end, margin = 8.dp)
end.linkTo(parent.end, margin = 16.dp)
},
mediaItem = mediaItem
)
IconButton(
onClick = { appNavController.popBackStack() },
modifier = Modifier
.constrainAs(backButton) {
top.linkTo(parent.top)//, 8.dp)
start.linkTo(parent.start, 12.dp)
bottom.linkTo(posterImage.top)
}
.background(
brush = Brush.radialGradient(
colors = listOf(
Color.Black,
Color.Transparent
)
BackButton(
modifier = Modifier.constrainAs(backButton) {
top.linkTo(parent.top)//, 8.dp)
start.linkTo(parent.start, 12.dp)
bottom.linkTo(posterImage.top)
},
appNavController = appNavController
)
ContentColumn(
modifier = Modifier.constrainAs(contentColumn) {
top.linkTo(backdropImage.bottom, margin = 8.dp)
},
itemId = itemId,
mediaItem = mediaItem,
service = service
)
}
}
@Composable
private fun Backdrop(modifier: Modifier, mediaItem: MutableState<DetailedItem?>) {
// val images = remember { mutableStateOf<ImageCollection?>(null) }
// itemId?.let {
// if (images.value == null) {
// fetchImages(itemId, service, images)
// }
// }
BackdropImage(
modifier = modifier
.fillMaxWidth()
.height(280.dp),
imageUrl = TmdbUtils.getFullBackdropPath(mediaItem.value),
// collection = images.value
)
}
@Composable
private fun TitleText(modifier: Modifier, mediaItem: MutableState<DetailedItem?>) {
Text(
text = mediaItem.value?.title ?: "",
color = MaterialTheme.colorScheme.primary,
modifier = modifier
.padding(start = 16.dp, end = 16.dp)
.fillMaxWidth(.6f),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Start,
softWrap = true
)
}
@Composable
private fun BackButton(modifier: Modifier, appNavController: NavController) {
IconButton(
onClick = { appNavController.popBackStack() },
modifier = modifier
.background(
brush = Brush.radialGradient(
colors = listOf(
Color.Black,
Color.Transparent
)
)
.wrapContentSize()
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
.wrapContentSize()
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
}
val castAndCrew = remember { mutableStateOf<CastAndCrew?>(null) }
itemId?.let {
if (castAndCrew.value == null) {
fetchCastAndCrew(itemId, service, castAndCrew)
}
}
@Composable
private fun ContentColumn(modifier: Modifier,
itemId: Int?,
mediaItem: MutableState<DetailedItem?>,
service: DetailService
) {
Column(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 16.dp)
) {
OverviewCard(mediaItem = mediaItem)
Column(
CastCard(itemId = itemId, service = service)
}
}
@Composable
private fun OverviewCard(mediaItem: MutableState<DetailedItem?>) {
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 12.dp),
shape = RoundedCornerShape(10.dp),
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
elevation = 8.dp
) {
Text(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 16.dp)
.constrainAs(contentColumn) {
top.linkTo(backdropImage.bottom, margin = 8.dp)
}
) {
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 12.dp),
shape = RoundedCornerShape(10.dp),
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
elevation = 8.dp
) {
Text(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(vertical = 12.dp, horizontal = 16.dp),
text = mediaItem.value?.overview ?: "",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
}
.padding(vertical = 12.dp, horizontal = 16.dp),
text = mediaItem.value?.overview ?: "",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
}
}
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
shape = RoundedCornerShape(10.dp),
backgroundColor = MaterialTheme.colorScheme.primary,
elevation = 8.dp
) {
LazyRow(modifier = Modifier
.fillMaxSize()
.padding(12.dp)
) {
items(castAndCrew.value?.cast?.size ?: 0) { i ->
val castMember = castAndCrew.value!!.cast[i]
Column(
modifier = Modifier
.width(124.dp)
.wrapContentHeight()
.padding(end = 12.dp)
) {
Image(
modifier = Modifier
.size(width = 120.dp, height = 180.dp),
painter = rememberImagePainter(
data = TmdbUtils.getFullPersonImagePath(castMember),
builder = {
transformations(RoundedCornersTransformation(5f.dpToPx(context)))
placeholder(R.drawable.placeholder)
}
),
contentDescription = ""
)
val nameLineHeight = MaterialTheme.typography.bodyMedium.fontSize*4/3
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 5.dp)
.sizeIn(
minHeight = with(LocalDensity.current) {
(nameLineHeight * 2).toDp()
}
),
text = castMember.name,
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.bodyMedium,
lineHeight = nameLineHeight
)
val characterLineHeight = MaterialTheme.typography.bodySmall.fontSize*4/3
Text(
modifier = Modifier
.fillMaxWidth()
.sizeIn(
minHeight = with(LocalDensity.current) {
(characterLineHeight * 2).toDp()
}
),
text = castMember.character,
style = MaterialTheme.typography.bodySmall,
lineHeight = characterLineHeight
)
}
}
}
@Composable
private fun CastCard(itemId: Int?, service: DetailService) {
val castAndCrew = remember { mutableStateOf<CastAndCrew?>(null) }
itemId?.let {
if (castAndCrew.value == null) {
fetchCastAndCrew(itemId, service, castAndCrew)
}
}
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
shape = RoundedCornerShape(10.dp),
backgroundColor = MaterialTheme.colorScheme.primary,
elevation = 8.dp
) {
LazyRow(modifier = Modifier
.fillMaxSize()
.padding(12.dp)
) {
items(castAndCrew.value?.cast?.size ?: 0) { i ->
val castMember = castAndCrew.value!!.cast[i]
CastCrewCard(person = castMember)
}
}
}
}
@Composable
private fun CastCrewCard(person: Person) {
val context = LocalContext.current
Column(
modifier = Modifier
.width(124.dp)
.wrapContentHeight()
.padding(end = 12.dp)
) {
Image(
modifier = Modifier
.size(width = 120.dp, height = 180.dp),
painter = rememberImagePainter(
data = TmdbUtils.getFullPersonImagePath(person),
builder = {
transformations(RoundedCornersTransformation(5f.dpToPx(context)))
placeholder(R.drawable.placeholder)
}
),
contentDescription = ""
)
MinLinesText(
modifier = Modifier
.fillMaxWidth()
.padding(top = 5.dp),
minLines = 2,
text = person.name,
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.bodyMedium
)
MinLinesText(
modifier = Modifier
.fillMaxWidth(),
minLines = 2,
text = when (person) {
is CastMember -> person.character
is CrewMember -> person.job
else -> ""
},
style = MaterialTheme.typography.bodySmall
)
}
}
private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) {
CoroutineScope(Dispatchers.IO).launch {
val response = service.getById(id)