• 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.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.content.res.Configuration.UI_MODE_NIGHT_MASK
24 import android.content.res.Configuration.UI_MODE_NIGHT_YES
25 import android.view.View
26 import android.view.ViewGroup
27 import android.view.ViewTreeObserver.OnGlobalLayoutListener
28 import android.widget.ImageView
29 import android.widget.TextView
30 import androidx.core.graphics.drawable.DrawableCompat
31 import androidx.core.view.isVisible
32 import androidx.lifecycle.Lifecycle
33 import androidx.lifecycle.LifecycleOwner
34 import androidx.lifecycle.lifecycleScope
35 import androidx.lifecycle.repeatOnLifecycle
36 import androidx.recyclerview.widget.LinearLayoutManager
37 import androidx.recyclerview.widget.RecyclerView
38 import com.android.customization.picker.clock.shared.ClockSize
39 import com.android.customization.picker.clock.shared.model.ClockMetadataModel
40 import com.android.customization.picker.color.ui.binder.ColorOptionIconBinder2
41 import com.android.customization.picker.color.ui.view.ColorOptionIconView2
42 import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
43 import com.android.customization.picker.common.ui.view.SingleRowListItemSpacing
44 import com.android.themepicker.R
45 import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerLockCustomizationOption.CLOCK
46 import com.android.wallpaper.customization.ui.viewmodel.ClockFloatingSheetHeightsViewModel
47 import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel.ClockStyleModel
48 import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel.Tab
49 import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel
50 import com.android.wallpaper.picker.customization.ui.binder.ColorUpdateBinder
51 import com.android.wallpaper.picker.customization.ui.view.FloatingToolbar
52 import com.android.wallpaper.picker.customization.ui.view.adapter.FloatingToolbarTabAdapter
53 import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel
54 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter2
55 import com.google.android.material.materialswitch.MaterialSwitch
56 import com.google.android.material.slider.LabelFormatter
57 import com.google.android.material.slider.Slider
58 import com.google.android.material.slider.Slider.OnSliderTouchListener
59 import java.lang.ref.WeakReference
60 import kotlin.math.roundToInt
61 import kotlinx.coroutines.DisposableHandle
62 import kotlinx.coroutines.flow.Flow
63 import kotlinx.coroutines.flow.MutableStateFlow
64 import kotlinx.coroutines.flow.asStateFlow
65 import kotlinx.coroutines.flow.combine
66 import kotlinx.coroutines.flow.filterNotNull
67 import kotlinx.coroutines.launch
68 
69 object ClockFloatingSheetBinder {
70     private const val SLIDER_ENABLED_ALPHA = 1f
71     private const val SLIDER_DISABLED_ALPHA = .3f
72     private const val ANIMATION_DURATION = 200L
73 
74     private val _clockFloatingSheetHeights: MutableStateFlow<ClockFloatingSheetHeightsViewModel> =
75         MutableStateFlow(ClockFloatingSheetHeightsViewModel())
76     private val clockFloatingSheetHeights: Flow<ClockFloatingSheetHeightsViewModel> =
77         _clockFloatingSheetHeights.asStateFlow().filterNotNull()
78 
79     fun bind(
80         view: View,
81         optionsViewModel: ThemePickerCustomizationOptionsViewModel,
82         colorUpdateViewModel: ColorUpdateViewModel,
83         lifecycleOwner: LifecycleOwner,
84     ) {
85         val viewModel = optionsViewModel.clockPickerViewModel
86         val appContext = view.context.applicationContext
87         val isFloatingSheetActive = { optionsViewModel.selectedOption.value == CLOCK }
88 
89         val tabs: FloatingToolbar = view.requireViewById(R.id.floating_toolbar)
90         val tabContainer =
91             tabs.findViewById<ViewGroup>(com.android.wallpaper.R.id.floating_toolbar_tab_container)
92         ColorUpdateBinder.bind(
93             setColor = { color ->
94                 DrawableCompat.setTint(DrawableCompat.wrap(tabContainer.background), color)
95             },
96             color = colorUpdateViewModel.floatingToolbarBackground,
97             shouldAnimate = isFloatingSheetActive,
98             lifecycleOwner = lifecycleOwner,
99         )
100         val tabAdapter =
101             FloatingToolbarTabAdapter(
102                     colorUpdateViewModel = WeakReference(colorUpdateViewModel),
103                     shouldAnimateColor = isFloatingSheetActive,
104                 )
105                 .also { tabs.setAdapter(it) }
106 
107         val floatingSheetContainer: ViewGroup =
108             view.requireViewById(R.id.floating_sheet_content_container)
109         ColorUpdateBinder.bind(
110             setColor = { color ->
111                 DrawableCompat.setTint(
112                     DrawableCompat.wrap(floatingSheetContainer.background),
113                     color,
114                 )
115             },
116             color = colorUpdateViewModel.colorSurfaceBright,
117             shouldAnimate = isFloatingSheetActive,
118             lifecycleOwner = lifecycleOwner,
119         )
120 
121         // Clock style
122         val clockStyleContent: View = view.requireViewById(R.id.clock_floating_sheet_style_content)
123         val isClockStyleActive = {
124             isFloatingSheetActive() && viewModel.selectedTab.value == Tab.STYLE
125         }
126         val clockStyleAdapter =
127             createClockStyleOptionItemAdapter(
128                 colorUpdateViewModel = colorUpdateViewModel,
129                 shouldAnimateColor = isClockStyleActive,
130                 lifecycleOwner = lifecycleOwner,
131             )
132         val clockStyleList: RecyclerView = view.requireViewById(R.id.clock_style_list)
133         clockStyleList.initStyleList(appContext, clockStyleAdapter)
134         val axisPresetSlider: Slider =
135             clockStyleContent.requireViewById(R.id.clock_axis_preset_slider)
136 
137         // Clock color
138         val clockColorContent: View = view.requireViewById(R.id.clock_floating_sheet_color_content)
139 
140         val clockColorAdapter =
141             createClockColorOptionItemAdapter(
142                 uiMode = view.resources.configuration.uiMode,
143                 colorUpdateViewModel = colorUpdateViewModel,
144                 shouldAnimateColor = isFloatingSheetActive,
145                 lifecycleOwner = lifecycleOwner,
146             )
147         val clockColorList: RecyclerView = view.requireViewById(R.id.clock_color_list)
148         clockColorList.adapter = clockColorAdapter
149         clockColorList.layoutManager =
150             LinearLayoutManager(appContext, LinearLayoutManager.HORIZONTAL, false)
151 
152         val clockColorSlider: Slider = view.requireViewById(R.id.clock_color_slider)
153         SliderColorBinder.bind(
154             slider = clockColorSlider,
155             colorUpdateViewModel = colorUpdateViewModel,
156             shouldAnimateColor = isFloatingSheetActive,
157             lifecycleOwner = lifecycleOwner,
158         )
159 
160         clockColorSlider.apply {
161             valueFrom = ClockMetadataModel.MIN_COLOR_TONE_PROGRESS.toFloat()
162             valueTo = ClockMetadataModel.MAX_COLOR_TONE_PROGRESS.toFloat()
163             labelBehavior = LabelFormatter.LABEL_GONE
164             addOnChangeListener { _, value, fromUser ->
165                 if (fromUser) {
166                     viewModel.onSliderProgressChanged(value.roundToInt())
167                 }
168             }
169         }
170         val isClockColorActive = {
171             isFloatingSheetActive() && viewModel.selectedTab.value == Tab.COLOR
172         }
173         ColorUpdateBinder.bind(
174             setColor = { color ->
175                 clockColorContent
176                     .requireViewById<TextView>(R.id.clock_color_title)
177                     .setTextColor(color)
178             },
179             color = colorUpdateViewModel.colorOnSurface,
180             shouldAnimate = isClockColorActive,
181             lifecycleOwner = lifecycleOwner,
182         )
183         ColorUpdateBinder.bind(
184             setColor = { color ->
185                 clockColorContent
186                     .requireViewById<TextView>(R.id.clock_color_description)
187                     .setTextColor(color)
188             },
189             color = colorUpdateViewModel.colorOnSurfaceVariant,
190             shouldAnimate = isClockColorActive,
191             lifecycleOwner = lifecycleOwner,
192         )
193 
194         // Clock size
195         val clockSizeContent: View = view.requireViewById(R.id.clock_floating_sheet_size_content)
196         val clockSizeSwitch: MaterialSwitch =
197             clockSizeContent.requireViewById(R.id.clock_style_clock_size_switch)
198         ColorUpdateBinder.bind(
199             setColor = { color ->
200                 clockSizeContent
201                     .requireViewById<TextView>(R.id.clock_style_clock_size_title)
202                     .setTextColor(color)
203             },
204             color = colorUpdateViewModel.colorOnSurface,
205             shouldAnimate = isClockStyleActive,
206             lifecycleOwner = lifecycleOwner,
207         )
208         ColorUpdateBinder.bind(
209             setColor = { color ->
210                 clockSizeContent
211                     .requireViewById<TextView>(R.id.clock_style_clock_size_description)
212                     .setTextColor(color)
213             },
214             color = colorUpdateViewModel.colorOnSurfaceVariant,
215             shouldAnimate = isClockStyleActive,
216             lifecycleOwner = lifecycleOwner,
217         )
218 
219         clockStyleContent.viewTreeObserver.addOnGlobalLayoutListener(
220             object : OnGlobalLayoutListener {
221                 override fun onGlobalLayout() {
222                     if (
223                         clockStyleContent.height != 0 &&
224                             axisPresetSlider.height != 0 &&
225                             _clockFloatingSheetHeights.value.axisPresetSliderHeight == null &&
226                             _clockFloatingSheetHeights.value.clockStyleContentHeight == null
227                     ) {
228                         _clockFloatingSheetHeights.value =
229                             _clockFloatingSheetHeights.value.copy(
230                                 clockStyleContentHeight = clockStyleContent.height,
231                                 axisPresetSliderHeight = axisPresetSlider.height,
232                             )
233                         clockStyleContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
234                     }
235                 }
236             }
237         )
238 
239         clockColorContent.viewTreeObserver.addOnGlobalLayoutListener(
240             object : OnGlobalLayoutListener {
241                 override fun onGlobalLayout() {
242                     if (
243                         clockColorContent.height != 0 &&
244                             _clockFloatingSheetHeights.value.clockColorContentHeight == null
245                     ) {
246                         _clockFloatingSheetHeights.value =
247                             _clockFloatingSheetHeights.value.copy(
248                                 clockColorContentHeight = clockColorContent.height
249                             )
250                         clockColorContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
251                     }
252                 }
253             }
254         )
255 
256         clockSizeContent.viewTreeObserver.addOnGlobalLayoutListener(
257             object : OnGlobalLayoutListener {
258                 override fun onGlobalLayout() {
259                     if (
260                         clockSizeContent.height != 0 &&
261                             _clockFloatingSheetHeights.value.clockSizeContentHeight == null
262                     ) {
263                         _clockFloatingSheetHeights.value =
264                             _clockFloatingSheetHeights.value.copy(
265                                 clockSizeContentHeight = clockSizeContent.height
266                             )
267                         clockSizeContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
268                     }
269                 }
270             }
271         )
272 
273         lifecycleOwner.lifecycleScope.launch {
274             var currentTab: Tab = Tab.STYLE
275             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
276                 launch { viewModel.tabs.collect { tabAdapter.submitList(it) } }
277 
278                 launch {
279                     combine(
280                             clockFloatingSheetHeights,
281                             viewModel.selectedTab,
282                             viewModel.shouldShowPresetSlider,
283                             ::Triple,
284                         )
285                         .collect { (heights, selectedTab, shouldShowPresetSlider) ->
286                             val (
287                                 clockStyleContentHeight,
288                                 clockColorContentHeight,
289                                 clockSizeContentHeight,
290                                 axisPresetSliderHeight) =
291                                 heights
292                             clockStyleContentHeight ?: return@collect
293                             clockColorContentHeight ?: return@collect
294                             clockSizeContentHeight ?: return@collect
295                             axisPresetSliderHeight ?: return@collect
296 
297                             val fromHeight = floatingSheetContainer.height
298                             val toHeight =
299                                 when (selectedTab) {
300                                     Tab.STYLE ->
301                                         if (shouldShowPresetSlider) clockStyleContentHeight
302                                         else clockStyleContentHeight - axisPresetSliderHeight
303                                     Tab.COLOR -> clockColorContentHeight
304                                     Tab.SIZE -> clockSizeContentHeight
305                                 }
306                             val currentContent: View =
307                                 when (currentTab) {
308                                     Tab.STYLE -> clockStyleContent
309                                     Tab.COLOR -> clockColorContent
310                                     Tab.SIZE -> clockSizeContent
311                                 }
312                             val shouldCurrentContentFadeOut = currentTab != selectedTab
313                             // Start to animate the content height
314                             ValueAnimator.ofInt(fromHeight, toHeight)
315                                 .apply {
316                                     addUpdateListener { valueAnimator ->
317                                         val value = valueAnimator.animatedValue as Int
318                                         floatingSheetContainer.layoutParams =
319                                             floatingSheetContainer.layoutParams.apply {
320                                                 height = value
321                                             }
322                                         if (shouldCurrentContentFadeOut) {
323                                             currentContent.alpha =
324                                                 getAlpha(fromHeight, toHeight, value)
325                                         }
326                                     }
327                                     duration = ANIMATION_DURATION
328                                     addListener(
329                                         object : AnimatorListenerAdapter() {
330                                             override fun onAnimationEnd(animation: Animator) {
331                                                 clockStyleContent.isVisible =
332                                                     selectedTab == Tab.STYLE
333                                                 clockStyleContent.alpha = 1f
334                                                 clockColorContent.isVisible =
335                                                     selectedTab == Tab.COLOR
336                                                 clockColorContent.alpha = 1f
337                                                 clockSizeContent.isVisible = selectedTab == Tab.SIZE
338                                                 clockSizeContent.alpha = 1f
339                                             }
340                                         }
341                                     )
342                                 }
343                                 .start()
344                             currentTab = selectedTab
345                         }
346                 }
347 
348                 launch {
349                     viewModel.shouldShowPresetSlider.collect { axisPresetSlider.isVisible = it }
350                 }
351 
352                 launch {
353                     viewModel.clockStyleOptions.collect { styleOptions ->
354                         clockStyleAdapter.setItems(styleOptions) {
355                             var indexToFocus = styleOptions.indexOfFirst { it.isSelected.value }
356                             indexToFocus = if (indexToFocus < 0) 0 else indexToFocus
357                             (clockStyleList.layoutManager as LinearLayoutManager)
358                                 .scrollToPositionWithOffset(indexToFocus, 0)
359                         }
360                     }
361                 }
362 
363                 launch {
364                     viewModel.clockColorOptions.collect { colorOptions ->
365                         clockColorAdapter.setItems(colorOptions) {
366                             var indexToFocus = colorOptions.indexOfFirst { it.isSelected.value }
367                             indexToFocus = if (indexToFocus < 0) 0 else indexToFocus
368                             (clockColorList.layoutManager as LinearLayoutManager)
369                                 .scrollToPositionWithOffset(indexToFocus, 0)
370                         }
371                     }
372                 }
373 
374                 launch {
375                     viewModel.previewingSliderProgress.collect { progress ->
376                         clockColorSlider.value = progress.toFloat()
377                     }
378                 }
379 
380                 launch {
381                     viewModel.isSliderEnabled.collect { isEnabled ->
382                         clockColorSlider.isEnabled = isEnabled
383                         clockColorSlider.alpha =
384                             if (isEnabled) SLIDER_ENABLED_ALPHA else SLIDER_DISABLED_ALPHA
385                     }
386                 }
387 
388                 launch {
389                     var binding: SwitchColorBinder.Binding? = null
390                     viewModel.previewingClockSize.collect { size ->
391                         when (size) {
392                             ClockSize.DYNAMIC -> clockSizeSwitch.isChecked = true
393                             ClockSize.SMALL -> clockSizeSwitch.isChecked = false
394                         }
395                         binding?.destroy()
396                         binding =
397                             SwitchColorBinder.bind(
398                                 switch = clockSizeSwitch,
399                                 isChecked =
400                                     when (size) {
401                                         ClockSize.DYNAMIC -> true
402                                         ClockSize.SMALL -> false
403                                     },
404                                 colorUpdateViewModel = colorUpdateViewModel,
405                                 shouldAnimateColor = isClockStyleActive,
406                                 lifecycleOwner = lifecycleOwner,
407                             )
408                     }
409                 }
410 
411                 launch {
412                     viewModel.onClockSizeSwitchCheckedChange.collect { onCheckedChange ->
413                         clockSizeSwitch.setOnCheckedChangeListener { _, _ ->
414                             onCheckedChange.invoke()
415                         }
416                     }
417                 }
418 
419                 launch {
420                     viewModel.axisPresetsSliderViewModel.collect {
421                         val axisPresetsSliderViewModel = it ?: return@collect
422                         axisPresetSlider.valueFrom = axisPresetsSliderViewModel.valueFrom
423                         axisPresetSlider.valueTo = axisPresetsSliderViewModel.valueTo
424                         axisPresetSlider.stepSize = axisPresetsSliderViewModel.stepSize
425                         axisPresetSlider.clearOnSliderTouchListeners()
426                         axisPresetSlider.addOnSliderTouchListener(
427                             object : OnSliderTouchListener {
428                                 override fun onStartTrackingTouch(slider: Slider) {}
429 
430                                 override fun onStopTrackingTouch(slider: Slider) {
431                                     axisPresetsSliderViewModel.onSliderStopTrackingTouch(
432                                         slider.value
433                                     )
434                                 }
435                             }
436                         )
437                     }
438                 }
439 
440                 launch {
441                     viewModel.axisPresetsSliderSelectedValue.collect { axisPresetSlider.value = it }
442                 }
443             }
444         }
445     }
446 
447     private fun createClockStyleOptionItemAdapter(
448         colorUpdateViewModel: ColorUpdateViewModel,
449         shouldAnimateColor: () -> Boolean,
450         lifecycleOwner: LifecycleOwner,
451     ): OptionItemAdapter2<ClockStyleModel> =
452         OptionItemAdapter2(
453             layoutResourceId = R.layout.clock_style_option,
454             lifecycleOwner = lifecycleOwner,
455             bindPayload = { view: View, styleModel: ClockStyleModel ->
456                 view
457                     .findViewById<ImageView>(R.id.foreground)
458                     ?.setImageDrawable(styleModel.thumbnail)
459                 return@OptionItemAdapter2 null
460             },
461             colorUpdateViewModel = WeakReference(colorUpdateViewModel),
462             shouldAnimateColor = shouldAnimateColor,
463         )
464 
465     private fun RecyclerView.initStyleList(
466         context: Context,
467         adapter: OptionItemAdapter2<ClockStyleModel>,
468     ) {
469         this.adapter = adapter
470         layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
471         addItemDecoration(
472             SingleRowListItemSpacing(
473                 context.resources.getDimensionPixelSize(
474                     R.dimen.floating_sheet_content_horizontal_padding
475                 ),
476                 context.resources.getDimensionPixelSize(
477                     R.dimen.floating_sheet_list_item_horizontal_space
478                 ),
479             )
480         )
481     }
482 
483     private fun createClockColorOptionItemAdapter(
484         uiMode: Int,
485         colorUpdateViewModel: ColorUpdateViewModel,
486         shouldAnimateColor: () -> Boolean,
487         lifecycleOwner: LifecycleOwner,
488     ): OptionItemAdapter2<ColorOptionIconViewModel> =
489         OptionItemAdapter2(
490             layoutResourceId = R.layout.color_option2,
491             lifecycleOwner = lifecycleOwner,
492             bindPayload = { itemView: View, colorIcon: ColorOptionIconViewModel ->
493                 val colorOptionIconView: ColorOptionIconView2 =
494                     itemView.requireViewById(com.android.wallpaper.R.id.background)
495                 val night = uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
496                 val binding =
497                     ColorOptionIconBinder2.bind(
498                         view = colorOptionIconView,
499                         viewModel = colorIcon,
500                         darkTheme = night,
501                         colorUpdateViewModel = colorUpdateViewModel,
502                         shouldAnimateColor = shouldAnimateColor,
503                         lifecycleOwner = lifecycleOwner,
504                     )
505                 return@OptionItemAdapter2 DisposableHandle { binding.destroy() }
506             },
507             colorUpdateViewModel = WeakReference(colorUpdateViewModel),
508             shouldAnimateColor = shouldAnimateColor,
509         )
510 
511     // Alpha is 1 when current height is from height, and 0 when current height is to height.
512     private fun getAlpha(fromHeight: Int, toHeight: Int, currentHeight: Int): Float =
513         (1 - (currentHeight - fromHeight).toFloat() / (toHeight - fromHeight).toFloat())
514 }
515