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 package com.android.wallpaper.picker.preview.ui.binder
17 
18 import android.animation.Animator
19 import android.animation.AnimatorListenerAdapter
20 import android.graphics.Point
21 import android.graphics.Rect
22 import android.graphics.RenderEffect
23 import android.graphics.Shader
24 import android.view.View
25 import android.view.animation.Interpolator
26 import android.view.animation.PathInterpolator
27 import android.widget.ImageView
28 import androidx.core.view.doOnLayout
29 import androidx.core.view.isVisible
30 import com.android.app.tracing.TraceUtils.trace
31 import com.android.wallpaper.picker.preview.shared.model.CropSizeModel
32 import com.android.wallpaper.picker.preview.shared.model.FullPreviewCropModel
33 import com.android.wallpaper.picker.preview.ui.util.FullResImageViewUtil
34 import com.android.wallpaper.picker.preview.ui.viewmodel.StaticWallpaperPreviewViewModel
35 import com.android.wallpaper.util.RtlUtils
36 import com.android.wallpaper.util.WallpaperCropUtils
37 import com.android.wallpaper.util.WallpaperSurfaceCallback.LOW_RES_BITMAP_BLUR_RADIUS
38 import com.davemorrissey.labs.subscaleview.ImageSource
39 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
40 import kotlin.math.max
41 import kotlin.math.min
42 import kotlinx.coroutines.CoroutineScope
43 import kotlinx.coroutines.launch
44 
45 object StaticWallpaperPreviewBinder {
46 
47     private val ALPHA_OUT: Interpolator = PathInterpolator(0f, 0f, 0.8f, 1f)
48     private const val CROSS_FADE_DURATION: Long = 200
49 
50     fun bind(
51         lowResImageView: ImageView,
52         fullResImageView: SubsamplingScaleImageView,
53         viewModel: StaticWallpaperPreviewViewModel,
54         displaySize: Point,
55         parentCoroutineScope: CoroutineScope,
56         isFullScreen: Boolean = false,
57     ) {
58         lowResImageView.initLowResImageView()
59         fullResImageView.initFullResImageView()
60 
61         parentCoroutineScope.launch {
62             // Show low res image only for small preview with supported wallpaper
63             if (!isFullScreen) {
64                 launch {
65                     viewModel.lowResBitmap.collect {
66                         it?.let {
67                             lowResImageView.setImageBitmap(it)
68                             lowResImageView.isVisible = true
69                         }
70                     }
71                 }
72             }
73 
74             launch {
75                 viewModel.subsamplingScaleImageViewModel.collect { imageModel ->
76                     trace(TAG) {
77                         val cropHint = imageModel.fullPreviewCropModels?.get(displaySize)?.cropHint
78                         fullResImageView.setFullResImage(
79                             ImageSource.cachedBitmap(imageModel.rawWallpaperBitmap),
80                             imageModel.rawWallpaperSize,
81                             displaySize,
82                             cropHint,
83                             RtlUtils.isRtl(lowResImageView.context),
84                             isFullScreen,
85                         )
86 
87                         // Fill in the default crop region if the displaySize for this preview
88                         // is missing.
89                         val imageSize = Point(fullResImageView.width, fullResImageView.height)
90                         viewModel.updateDefaultPreviewCropModel(
91                             displaySize,
92                             FullPreviewCropModel(
93                                 cropHint =
94                                     WallpaperCropUtils.calculateVisibleRect(
95                                         imageModel.rawWallpaperSize,
96                                         imageSize,
97                                     ),
98                                 cropSizeModel =
99                                     CropSizeModel(
100                                         wallpaperZoom =
101                                             WallpaperCropUtils.calculateMinZoom(
102                                                 imageModel.rawWallpaperSize,
103                                                 imageSize,
104                                             ),
105                                         hostViewSize = imageSize,
106                                         cropViewSize =
107                                             WallpaperCropUtils.calculateCropSurfaceSize(
108                                                 fullResImageView.resources,
109                                                 max(imageSize.x, imageSize.y),
110                                                 min(imageSize.x, imageSize.y),
111                                                 imageSize.x,
112                                                 imageSize.y,
113                                             ),
114                                     ),
115                             ),
116                         )
117 
118                         if (lowResImageView.isVisible) {
119                             crossFadeInFullResImageView(lowResImageView, fullResImageView)
120                         }
121                     }
122                 }
123             }
124         }
125     }
126 
127     private fun ImageView.initLowResImageView() {
128         setRenderEffect(
129             RenderEffect.createBlurEffect(
130                 LOW_RES_BITMAP_BLUR_RADIUS,
131                 LOW_RES_BITMAP_BLUR_RADIUS,
132                 Shader.TileMode.CLAMP
133             )
134         )
135     }
136 
137     private fun SubsamplingScaleImageView.initFullResImageView() {
138         setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
139         setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
140     }
141 
142     private fun SubsamplingScaleImageView.setFullResImage(
143         imageSource: ImageSource,
144         rawWallpaperSize: Point,
145         displaySize: Point,
146         cropHint: Rect?,
147         isRtl: Boolean,
148         isFullScreen: Boolean,
149     ) {
150         // Set the full res image
151         setImage(imageSource)
152         // Calculate the scale and the center point for the full res image
153         doOnLayout {
154             FullResImageViewUtil.getScaleAndCenter(
155                     Point(measuredWidth, measuredHeight),
156                     rawWallpaperSize,
157                     displaySize,
158                     cropHint,
159                     isRtl,
160                     systemScale =
161                         if (isFullScreen) 1f
162                         else
163                             WallpaperCropUtils.getSystemWallpaperMaximumScale(
164                                 context.applicationContext,
165                             ),
166                 )
167                 .let { scaleAndCenter ->
168                     minScale = scaleAndCenter.minScale
169                     maxScale = scaleAndCenter.maxScale
170                     setScaleAndCenter(scaleAndCenter.defaultScale, scaleAndCenter.center)
171                 }
172         }
173     }
174 
175     private fun crossFadeInFullResImageView(lowResImageView: ImageView, fullResImageView: View) {
176         fullResImageView.alpha = 0f
177         fullResImageView
178             .animate()
179             .alpha(1f)
180             .setInterpolator(ALPHA_OUT)
181             .setDuration(CROSS_FADE_DURATION)
182             .setListener(
183                 object : AnimatorListenerAdapter() {
184                     override fun onAnimationEnd(animation: Animator) {
185                         lowResImageView.setImageBitmap(null)
186                     }
187                 }
188             )
189     }
190 
191     private const val TAG = "StaticWallpaperPreviewBinder"
192 }
193