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