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