• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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