summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2023-03-17 02:31:04 -0700
committer Andrey Epin <ayepin@google.com> 2023-03-21 08:02:19 -0700
commitf1870096ee8ad86d45887b1de5aee70a7f93dea3 (patch)
tree79ade61d49594920e747b1c4b75efd6f45ebf84c /java
parent7fa80cd4f6e7dad8f6bc9d21f6cf53e96f1e9797 (diff)
Maintain previews aspect ratio
Content preview preserves previews apspect raio (limiting between 0.4 and 2.5). ScrollableImagePreviewView now makes an initial pass over provided URIs to determine preview aspect ratio; previews that successfully loaded are progressively added to the dislay list. Previews that failed to load counted in the "+N" item at the list end. Previews are displayed in the order they are loaded (concurrently) to facilitate the fastest preview display. Preivew item height and spacings are change accroding to the latest specs. Bug: 271613784 Test: manual testing Change-Id: I430f1e7fb39c97da91bdc25914a0cb804a2b6ffa
Diffstat (limited to 'java')
-rw-r--r--java/res/layout/image_preview_image_item.xml33
-rw-r--r--java/res/layout/image_preview_other_item.xml2
-rw-r--r--java/res/layout/scrollable_image_preview_view.xml25
-rw-r--r--java/res/values/attrs.xml6
-rw-r--r--java/res/values/dimens.xml1
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java4
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java3
-rw-r--r--java/src/com/android/intentresolver/util/Flow.kt84
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt259
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java2
10 files changed, 367 insertions, 52 deletions
diff --git a/java/res/layout/image_preview_image_item.xml b/java/res/layout/image_preview_image_item.xml
index 81fa5c8e..a8a2c754 100644
--- a/java/res/layout/image_preview_image_item.xml
+++ b/java/res/layout/image_preview_image_item.xml
@@ -14,25 +14,30 @@
~ limitations under the License.
-->
-<FrameLayout
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="@dimen/chooser_preview_image_width"
- android:layout_height="@dimen/chooser_preview_image_height" >
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/chooser_preview_image_height_tall">
-<com.android.intentresolver.widget.RoundedRectImageView
- android:id="@+id/image"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_alignParentTop="true"
- android:adjustViewBounds="false"
- android:scaleType="centerCrop"
- app:radius="@dimen/chooser_corner_radius_small" />
+ <com.android.intentresolver.widget.RoundedRectImageView
+ android:id="@+id/image"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ app:layout_constraintDimensionRatio="W,1:1"
+ android:layout_alignParentTop="true"
+ android:adjustViewBounds="false"
+ android:scaleType="centerCrop"
+ app:radius="@dimen/chooser_corner_radius_small" />
<FrameLayout
android:id="@+id/badge_frame"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintStart_toStartOf="@+id/image"
+ app:layout_constraintEnd_toEndOf="@+id/image"
+ app:layout_constraintTop_toTopOf="@+id/image"
+ app:layout_constraintBottom_toBottomOf="@+id/image"
android:background="@drawable/content_preview_badge_bg">
<ImageView
@@ -45,4 +50,4 @@
android:tint="@android:color/white"
android:layout_gravity="bottom|end" />
</FrameLayout>
-</FrameLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/java/res/layout/image_preview_other_item.xml b/java/res/layout/image_preview_other_item.xml
index b7cc4350..07f87e3a 100644
--- a/java/res/layout/image_preview_other_item.xml
+++ b/java/res/layout/image_preview_other_item.xml
@@ -17,7 +17,7 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/chooser_preview_image_width"
- android:layout_height="@dimen/chooser_preview_image_height">
+ android:layout_height="@dimen/chooser_preview_image_height_tall">
<TextView
android:id="@+id/label"
diff --git a/java/res/layout/scrollable_image_preview_view.xml b/java/res/layout/scrollable_image_preview_view.xml
index c6c310e6..0d41f1ae 100644
--- a/java/res/layout/scrollable_image_preview_view.xml
+++ b/java/res/layout/scrollable_image_preview_view.xml
@@ -15,12 +15,21 @@
~ limitations under the License.
-->
-<com.android.intentresolver.widget.ScrollableImagePreviewView
- xmlns:android="http://schemas.android.com/apk/res/android"
+<!-- TODO: the unnecessary FrameLayout wrapping is a workaround for ViewStub (it ignores this view's
+ width and height specs); remove when when the legacy image preview is removed -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:paddingStart="@dimen/chooser_edge_margin_normal"
- android:paddingEnd="@dimen/chooser_edge_margin_normal"
- android:paddingBottom="@dimen/chooser_view_spacing"
- android:background="?android:attr/colorBackground" />
+ android:layout_height="wrap_content">
+
+ <com.android.intentresolver.widget.ScrollableImagePreviewView
+ android:id="@+id/scrollable_image_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/chooser_preview_image_height_tall"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing"
+ android:background="?android:attr/colorBackground"
+ app:itemInnerSpacing="3dp"
+ app:itemOuterSpacing="@dimen/chooser_edge_margin_normal"
+ app:maxWidthHint="@dimen/chooser_width" />
+</FrameLayout>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index eba6b9b7..67acb3ae 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -45,4 +45,10 @@
<declare-styleable name="RoundedRectImageView">
<attr name="radius" format="dimension" />
</declare-styleable>
+
+ <declare-styleable name="ScrollableImagePreviewView">
+ <attr name="itemInnerSpacing" format="dimension" />
+ <attr name="itemOuterSpacing" format="dimension" />
+ <attr name="maxWidthHint" format="dimension" />
+ </declare-styleable>
</resources>
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index af90c4ef..7daa9206 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -28,6 +28,7 @@
<dimen name="chooser_preview_image_border">1dp</dimen>
<dimen name="chooser_preview_image_width">120dp</dimen>
<dimen name="chooser_preview_image_height">104dp</dimen>
+ <dimen name="chooser_preview_image_height_tall">192dp</dimen>
<dimen name="chooser_preview_image_max_dimen">200dp</dimen>
<dimen name="chooser_preview_width">-1px</dimen>
<dimen name="chooser_header_scroll_elevation">4dp</dimen>
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 37a17e79..44000bef 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -1299,7 +1299,9 @@ public class ChooserActivity extends ResolverActivity implements
final int cacheSize;
if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) {
float chooserWidth = getResources().getDimension(R.dimen.chooser_width);
- float imageWidth = getResources().getDimension(R.dimen.chooser_preview_image_width);
+ // imageWidth = imagePreviewHeight / minAspectRatio (see ScrollableImagePreviewView)
+ float imageWidth =
+ getResources().getDimension(R.dimen.chooser_preview_image_height_tall) * 5 / 2;
cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2);
} else {
cacheSize = 3;
diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
index ee24d18f..c4e6feb7 100644
--- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
@@ -152,8 +152,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
stub.setLayoutResource(R.layout.scrollable_image_preview_view);
stub.inflate();
}
- return previewLayout.findViewById(
- com.android.internal.R.id.content_preview_image_area);
+ return previewLayout.findViewById(R.id.scrollable_image_preview);
}
private void setTextInImagePreviewVisibility(
diff --git a/java/src/com/android/intentresolver/util/Flow.kt b/java/src/com/android/intentresolver/util/Flow.kt
new file mode 100644
index 00000000..1155b9fe
--- /dev/null
+++ b/java/src/com/android/intentresolver/util/Flow.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util
+
+import android.os.SystemClock
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.launch
+
+/**
+ * Returns a flow that mirrors the original flow, but delays values following emitted values for the
+ * given [periodMs]. If the original flow emits more than one value during this period, only the
+ * latest value is emitted.
+ *
+ * Example:
+ *
+ * ```kotlin
+ * flow {
+ * emit(1) // t=0ms
+ * delay(90)
+ * emit(2) // t=90ms
+ * delay(90)
+ * emit(3) // t=180ms
+ * delay(1010)
+ * emit(4) // t=1190ms
+ * delay(1010)
+ * emit(5) // t=2200ms
+ * }.throttle(1000)
+ * ```
+ *
+ * produces the following emissions at the following times
+ *
+ * ```text
+ * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
+ * ```
+ */
+// A SystemUI com.android.systemui.util.kotlin.throttle copy.
+fun <T> Flow<T>.throttle(periodMs: Long): Flow<T> = channelFlow {
+ coroutineScope {
+ var previousEmitTimeMs = 0L
+ var delayJob: Job? = null
+ var sendJob: Job? = null
+ val outerScope = this
+
+ collect {
+ delayJob?.cancel()
+ sendJob?.join()
+ val currentTimeMs = SystemClock.elapsedRealtime()
+ val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs
+ val timeUntilNextEmit = maxOf(0L, periodMs - timeSinceLastEmit)
+ if (timeUntilNextEmit > 0L) {
+ // We create delayJob to allow cancellation during the delay period
+ delayJob = launch {
+ delay(timeUntilNextEmit)
+ sendJob = outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ send(it)
+ previousEmitTimeMs = SystemClock.elapsedRealtime()
+ }
+ }
+ } else {
+ send(it)
+ previousEmitTimeMs = currentTimeMs
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
index c02a10a2..d1b0f5b4 100644
--- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
@@ -17,6 +17,7 @@
package com.android.intentresolver.widget
import android.content.Context
+import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import android.util.AttributeSet
@@ -27,20 +28,32 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.intentresolver.R
+import com.android.intentresolver.util.throttle
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
-import kotlin.math.sign
+import java.util.ArrayDeque
+import kotlin.math.roundToInt
private const val TRANSITION_NAME = "screenshot_preview_image"
private const val PLURALS_COUNT = "count"
+private const val ADAPTER_UPDATE_INTERVAL_MS = 150L
+private const val MIN_ASPECT_RATIO = 0.4f
+private const val MIN_ASPECT_RATIO_STRING = "2:5"
+private const val MAX_ASPECT_RATIO = 2.5f
+private const val MAX_ASPECT_RATIO_STRING = "5:2"
class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
constructor(context: Context) : this(context, null)
@@ -50,14 +63,53 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
) : super(context, attrs, defStyleAttr) {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = Adapter(context)
- val spacing = TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP, 5f, context.resources.displayMetrics
- ).toInt()
- addItemDecoration(SpacingDecoration(spacing))
+
+ context.obtainStyledAttributes(
+ attrs, R.styleable.ScrollableImagePreviewView, defStyleAttr, 0
+ ).use { a ->
+ var innerSpacing = a.getDimensionPixelSize(
+ R.styleable.ScrollableImagePreviewView_itemInnerSpacing, -1
+ )
+ if (innerSpacing < 0) {
+ innerSpacing = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 3f, context.resources.displayMetrics
+ ).toInt()
+ }
+ var outerSpacing = a.getDimensionPixelSize(
+ R.styleable.ScrollableImagePreviewView_itemOuterSpacing, -1
+ )
+ if (outerSpacing < 0) {
+ outerSpacing = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics
+ ).toInt()
+ }
+ addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing))
+
+ maxWidthHint = a.getDimensionPixelSize(
+ R.styleable.ScrollableImagePreviewView_maxWidthHint, -1
+ )
+ }
}
+ private var batchLoader: BatchPreviewLoader? = null
private val previewAdapter get() = adapter as Adapter
+ /**
+ * A hint about the maximum width this view can grow to, this helps to optimize preview
+ * loading.
+ */
+ var maxWidthHint: Int = -1
+ private var requestedHeight: Int = 0
+ private var isMeasured = false
+
+ override fun onMeasure(widthSpec: Int, heightSpec: Int) {
+ super.onMeasure(widthSpec, heightSpec)
+ if (!isMeasured) {
+ isMeasured = true
+ batchLoader?.loadAspectRatios(getMaxWidth(), this::calcPreviewWidth)
+ }
+ }
+
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
setOverScrollMode(
@@ -69,32 +121,103 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
previewAdapter.transitionStatusElementCallback = callback
}
- fun setPreviews(previews: List<Preview>, otherItemCount: Int, imageLoader: ImageLoader) =
- previewAdapter.setPreviews(previews, otherItemCount, imageLoader)
+ fun setPreviews(previews: List<Preview>, otherItemCount: Int, imageLoader: ImageLoader) {
+ previewAdapter.reset(0, imageLoader)
+ batchLoader?.cancel()
+ batchLoader = BatchPreviewLoader(
+ previewAdapter,
+ imageLoader,
+ previews,
+ otherItemCount,
+ ).apply {
+ if (isMeasured) {
+ loadAspectRatios(getMaxWidth(), this@ScrollableImagePreviewView::calcPreviewWidth)
+ }
+ }
+ }
+
+ private fun getMaxWidth(): Int =
+ when {
+ maxWidthHint > 0 -> maxWidthHint
+ isLaidOut -> width
+ else -> measuredWidth
+ }
+
+ private fun calcPreviewWidth(bitmap: Bitmap): Int {
+ val effectiveHeight = if (isLaidOut) height else measuredHeight
+ return if (bitmap.width <= 0 || bitmap.height <= 0) {
+ effectiveHeight
+ } else {
+ val ar = (bitmap.width.toFloat() / bitmap.height.toFloat())
+ .coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO)
+ (effectiveHeight * ar).roundToInt()
+ }
+ }
+
+ class Preview internal constructor(
+ val type: PreviewType,
+ val uri: Uri,
+ internal var aspectRatioString: String
+ ) {
+ constructor(type: PreviewType, uri: Uri) : this(type, uri, "1:1")
+
+ internal var bitmap: Bitmap? = null
+
+ internal fun updateAspectRatio(width: Int, height: Int) {
+ if (width <= 0 || height <= 0) return
+ val aspectRatio = width.toFloat() / height.toFloat()
+ aspectRatioString = when {
+ aspectRatio <= MIN_ASPECT_RATIO -> MIN_ASPECT_RATIO_STRING
+ aspectRatio >= MAX_ASPECT_RATIO -> MAX_ASPECT_RATIO_STRING
+ else -> "$width:$height"
+ }
+ }
+ }
- class Preview(val type: PreviewType, val uri: Uri)
enum class PreviewType {
Image, Video, File
}
- private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() {
+ private class Adapter(
+ private val context: Context
+ ) : RecyclerView.Adapter<ViewHolder>() {
private val previews = ArrayList<Preview>()
private var imageLoader: ImageLoader? = null
private var firstImagePos = -1
+ private var totalItemCount: Int = 0
+
+ private val hasOtherItem get() = previews.size < totalItemCount
+
var transitionStatusElementCallback: TransitionElementStatusCallback? = null
- private var otherItemCount = 0
- fun setPreviews(
- previews: List<Preview>, otherItemCount: Int, imageLoader: ImageLoader
- ) {
- this.previews.clear()
- this.previews.addAll(previews)
+ fun reset(totalItemCount: Int, imageLoader: ImageLoader) {
this.imageLoader = imageLoader
- firstImagePos = previews.indexOfFirst { it.type == PreviewType.Image }
- this.otherItemCount = maxOf(0, otherItemCount)
+ firstImagePos = -1
+ previews.clear()
+ this.totalItemCount = maxOf(0, totalItemCount)
notifyDataSetChanged()
}
+ fun addPreviews(newPreviews: Collection<Preview>) {
+ if (newPreviews.isEmpty()) return
+ val insertPos = previews.size
+ val hadOtherItem = hasOtherItem
+ previews.addAll(newPreviews)
+ if (firstImagePos < 0) {
+ val pos = newPreviews.indexOfFirst { it.type == PreviewType.Image }
+ if (pos >= 0) firstImagePos = insertPos + pos
+ }
+ notifyItemRangeInserted(insertPos, newPreviews.size)
+ when {
+ hadOtherItem && previews.size >= totalItemCount -> {
+ notifyItemRemoved(previews.size)
+ }
+ !hadOtherItem && previews.size < totalItemCount -> {
+ notifyItemInserted(previews.size)
+ }
+ }
+ }
+
override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(itemType, parent, false);
return if (itemType == R.layout.image_preview_other_item) {
@@ -104,7 +227,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
}
- override fun getItemCount(): Int = previews.size + otherItemCount.sign
+ override fun getItemCount(): Int = previews.size + if (hasOtherItem) 1 else 0
override fun getItemViewType(position: Int): Int {
return if (position == previews.size) {
@@ -116,7 +239,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
override fun onBindViewHolder(vh: ViewHolder, position: Int) {
when (vh) {
- is OtherItemViewHolder -> vh.bind(otherItemCount)
+ is OtherItemViewHolder -> vh.bind(totalItemCount - previews.size)
is PreviewViewHolder -> vh.bind(
previews[position],
imageLoader ?: error("ImageLoader is missing"),
@@ -163,6 +286,9 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
previewReadyCallback: ((String) -> Unit)?
) {
image.setImageDrawable(null)
+ (image.layoutParams as? ConstraintLayout.LayoutParams)?.let { params ->
+ params.dimensionRatio = preview.aspectRatioString
+ }
image.transitionName = if (previewReadyCallback != null) {
TRANSITION_NAME
} else {
@@ -180,7 +306,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
}
resetScope().launch {
- loadImage(preview.uri, imageLoader)
+ loadImage(preview, imageLoader)
if (preview.type == PreviewType.Image) {
previewReadyCallback?.let { callback ->
image.waitForPreDraw()
@@ -190,11 +316,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
}
- private suspend fun loadImage(uri: Uri, imageLoader: ImageLoader) {
- val bitmap = runCatching {
+ private suspend fun loadImage(preview: Preview, imageLoader: ImageLoader) {
+ val bitmap = preview.bitmap ?: runCatching {
// it's expected for all loading/caching optimizations to be implemented by the
// loader
- imageLoader(uri)
+ imageLoader(preview.uri)
}.getOrNull()
image.setImageBitmap(bitmap)
}
@@ -225,9 +351,92 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
override fun unbind() = Unit
}
- private class SpacingDecoration(private val margin: Int) : ItemDecoration() {
+ private class SpacingDecoration(
+ private val innerSpacing: Int,
+ private val outerSpacing: Int
+ ) : ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
- outRect.set(margin, 0, margin, 0)
+ val itemCount = parent.adapter?.itemCount ?: return
+ val pos = parent.getChildAdapterPosition(view)
+ var leftMargin = if (pos == 0) outerSpacing else innerSpacing
+ var rightMargin = if (pos == itemCount - 1) outerSpacing else 0
+ outRect.set(leftMargin, 0, rightMargin, 0)
+ }
+ }
+
+ private class BatchPreviewLoader(
+ private val adapter: Adapter,
+ private val imageLoader: ImageLoader,
+ previews: List<Preview>,
+ otherItemCount: Int,
+ ) {
+ private val pendingPreviews = ArrayDeque<Preview>(previews)
+ private val totalItemCount = previews.size + otherItemCount
+ private var scope: CoroutineScope? = MainScope() + Dispatchers.Main.immediate
+
+ fun cancel() {
+ scope?.cancel()
+ scope = null
+ }
+
+ fun loadAspectRatios(maxWidth: Int, previewWidthCalculator: (Bitmap) -> Int) {
+ val scope = this.scope ?: return
+ val updates = ArrayDeque<Preview>(pendingPreviews.size)
+ // replay 2 items to guarantee that we'd get at least one update
+ val reportFlow = MutableSharedFlow<Any>(replay = 2)
+ var isFirstUpdate = true
+ val updateEvent = Any()
+ val completedEvent = Any()
+ // throttle adapter updates by waiting on the channel, the channel first notified
+ // when enough preview elements is loaded and then periodically with a delay
+ scope.launch(Dispatchers.Main) {
+ reportFlow
+ .takeWhile { it !== completedEvent }
+ .throttle(ADAPTER_UPDATE_INTERVAL_MS)
+ .collect {
+ if (isFirstUpdate) {
+ isFirstUpdate = false
+ adapter.reset(totalItemCount, imageLoader)
+ }
+ if (updates.isNotEmpty()) {
+ adapter.addPreviews(updates)
+ updates.clear()
+ }
+ }
+ }
+
+ scope.launch {
+ var loadedPreviewWidth = 0
+ List<Job>(4) {
+ launch {
+ while (pendingPreviews.isNotEmpty()) {
+ val preview = pendingPreviews.poll() ?: continue
+ val bitmap = runCatching {
+ // TODO: decide on adding a timeout
+ imageLoader(preview.uri)
+ }.getOrNull() ?: continue
+ preview.updateAspectRatio(bitmap.width, bitmap.height)
+ updates.add(preview)
+ if (loadedPreviewWidth < maxWidth) {
+ loadedPreviewWidth += previewWidthCalculator(bitmap)
+ // cache bitmaps for the first preview items to aovid potential
+ // double-loading (in case those values are evicted from the image
+ // loader's cache)
+ preview.bitmap = bitmap
+ if (loadedPreviewWidth >= maxWidth) {
+ // notify that the preview now can be displayed
+ reportFlow.emit(updateEvent)
+ }
+ } else {
+ reportFlow.emit(updateEvent)
+ }
+ }
+ }
+ }.joinAll()
+ // in case all previews have failed to load
+ reportFlow.emit(updateEvent)
+ reportFlow.emit(completedEvent)
+ }
}
}
}
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index 596b546e..8b9aaa60 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -1030,7 +1030,7 @@ public class UnbundledChooserActivityTest {
setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
- onView(withId(com.android.internal.R.id.content_preview_image_area))
+ onView(withId(R.id.scrollable_image_preview))
.perform(RecyclerViewActions.scrollToLastPosition())
.check((view, exception) -> {
if (exception != null) {