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