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