Skip to content

Commit

Permalink
Start of migrating show details to Compose
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Apr 15, 2020
1 parent fdd52d1 commit 28629cf
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.state
import androidx.compose.stateFor
import androidx.core.graphics.drawable.toBitmap
import androidx.ui.animation.Transition
import androidx.ui.core.Alignment
import androidx.ui.core.Modifier
import androidx.ui.core.onChildPositioned
import androidx.ui.core.onPositioned
Expand All @@ -35,6 +36,7 @@ import androidx.ui.geometry.Offset
import androidx.ui.graphics.Canvas
import androidx.ui.graphics.ImageAsset
import androidx.ui.graphics.Paint
import androidx.ui.graphics.ScaleFit
import androidx.ui.graphics.asImageAsset
import androidx.ui.graphics.painter.ImagePainter
import androidx.ui.graphics.painter.Painter
Expand Down Expand Up @@ -93,7 +95,9 @@ private val imageSaturationTransitionDef = transitionDefinition {
@Composable
fun LoadNetworkImageWithCrossfade(
modifier: Modifier = Modifier.None,
data: Any
data: Any,
alignment: Alignment = Alignment.Center,
scaleFit: ScaleFit = ScaleFit.Fit
) {
var childSize by state { IntPxSize(IntPx.Zero, IntPx.Zero) }
var imgLoadState by stateFor(data, childSize) { ImageLoadState.Empty }
Expand Down Expand Up @@ -122,7 +126,11 @@ fun LoadNetworkImageWithCrossfade(
// Unfortunately ColorMatrixColorFilter is not mutable so we have to create a new
// instance every time
val cf = ColorMatrixColorFilter(matrix)
childModifier = childModifier.paint(AndroidColorMatrixImagePainter(image, cf))
childModifier = childModifier.paint(
painter = AndroidColorMatrixImagePainter(image, cf),
scaleFit = scaleFit,
alignment = alignment
)
}

Box(modifier = childModifier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.ui.layout.absolutePadding
import androidx.ui.unit.Dp
import androidx.ui.unit.PxBounds
import androidx.ui.unit.PxPosition
import androidx.ui.unit.dp
import androidx.ui.unit.toPxSize

inline val PxBounds.center: PxPosition
Expand All @@ -33,6 +34,6 @@ inline val LayoutCoordinates.positionInParent: PxPosition
inline val LayoutCoordinates.boundsInParent: PxBounds
get() = PxBounds(positionInParent, size.toPxSize())

fun Modifier.padding(horizontal: Dp, vertical: Dp): Modifier {
fun Modifier.paddingHV(horizontal: Dp = 0.dp, vertical: Dp = 0.dp): Modifier {
return absolutePadding(left = horizontal, top = vertical, right = horizontal, bottom = vertical)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import androidx.ui.geometry.Offset
import androidx.ui.graphics.Canvas
import androidx.ui.graphics.Color
import androidx.ui.graphics.Paint
import androidx.ui.graphics.ScaleFit
import androidx.ui.graphics.withSave
import androidx.ui.layout.Column
import androidx.ui.layout.ColumnAlign
Expand Down Expand Up @@ -94,7 +95,7 @@ import app.tivi.common.compose.boundsInParent
import app.tivi.common.compose.center
import app.tivi.common.compose.observe
import app.tivi.common.compose.observeInsets
import app.tivi.common.compose.padding
import app.tivi.common.compose.paddingHV
import app.tivi.common.compose.setContentWithLifecycle
import app.tivi.data.entities.Episode
import app.tivi.data.entities.EpisodeWatchEntry
Expand Down Expand Up @@ -224,7 +225,8 @@ private fun Backdrop(season: Season, episode: Episode) {
if (episode.tmdbBackdropPath != null) {
LoadNetworkImageWithCrossfade(
modifier = Modifier.matchParent(),
data = episode
data = episode,
scaleFit = ScaleFit.FillMaxDimension
)
}

Expand Down Expand Up @@ -345,7 +347,7 @@ private fun EpisodeWatchesHeader(onSweepWatchesClick: () -> Unit) {
Row {
ProvideEmphasis(emphasis = EmphasisAmbient.current.high) {
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
modifier = Modifier.paddingHV(horizontal = 16.dp, vertical = 8.dp)
.gravity(RowAlign.Center)
.weight(1f),
text = stringResource(R.string.episode_watches),
Expand All @@ -368,7 +370,7 @@ private fun EpisodeWatchesHeader(onSweepWatchesClick: () -> Unit) {
private fun EpisodeWatch(episodeWatchEntry: EpisodeWatchEntry) {
Surface {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
modifier = Modifier.paddingHV(horizontal = 16.dp, vertical = 8.dp)
.preferredSizeIn(minWidth = 40.dp, minHeight = 40.dp)
) {
ProvideEmphasis(emphasis = EmphasisAmbient.current.high) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,245 @@
package app.tivi.showdetails.details.view

import android.view.ViewGroup
import androidx.compose.Composable
import androidx.compose.Providers
import androidx.compose.staticAmbientOf
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.ui.core.Alignment
import androidx.ui.core.Modifier
import androidx.ui.foundation.Box
import androidx.ui.foundation.Text
import androidx.ui.foundation.VerticalScroller
import androidx.ui.foundation.drawBorder
import androidx.ui.foundation.shape.corner.RoundedCornerShape
import androidx.ui.graphics.ScaleFit
import androidx.ui.layout.Column
import androidx.ui.layout.FlowRow
import androidx.ui.layout.Row
import androidx.ui.layout.SizeMode
import androidx.ui.layout.Spacer
import androidx.ui.layout.Stack
import androidx.ui.layout.aspectRatio
import androidx.ui.layout.fillMaxWidth
import androidx.ui.layout.padding
import androidx.ui.layout.preferredHeight
import androidx.ui.layout.preferredSize
import androidx.ui.layout.preferredWidth
import androidx.ui.layout.wrapContentHeight
import androidx.ui.material.MaterialTheme
import androidx.ui.material.Surface
import androidx.ui.res.stringResource
import androidx.ui.unit.dp
import app.tivi.common.compose.InsetsHolder
import app.tivi.common.compose.LoadNetworkImageWithCrossfade
import app.tivi.common.compose.MaterialThemeFromAndroidTheme
import app.tivi.common.compose.WrapWithAmbients
import app.tivi.common.compose.observe
import app.tivi.common.compose.observeInsets
import app.tivi.common.compose.paddingHV
import app.tivi.common.compose.setContentWithLifecycle
import app.tivi.data.entities.ImageType
import app.tivi.data.entities.ShowTmdbImage
import app.tivi.data.entities.TiviShow
import app.tivi.showdetails.details.ShowDetailsAction
import app.tivi.showdetails.details.ShowDetailsViewState
import app.tivi.util.TiviDateFormatter

val ShowDetailsTextCreatorAmbient = staticAmbientOf<ShowDetailsTextCreator>()

fun ViewGroup.composeShowDetails(
lifecycleOwner: LifecycleOwner,
state: LiveData<ShowDetailsViewState>,
insets: LiveData<WindowInsetsCompat>,
actioner: (ShowDetailsAction) -> Unit,
tiviDateFormatter: TiviDateFormatter
tiviDateFormatter: TiviDateFormatter,
textCreator: ShowDetailsTextCreator
): Any = setContentWithLifecycle(lifecycleOwner) {
WrapWithAmbients(tiviDateFormatter, InsetsHolder()) {
observeInsets(insets)
Providers(ShowDetailsTextCreatorAmbient provides textCreator) {
observeInsets(insets)

val viewState = observe(state)
if (viewState != null) {
MaterialThemeFromAndroidTheme(context) {
ShowDetails(viewState, actioner)
}
}
}
}
}

@Suppress("UNUSED_PARAMETER")
@Composable
fun ShowDetails(
viewState: ShowDetailsViewState,
actioner: (ShowDetailsAction) -> Unit
) {
VerticalScroller {
Column {
val backdropImage = viewState.backdropImage
Surface(modifier = Modifier.aspectRatio(16f / 10)) {
Stack {
if (backdropImage != null) {
LoadNetworkImageWithCrossfade(
modifier = Modifier.matchParent(),
data = backdropImage,
scaleFit = ScaleFit.FillMinDimension
)
}
}
}

Surface(
modifier = Modifier.fillMaxWidth().wrapContentHeight(Alignment.TopStart),
elevation = 2.dp
) {
Column {
Text(
modifier = Modifier.padding(16.dp),
text = viewState.show.title ?: "Show",
style = MaterialTheme.typography.h6
)

Row {
Spacer(modifier = Modifier.preferredWidth(16.dp))

val poster = viewState.posterImage
if (poster != null) {
LoadNetworkImageWithCrossfade(
modifier = Modifier.weight(1f, fill = false)
.aspectRatio(2 / 3f),
data = poster,
scaleFit = ScaleFit.FillMinDimension,
alignment = Alignment.TopStart
)
}

Spacer(modifier = Modifier.preferredWidth(16.dp))

Box(Modifier.weight(1f, fill = false)) {
FlowRow(
mainAxisSize = SizeMode.Expand,
mainAxisSpacing = 8.dp,
crossAxisSpacing = 8.dp
) {
TraktNetworkInfoPanel(viewState.show)

CertificateInfoPanel(viewState.show)

RuntimeInfoPanel(viewState.show)

AirsInfoPanel(viewState.show)
}
}

Spacer(modifier = Modifier.preferredWidth(16.dp))
}
}
}
}
}
}

@Composable
private fun TraktNetworkInfoPanel(
show: TiviShow,
modifier: Modifier = Modifier.None
) {
Column(modifier) {
Text(
text = stringResource(id = R.string.network_title),
style = MaterialTheme.typography.subtitle2
)

Spacer(Modifier.preferredHeight(4.dp))

val viewState = observe(state)
if (viewState != null) {
MaterialThemeFromAndroidTheme(context) {
// TODO
val networkLogo = show.networkLogoPath
if (!networkLogo.isNullOrEmpty()) {
LoadNetworkImageWithCrossfade(
modifier = Modifier.preferredSize(72.dp, 32.dp),
data = ShowTmdbImage(path = networkLogo, type = ImageType.LOGO, showId = 0)
)
} else {
val network = show.network
if (network != null) {
Text(
text = network,
style = MaterialTheme.typography.body2
)
}
}
}
}
}

@Composable
private fun RuntimeInfoPanel(
show: TiviShow,
modifier: Modifier = Modifier.None
) {
Column(modifier) {
Text(
text = stringResource(R.string.runtime_title),
style = MaterialTheme.typography.subtitle2
)

Spacer(Modifier.preferredHeight(4.dp))

Text(
text = stringResource(R.string.minutes_format, show.runtime ?: 0),
style = MaterialTheme.typography.body2
)
}
}

@Composable
private fun AirsInfoPanel(
show: TiviShow,
modifier: Modifier = Modifier.None
) {
val textCreator = ShowDetailsTextCreatorAmbient.current
val text = textCreator.airsText(show)?.toString()

if (!text.isNullOrEmpty()) {
Column(modifier) {
Text(
text = stringResource(R.string.airs_title),
style = MaterialTheme.typography.subtitle2
)

Spacer(Modifier.preferredHeight(4.dp))

Text(text = text, style = MaterialTheme.typography.body2)
}
}
}

@Composable
private fun CertificateInfoPanel(
show: TiviShow,
modifier: Modifier = Modifier.None
) {
val cert = show.certification
if (!cert.isNullOrEmpty()) {
Column(modifier) {
Text(
text = stringResource(R.string.certificate_title),
style = MaterialTheme.typography.subtitle2
)

Spacer(Modifier.preferredHeight(4.dp))

Text(
text = cert,
style = MaterialTheme.typography.body2,
modifier = Modifier.drawBorder(
size = 1.dp,
color = MaterialTheme.colors.onSurface,
shape = RoundedCornerShape(2.dp)
).paddingHV(horizontal = 4.dp, vertical = 2.dp)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import app.tivi.TiviFragment
import app.tivi.common.compose.observeWindowInsets
import app.tivi.extensions.scheduleStartPostponedTransitions
import app.tivi.showdetails.details.view.ShowDetailsTextCreator
import app.tivi.showdetails.details.view.composeShowDetails
import app.tivi.util.TiviDateFormatter
Expand All @@ -50,11 +51,18 @@ class ShowDetailsFragment : TiviFragment(), ShowDetailsFragmentViewModel.Factory
viewModel.observeAsLiveData(),
observeWindowInsets(),
viewModel::submitAction,
tiviDateFormatter
tiviDateFormatter,
textCreator
)
}
}

override fun onStart() {
super.onStart()
// TODO move this once we know how to handle transitions in Compose
scheduleStartPostponedTransitions()
}

override fun invalidate() = Unit

override fun provideFactory(): ShowDetailsFragmentViewModel.Factory = showDetailsViewModelFactory
Expand Down

0 comments on commit 28629cf

Please sign in to comment.