1 /*
<lambda>null2  * Copyright (C) 2024 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 package com.android.wallpaper.customization.ui.binder
18 
19 import android.animation.ValueAnimator
20 import android.content.Context
21 import android.view.View
22 import android.view.ViewGroup
23 import android.view.ViewTreeObserver.OnGlobalLayoutListener
24 import android.widget.ImageView
25 import androidx.core.view.isVisible
26 import androidx.lifecycle.Lifecycle
27 import androidx.lifecycle.LifecycleOwner
28 import androidx.lifecycle.lifecycleScope
29 import androidx.lifecycle.repeatOnLifecycle
30 import androidx.recyclerview.widget.LinearLayoutManager
31 import androidx.recyclerview.widget.RecyclerView
32 import com.android.customization.picker.common.ui.view.SingleRowListItemSpacing
33 import com.android.customization.picker.grid.ui.binder.GridIconViewBinder
34 import com.android.customization.picker.grid.ui.viewmodel.GridIconViewModel
35 import com.android.customization.picker.grid.ui.viewmodel.ShapeIconViewModel
36 import com.android.themepicker.R
37 import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption.APP_SHAPE_GRID
38 import com.android.wallpaper.customization.ui.viewmodel.ShapeGridFloatingSheetHeightsViewModel
39 import com.android.wallpaper.customization.ui.viewmodel.ShapeGridPickerViewModel.Tab.GRID
40 import com.android.wallpaper.customization.ui.viewmodel.ShapeGridPickerViewModel.Tab.SHAPE
41 import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel
42 import com.android.wallpaper.picker.customization.ui.view.FloatingToolbar
43 import com.android.wallpaper.picker.customization.ui.view.adapter.FloatingToolbarTabAdapter
44 import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel
45 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
46 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter2
47 import com.android.wallpaper.picker.option.ui.binder.OptionItemBinder
48 import java.lang.ref.WeakReference
49 import kotlinx.coroutines.CoroutineDispatcher
50 import kotlinx.coroutines.flow.Flow
51 import kotlinx.coroutines.flow.MutableStateFlow
52 import kotlinx.coroutines.flow.asStateFlow
53 import kotlinx.coroutines.flow.combine
54 import kotlinx.coroutines.flow.filter
55 import kotlinx.coroutines.flow.filterNotNull
56 import kotlinx.coroutines.launch
57 
58 object ShapeGridFloatingSheetBinder {
59     private const val ANIMATION_DURATION = 200L
60 
61     private val _shapeGridFloatingSheetHeights:
62         MutableStateFlow<ShapeGridFloatingSheetHeightsViewModel?> =
63         MutableStateFlow(null)
64     private val shapeGridFloatingSheetHeights: Flow<ShapeGridFloatingSheetHeightsViewModel> =
65         _shapeGridFloatingSheetHeights.asStateFlow().filterNotNull().filter {
66             it.shapeContentHeight != null && it.gridContentHeight != null
67         }
68 
69     fun bind(
70         view: View,
71         optionsViewModel: ThemePickerCustomizationOptionsViewModel,
72         colorUpdateViewModel: ColorUpdateViewModel,
73         lifecycleOwner: LifecycleOwner,
74         backgroundDispatcher: CoroutineDispatcher,
75     ) {
76         val floatingSheetContentVerticalPadding =
77             view.resources.getDimensionPixelSize(R.dimen.floating_sheet_content_vertical_padding)
78         val viewModel = optionsViewModel.shapeGridPickerViewModel
79 
80         val tabs = view.requireViewById<FloatingToolbar>(R.id.floating_toolbar)
81         val tabAdapter =
82             FloatingToolbarTabAdapter(
83                     colorUpdateViewModel = WeakReference(colorUpdateViewModel),
84                     shouldAnimateColor = { optionsViewModel.selectedOption.value == APP_SHAPE_GRID },
85                 )
86                 .also { tabs.setAdapter(it) }
87 
88         val floatingSheetContainer =
89             view.requireViewById<ViewGroup>(R.id.shape_grid_floating_sheet_content_container)
90 
91         val shapeContent = view.requireViewById<View>(R.id.app_shape_container)
92         val shapeOptionListAdapter =
93             createShapeOptionItemAdapter(view.context, lifecycleOwner, backgroundDispatcher)
94         val shapeOptionList =
95             view.requireViewById<RecyclerView>(R.id.shape_options).also {
96                 it.initShapeOptionList(view.context, shapeOptionListAdapter)
97             }
98 
99         val gridContent = view.requireViewById<View>(R.id.app_grid_container)
100         val gridOptionListAdapter =
101             createGridOptionItemAdapter(lifecycleOwner, backgroundDispatcher)
102         val gridOptionList =
103             view.requireViewById<RecyclerView>(R.id.grid_options).also {
104                 it.initGridOptionList(view.context, gridOptionListAdapter)
105             }
106 
107         // Get the shape content height when it is ready
108         shapeContent.viewTreeObserver.addOnGlobalLayoutListener(
109             object : OnGlobalLayoutListener {
110                 override fun onGlobalLayout() {
111                     if (shapeContent.height != 0) {
112                         _shapeGridFloatingSheetHeights.value =
113                             _shapeGridFloatingSheetHeights.value?.copy(
114                                 shapeContentHeight = shapeContent.height
115                             )
116                                 ?: ShapeGridFloatingSheetHeightsViewModel(
117                                     shapeContentHeight = shapeContent.height
118                                 )
119                     }
120                     shapeContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
121                 }
122             }
123         )
124         // Get the grid content height when it is ready
125         gridContent.viewTreeObserver.addOnGlobalLayoutListener(
126             object : OnGlobalLayoutListener {
127                 override fun onGlobalLayout() {
128                     if (gridContent.height != 0) {
129                         _shapeGridFloatingSheetHeights.value =
130                             _shapeGridFloatingSheetHeights.value?.copy(
131                                 gridContentHeight = gridContent.height
132                             )
133                                 ?: ShapeGridFloatingSheetHeightsViewModel(
134                                     gridContentHeight = shapeContent.height
135                                 )
136                     }
137                     shapeContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
138                 }
139             }
140         )
141 
142         lifecycleOwner.lifecycleScope.launch {
143             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
144                 launch { viewModel.tabs.collect { tabAdapter.submitList(it) } }
145 
146                 launch {
147                     combine(shapeGridFloatingSheetHeights, viewModel.selectedTab) {
148                             heights,
149                             selectedTab ->
150                             heights to selectedTab
151                         }
152                         .collect { (heights, selectedTab) ->
153                             val (shapeContentHeight, gridContentHeight) = heights
154                             shapeContentHeight ?: return@collect
155                             gridContentHeight ?: return@collect
156                             // Make sure the recycler view height is the same as its parent. It's
157                             // possible that the recycler view is shorter than expected.
158                             gridOptionList.layoutParams =
159                                 gridOptionList.layoutParams.apply { height = gridContentHeight }
160                             val targetHeight =
161                                 when (selectedTab) {
162                                     SHAPE -> shapeContentHeight
163                                     GRID -> gridContentHeight
164                                 } + floatingSheetContentVerticalPadding * 2
165 
166                             ValueAnimator.ofInt(floatingSheetContainer.height, targetHeight)
167                                 .apply {
168                                     addUpdateListener { valueAnimator ->
169                                         val value = valueAnimator.animatedValue as Int
170                                         floatingSheetContainer.layoutParams =
171                                             floatingSheetContainer.layoutParams.apply {
172                                                 height = value
173                                             }
174                                     }
175                                     duration = ANIMATION_DURATION
176                                 }
177                                 .start()
178 
179                             shapeContent.isVisible = selectedTab == SHAPE
180                             gridContent.isVisible = selectedTab == GRID
181                         }
182                 }
183 
184                 launch {
185                     viewModel.gridOptions.collect { options ->
186                         gridOptionListAdapter.setItems(options) {
187                             val indexToFocus =
188                                 options.indexOfFirst { it.isSelected.value }.coerceAtLeast(0)
189                             (gridOptionList.layoutManager as LinearLayoutManager).scrollToPosition(
190                                 indexToFocus
191                             )
192                         }
193                     }
194                 }
195 
196                 launch {
197                     viewModel.shapeOptions.collect { options ->
198                         shapeOptionListAdapter.setItems(options) {
199                             val indexToFocus =
200                                 options.indexOfFirst { it.isSelected.value }.coerceAtLeast(0)
201                             (shapeOptionList.layoutManager as LinearLayoutManager).scrollToPosition(
202                                 indexToFocus
203                             )
204                         }
205                     }
206                 }
207             }
208         }
209     }
210 
211     private fun createShapeOptionItemAdapter(
212         context: Context,
213         lifecycleOwner: LifecycleOwner,
214         backgroundDispatcher: CoroutineDispatcher,
215     ): OptionItemAdapter<ShapeIconViewModel> =
216         OptionItemAdapter(
217             layoutResourceId = R.layout.shape_option,
218             lifecycleOwner = lifecycleOwner,
219             backgroundDispatcher = backgroundDispatcher,
220             foregroundTintSpec =
221                 OptionItemBinder.TintSpec(
222                     selectedColor =
223                         context.getColor(com.android.wallpaper.R.color.system_on_surface),
224                     unselectedColor =
225                         context.getColor(com.android.wallpaper.R.color.system_on_surface),
226                 ),
227             bindIcon = { foregroundView: View, shapeIcon: ShapeIconViewModel ->
228                 val imageView = foregroundView as? ImageView
229                 imageView?.let { ShapeIconViewBinder.bind(imageView, shapeIcon) }
230             },
231         )
232 
233     private fun RecyclerView.initShapeOptionList(
234         context: Context,
235         adapter: OptionItemAdapter<ShapeIconViewModel>,
236     ) {
237         apply {
238             this.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
239             addItemDecoration(
240                 SingleRowListItemSpacing(
241                     edgeItemSpacePx =
242                         context.resources.getDimensionPixelSize(
243                             R.dimen.floating_sheet_content_horizontal_padding
244                         ),
245                     itemHorizontalSpacePx =
246                         context.resources.getDimensionPixelSize(
247                             R.dimen.floating_sheet_list_item_horizontal_space
248                         ),
249                 )
250             )
251             this.adapter = adapter
252         }
253     }
254 
255     private fun createGridOptionItemAdapter(
256         lifecycleOwner: LifecycleOwner,
257         backgroundDispatcher: CoroutineDispatcher,
258     ): OptionItemAdapter2<GridIconViewModel> =
259         OptionItemAdapter2(
260             layoutResourceId = R.layout.grid_option2,
261             lifecycleOwner = lifecycleOwner,
262             backgroundDispatcher = backgroundDispatcher,
263             bindPayload = { view: View, gridIcon: GridIconViewModel ->
264                 val imageView = view.findViewById(R.id.foreground) as? ImageView
265                 imageView?.let { GridIconViewBinder.bind(imageView, gridIcon) }
266                 return@OptionItemAdapter2 null
267             },
268         )
269 
270     private fun RecyclerView.initGridOptionList(
271         context: Context,
272         adapter: OptionItemAdapter2<GridIconViewModel>,
273     ) {
274         apply {
275             this.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
276             addItemDecoration(
277                 SingleRowListItemSpacing(
278                     edgeItemSpacePx =
279                         context.resources.getDimensionPixelSize(
280                             R.dimen.floating_sheet_content_horizontal_padding
281                         ),
282                     itemHorizontalSpacePx =
283                         context.resources.getDimensionPixelSize(
284                             R.dimen.floating_sheet_grid_list_item_horizontal_space
285                         ),
286                 )
287             )
288             this.adapter = adapter
289         }
290     }
291 }
292