diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 19d9efb2..fde3c6cc 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -20,6 +20,6 @@
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index b5081e6d..2bf4dc26 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -20,6 +20,6 @@
diff --git a/feature-info/build.gradle.kts b/feature-info/build.gradle.kts
index 951ac7b2..5286b5ea 100644
--- a/feature-info/build.gradle.kts
+++ b/feature-info/build.gradle.kts
@@ -21,3 +21,7 @@ plugins {
android {
namespace = "com.paulrybitskyi.gamedge.feature.info"
}
+
+dependencies {
+ implementation(libs.materialComponents)
+}
diff --git a/feature-info/src/main/java/com/paulrybitskyi/gamedge/feature/info/presentation/widgets/header/GameInfoHeader.kt b/feature-info/src/main/java/com/paulrybitskyi/gamedge/feature/info/presentation/widgets/header/GameInfoHeader.kt
index ab51ea66..59e349e0 100644
--- a/feature-info/src/main/java/com/paulrybitskyi/gamedge/feature/info/presentation/widgets/header/GameInfoHeader.kt
+++ b/feature-info/src/main/java/com/paulrybitskyi/gamedge/feature/info/presentation/widgets/header/GameInfoHeader.kt
@@ -18,9 +18,8 @@
package com.paulrybitskyi.gamedge.feature.info.presentation.widgets.header
-import androidx.compose.animation.graphics.res.animatedVectorResource
-import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
-import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import android.content.Context
+import android.content.res.ColorStateList
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
@@ -31,7 +30,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.ripple
@@ -49,15 +47,22 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.viewinterop.NoOpUpdate
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintSet
import androidx.constraintlayout.compose.Dimension
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.paulrybitskyi.commons.ktx.getCompatDrawable
+import com.paulrybitskyi.commons.ktx.onClick
import com.paulrybitskyi.gamedge.common.ui.clickable
import com.paulrybitskyi.gamedge.common.ui.theme.GamedgeTheme
import com.paulrybitskyi.gamedge.common.ui.theme.lightScrim
@@ -97,6 +102,8 @@ internal fun GameInfoHeader(
onCoverClicked: () -> Unit,
onLikeButtonClicked: () -> Unit,
) {
+ val colors = GamedgeTheme.colors
+ val density = LocalDensity.current
val artworks = headerInfo.artworks
val isPageIndicatorVisible by remember(artworks) { mutableStateOf(artworks.size > 1) }
var selectedArtworkPage by rememberSaveable { mutableIntStateOf(0) }
@@ -201,27 +208,29 @@ internal fun GameInfoHeader(
onCoverClicked = if (headerInfo.hasCoverImageUrl) onCoverClicked else null,
)
- FloatingActionButton(
- onClick = onLikeButtonClicked,
+ // Animated selector drawables are not currently supported by the Jetpack Compose:
+ // https://issuetracker.google.com/issues/212418566. However, since the link/unlike
+ // animation is so gorgeous, it'd sad if we didn't use it, so we are using the legacy
+ // View here to render it. Consider to migrate to the Jetpack Compose when the support
+ // arrives.
+ AndroidView(
+ factory = { context ->
+ LikeButton(context).apply {
+ supportBackgroundTintList = ColorStateList.valueOf(colors.secondary.toArgb())
+ size = FloatingActionButton.SIZE_NORMAL
+ setMaxImageSize(with(density) { 52.dp.toPx().toInt() })
+ setImageDrawable(context.getCompatDrawable(CoreR.drawable.heart_animated_selector))
+ supportImageTintList = ColorStateList.valueOf(colors.onSecondary.toArgb())
+ onClick { onLikeButtonClicked() }
+ }
+ },
modifier = Modifier.layoutId(ConstraintIdLikeButton),
- backgroundColor = GamedgeTheme.colors.secondary,
- ) {
- // Animated selector drawables are not currently supported by the Jetpack Compose.
- // https://issuetracker.google.com/issues/212418566
- // Consider to use the R.drawable.heart_animated_selector when the support arrives.
-
- Icon(
- painter = rememberAnimatedVectorPainter(
- animatedImageVector = AnimatedImageVector.animatedVectorResource(
- CoreR.drawable.heart_animated_fill,
- ),
- atEnd = headerInfo.isLiked,
- ),
- contentDescription = null,
- modifier = Modifier.size(52.dp),
- tint = GamedgeTheme.colors.onSecondary,
- )
- }
+ // Have to provide any lambda here for optimizations to kick in (even if it's a no-op)
+ onReset = NoOpUpdate,
+ update = { view ->
+ view.isLiked = headerInfo.isLiked
+ },
+ )
Text(
text = headerInfo.title,
@@ -302,6 +311,35 @@ internal fun GameInfoHeader(
}
}
+private class LikeButton(context: Context) : FloatingActionButton(context) {
+
+ private companion object {
+ const val STATE_CHECKED = android.R.attr.state_checked
+ const val STATE_CHECKED_ON = (STATE_CHECKED * 1)
+ const val STATE_CHECKED_OFF = (STATE_CHECKED * -1)
+ }
+
+ var isLiked: Boolean
+ set(value) {
+ setImageState(intArrayOf(if (value) STATE_CHECKED_ON else STATE_CHECKED_OFF), true)
+ }
+ get() = drawableState.contains(STATE_CHECKED_ON)
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+
+ // This is a hacky solution to fix a very strange case, where a user likes a game,
+ // scrolls the view out of the screen or goes to a another screen (e.g., a related game)
+ // and comes back, then the like button resets its icon from a filled heart to an empty
+ // heart. To fix it, when this view gets reattached to the window, we are asking the button
+ // to reset its state and then go to the liked state again.
+ if (isLiked) {
+ isLiked = false
+ isLiked = true
+ }
+ }
+}
+
@Composable
private fun constructExpandedConstraintSet(): ConstraintSet {
val artworksHeight = 240.dp
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 539daa68..7f6ffbc9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -36,6 +36,7 @@ constraintLayout = "1.1.0-beta01"
composeHilt = "1.2.0"
# Google
+materialComponents = "1.12.0"
protobuf = "4.28.2"
# Square
@@ -127,6 +128,7 @@ composeConstraintLayout = { module = "androidx.constraintlayout:constraintlayout
composeHilt = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "composeHilt" }
# Google
+materialComponents = { module = "com.google.android.material:material", version.ref = "materialComponents" }
daggerHiltCore = { module = "com.google.dagger:hilt-core", version.ref = "daggerHilt" }
daggerHiltCoreCompiler = { module = "com.google.dagger:hilt-compiler", version.ref = "daggerHilt" }
daggerHiltAndroid = { module = "com.google.dagger:hilt-android", version.ref = "daggerHilt" }