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