• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.animation.ObjectAnimator
20 import android.content.Context
21 import android.net.Uri
22 import android.util.AttributeSet
23 import android.view.LayoutInflater
24 import android.view.animation.DecelerateInterpolator
25 import android.widget.RelativeLayout
26 import androidx.core.view.isVisible
27 import com.android.intentresolver.R
28 import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
29 import kotlinx.coroutines.Job
30 import kotlinx.coroutines.MainScope
31 import kotlinx.coroutines.coroutineScope
32 import kotlinx.coroutines.launch
33 import com.android.internal.R as IntR
34 
35 private const val IMAGE_FADE_IN_MILLIS = 150L
36 
37 class ChooserImagePreviewView : RelativeLayout, ImagePreviewView {
38 
39     constructor(context: Context) : this(context, null)
40     constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
41 
42     constructor(
43         context: Context, attrs: AttributeSet?, defStyleAttr: Int
44     ) : this(context, attrs, defStyleAttr, 0)
45 
46     constructor(
47         context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
48     ) : super(context, attrs, defStyleAttr, defStyleRes)
49 
50     private val coroutineScope = MainScope()
51     private lateinit var mainImage: RoundedRectImageView
52     private lateinit var secondLargeImage: RoundedRectImageView
53     private lateinit var secondSmallImage: RoundedRectImageView
54     private lateinit var thirdImage: RoundedRectImageView
55 
56     private var loadImageJob: Job? = null
57     private var transitionStatusElementCallback: TransitionElementStatusCallback? = null
58 
onFinishInflatenull59     override fun onFinishInflate() {
60         LayoutInflater.from(context)
61             .inflate(R.layout.chooser_image_preview_view_internals, this, true)
62         mainImage = requireViewById(IntR.id.content_preview_image_1_large)
63         secondLargeImage = requireViewById(IntR.id.content_preview_image_2_large)
64         secondSmallImage = requireViewById(IntR.id.content_preview_image_2_small)
65         thirdImage = requireViewById(IntR.id.content_preview_image_3_small)
66     }
67 
68     /**
69      * Specifies a transition animation target readiness callback. The callback will be
70      * invoked once when views preparation is done.
71      * Should be called before [setImages].
72      */
setTransitionElementStatusCallbacknull73     override fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) {
74         transitionStatusElementCallback = callback
75     }
76 
setImagesnull77     override fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
78         loadImageJob?.cancel()
79         loadImageJob = coroutineScope.launch {
80             when (uris.size) {
81                 0 -> hideAllViews()
82                 1 -> showOneImage(uris, imageLoader)
83                 2 -> showTwoImages(uris, imageLoader)
84                 else -> showThreeImages(uris, imageLoader)
85             }
86         }
87     }
88 
hideAllViewsnull89     private fun hideAllViews() {
90         mainImage.isVisible = false
91         secondLargeImage.isVisible = false
92         secondSmallImage.isVisible = false
93         thirdImage.isVisible = false
94         invokeTransitionViewReadyCallback()
95     }
96 
showOneImagenull97     private suspend fun showOneImage(uris: List<Uri>, imageLoader: ImageLoader) {
98         secondLargeImage.isVisible = false
99         secondSmallImage.isVisible = false
100         thirdImage.isVisible = false
101         showImages(uris, imageLoader, mainImage)
102     }
103 
showTwoImagesnull104     private suspend fun showTwoImages(uris: List<Uri>, imageLoader: ImageLoader) {
105         secondSmallImage.isVisible = false
106         thirdImage.isVisible = false
107         showImages(uris, imageLoader, mainImage, secondLargeImage)
108     }
109 
showThreeImagesnull110     private suspend fun showThreeImages(uris: List<Uri>, imageLoader: ImageLoader) {
111         secondLargeImage.isVisible = false
112         showImages(uris, imageLoader, mainImage, secondSmallImage, thirdImage)
113         thirdImage.setExtraImageCount(uris.size - 3)
114     }
115 
showImagesnull116     private suspend fun showImages(
117         uris: List<Uri>, imageLoader: ImageLoader, vararg views: RoundedRectImageView
118     ) = coroutineScope {
119         for (i in views.indices) {
120             launch {
121                 loadImageIntoView(views[i], uris[i], imageLoader)
122             }
123         }
124     }
125 
loadImageIntoViewnull126     private suspend fun loadImageIntoView(
127         view: RoundedRectImageView, uri: Uri, imageLoader: ImageLoader
128     ) {
129         val bitmap = runCatching {
130             imageLoader(uri)
131         }.getOrDefault(null)
132         if (bitmap == null) {
133             view.isVisible = false
134             if (view === mainImage) {
135                 invokeTransitionViewReadyCallback()
136             }
137         } else {
138             view.isVisible = true
139             view.setImageBitmap(bitmap)
140 
141             view.alpha = 0f
142             ObjectAnimator.ofFloat(view, "alpha", 0.0f, 1.0f).apply {
143                 interpolator = DecelerateInterpolator(1.0f)
144                 duration = IMAGE_FADE_IN_MILLIS
145                 start()
146             }
147             if (view === mainImage && transitionStatusElementCallback != null) {
148                 view.waitForPreDraw()
149                 invokeTransitionViewReadyCallback()
150             }
151         }
152     }
153 
invokeTransitionViewReadyCallbacknull154     private fun invokeTransitionViewReadyCallback() {
155         transitionStatusElementCallback?.apply {
156             if (mainImage.isVisible && mainImage.drawable != null) {
157                 mainImage.transitionName?.let { onTransitionElementReady(it) }
158             }
159             onAllTransitionElementsReady()
160         }
161         transitionStatusElementCallback = null
162     }
163 }
164