diff --git a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivImageBinder.kt b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivImageBinder.kt index e70d43680..24a6736a9 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivImageBinder.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/DivImageBinder.kt @@ -55,10 +55,10 @@ internal class DivImageBinder @Inject constructor( div.scale.observeAndGet(expressionResolver) { scale -> view.imageScale = scale.toImageScale() } ) view.observeContentAlignment(expressionResolver, div.contentAlignmentHorizontal, div.contentAlignmentVertical) - view.observePreview(divView, expressionResolver, errorCollector, div) view.addSubscription( div.imageUrl.observeAndGet(expressionResolver) { view.applyImage(divView, expressionResolver, errorCollector, div) } ) + view.observePreview(divView, expressionResolver, errorCollector, div) view.observeTint(expressionResolver, div.tintColor, div.tintMode) view.observeFilters(div.filters, divView, subscriber, expressionResolver) } @@ -122,12 +122,10 @@ internal class DivImageBinder @Inject constructor( div: DivImage) { div.preview?.let { val callback = { _: Any -> - if (!isImageLoaded) { - applyPreview(divView, resolver, div, errorCollector, synchronous = isHighPriorityShow(resolver, this, div)) - } + applyPreview(divView, resolver, div, errorCollector, synchronous = isHighPriorityShow(resolver, this, div)) } - addSubscription(it.observe(resolver, callback)) + addSubscription(it.observeAndGet(resolver, callback)) } } @@ -136,10 +134,24 @@ internal class DivImageBinder @Inject constructor( div: DivImage, errorCollector: ErrorCollector, synchronous: Boolean) { + if (isImageLoaded) { + return + } + + val newPreview = div.preview?.evaluate(resolver) + + if (preview != null && newPreview == preview) { + return + } + + resetPreviewLoaded() + + preview = newPreview + placeholderLoader.applyPlaceholder( this, errorCollector, - div.preview?.evaluate(resolver), + newPreview, div.placeholderColor.evaluate(resolver), synchronous = synchronous, onSetPlaceholder = { drawable -> @@ -173,6 +185,7 @@ internal class DivImageBinder @Inject constructor( val isHighPriorityShowPreview = isHighPriorityShow(resolver, this, div) resetImageLoaded() + resetPreviewLoaded() applyPreview(divView, resolver, div, errorCollector, synchronous = isHighPriorityShowPreview) diff --git a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/widgets/DivImageView.kt b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/widgets/DivImageView.kt index 14b025c12..bd2340c2a 100644 --- a/client/android/div/src/main/java/com/yandex/div/core/view2/divs/widgets/DivImageView.kt +++ b/client/android/div/src/main/java/com/yandex/div/core/view2/divs/widgets/DivImageView.kt @@ -25,6 +25,7 @@ internal open class DivImageView @JvmOverloads constructor( internal var div: DivImage? = null internal var imageUrl: Uri? = null + internal var preview: String? = null private var borderDrawer: DivBorderDrawer? = null override val border: DivBorder? @@ -51,6 +52,11 @@ internal open class DivImageView @JvmOverloads constructor( imageUrl = null } + fun resetPreviewLoaded() { + super.resetImageLoaded() + preview = null + } + override fun setAdjustViewBounds(adjustViewBounds: Boolean) = Unit override fun setCropToPadding(cropToPadding: Boolean) = Unit diff --git a/client/android/div/src/test/java/com/yandex/div/core/view2/divs/DivImageBinderTest.kt b/client/android/div/src/test/java/com/yandex/div/core/view2/divs/DivImageBinderTest.kt index 796ac5216..ecb86238f 100644 --- a/client/android/div/src/test/java/com/yandex/div/core/view2/divs/DivImageBinderTest.kt +++ b/client/android/div/src/test/java/com/yandex/div/core/view2/divs/DivImageBinderTest.kt @@ -15,18 +15,7 @@ import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.spy -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions -import org.mockito.kotlin.whenever +import org.mockito.kotlin.* import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -65,7 +54,13 @@ class DivImageBinderTest : DivBinderTest() { @Test fun `bind image when image wasn't previously bound`() { - val (view, divContainer) = createTestDiv("with_action.json") + val (view, _) = createTestDiv("with_action.json") + + val divContainer = DivImage( + imageUrl = Uri.parse("https://foo.bar/foo.png").asExpression(), + preview = PREVIEW.asExpression(), + highPriorityPreviewShow = true.asExpression() + ) binder.bindView(view, divContainer, divView) @@ -75,10 +70,22 @@ class DivImageBinderTest : DivBinderTest() { @Test fun `do not rebind image when imageUrl did not change and bitmap was not loaded`() { - val (view, divContainer) = createTestDiv("with_action.json") + val (view, _) = createTestDiv("with_action.json") + + val divContainer = DivImage( + imageUrl = Uri.parse("https://foo.bar/foo.png").asExpression(), + preview = PREVIEW.asExpression(), + highPriorityPreviewShow = true.asExpression() + ) + binder.bindView(view, divContainer, divView) - val (_, nextDivContainer) = createTestDiv("with_action.json") + val nextDivContainer = DivImage( + imageUrl = Uri.parse("https://foo.bar/foo.png").asExpression(), + preview = PREVIEW.asExpression(), + highPriorityPreviewShow = true.asExpression() + ) + binder.bindView(view, nextDivContainer, divView) verify(placeholderLoader).applyPlaceholder(any(), any(), anyOrNull(), any(), any(), any(), any()) @@ -90,6 +97,7 @@ class DivImageBinderTest : DivBinderTest() { val (view, divContainer) = createTestDiv("with_action.json") binder.bindView(view, divContainer, divView) + reset(placeholderLoader) whenImageLoaded(divContainer.imageUrl.evaluate(ExpressionResolver.EMPTY).toString()) @@ -101,7 +109,13 @@ class DivImageBinderTest : DivBinderTest() { @Test fun `bind image when bitmap was loaded but imageUrl changed`() { - val (view, divContainer) = createTestDiv("with_action.json") + val (view, _) = createTestDiv("with_action.json") + + val divContainer = DivImage( + imageUrl = Uri.parse("https://foo.bar/foo.png").asExpression(), + preview = PREVIEW.asExpression(), + highPriorityPreviewShow = true.asExpression() + ) binder.bindView(view, divContainer, divView) @@ -110,7 +124,7 @@ class DivImageBinderTest : DivBinderTest() { val nextDivContainer = DivImage(imageUrl = Uri.parse("https://foo.bar/foo.png").asExpression()) binder.bindView(view, nextDivContainer, divView) - verify(placeholderLoader, times(2)).applyPlaceholder(any(), any(), anyOrNull(), any(), any(), any(), any()) + verify(placeholderLoader).applyPlaceholder(any(), any(), anyOrNull(), any(), any(), any(), any()) verify(imageLoader).loadImage(eq(nextDivContainer.imageUrl.evaluate(ExpressionResolver.EMPTY).toString()), any()) } @@ -120,6 +134,7 @@ class DivImageBinderTest : DivBinderTest() { val divContainer = DivImage( imageUrl = Uri.parse("https://foo.bar/foo.png").asExpression(), + preview = PREVIEW.asExpression(), highPriorityPreviewShow = true.asExpression() ) binder.bindView(view, divContainer, divView) @@ -132,6 +147,7 @@ class DivImageBinderTest : DivBinderTest() { val nextDivContainer = DivImage( imageUrl = Uri.parse("https://foo.bar/bar.png").asExpression(), + preview = PREVIEW.asExpression(), highPriorityPreviewShow = true.asExpression() ) binder.bindView(view, nextDivContainer, divView) @@ -217,11 +233,10 @@ class DivImageBinderTest : DivBinderTest() { @Test fun `tint color applied to preview`() { - val preview = "iVBORw0KGgoAAAANSUhEUgAAAEEAAABACAYAAABFqxrgAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAIsSURBVHgB7ZqNbcIwEIXdTsAI6QRtN2CDMgIblA1CJ2g3gE5QOkEYgQ3MBnSDV1s2KjKOf0gVfOY+yUImF8Kdz+aeDiEYhmEYhmEYhjwAJmq0anRqHGA42Plc1I5ycnbieB9SjUbUiF39VHSgnkRN6JVFPjoQE5HJvSiXzvPemxoPdwr1+qzGp3NdB2AhakCntWeVZz22y//IhuJQTiwcxzYRe/fgnIoMSt0OjTPfRuy/I/cHoRKEGPTT/whMUfTuOQ+6yH1yyHYoBsSLor6DsXUNBTVg6oEOabSwlaG9z5c1K0EF/OmBvtWPlco+JKiUz+qLTnG+j0+dX9ogLZDOdcpmZKo7mBT+CjjSuSupPycQsCPyWgHIUnfq9RXh1J8FntXYYHQoRUojX93tAtc/QK3MxWXqzkcHwr/n0uOQPsgae12Ln3XAeZ0ZdJUehqk7zQrUFR5GVndjkSugGme+jdgPUndjkRuE3FQmkfq5Qdg585eI/aMz3wvqwJS7qQdj6xqKWoBf8WmHG3u9T91pJGpolvRkQy4dqDdLkKfuQqxIBwPp6k4LrXXErg0FAyX3IpGh7qztBuGAzT3PqK8XaZ2SKcFA7b1IxLeTRD40u08wQksmOpmiVpeCIjDnRezwvI1eZCAYt9OLVB33vRpzca4+t5Fbq+xF/og8BqV/qUFgtQpWqwZcrlbp9CJj4DK1KlHbX/lAoRc5Bii5FzkmKLEXyTAMwzAMw9wQvznV1eV5FJ3aAAAAAElFTkSuQmCC" val (view, _) = createTestDiv("with_action.json") val divContainer = DivImage( imageUrl = Uri.parse("https://foo.bar/foo.png").asExpression(), - preview = preview.asExpression(), + preview = PREVIEW.asExpression(), tintColor = Color.parseColor("#ffffff").asExpression()) binder.bindView(view, divContainer, divView) @@ -232,7 +247,6 @@ class DivImageBinderTest : DivBinderTest() { private fun whenImageLoaded(imageUrl: String) { val imageDownloadCallbackCaptor = argumentCaptor() - verify(placeholderLoader).applyPlaceholder(any(), any(), anyOrNull(), any(), any(), any(), any()) verify(imageLoader).loadImage(eq(imageUrl), imageDownloadCallbackCaptor.capture()) val cachedBitmap = mock { on { bitmap } doReturn mock() @@ -257,5 +271,6 @@ class DivImageBinderTest : DivBinderTest() { companion object { private const val IMAGE_DIR = "div-image" + private const val PREVIEW = "iVBORw0KGgoAAAANSUhEUgAAAEEAAABACAYAAABFqxrgAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAIsSURBVHgB7ZqNbcIwEIXdTsAI6QRtN2CDMgIblA1CJ2g3gE5QOkEYgQ3MBnSDV1s2KjKOf0gVfOY+yUImF8Kdz+aeDiEYhmEYhmEYhjwAJmq0anRqHGA42Plc1I5ycnbieB9SjUbUiF39VHSgnkRN6JVFPjoQE5HJvSiXzvPemxoPdwr1+qzGp3NdB2AhakCntWeVZz22y//IhuJQTiwcxzYRe/fgnIoMSt0OjTPfRuy/I/cHoRKEGPTT/whMUfTuOQ+6yH1yyHYoBsSLor6DsXUNBTVg6oEOabSwlaG9z5c1K0EF/OmBvtWPlco+JKiUz+qLTnG+j0+dX9ogLZDOdcpmZKo7mBT+CjjSuSupPycQsCPyWgHIUnfq9RXh1J8FntXYYHQoRUojX93tAtc/QK3MxWXqzkcHwr/n0uOQPsgae12Ln3XAeZ0ZdJUehqk7zQrUFR5GVndjkSugGme+jdgPUndjkRuE3FQmkfq5Qdg585eI/aMz3wvqwJS7qQdj6xqKWoBf8WmHG3u9T91pJGpolvRkQy4dqDdLkKfuQqxIBwPp6k4LrXXErg0FAyX3IpGh7qztBuGAzT3PqK8XaZ2SKcFA7b1IxLeTRD40u08wQksmOpmiVpeCIjDnRezwvI1eZCAYt9OLVB33vRpzca4+t5Fbq+xF/og8BqV/qUFgtQpWqwZcrlbp9CJj4DK1KlHbX/lAoRc5Bii5FzkmKLEXyTAMwzAMw9wQvznV1eV5FJ3aAAAAAElFTkSuQmCC" } }