Skip to content

Commit

Permalink
[Paywalls V2] Adds ImageComponentView (#1959)
Browse files Browse the repository at this point in the history
### Description
This adds a new component `ImageComponentView` and the corresponding
`ImageComponentStyle`. It doesn't include transformations from
`ImageComponent` to `ImageComponentStyle`.

- Placeholder to image loading:


https://github.com/user-attachments/assets/ae010171-2ae4-4d82-b84c-68fa9015d2bd

- Previews
<img width="643" alt="image"
src="https://github.com/user-attachments/assets/0483020f-c3ad-400e-982d-1d33c0bef517">
  • Loading branch information
tonidero authored Dec 5, 2024
1 parent 2ed3344 commit f8155ad
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
@file:JvmSynthetic

package com.revenuecat.purchases.ui.revenuecatui.components.image

import android.graphics.Color
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.revenuecat.purchases.paywalls.components.properties.ImageUrls
import com.revenuecat.purchases.paywalls.components.properties.Size
import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint
import com.revenuecat.purchases.paywalls.components.properties.ThemeImageUrls
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.urlsForCurrentTheme
import com.revenuecat.purchases.ui.revenuecatui.components.modifier.overlay
import com.revenuecat.purchases.ui.revenuecatui.components.modifier.size
import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle
import com.revenuecat.purchases.ui.revenuecatui.components.style.ImageComponentStyle
import com.revenuecat.purchases.ui.revenuecatui.composables.RemoteImage
import com.revenuecat.purchases.ui.revenuecatui.extensions.applyIfNotNull
import java.net.URL
import androidx.compose.ui.graphics.Color as ComposeColor

@JvmSynthetic
@Composable
internal fun ImageComponentView(
style: ImageComponentStyle,
modifier: Modifier = Modifier,
) {
if (style.visible) {
val currentUrls = style.themeImageUrls.urlsForCurrentTheme
RemoteImage(
urlString = currentUrls.webp.toString(),
modifier = modifier
.size(style.adjustedSize())
.applyIfNotNull(style.overlay) { overlay(it, style.shape ?: RectangleShape) }
.applyIfNotNull(style.shape) { clip(it) },
placeholderUrlString = currentUrls.webpLowRes.toString(),
contentScale = style.contentScale,
)
}
}

@Preview
@Composable
private fun ImageComponentView_Preview_Default() {
Box(modifier = Modifier.background(ComposeColor.Red)) {
ImageComponentView(
style = previewImageComponentStyle(),
)
}
}

@Suppress("MagicNumber")
@Preview
@Composable
private fun ImageComponentView_Preview_LinearGradient() {
Box(modifier = Modifier.background(ComposeColor.Red)) {
ImageComponentView(
style = previewImageComponentStyle(
overlay = ColorStyle.Gradient(
Brush.verticalGradient(
Pair(0f, ComposeColor(Color.parseColor("#88FF0000"))),
Pair(0.5f, ComposeColor(Color.parseColor("#8800FF00"))),
Pair(1f, ComposeColor(Color.parseColor("#880000FF"))),
),
),
),
)
}
}

@Suppress("MagicNumber")
@Preview
@Composable
private fun ImageComponentView_Preview_RadialGradient() {
Box(modifier = Modifier.background(ComposeColor.Red)) {
ImageComponentView(
style = previewImageComponentStyle(
overlay = ColorStyle.Gradient(
Brush.radialGradient(
Pair(0f, ComposeColor(Color.parseColor("#88FF0000"))),
Pair(0.5f, ComposeColor(Color.parseColor("#8800FF00"))),
Pair(1f, ComposeColor(Color.parseColor("#880000FF"))),
),
),
),
)
}
}

@Suppress("LongParameterList")
@Composable
private fun previewImageComponentStyle(
url: URL = URL("https://sample-videos.com/img/Sample-jpg-image-5mb.jpg"),
lowResURL: URL = URL("https://assets.pawwalls.com/954459_1701163461.jpg"),
visible: Boolean = true,
size: Size = Size(width = SizeConstraint.Fixed(400u), height = SizeConstraint.Fit),
contentScale: ContentScale = ContentScale.Crop,
overlay: ColorStyle? = null,
) = ImageComponentStyle(
visible = visible,
themeImageUrls = ThemeImageUrls(light = ImageUrls(url, url, lowResURL, 1000u, 1000u)),
size = size,
shape = RoundedCornerShape(20.dp, 20.dp, 20.dp, 20.dp),
overlay = overlay,
contentScale = contentScale,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ package com.revenuecat.purchases.ui.revenuecatui.components.ktx

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import com.revenuecat.purchases.paywalls.components.properties.ImageUrls
import com.revenuecat.purchases.paywalls.components.properties.ThemeImageUrls

internal val ThemeImageUrls.urlsForCurrentTheme: ImageUrls
@Composable get() = if (isSystemInDarkTheme()) dark ?: light else light
@ReadOnlyComposable @Composable
get() = if (isSystemInDarkTheme()) dark ?: light else light
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@file:JvmSynthetic

package com.revenuecat.purchases.ui.revenuecatui.components.modifier

import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawOutline
import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle

@JvmSynthetic
@Stable
internal fun Modifier.overlay(
color: ColorStyle,
shape: Shape = RectangleShape,
): Modifier = this then drawWithCache {
val outline = shape.createOutline(size, layoutDirection, this)

onDrawWithContent {
drawContent()
when (color) {
is ColorStyle.Solid -> drawOutline(outline, color.color)
is ColorStyle.Gradient -> drawOutline(outline, color.brush)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.revenuecat.purchases.paywalls.components.properties.Size
import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit
import com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAlignment
import com.revenuecat.purchases.paywalls.components.properties.VerticalAlignment
import com.revenuecat.purchases.ui.revenuecatui.components.image.ImageComponentView
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toHorizontalAlignmentOrNull
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toHorizontalArrangement
Expand All @@ -41,6 +42,7 @@ import com.revenuecat.purchases.ui.revenuecatui.components.properties.Background
import com.revenuecat.purchases.ui.revenuecatui.components.properties.BorderStyle
import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle.Solid
import com.revenuecat.purchases.ui.revenuecatui.components.properties.ShadowStyle
import com.revenuecat.purchases.ui.revenuecatui.components.style.ImageComponentStyle
import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentStyle
import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle
import com.revenuecat.purchases.ui.revenuecatui.components.text.TextComponentView
Expand Down Expand Up @@ -70,6 +72,7 @@ internal fun StackComponentView(
when (child) {
is StackComponentStyle -> StackComponentView(style = child)
is TextComponentStyle -> TextComponentView(style = child)
is ImageComponentStyle -> ImageComponentView(style = child)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.revenuecat.purchases.ui.revenuecatui.components.style

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import com.revenuecat.purchases.paywalls.components.properties.ImageUrls
import com.revenuecat.purchases.paywalls.components.properties.Size
import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fill
import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit
import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fixed
import com.revenuecat.purchases.paywalls.components.properties.ThemeImageUrls
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.urlsForCurrentTheme
import com.revenuecat.purchases.ui.revenuecatui.components.properties.ColorStyle

@Suppress("LongParameterList")
@Immutable
internal class ImageComponentStyle private constructor(
@get:JvmSynthetic
val visible: Boolean,
@get:JvmSynthetic
val themeImageUrls: ThemeImageUrls,
@get:JvmSynthetic
val size: Size,
@get:JvmSynthetic
val shape: Shape?,
@get:JvmSynthetic
val overlay: ColorStyle?,
@get:JvmSynthetic
val contentScale: ContentScale,
) : ComponentStyle {

companion object {

@Suppress("LongParameterList")
@JvmSynthetic
@Composable
operator fun invoke(
visible: Boolean,
size: Size,
themeImageUrls: ThemeImageUrls,
shape: Shape?,
overlay: ColorStyle?,
contentScale: ContentScale,
): ImageComponentStyle {
return ImageComponentStyle(
visible = visible,
size = size,
themeImageUrls = themeImageUrls,
shape = shape,
overlay = overlay,
contentScale = contentScale,
)
}
}

@JvmSynthetic
@ReadOnlyComposable
@Composable
fun adjustedSize(): Size {
val density = LocalDensity.current
return size.adjustForImage(imageUrls = themeImageUrls.urlsForCurrentTheme, density = density)
}
}

private fun Size.adjustForImage(imageUrls: ImageUrls, density: Density): Size =
Size(
width = when (width) {
is Fit -> Fixed(with(density) { imageUrls.width.toInt().toDp().value.toUInt() })
is Fill,
is Fixed,
-> width
},
height = when (height) {
is Fit -> Fixed(with(density) { imageUrls.height.toInt().toDp().value.toUInt() })
is Fill,
is Fixed,
-> height
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.transform.Transformation
import com.revenuecat.purchases.ui.revenuecatui.R
import com.revenuecat.purchases.ui.revenuecatui.UIConstant
import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger
import com.revenuecat.purchases.ui.revenuecatui.helpers.isInPreviewMode
Expand All @@ -37,6 +40,7 @@ internal fun LocalImage(
) {
Image(
source = ImageSource.Local(resource),
placeholderSource = null,
modifier = modifier,
contentScale = contentScale,
contentDescription = contentDescription,
Expand All @@ -50,13 +54,15 @@ internal fun LocalImage(
internal fun RemoteImage(
urlString: String,
modifier: Modifier = Modifier,
placeholderUrlString: String? = null,
contentScale: ContentScale = ContentScale.Fit,
contentDescription: String? = null,
transformation: Transformation? = null,
alpha: Float = 1f,
) {
Image(
source = ImageSource.Remote(urlString),
placeholderSource = placeholderUrlString?.let { ImageSource.Remote(it) },
modifier = modifier,
contentScale = contentScale,
contentDescription = contentDescription,
Expand All @@ -80,6 +86,7 @@ private sealed class ImageSource {
@Composable
private fun Image(
source: ImageSource,
placeholderSource: ImageSource?,
modifier: Modifier = Modifier,
contentScale: ContentScale,
contentDescription: String?,
Expand All @@ -106,6 +113,7 @@ private fun Image(
if (useCache) {
AsyncImage(
source = source,
placeholderSource = placeholderSource,
imageRequest = imageRequest,
contentDescription = contentDescription,
imageLoader = imageLoader,
Expand All @@ -120,6 +128,7 @@ private fun Image(
} else {
AsyncImage(
source = source,
placeholderSource = placeholderSource,
imageRequest = imageRequest,
contentDescription = contentDescription,
imageLoader = imageLoader,
Expand All @@ -134,6 +143,7 @@ private fun Image(
@Composable
private fun AsyncImage(
source: ImageSource,
placeholderSource: ImageSource?,
imageRequest: ImageRequest,
imageLoader: ImageLoader,
modifier: Modifier = Modifier,
Expand All @@ -145,23 +155,29 @@ private fun AsyncImage(
AsyncImage(
model = imageRequest,
contentDescription = contentDescription,
placeholder = placeholderSource?.let {
rememberAsyncImagePainter(
model = it.data,
placeholder = if (isInPreviewMode()) painterResource(R.drawable.android) else null,
imageLoader = imageLoader,
contentScale = contentScale,
onError = { errorState ->
Logger.e("Error loading placeholder image", errorState.result.throwable)
},
)
} ?: if (isInPreviewMode()) painterResource(R.drawable.android) else null,
imageLoader = imageLoader,
modifier = modifier,
contentScale = contentScale,
alpha = alpha,
onState = {
when (it) {
is AsyncImagePainter.State.Error -> {
val error = when (source) {
is ImageSource.Local -> "Error loading local image: '${source.resource}'"
is ImageSource.Remote -> "Error loading image from '${source.urlString}'"
}

Logger.e(error, it.result.throwable)
onError?.invoke(it)
}
else -> {}
onError = {
val error = when (source) {
is ImageSource.Local -> "Error loading local image: '${source.resource}'"
is ImageSource.Remote -> "Error loading image from '${source.urlString}'"
}

Logger.e(error, it.result.throwable)
onError?.invoke(it)
},
)
}
Expand Down

0 comments on commit f8155ad

Please sign in to comment.