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 overview: String?,
@Transient open val productionCompanies: List<ProductionCompany>, @Transient open val productionCompanies: List<ProductionCompany>,
@Transient open val status: String, @Transient open val status: String,
@Transient open val tagline: String? @Transient open val tagline: String?,
@Transient open val voteAverage: Float
): TmdbItem(id, title, posterPath) ): TmdbItem(id, title, posterPath)

View File

@@ -12,9 +12,10 @@ class DetailedMovie(
@SerializedName("production_companies") override val productionCompanies: List<ProductionCompany>, @SerializedName("production_companies") override val productionCompanies: List<ProductionCompany>,
@SerializedName("status") override val status: String, @SerializedName("status") override val status: String,
@SerializedName("tagline") override val tagline: String?, @SerializedName("tagline") override val tagline: String?,
@SerializedName("vote_average") override val voteAverage: Float,
@SerializedName("adult") val isAdult: Boolean, @SerializedName("adult") val isAdult: Boolean,
@SerializedName("budget") val budget: Int, @SerializedName("budget") val budget: Int,
@SerializedName("release_date") val releaseDate: String, @SerializedName("release_date") val releaseDate: String,
@SerializedName("revenue") val revenue: Int, @SerializedName("revenue") val revenue: Int,
@SerializedName("runtime") val runtime: 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("production_companies") override val productionCompanies: List<ProductionCompany>,
@SerializedName("status") override val status: String, @SerializedName("status") override val status: String,
@SerializedName("tagline") override val tagline: String?, @SerializedName("tagline") override val tagline: String?,
@SerializedName("vote_average") override val voteAverage: Float,
@SerializedName("created_by") val createdBy: List<Person>, @SerializedName("created_by") val createdBy: List<Person>,
@SerializedName("first_air_date") val firstAirDate: String, @SerializedName("first_air_date") val firstAirDate: String,
@SerializedName("in_production") val inProduction: Boolean, @SerializedName("in_production") val inProduction: Boolean,
@@ -19,4 +20,4 @@ class DetailedTv(
@SerializedName("number_of_episodes") val numberOfEpisodes: Int, @SerializedName("number_of_episodes") val numberOfEpisodes: Int,
@SerializedName("number_of_seasons") val numberOfSeasons: Int, @SerializedName("number_of_seasons") val numberOfSeasons: Int,
@SerializedName("seasons") val seasons: List<Season> @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.Card
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.*
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -23,10 +20,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity 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.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -155,3 +162,48 @@ fun SearchFab() {
fun SearchFabPreview() { fun SearchFabPreview() {
SearchFab() 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.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.rememberImagePainter 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.MoviesService
import com.owenlejeune.tvtime.api.tmdb.TmdbUtils import com.owenlejeune.tvtime.api.tmdb.TmdbUtils
import com.owenlejeune.tvtime.api.tmdb.TvService import com.owenlejeune.tvtime.api.tmdb.TvService
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew import com.owenlejeune.tvtime.api.tmdb.model.*
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection
import com.owenlejeune.tvtime.extensions.dpToPx import com.owenlejeune.tvtime.extensions.dpToPx
import com.owenlejeune.tvtime.ui.components.BackdropImage import com.owenlejeune.tvtime.ui.components.BackdropImage
import com.owenlejeune.tvtime.ui.components.MinLinesText
import com.owenlejeune.tvtime.ui.components.PosterItem import com.owenlejeune.tvtime.ui.components.PosterItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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() val scrollState = rememberScrollState()
ConstraintLayout( ConstraintLayout(
@@ -81,23 +68,15 @@ fun DetailView(
.verticalScroll(state = scrollState) .verticalScroll(state = scrollState)
) { ) {
val ( val (
backButton, backButton, backdropImage, posterImage, titleText, contentColumn
backdropImage,
posterImage,
titleText,
contentColumn
) = createRefs() ) = createRefs()
BackdropImage( Backdrop(
modifier = Modifier modifier = Modifier.constrainAs(backdropImage) {
.constrainAs(backdropImage) { top.linkTo(parent.top)
top.linkTo(parent.top) start.linkTo(parent.start)
start.linkTo(parent.start) },
} mediaItem = mediaItem
.fillMaxWidth()
.height(280.dp),
imageUrl = TmdbUtils.getFullBackdropPath(mediaItem.value),
// collection = images.value
) )
PosterItem( PosterItem(
@@ -110,151 +89,204 @@ fun DetailView(
} }
) )
Text( TitleText(
text = mediaItem.value?.title ?: "", modifier = Modifier.constrainAs(titleText) {
color = MaterialTheme.colorScheme.primary, bottom.linkTo(posterImage.bottom)
modifier = Modifier start.linkTo(posterImage.end, margin = 8.dp)
.constrainAs(titleText) { end.linkTo(parent.end, margin = 16.dp)
bottom.linkTo(posterImage.bottom) },
start.linkTo(posterImage.end, margin = 8.dp) mediaItem = mediaItem
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
) )
IconButton( BackButton(
onClick = { appNavController.popBackStack() }, modifier = Modifier.constrainAs(backButton) {
modifier = Modifier top.linkTo(parent.top)//, 8.dp)
.constrainAs(backButton) { start.linkTo(parent.start, 12.dp)
top.linkTo(parent.top)//, 8.dp) bottom.linkTo(posterImage.top)
start.linkTo(parent.start, 12.dp) },
bottom.linkTo(posterImage.top) appNavController = appNavController
} )
.background(
brush = Brush.radialGradient( ContentColumn(
colors = listOf( modifier = Modifier.constrainAs(contentColumn) {
Color.Black, top.linkTo(backdropImage.bottom, margin = 8.dp)
Color.Transparent },
) 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) } @Composable
itemId?.let { private fun ContentColumn(modifier: Modifier,
if (castAndCrew.value == null) { itemId: Int?,
fetchCastAndCrew(itemId, service, castAndCrew) 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 modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(horizontal = 16.dp) .padding(vertical = 12.dp, horizontal = 16.dp),
.constrainAs(contentColumn) { text = mediaItem.value?.overview ?: "",
top.linkTo(backdropImage.bottom, margin = 8.dp) color = MaterialTheme.colorScheme.onSurfaceVariant,
} style = MaterialTheme.typography.bodyMedium
) { )
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
)
}
Card( @Composable
modifier = Modifier private fun CastCard(itemId: Int?, service: DetailService) {
.fillMaxWidth() val castAndCrew = remember { mutableStateOf<CastAndCrew?>(null) }
.wrapContentHeight(), itemId?.let {
shape = RoundedCornerShape(10.dp), if (castAndCrew.value == null) {
backgroundColor = MaterialTheme.colorScheme.primary, fetchCastAndCrew(itemId, service, castAndCrew)
elevation = 8.dp }
) { }
LazyRow(modifier = Modifier
.fillMaxSize() Card(
.padding(12.dp) modifier = Modifier
) { .fillMaxWidth()
items(castAndCrew.value?.cast?.size ?: 0) { i -> .wrapContentHeight(),
val castMember = castAndCrew.value!!.cast[i] shape = RoundedCornerShape(10.dp),
Column( backgroundColor = MaterialTheme.colorScheme.primary,
modifier = Modifier elevation = 8.dp
.width(124.dp) ) {
.wrapContentHeight() LazyRow(modifier = Modifier
.padding(end = 12.dp) .fillMaxSize()
) { .padding(12.dp)
Image( ) {
modifier = Modifier items(castAndCrew.value?.cast?.size ?: 0) { i ->
.size(width = 120.dp, height = 180.dp), val castMember = castAndCrew.value!!.cast[i]
painter = rememberImagePainter(
data = TmdbUtils.getFullPersonImagePath(castMember), CastCrewCard(person = 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 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?>) { private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val response = service.getById(id) val response = service.getById(id)