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
24 import android.view.View
25 import android.view.ViewGroup
26 import android.view.ViewTreeObserver.OnGlobalLayoutListener
27 import android.widget.FrameLayout
28 import android.widget.ImageView
29 import android.widget.SeekBar
30 import android.widget.Switch
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.color.ui.binder.ColorOptionIconBinder
40 import com.android.customization.picker.color.ui.view.ColorOptionIconView
41 import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
42 import com.android.customization.picker.common.ui.view.SingleRowListItemSpacing
43 import com.android.systemui.plugins.clocks.AxisType
44 import com.android.themepicker.R
45 import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerLockCustomizationOption.CLOCK
46 import com.android.wallpaper.customization.ui.view.ClockFontSliderViewHolder
47 import com.android.wallpaper.customization.ui.view.ClockFontSwitchViewHolder
48 import com.android.wallpaper.customization.ui.viewmodel.ClockFloatingSheetHeightsViewModel
49 import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel
50 import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel.ClockStyleModel
51 import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel.Tab
52 import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel
53 import com.android.wallpaper.picker.customization.ui.view.FloatingToolbar
54 import com.android.wallpaper.picker.customization.ui.view.adapter.FloatingToolbarTabAdapter
55 import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel
56 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
57 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter2
58 import java.lang.ref.WeakReference
59 import kotlinx.coroutines.DisposableHandle
60 import kotlinx.coroutines.flow.Flow
61 import kotlinx.coroutines.flow.MutableStateFlow
62 import kotlinx.coroutines.flow.asStateFlow
63 import kotlinx.coroutines.flow.combine
64 import kotlinx.coroutines.flow.filterNotNull
65 import kotlinx.coroutines.launch
66 
67 object ClockFloatingSheetBinder {
68     private const val SLIDER_ENABLED_ALPHA = 1f
69     private const val SLIDER_DISABLED_ALPHA = .3f
70     private const val ANIMATION_DURATION = 200L
71 
72     private val _clockFloatingSheetHeights: MutableStateFlow<ClockFloatingSheetHeightsViewModel> =
73         MutableStateFlow(ClockFloatingSheetHeightsViewModel())
74     private val clockFloatingSheetHeights: Flow<ClockFloatingSheetHeightsViewModel> =
75         _clockFloatingSheetHeights.asStateFlow().filterNotNull()
76 
77     fun bind(
78         view: View,
79         optionsViewModel: ThemePickerCustomizationOptionsViewModel,
80         colorUpdateViewModel: ColorUpdateViewModel,
81         lifecycleOwner: LifecycleOwner,
82     ) {
83         val viewModel = optionsViewModel.clockPickerViewModel
84 
85         val appContext = view.context.applicationContext
86 
87         val tabs = view.requireViewById<FloatingToolbar>(R.id.floating_toolbar)
88         val tabAdapter =
89             FloatingToolbarTabAdapter(
90                     colorUpdateViewModel = WeakReference(colorUpdateViewModel),
91                     shouldAnimateColor = { optionsViewModel.selectedOption.value == CLOCK },
92                 )
93                 .also { tabs.setAdapter(it) }
94 
95         val floatingSheetContainer =
96             view.requireViewById<FrameLayout>(R.id.clock_floating_sheet_content_container)
97 
98         // Clock style
99         val clockStyleContent = view.requireViewById<View>(R.id.clock_floating_sheet_style_content)
100         val clockStyleAdapter = createClockStyleOptionItemAdapter(lifecycleOwner)
101         val clockStyleList =
102             view.requireViewById<RecyclerView>(R.id.clock_style_list).apply {
103                 initStyleList(appContext, clockStyleAdapter)
104             }
105 
106         // Clock font editor
107         val clockFontContent =
108             view.requireViewById<ViewGroup>(R.id.clock_floating_sheet_font_content)
109         val clockFontToolbar = view.requireViewById<ViewGroup>(R.id.clock_font_toolbar)
110         clockFontToolbar.requireViewById<View>(R.id.clock_font_revert).setOnClickListener {
111             viewModel.cancelFontAxes()
112         }
113         clockFontToolbar.requireViewById<View>(R.id.clock_font_apply).setOnClickListener {
114             viewModel.confirmFontAxes()
115         }
116 
117         // Clock color
118         val clockColorContent = view.requireViewById<View>(R.id.clock_floating_sheet_color_content)
119         val clockColorAdapter =
120             createClockColorOptionItemAdapter(view.resources.configuration.uiMode, lifecycleOwner)
121         val clockColorList =
122             view.requireViewById<RecyclerView>(R.id.clock_color_list).apply {
123                 initColorList(appContext, clockColorAdapter)
124             }
125         val clockColorSlider: SeekBar = view.requireViewById(R.id.clock_color_slider)
126         clockColorSlider.setOnSeekBarChangeListener(
127             object : SeekBar.OnSeekBarChangeListener {
128                 override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) {
129                     if (fromUser) {
130                         viewModel.onSliderProgressChanged(progress)
131                     }
132                 }
133 
134                 override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
135 
136                 override fun onStopTrackingTouch(seekBar: SeekBar?) = Unit
137             }
138         )
139 
140         // Clock size switch
141         val clockSizeSwitch = view.requireViewById<Switch>(R.id.clock_style_clock_size_switch)
142 
143         clockStyleContent.viewTreeObserver.addOnGlobalLayoutListener(
144             object : OnGlobalLayoutListener {
145                 override fun onGlobalLayout() {
146                     if (
147                         clockStyleContent.height != 0 &&
148                             _clockFloatingSheetHeights.value.clockStyleContentHeight == null
149                     ) {
150                         _clockFloatingSheetHeights.value =
151                             _clockFloatingSheetHeights.value.copy(
152                                 clockStyleContentHeight = clockStyleContent.height
153                             )
154                         clockStyleContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
155                     }
156                 }
157             }
158         )
159 
160         clockColorContent.viewTreeObserver.addOnGlobalLayoutListener(
161             object : OnGlobalLayoutListener {
162                 override fun onGlobalLayout() {
163                     if (
164                         clockColorContent.height != 0 &&
165                             _clockFloatingSheetHeights.value.clockColorContentHeight == null
166                     ) {
167                         _clockFloatingSheetHeights.value =
168                             _clockFloatingSheetHeights.value.copy(
169                                 clockColorContentHeight = clockColorContent.height
170                             )
171                         clockColorContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
172                     }
173                 }
174             }
175         )
176 
177         clockFontContent.viewTreeObserver.addOnGlobalLayoutListener(
178             object : OnGlobalLayoutListener {
179                 override fun onGlobalLayout() {
180                     if (
181                         clockFontContent.height != 0 &&
182                             _clockFloatingSheetHeights.value.clockFontContentHeight == null
183                     ) {
184                         _clockFloatingSheetHeights.value =
185                             _clockFloatingSheetHeights.value.copy(
186                                 clockFontContentHeight = clockFontContent.height
187                             )
188                         clockColorContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
189                     }
190                 }
191             }
192         )
193 
194         lifecycleOwner.lifecycleScope.launch {
195             var currentContent: View = clockStyleContent
196             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
197                 launch { viewModel.tabs.collect { tabAdapter.submitList(it) } }
198 
199                 launch {
200                     combine(clockFloatingSheetHeights, viewModel.selectedTab, ::Pair).collect {
201                         (heights, selectedTab) ->
202                         val (
203                             clockStyleContentHeight,
204                             clockColorContentHeight,
205                             clockFontContentHeight) =
206                             heights
207                         clockStyleContentHeight ?: return@collect
208                         clockColorContentHeight ?: return@collect
209                         clockFontContentHeight ?: return@collect
210 
211                         val fromHeight = floatingSheetContainer.height
212                         val toHeight =
213                             when (selectedTab) {
214                                 Tab.STYLE -> clockStyleContentHeight
215                                 Tab.COLOR -> clockColorContentHeight
216                                 Tab.FONT -> clockFontContentHeight
217                             }
218                         // Start to animate the content height
219                         ValueAnimator.ofInt(fromHeight, toHeight)
220                             .apply {
221                                 addUpdateListener { valueAnimator ->
222                                     val value = valueAnimator.animatedValue as Int
223                                     floatingSheetContainer.layoutParams =
224                                         floatingSheetContainer.layoutParams.apply { height = value }
225                                     currentContent.alpha = getAlpha(fromHeight, toHeight, value)
226                                 }
227                                 duration = ANIMATION_DURATION
228                                 addListener(
229                                     object : AnimatorListenerAdapter() {
230                                         override fun onAnimationEnd(animation: Animator) {
231                                             clockStyleContent.isVisible = selectedTab == Tab.STYLE
232                                             clockStyleContent.alpha = 1f
233                                             clockColorContent.isVisible = selectedTab == Tab.COLOR
234                                             clockColorContent.alpha = 1f
235                                             clockFontContent.isVisible = selectedTab == Tab.FONT
236                                             clockFontContent.alpha = 1f
237                                             currentContent =
238                                                 when (selectedTab) {
239                                                     Tab.STYLE -> clockStyleContent
240                                                     Tab.COLOR -> clockColorContent
241                                                     Tab.FONT -> clockFontContent
242                                                 }
243                                             // Also update the floating toolbar when the height
244                                             // animation ends.
245                                             tabs.isVisible = selectedTab != Tab.FONT
246                                             clockFontToolbar.isVisible = selectedTab == Tab.FONT
247                                         }
248                                     }
249                                 )
250                             }
251                             .start()
252                     }
253                 }
254 
255                 launch {
256                     viewModel.clockStyleOptions.collect { styleOptions ->
257                         clockStyleAdapter.setItems(styleOptions) {
258                             var indexToFocus = styleOptions.indexOfFirst { it.isSelected.value }
259                             indexToFocus = if (indexToFocus < 0) 0 else indexToFocus
260                             (clockStyleList.layoutManager as LinearLayoutManager)
261                                 .scrollToPositionWithOffset(indexToFocus, 0)
262                         }
263                     }
264                 }
265 
266                 launch {
267                     viewModel.clockColorOptions.collect { colorOptions ->
268                         clockColorAdapter.setItems(colorOptions) {
269                             var indexToFocus = colorOptions.indexOfFirst { it.isSelected.value }
270                             indexToFocus = if (indexToFocus < 0) 0 else indexToFocus
271                             (clockColorList.layoutManager as LinearLayoutManager)
272                                 .scrollToPositionWithOffset(indexToFocus, 0)
273                         }
274                     }
275                 }
276 
277                 launch {
278                     viewModel.previewingSliderProgress.collect { progress ->
279                         clockColorSlider.setProgress(progress, true)
280                     }
281                 }
282 
283                 launch {
284                     viewModel.isSliderEnabled.collect { isEnabled ->
285                         clockColorSlider.isEnabled = isEnabled
286                         clockColorSlider.alpha =
287                             if (isEnabled) SLIDER_ENABLED_ALPHA else SLIDER_DISABLED_ALPHA
288                     }
289                 }
290 
291                 launch {
292                     viewModel.previewingClockSize.collect { size ->
293                         when (size) {
294                             ClockSize.DYNAMIC -> clockSizeSwitch.isChecked = true
295                             ClockSize.SMALL -> clockSizeSwitch.isChecked = false
296                         }
297                     }
298                 }
299 
300                 launch {
301                     viewModel.onClockSizeSwitchCheckedChange.collect { onCheckedChange ->
302                         clockSizeSwitch.setOnCheckedChangeListener { _, _ ->
303                             onCheckedChange.invoke()
304                         }
305                     }
306                 }
307             }
308         }
309 
310         bindClockFontContent(
311             clockFontContent = clockFontContent,
312             viewModel = viewModel,
313             lifecycleOwner = lifecycleOwner,
314         )
315     }
316 
317     private fun bindClockFontContent(
318         clockFontContent: View,
319         viewModel: ClockPickerViewModel,
320         lifecycleOwner: LifecycleOwner,
321     ) {
322         val sliderViewList =
323             listOf(
324                 ClockFontSliderViewHolder(
325                     name = clockFontContent.requireViewById(R.id.clock_axis_slider_name1),
326                     slider = clockFontContent.requireViewById(R.id.clock_axis_slider1),
327                 ),
328                 ClockFontSliderViewHolder(
329                     name = clockFontContent.requireViewById(R.id.clock_axis_slider_name2),
330                     slider = clockFontContent.requireViewById(R.id.clock_axis_slider2),
331                 ),
332             )
333         val switchViewList =
334             listOf(
335                 ClockFontSwitchViewHolder(
336                     name = clockFontContent.requireViewById(R.id.clock_axis_switch_name1),
337                     switch = clockFontContent.requireViewById(R.id.clock_axis_switch1),
338                 ),
339                 ClockFontSwitchViewHolder(
340                     name = clockFontContent.requireViewById(R.id.clock_axis_switch_name2),
341                     switch = clockFontContent.requireViewById(R.id.clock_axis_switch2),
342                 ),
343             )
344         val sliderViewMap: MutableMap<String, ClockFontSliderViewHolder> = mutableMapOf()
345         val switchViewMap: MutableMap<String, ClockFontSwitchViewHolder> = mutableMapOf()
346 
347         lifecycleOwner.lifecycleScope.launch {
348             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
349                 launch {
350                     viewModel.selectedClockFontAxes.filterNotNull().collect { fontAxes ->
351                         // This data flow updates only when a new clock style is selected. We
352                         // initiate the clock font content with regard to that clock style.
353                         sliderViewMap.clear()
354                         switchViewMap.clear()
355 
356                         // Initiate the slider views
357                         val floatAxisList = fontAxes.filter { it.type == AxisType.Float }
358                         sliderViewList.forEachIndexed { i, viewHolder ->
359                             val floatAxis = floatAxisList.getOrNull(i)
360                             viewHolder.setIsVisible(floatAxis != null)
361                             floatAxis?.let {
362                                 sliderViewMap[floatAxis.key] = viewHolder
363                                 viewHolder.initView(it) { value ->
364                                     viewModel.updatePreviewFontAxis(floatAxis.key, value)
365                                 }
366                             }
367                         }
368 
369                         // Initiate the switch views
370                         val booleanAxisList = fontAxes.filter { it.type == AxisType.Boolean }
371                         switchViewList.forEachIndexed { i, viewHolder ->
372                             val booleanAxis = booleanAxisList.getOrNull(i)
373                             viewHolder.setIsVisible(booleanAxis != null)
374                             booleanAxis?.let {
375                                 switchViewMap[it.key] = viewHolder
376                                 viewHolder.initView(booleanAxis) { value ->
377                                     viewModel.updatePreviewFontAxis(booleanAxis.key, value)
378                                 }
379                             }
380                         }
381                     }
382                 }
383 
384                 launch {
385                     viewModel.previewingClockFontAxisMap.collect { axisMap ->
386                         // This data flow updates when user configures the sliders and switches
387                         // in the clock font content.
388                         axisMap.forEach { (key, value) ->
389                             sliderViewMap[key]?.setValue(value)
390                             switchViewMap[key]?.setValue(value)
391                         }
392                     }
393                 }
394             }
395         }
396     }
397 
398     private fun createClockStyleOptionItemAdapter(
399         lifecycleOwner: LifecycleOwner
400     ): OptionItemAdapter2<ClockStyleModel> =
401         OptionItemAdapter2(
402             layoutResourceId = R.layout.clock_style_option,
403             lifecycleOwner = lifecycleOwner,
404             bindPayload = { view: View, styleModel: ClockStyleModel ->
405                 view
406                     .findViewById<ImageView>(R.id.foreground)
407                     ?.setImageDrawable(styleModel.thumbnail)
408                 val job =
409                     lifecycleOwner.lifecycleScope.launch {
410                         lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
411                             styleModel.showEditButton.collect {
412                                 view.findViewById<ImageView>(R.id.edit_icon)?.isVisible = it
413                             }
414                         }
415                     }
416                 return@OptionItemAdapter2 DisposableHandle { job.cancel() }
417             },
418         )
419 
420     private fun RecyclerView.initStyleList(
421         context: Context,
422         adapter: OptionItemAdapter2<ClockStyleModel>,
423     ) {
424         this.adapter = adapter
425         layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
426         addItemDecoration(
427             SingleRowListItemSpacing(
428                 context.resources.getDimensionPixelSize(
429                     R.dimen.floating_sheet_content_horizontal_padding
430                 ),
431                 context.resources.getDimensionPixelSize(
432                     R.dimen.floating_sheet_list_item_horizontal_space
433                 ),
434             )
435         )
436     }
437 
438     private fun createClockColorOptionItemAdapter(
439         uiMode: Int,
440         lifecycleOwner: LifecycleOwner,
441     ): OptionItemAdapter<ColorOptionIconViewModel> =
442         OptionItemAdapter(
443             layoutResourceId = R.layout.color_option,
444             lifecycleOwner = lifecycleOwner,
445             bindIcon = { foregroundView: View, colorIcon: ColorOptionIconViewModel ->
446                 val colorOptionIconView = foregroundView as? ColorOptionIconView
447                 val night =
448                     uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
449                 colorOptionIconView?.let { ColorOptionIconBinder.bind(it, colorIcon, night) }
450             },
451         )
452 
453     private fun RecyclerView.initColorList(
454         context: Context,
455         adapter: OptionItemAdapter<ColorOptionIconViewModel>,
456     ) {
457         apply {
458             this.adapter = adapter
459             layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
460             addItemDecoration(
461                 SingleRowListItemSpacing(
462                     context.resources.getDimensionPixelSize(
463                         R.dimen.floating_sheet_content_horizontal_padding
464                     ),
465                     context.resources.getDimensionPixelSize(
466                         R.dimen.floating_sheet_list_item_horizontal_space
467                     ),
468                 )
469             )
470         }
471     }
472 
473     // Alpha is 1 when current height is from height, and 0 when current height is to height.
474     private fun getAlpha(fromHeight: Int, toHeight: Int, currentHeight: Int): Float =
475         (1 - (currentHeight - fromHeight).toFloat() / (toHeight - fromHeight).toFloat())
476 }
477