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 */ 17 18 package com.android.wallpaper.picker.customization.ui.binder 19 20 import android.animation.ValueAnimator 21 import android.view.View 22 import android.widget.ImageView 23 import androidx.core.view.isVisible 24 import androidx.core.view.updateLayoutParams 25 import androidx.lifecycle.LifecycleOwner 26 import androidx.lifecycle.lifecycleScope 27 import com.android.wallpaper.R 28 import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperQuickSwitchOptionViewModel 29 import kotlinx.coroutines.flow.distinctUntilChanged 30 import kotlinx.coroutines.launch 31 32 /** 33 * Binds between the view and view-model for a single wallpaper quick switch option. 34 * 35 * The options are presented to the user in some sort of collection and clicking on one of the 36 * options selects that wallpaper. 37 */ 38 object WallpaperQuickSwitchOptionBinder { 39 40 /** Binds the given view to the given view-model. */ 41 fun bind( 42 view: View, 43 viewModel: WallpaperQuickSwitchOptionViewModel, 44 lifecycleOwner: LifecycleOwner, 45 smallOptionWidthPx: Int, 46 largeOptionWidthPx: Int, 47 isThumbnailFadeAnimationEnabled: Boolean, 48 position: Int, 49 titleMap: MutableMap<String, Int>, 50 ) { 51 val selectionBorder: View = view.requireViewById(R.id.selection_border) 52 val selectionIcon: View = view.requireViewById(R.id.selection_icon) 53 val thumbnailView: ImageView = view.requireViewById(R.id.thumbnail) 54 val placeholder: ImageView = view.requireViewById(R.id.placeholder) 55 56 placeholder.setBackgroundColor(viewModel.placeholderColor) 57 58 if (viewModel.title != null) { 59 viewModel.title 60 val latestIndex = titleMap.getOrDefault(viewModel.title, 0) + 1 61 62 view.contentDescription = 63 view.resources.getString( 64 R.string.recents_wallpaper_label, 65 viewModel.title, 66 latestIndex, 67 ) 68 titleMap[viewModel.title] = position + 1 69 } else { 70 // if the content description is missing then the default description will be the 71 // default wallpaper title and its position 72 view.contentDescription = 73 view.resources.getString( 74 R.string.recents_wallpaper_label, 75 view.resources.getString(R.string.default_wallpaper_title), 76 position + 1, 77 ) 78 } 79 80 lifecycleOwner.lifecycleScope.launch { 81 launch { 82 viewModel.onSelected.collect { onSelectedOrNull -> 83 view.setOnClickListener( 84 if (onSelectedOrNull != null) { 85 { onSelectedOrNull.invoke() } 86 } else { 87 null 88 } 89 ) 90 } 91 } 92 93 launch { 94 // We want to skip animating the first width update. 95 var isFirstValue = true 96 viewModel.isLarge.collect { isLarge -> 97 updateWidth( 98 view = view, 99 targetWidthPx = if (isLarge) largeOptionWidthPx else smallOptionWidthPx, 100 animate = !isFirstValue, 101 ) 102 isFirstValue = false 103 } 104 } 105 106 launch { 107 viewModel.isSelectionIndicatorVisible.distinctUntilChanged().collect { isSelected -> 108 // Update the content description to announce the selection status 109 view.isSelected = isSelected 110 } 111 } 112 113 launch { 114 // We want to skip animating the first update so it doesn't "blink" when the 115 // activity is recreated. 116 var isFirstValue = true 117 viewModel.isSelectionIndicatorVisible.collect { 118 if (!isFirstValue) { 119 selectionBorder.animatedVisibility(isVisible = it) 120 selectionIcon.animatedVisibility(isVisible = it) 121 } else { 122 selectionBorder.isVisible = it 123 selectionIcon.isVisible = it 124 } 125 isFirstValue = false 126 selectionIcon.animatedVisibility(isVisible = it) 127 } 128 } 129 130 launch { 131 val thumbnail = viewModel.thumbnail() 132 if (thumbnailView.tag != thumbnail) { 133 thumbnailView.tag = thumbnail 134 if (thumbnail != null) { 135 thumbnailView.setImageBitmap(thumbnail) 136 if (isThumbnailFadeAnimationEnabled) { 137 thumbnailView.fadeIn() 138 } else { 139 thumbnailView.isVisible = true 140 } 141 } else if (isThumbnailFadeAnimationEnabled) { 142 thumbnailView.fadeOut() 143 } else { 144 thumbnailView.isVisible = false 145 } 146 } 147 } 148 } 149 } 150 151 /** 152 * Updates the view width. 153 * 154 * @param view The [View] to update. 155 * @param targetWidthPx The width we want the view to have. 156 * @param animate Whether the update should be animated. 157 */ 158 private fun updateWidth( 159 view: View, 160 targetWidthPx: Int, 161 animate: Boolean, 162 ) { 163 fun setWidth(widthPx: Int) { 164 view.updateLayoutParams { width = widthPx } 165 } 166 167 if (!animate) { 168 setWidth(widthPx = targetWidthPx) 169 return 170 } 171 172 ValueAnimator.ofInt( 173 view.width, 174 targetWidthPx, 175 ) 176 .apply { 177 addUpdateListener { setWidth(it.animatedValue as Int) } 178 start() 179 } 180 } 181 182 private fun View.animatedVisibility( 183 isVisible: Boolean, 184 ) { 185 if (isVisible) { 186 fadeIn() 187 } else { 188 fadeOut() 189 } 190 } 191 192 private fun View.fadeIn() { 193 if (isVisible) { 194 return 195 } 196 197 alpha = 0f 198 isVisible = true 199 animate().alpha(1f).start() 200 } 201 202 private fun View.fadeOut() { 203 if (!isVisible) { 204 return 205 } 206 207 animate().alpha(0f).withEndAction { isVisible = false }.start() 208 } 209 } 210