diff options
| author | 2023-09-15 02:34:55 +0000 | |
|---|---|---|
| committer | 2023-09-15 02:34:55 +0000 | |
| commit | c7d61dc6a727ffad6e6d26f312e6d5928c93bd6b (patch) | |
| tree | 96ee12cd4e4d659e785d75927fa8ee58d655dd86 /java | |
| parent | 3d452b32fd57c504287e1c4608c80e7fa91772d6 (diff) | |
| parent | 6d855ba6e08a42acff5a6f916fe73c66879cb1da (diff) | |
Merge "Add fade-in animation for preview thumbnails" into main
Diffstat (limited to 'java')
| -rw-r--r-- | java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt | 137 |
1 files changed, 125 insertions, 12 deletions
diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index 3bbafc40..7fe16091 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -26,11 +26,16 @@ import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.animation.Animation.AnimationListener +import android.view.animation.DecelerateInterpolator import android.widget.ImageView import android.widget.TextView import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.intentresolver.R @@ -45,6 +50,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine private const val TRANSITION_NAME = "screenshot_preview_image" private const val PLURALS_COUNT = "count" @@ -65,7 +71,6 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { defStyleAttr: Int ) : super(context, attrs, defStyleAttr) { layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) - adapter = Adapter(context) context .obtainStyledAttributes(attrs, R.styleable.ScrollableImagePreviewView, defStyleAttr, 0) @@ -98,11 +103,14 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { ) .toInt() } - addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing)) + super.addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing)) maxWidthHint = a.getDimensionPixelSize(R.styleable.ScrollableImagePreviewView_maxWidthHint, -1) } + val itemAnimator = ItemAnimator() + super.setItemAnimator(itemAnimator) + super.setAdapter(Adapter(context, itemAnimator.getAddDuration())) } private var batchLoader: BatchPreviewLoader? = null @@ -167,6 +175,14 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { return null } + override fun setAdapter(adapter: RecyclerView.Adapter<*>?) { + error("This method is not supported") + } + + override fun setItemAnimator(animator: RecyclerView.ItemAnimator?) { + error("This method is not supported") + } + fun setImageLoader(imageLoader: CachingImageLoader) { previewAdapter.imageLoader = imageLoader } @@ -269,7 +285,10 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { File } - private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() { + private class Adapter( + private val context: Context, + private val fadeInDurationMs: Long, + ) : RecyclerView.Adapter<ViewHolder>() { private val previews = ArrayList<Preview>() private val imagePreviewDescription = context.resources.getString(R.string.image_preview_a11y_description) @@ -311,15 +330,17 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { if (newPreviews.isEmpty()) return val insertPos = previews.size val hadOtherItem = hasOtherItem - val wasEmpty = previews.isEmpty() + val oldItemCount = getItemCount() previews.addAll(newPreviews) if (firstImagePos < 0) { val pos = newPreviews.indexOfFirst { it.type == PreviewType.Image } if (pos >= 0) firstImagePos = insertPos + pos } - if (wasEmpty) { - // we don't want any item animation in that case - notifyDataSetChanged() + if (insertPos == 0) { + if (oldItemCount > 0) { + notifyItemRangeRemoved(0, oldItemCount) + } + notifyItemRangeInserted(insertPos, getItemCount()) } else { notifyItemRangeInserted(insertPos, newPreviews.size) when { @@ -366,6 +387,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { vh.bind( previews[position], imageLoader ?: error("ImageLoader is missing"), + fadeInDurationMs, isSharedTransitionElement = position == firstImagePos, previewReadyCallback = if ( @@ -416,10 +438,13 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { fun bind( preview: Preview, imageLoader: CachingImageLoader, + fadeInDurationMs: Long, isSharedTransitionElement: Boolean, previewReadyCallback: ((String) -> Unit)? ) { image.setImageDrawable(null) + image.alpha = 1f + image.clearAnimation() (image.layoutParams as? ConstraintLayout.LayoutParams)?.let { params -> params.dimensionRatio = preview.aspectRatioString } @@ -453,11 +478,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } resetScope().launch { loadImage(preview, imageLoader) - if (preview.type == PreviewType.Image) { - previewReadyCallback?.let { callback -> - image.waitForPreDraw() - callback(TRANSITION_NAME) - } + if (preview.type == PreviewType.Image && previewReadyCallback != null) { + image.waitForPreDraw() + previewReadyCallback(TRANSITION_NAME) + } else if (image.isAttachedToWindow()) { + fadeInPreview(fadeInDurationMs) } } } @@ -473,6 +498,30 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { image.setImageBitmap(bitmap) } + private suspend fun fadeInPreview(durationMs: Long) = + suspendCancellableCoroutine { continuation -> + val animation = + AlphaAnimation(0f, 1f).apply { + duration = durationMs + interpolator = DecelerateInterpolator() + setAnimationListener( + object : AnimationListener { + override fun onAnimationStart(animation: Animation?) = Unit + override fun onAnimationRepeat(animation: Animation?) = Unit + + override fun onAnimationEnd(animation: Animation?) { + continuation.resumeWith(Result.success(Unit)) + } + } + ) + } + image.startAnimation(animation) + continuation.invokeOnCancellation { + image.clearAnimation() + image.alpha = 1f + } + } + private fun resetScope(): CoroutineScope = CoroutineScope(Dispatchers.Main.immediate).also { scope?.cancel() @@ -521,6 +570,70 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } } + /** + * ItemAnimator to handle a special case of addng first image items into the view. The view is + * used with wrap_content width spec thus after adding the first views it, generally, changes + * its size and position breaking the animation. This class handles that by preserving loading + * idicator position in this special case. + */ + private inner class ItemAnimator() : DefaultItemAnimator() { + private var animatedVH: ViewHolder? = null + private var originalTranslation = 0f + + override fun recordPreLayoutInformation( + state: State, + viewHolder: RecyclerView.ViewHolder, + changeFlags: Int, + payloads: MutableList<Any> + ): ItemHolderInfo { + return super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads).let { + holderInfo -> + if (viewHolder is LoadingItemViewHolder && getChildCount() == 1) { + LoadingItemHolderInfo(holderInfo, parentLeft = left) + } else { + holderInfo + } + } + } + + override fun animateDisappearance( + viewHolder: RecyclerView.ViewHolder, + preLayoutInfo: ItemHolderInfo, + postLayoutInfo: ItemHolderInfo? + ): Boolean { + if (viewHolder is LoadingItemViewHolder && preLayoutInfo is LoadingItemHolderInfo) { + val view = viewHolder.itemView + animatedVH = viewHolder + originalTranslation = view.getTranslationX() + view.setTranslationX( + (preLayoutInfo.parentLeft - left + preLayoutInfo.left).toFloat() - view.left + ) + } + return super.animateDisappearance(viewHolder, preLayoutInfo, postLayoutInfo) + } + + override fun onRemoveFinished(viewHolder: RecyclerView.ViewHolder) { + if (animatedVH === viewHolder) { + viewHolder.itemView.setTranslationX(originalTranslation) + animatedVH = null + } + super.onRemoveFinished(viewHolder) + } + + private inner class LoadingItemHolderInfo( + holderInfo: ItemHolderInfo, + val parentLeft: Int, + ) : ItemHolderInfo() { + init { + left = holderInfo.left + top = holderInfo.top + right = holderInfo.right + bottom = holderInfo.bottom + changeFlags = holderInfo.changeFlags + } + } + } + @VisibleForTesting class BatchPreviewLoader( private val imageLoader: CachingImageLoader, |