diff options
| author | 2023-01-17 20:00:17 -0800 | |
|---|---|---|
| committer | 2023-01-30 16:23:53 -0800 | |
| commit | 53d426f78b1702ae471ae57fc6818674f196954c (patch) | |
| tree | 883fea927febe00f06dba829a4572d07b71dcef6 /java | |
| parent | aafb031120eee089a9bce25043c9e55128e6e7da (diff) | |
Simple scrollable image preview view
A simple scrollale image preview view is created but not yet used.
Bug: 262280076
Test: Add a debug code that replaces legacy image preview view with the
new implementation. Verify basic functionality: small and large image
count, screenshot transition animation.
Change-Id: I5d8f00b8617abae66f76931b21872154f4726851
Diffstat (limited to 'java')
4 files changed, 238 insertions, 15 deletions
diff --git a/java/res/layout/image_preview_image_item.xml b/java/res/layout/image_preview_image_item.xml new file mode 100644 index 00000000..3895b6b4 --- /dev/null +++ b/java/res/layout/image_preview_image_item.xml @@ -0,0 +1,24 @@ +<!-- + ~ 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. + --> + +<com.android.intentresolver.widget.RoundedRectImageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/image" + android:layout_width="120dp" + android:layout_height="104dp" + android:layout_alignParentTop="true" + android:adjustViewBounds="false" + android:scaleType="centerCrop"/> diff --git a/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt b/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt new file mode 100644 index 00000000..a7906001 --- /dev/null +++ b/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt @@ -0,0 +1,36 @@ +/* + * 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.widget + +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +internal val RecyclerView.areAllChildrenVisible: Boolean + get() { + val count = getChildCount() + if (count == 0) return true + val first = getChildAt(0) + val last = getChildAt(count - 1) + val itemCount = adapter?.itemCount ?: 0 + return getChildAdapterPosition(first) == 0 + && getChildAdapterPosition(last) == itemCount - 1 + && isFullyVisible(first) + && isFullyVisible(last) + } + +private fun RecyclerView.isFullyVisible(view: View): Boolean = + view.left >= paddingLeft && view.right <= width - paddingRight diff --git a/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt index a941b97a..81630545 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt @@ -50,21 +50,6 @@ class ScrollableActionRow : RecyclerView, ActionRow { ) } - private val areAllChildrenVisible: Boolean - get() { - val count = getChildCount() - if (count == 0) return true - val first = getChildAt(0) - val last = getChildAt(count - 1) - return getChildAdapterPosition(first) == 0 - && getChildAdapterPosition(last) == actionsAdapter.itemCount - 1 - && isFullyVisible(first) - && isFullyVisible(last) - } - - private fun isFullyVisible(view: View): Boolean = - view.left >= paddingLeft && view.right <= width - paddingRight - private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() { private val iconSize: Int = context.resources.getDimensionPixelSize(R.dimen.chooser_action_view_icon_size) diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt new file mode 100644 index 00000000..467c404a --- /dev/null +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -0,0 +1,178 @@ +/* + * 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.widget + +import android.content.Context +import android.graphics.Rect +import android.net.Uri +import android.util.AttributeSet +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.intentresolver.R +import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus + +private const val TRANSITION_NAME = "screenshot_preview_image" + +class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { + constructor(context: Context) : this(context, null) + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + constructor( + context: Context, attrs: AttributeSet?, defStyleAttr: Int + ) : 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)) + } + + private val previewAdapter get() = adapter as Adapter + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, l, t, r, b) + setOverScrollMode( + if (areAllChildrenVisible) View.OVER_SCROLL_NEVER else View.OVER_SCROLL_ALWAYS + ) + } + + override fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) { + previewAdapter.transitionStatusElementCallback = callback + } + + override fun setImages(uris: List<Uri>, imageLoader: ImageLoader) { + previewAdapter.setImages(uris, imageLoader) + } + + private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() { + private val uris = ArrayList<Uri>() + private var imageLoader: ImageLoader? = null + var transitionStatusElementCallback: TransitionElementStatusCallback? = null + + fun setImages(uris: List<Uri>, imageLoader: ImageLoader) { + this.uris.clear() + this.uris.addAll(uris) + this.imageLoader = imageLoader + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context) + .inflate(R.layout.image_preview_image_item, parent, false) + ) + } + + override fun getItemCount(): Int = uris.size + + override fun onBindViewHolder(vh: ViewHolder, position: Int) { + vh.bind( + uris[position], + imageLoader ?: error("ImageLoader is missing"), + if (position == 0 && transitionStatusElementCallback != null) { + this::onTransitionElementReady + } else { + null + } + ) + } + + override fun onViewRecycled(vh: ViewHolder) { + vh.unbind() + } + + override fun onFailedToRecycleView(vh: ViewHolder): Boolean { + vh.unbind() + return super.onFailedToRecycleView(vh) + } + + private fun onTransitionElementReady(name: String) { + transitionStatusElementCallback?.apply { + onTransitionElementReady(name) + onAllTransitionElementsReady() + } + transitionStatusElementCallback = null + } + } + + private class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + private val image = view.requireViewById<ImageView>(R.id.image) + private var scope: CoroutineScope? = null + + fun bind( + uri: Uri, + imageLoader: ImageLoader, + previewReadyCallback: ((String) -> Unit)? + ) { + image.setImageDrawable(null) + image.transitionName = if (previewReadyCallback != null) { + TRANSITION_NAME + } else { + null + } + resetScope().launch { + loadImage(uri, imageLoader, previewReadyCallback) + } + } + + private suspend fun loadImage( + uri: Uri, + imageLoader: ImageLoader, + previewReadyCallback: ((String) -> Unit)? + ) { + val bitmap = runCatching { + // it's expected for all loading/caching optimizations to be implemented by the + // loader + imageLoader(uri) + }.getOrNull() + image.setImageBitmap(bitmap) + previewReadyCallback?.let { callback -> + image.waitForPreDraw() + callback(TRANSITION_NAME) + } + } + + private fun resetScope(): CoroutineScope = + (MainScope() + Dispatchers.Main.immediate).also { + scope?.cancel() + scope = it + } + + fun unbind() { + scope?.cancel() + scope = null + } + } + + private class SpacingDecoration(private val margin: Int) : RecyclerView.ItemDecoration() { + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) { + outRect.set(margin, 0, margin, 0) + } + } +} |