Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Paywalls V2] Adds ImageComponentView #1959

Merged
merged 10 commits into from
Dec 5, 2024
Merged

[Paywalls V2] Adds ImageComponentView #1959

merged 10 commits into from
Dec 5, 2024

Conversation

tonidero
Copy link
Contributor

@tonidero tonidero commented Dec 4, 2024

Description

This adds a new component ImageComponentView and the corresponding ImageComponentStyle. It doesn't include transformations from ImageComponent to ImageComponentStyle.

  • Placeholder to image loading:
Untitled2.mp4
  • Previews
image

Copy link

emerge-tools bot commented Dec 4, 2024

📸 Snapshot Test

3 added, 110 unchanged

Name Added Removed Modified Unchanged Errored Approval
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
3 0 0 110 0 ✅ Approved

🛸 Powered by Emerge Tools

Copy link

codecov bot commented Dec 4, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 81.87%. Comparing base (7c39fe6) to head (7f632f6).
Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1959   +/-   ##
=======================================
  Coverage   81.87%   81.87%           
=======================================
  Files         260      260           
  Lines        8493     8493           
  Branches     1226     1226           
=======================================
  Hits         6954     6954           
  Misses       1041     1041           
  Partials      498      498           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

modifier: Modifier = Modifier,
) {
if (style.visible) {
Box(modifier = modifier.applyIfNotNull(style.shape) { clip(it) }) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to apply the clipping in an outer box... If I applied it to the image itself, it didn't work together with the gradient overlays.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can change the drawWithCache modifier to draw an Outline instead of a Rect. That way we don't need the Box. Might be nice to turn it into a custom modifier (for readability and reusability)? Something like:

@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)
        }
    }
}

Which can then be used as:

.applyIfNotNull(style.colorStyle) { colorStyle ->
    overlay(colorStyle, style.shape ?: RectangleShape)
}
// This is now needed, as the Box is no longer clipping the image itself.
.applyIfNotNull(style.shape) { clip(it) }

@get:JvmSynthetic
val darkGradientColors: ColorInfo.Gradient?,
@get:JvmSynthetic
val contentScale: ContentScale,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this might require some transformations from the ImageComponent to this ImageComponentStyle. For example, this expects a ContentScale but in ImageComponent we have our own FitMode. I believe this translation should be done separately and in advance though.

Base automatically changed from pw2-stackcomponentview to main December 4, 2024 14:33
@tonidero tonidero force-pushed the pw2-imagecomponentview branch from d56f0c1 to 47a2d2e Compare December 4, 2024 14:53
@tonidero tonidero marked this pull request as ready for review December 4, 2024 16:02
@tonidero tonidero requested a review from a team December 4, 2024 16:02
colorStyle: ColorStyle? = null,
) = ImageComponentStyle(
visible = visible,
themeImageUrls = ThemeImageUrls(light = ImageUrls(url, url, lowResURL, 200u, 200u)),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the sizes of the images are affecting anything... Not according to my tests 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the width and height to already give the image view the appropriate size before the image is downloaded. That way the layout won't "jump" when the image arrives.

Since width and height are part of ThemeImageUrls, which is provided by ImageComponentStyle, I think we can remove the separate size from ImageComponentStyle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh ok that makes a lot of sense 😅 Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm as I was thinking about this, how would that work if we want the image to "Fill" or "Fit" the screen? In the ThemeImageUrls there is no way to pass if we want one of those two right? I think passing the specific width/height only makes sense if we want a "fixed" size...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of offline conversation:

  • We cannot remove the size as that indicates the size of the image view, not the image itself.
  • The ImageUrls width/height come into play when the image view is set to Fit.

@tonidero tonidero requested a review from JayShortway December 4, 2024 16:49
Copy link
Member

@JayShortway JayShortway left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice job on this! Just a few minor comments.

colorStyle: ColorStyle? = null,
) = ImageComponentStyle(
visible = visible,
themeImageUrls = ThemeImageUrls(light = ImageUrls(url, url, lowResURL, 200u, 200u)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the width and height to already give the image view the appropriate size before the image is downloaded. That way the layout won't "jump" when the image arrives.

Since width and height are part of ThemeImageUrls, which is provided by ImageComponentStyle, I think we can remove the separate size from ImageComponentStyle.

Comment on lines +158 to +168
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

modifier: Modifier = Modifier,
) {
if (style.visible) {
Box(modifier = modifier.applyIfNotNull(style.shape) { clip(it) }) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can change the drawWithCache modifier to draw an Outline instead of a Rect. That way we don't need the Box. Might be nice to turn it into a custom modifier (for readability and reusability)? Something like:

@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)
        }
    }
}

