1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver.widget 18 19 import android.content.Context 20 import android.graphics.Rect 21 import android.net.Uri 22 import android.util.AttributeSet 23 import android.util.TypedValue 24 import android.view.LayoutInflater 25 import android.view.View 26 import android.view.ViewGroup 27 import android.widget.ImageView 28 import androidx.recyclerview.widget.LinearLayoutManager 29 import androidx.recyclerview.widget.RecyclerView 30 import com.android.intentresolver.R 31 import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback 32 import kotlinx.coroutines.CoroutineScope 33 import kotlinx.coroutines.Dispatchers 34 import kotlinx.coroutines.MainScope 35 import kotlinx.coroutines.cancel 36 import kotlinx.coroutines.isActive 37 import kotlinx.coroutines.launch 38 import kotlinx.coroutines.plus 39 40 private const val TRANSITION_NAME = "screenshot_preview_image" 41 42 class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { 43 constructor(context: Context) : this(context, null) 44 constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 45 constructor( 46 context: Context, attrs: AttributeSet?, defStyleAttr: Int 47 ) : super(context, attrs, defStyleAttr) { 48 layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) 49 adapter = Adapter(context) 50 val spacing = TypedValue.applyDimension( 51 TypedValue.COMPLEX_UNIT_DIP, 5f, context.resources.displayMetrics 52 ).toInt() 53 addItemDecoration(SpacingDecoration(spacing)) 54 } 55 56 private val previewAdapter get() = adapter as Adapter 57 58 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { 59 super.onLayout(changed, l, t, r, b) 60 setOverScrollMode( 61 if (areAllChildrenVisible) View.OVER_SCROLL_NEVER else View.OVER_SCROLL_ALWAYS 62 ) 63 } 64 65 override fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) { 66 previewAdapter.transitionStatusElementCallback = callback 67 } 68 69 override fun setImages(uris: List<Uri>, imageLoader: ImageLoader) { 70 previewAdapter.setImages(uris, imageLoader) 71 } 72 73 private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() { 74 private val uris = ArrayList<Uri>() 75 private var imageLoader: ImageLoader? = null 76 var transitionStatusElementCallback: TransitionElementStatusCallback? = null 77 78 fun setImages(uris: List<Uri>, imageLoader: ImageLoader) { 79 this.uris.clear() 80 this.uris.addAll(uris) 81 this.imageLoader = imageLoader 82 notifyDataSetChanged() 83 } 84 85 override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder { 86 return ViewHolder( 87 LayoutInflater.from(context) 88 .inflate(R.layout.image_preview_image_item, parent, false) 89 ) 90 } 91 92 override fun getItemCount(): Int = uris.size 93 94 override fun onBindViewHolder(vh: ViewHolder, position: Int) { 95 vh.bind( 96 uris[position], 97 imageLoader ?: error("ImageLoader is missing"), 98 if (position == 0 && transitionStatusElementCallback != null) { 99 this::onTransitionElementReady 100 } else { 101 null 102 } 103 ) 104 } 105 106 override fun onViewRecycled(vh: ViewHolder) { 107 vh.unbind() 108 } 109 110 override fun onFailedToRecycleView(vh: ViewHolder): Boolean { 111 vh.unbind() 112 return super.onFailedToRecycleView(vh) 113 } 114 115 private fun onTransitionElementReady(name: String) { 116 transitionStatusElementCallback?.apply { 117 onTransitionElementReady(name) 118 onAllTransitionElementsReady() 119 } 120 transitionStatusElementCallback = null 121 } 122 } 123 124 private class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 125 private val image = view.requireViewById<ImageView>(R.id.image) 126 private var scope: CoroutineScope? = null 127 128 fun bind( 129 uri: Uri, 130 imageLoader: ImageLoader, 131 previewReadyCallback: ((String) -> Unit)? 132 ) { 133 image.setImageDrawable(null) 134 image.transitionName = if (previewReadyCallback != null) { 135 TRANSITION_NAME 136 } else { 137 null 138 } 139 resetScope().launch { 140 loadImage(uri, imageLoader, previewReadyCallback) 141 } 142 } 143 144 private suspend fun loadImage( 145 uri: Uri, 146 imageLoader: ImageLoader, 147 previewReadyCallback: ((String) -> Unit)? 148 ) { 149 val bitmap = runCatching { 150 // it's expected for all loading/caching optimizations to be implemented by the 151 // loader 152 imageLoader(uri) 153 }.getOrNull() 154 image.setImageBitmap(bitmap) 155 previewReadyCallback?.let { callback -> 156 image.waitForPreDraw() 157 callback(TRANSITION_NAME) 158 } 159 } 160 161 private fun resetScope(): CoroutineScope = 162 (MainScope() + Dispatchers.Main.immediate).also { 163 scope?.cancel() 164 scope = it 165 } 166 167 fun unbind() { 168 scope?.cancel() 169 scope = null 170 } 171 } 172 173 private class SpacingDecoration(private val margin: Int) : RecyclerView.ItemDecoration() { 174 override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) { 175 outRect.set(margin, 0, margin, 0) 176 } 177 } 178 } 179