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