Which can then be used as:

.applyIfNotNull(style.colorStyle) { colorStyle ->
    overlay(colorStyle, style.shape ?: RectangleShape)
}
// This is now needed, as the Box is no longer clipping the image itself.
.applyIfNotNull(style.shape) { clip(it) }

Copy link
Member

@JayShortway JayShortway left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤏 this close! Just a comment on px vs dp, and a suggestion.

Comment on lines 48 to 55
imageUrls: ImageUrls? = null,
): Modifier {
val widthModifier = when (val width = size.width) {
is Fit -> Modifier.wrapContentWidth(align = horizontalAlignment ?: Alignment.CenterHorizontally)
is Fit -> if (imageUrls == null) {
Modifier.wrapContentWidth(align = horizontalAlignment ?: Alignment.CenterHorizontally)
} else {
Modifier.width(imageUrls.width.toInt().dp)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nice! 2 comments:

  • The values in ImageUrls are pixels, so we should transform them to dp, instead of interpreting them as dp.
  • Maybe we can adjust the Size, taking into account the ImageUrls values before it gets to this modifier? That way this modifier doesn't need to know about ImageUrls.

To illustrate, both of those combined could look something like this:

@JvmSynthetic
@Composable
internal fun Size.adjustForImage(imageUrls: ImageUrls): Size {
    val density = LocalDensity.current
    return adjustForImage(imageUrls, density)
}

@JvmSynthetic
internal 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
        },
    )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that 👍 Thanks!

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not important, but I think we can make some composables like this read only composables.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea probably indeed, good call!

@JvmSynthetic
@ReadOnlyComposable
@Composable
fun adjustedSize(): Size {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this to the ImageComponentStyle since it felt like a better place to put it than as simple extensions of the Size class, since here is where we actually have the data to adjust the size. Not sure if there are other components that would require a similar adjustment, in which case, I think we can extract it to extension functions as suggested. Wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this!

Copy link
Member

@JayShortway JayShortway left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nice!

@JvmSynthetic
@ReadOnlyComposable
@Composable
fun adjustedSize(): Size {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this!

@tonidero tonidero force-pushed the pw2-imagecomponentview branch from 7498eff to 7f632f6 Compare December 5, 2024 15:05
@tonidero tonidero merged commit f8155ad into main Dec 5, 2024
12 checks passed
@tonidero tonidero deleted the pw2-imagecomponentview branch December 5, 2024 15:35
tonidero pushed a commit that referenced this pull request Dec 12, 2024
**This is an automatic release.**

## RevenueCatUI SDK
### 🐞 Bugfixes
* Fix multi-tier template to allow optional header image (#1971) via
Josh Holtz (@joshdholtz)

### 🔄 Other Changes
* [Paywalls V2] Adds first version of `LoadedPaywallComponents` (#1970)
via JayShortway (@JayShortway)
* [Paywalls V2] Add `ButtonComponentView` (#1963) via Toni Rico
(@tonidero)
* [Paywalls V2] Adds `PaywallState.Loaded.Components` (#1969) via
JayShortway (@JayShortway)
* [Paywalls V2] Adds image background tests (#1967) via JayShortway
(@JayShortway)
* [Paywalls V2] `TextComponentView` updates when the theme changes
(#1966) via JayShortway (@JayShortway)
* [Paywalls V2] Adds a `StyleFactory` (#1965) via JayShortway
(@JayShortway)
* [EXTERNAL] fix: update polish translations (#1919) via @radko93
(#1964) via JayShortway (@JayShortway)
* [Paywalls V2] Adds `ImageComponentView` (#1959) via Toni Rico
(@tonidero)
* WebPurchaseRedemption: Rename `AlreadyRedeemed` result to
`PurchaseBelongsToOtherUser` (#1962) via Toni Rico (@tonidero)
* [Paywalls V2] Extends support for blurred shadows to all Android
versions (#1957) via JayShortway (@JayShortway)

Co-authored-by: revenuecat-ops <ops@revenuecat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants