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.SurfaceView
25 import android.view.View
26 import android.view.animation.Interpolator
27 import android.view.animation.PathInterpolator
28 import android.widget.ImageView
29 import androidx.core.view.doOnLayout
30 import androidx.core.view.isVisible
31 import com.android.app.tracing.TraceUtils.trace
32 import com.android.wallpaper.R
33 import com.android.wallpaper.picker.preview.shared.model.CropSizeModel
34 import com.android.wallpaper.picker.preview.shared.model.FullPreviewCropModel
35 import com.android.wallpaper.picker.preview.ui.util.FullResImageViewUtil
36 import com.android.wallpaper.picker.preview.ui.view.SystemScaledSubsamplingScaleImageView
37 import com.android.wallpaper.picker.preview.ui.viewmodel.StaticWallpaperPreviewViewModel
38 import com.android.wallpaper.util.RtlUtils
39 import com.android.wallpaper.util.SurfaceViewUtils.attachView
40 import com.android.wallpaper.util.WallpaperCropUtils
41 import com.android.wallpaper.util.WallpaperSurfaceCallback.LOW_RES_BITMAP_BLUR_RADIUS
42 import com.davemorrissey.labs.subscaleview.ImageSource
43 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
44 import kotlin.math.max
45 import kotlin.math.min
46 import kotlinx.coroutines.CoroutineScope
47 import kotlinx.coroutines.launch
48 
49 object StaticWallpaperPreviewBinder {
50 
51     private val ALPHA_OUT: Interpolator = PathInterpolator(0f, 0f, 0.8f, 1f)
52     private const val CROSS_FADE_DURATION: Long = 200
53 
54     fun bind(
55         staticPreviewView: View,
56         wallpaperSurface: SurfaceView,
57         viewModel: StaticWallpaperPreviewViewModel,
58         displaySize: Point,
59         parentCoroutineScope: CoroutineScope,
60         isFullScreen: Boolean = false,
61     ) {
62         val fullResImageView =
63             staticPreviewView.requireViewById<SystemScaledSubsamplingScaleImageView>(
64                 R.id.full_res_image
65             )
66         val lowResImageView = staticPreviewView.requireViewById<ImageView>(R.id.low_res_image)
67 
68         // surfaceView.width and surfaceFrame.width here can be different,
69         // one represents the size of the view and the other represents the
70         // size of the surface. When setting a view to the surface host,
71         // we want to set it based on the surface's size not the view's size
72         adjustSizeAndAttachPreview(
73             wallpaperSurface.holder.surfaceFrame,
74             wallpaperSurface,
75             staticPreviewView,
76             fullResImageView,
77         )
78 
79         lowResImageView.initLowResImageView()
80         fullResImageView.initFullResImageView()
81 
82         parentCoroutineScope.launch {
83             // Show low res image only for small preview with supported wallpaper
84             if (!isFullScreen) {
85                 launch {
86                     viewModel.lowResBitmap.collect {
87                         it?.let {
88                             lowResImageView.setImageBitmap(it)
89                             lowResImageView.isVisible = true
90                         }
91                     }
92                 }
93             }
94 
95             launch {
96                 viewModel.subsamplingScaleImageViewModel.collect { imageModel ->
97                     trace(TAG) {
98                         val cropHint = imageModel.fullPreviewCropModels?.get(displaySize)?.cropHint
99                         fullResImageView.setFullResImage(
100                             ImageSource.cachedBitmap(imageModel.rawWallpaperBitmap),
101                             imageModel.rawWallpaperSize,
102                             displaySize,
103                             cropHint,
104                             RtlUtils.isRtl(lowResImageView.context),
105                             isFullScreen,
106                         )
107 
108                         // Fill in the default crop region if the displaySize for this preview
109                         // is missing.
110                         val imageSize = Point(fullResImageView.width, fullResImageView.height)
111                         viewModel.updateDefaultPreviewCropModel(
112                             displaySize,
113                             FullPreviewCropModel(
114                                 cropHint =
115                                     WallpaperCropUtils.calculateVisibleRect(
116                                         imageModel.rawWallpaperSize,
117                                         imageSize,
118                                     ),
119                                 cropSizeModel =
120                                     CropSizeModel(
121                                         wallpaperZoom =
122                                             WallpaperCropUtils.calculateMinZoom(
123                                                 imageModel.rawWallpaperSize,
124                                                 imageSize,
125                                             ),
126                                         hostViewSize = imageSize,
127                                         cropViewSize =
128                                             WallpaperCropUtils.calculateCropSurfaceSize(
129                                                 fullResImageView.resources,
130                                                 max(imageSize.x, imageSize.y),
131                                                 min(imageSize.x, imageSize.y),
132                                                 imageSize.x,
133                                                 imageSize.y,
134                                             ),
135                                     ),
136                             ),
137                         )
138 
139                         if (lowResImageView.isVisible) {
140                             crossFadeInFullResImageView(lowResImageView, fullResImageView)
141                         }
142                     }
143                 }
144             }
145         }
146     }
147 
148     private fun ImageView.initLowResImageView() {
149         setRenderEffect(
150             RenderEffect.createBlurEffect(
151                 LOW_RES_BITMAP_BLUR_RADIUS,
152                 LOW_RES_BITMAP_BLUR_RADIUS,
153                 Shader.TileMode.CLAMP,
154             )
155         )
156     }
157 
158     private fun SubsamplingScaleImageView.initFullResImageView() {
159         setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
160         setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
161     }
162 
163     private fun SubsamplingScaleImageView.setFullResImage(
164         imageSource: ImageSource,
165         rawWallpaperSize: Point,
166         displaySize: Point,
167         cropHint: Rect?,
168         isRtl: Boolean,
169         isFullScreen: Boolean,
170     ) {
171         // Set the full res image
172         setImage(imageSource)
173         // Calculate the scale and the center point for the full res image
174         doOnLayout {
175             FullResImageViewUtil.getScaleAndCenter(
176                     Point(measuredWidth, measuredHeight),
177                     rawWallpaperSize,
178                     displaySize,
179                     cropHint,
180                     isRtl,
181                 )
182                 .let { scaleAndCenter ->
183                     minScale = scaleAndCenter.minScale
184                     maxScale = scaleAndCenter.maxScale
185                     setScaleAndCenter(scaleAndCenter.defaultScale, scaleAndCenter.center)
186                 }
187         }
188     }
189 
190     private fun crossFadeInFullResImageView(lowResImageView: ImageView, fullResImageView: View) {
191         fullResImageView.alpha = 0f
192         fullResImageView
193             .animate()
194             .alpha(1f)
195             .setInterpolator(ALPHA_OUT)
196             .setDuration(CROSS_FADE_DURATION)
197             .setListener(
198                 object : AnimatorListenerAdapter() {
199                     override fun onAnimationEnd(animation: Animator) {
200                         lowResImageView.setImageBitmap(null)
201                     }
202                 }
203             )
204     }
205 
206     // When showing static wallpaper preview, we set the full res image view to be bigger than the
207     // image by N percent (usually 10%) as given by getSystemWallpaperMaximumScale via
208     // SystemScaledSubsamplingScaleImageView. This ensures that no matter what scale and pan is set
209     // by the user, at least N% of the source image in the preview will be preserved around the
210     // visible crop. This is needed for system zoom out animations.
211     private fun adjustSizeAndAttachPreview(
212         surfacePosition: Rect,
213         surfaceView: SurfaceView,
214         preview: View,
215         fullResView: SystemScaledSubsamplingScaleImageView,
216     ) {
217         val width = surfacePosition.width()
218         val height = surfacePosition.height()
219         preview.measure(
220             View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
221             View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
222         )
223         preview.layout(0, 0, width, height)
224 
225         fullResView.setSurfaceSize(Point(width, height))
226         surfaceView.attachView(preview, width, height)
227     }
228 
229     private const val TAG = "StaticWallpaperPreviewBinder"
230 }
231