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