1 /* <lambda>null2 * Copyright (C) 2023 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 package com.android.customization.picker.clock.ui.binder 17 18 import android.content.Context 19 import android.content.res.Configuration 20 import android.text.Spannable 21 import android.text.SpannableString 22 import android.text.style.TextAppearanceSpan 23 import android.view.LayoutInflater 24 import android.view.View 25 import android.view.ViewGroup 26 import android.widget.LinearLayout 27 import android.widget.RadioButton 28 import android.widget.RadioGroup 29 import android.widget.RadioGroup.OnCheckedChangeListener 30 import android.widget.SeekBar 31 import androidx.core.view.doOnPreDraw 32 import androidx.core.view.isInvisible 33 import androidx.core.view.isVisible 34 import androidx.lifecycle.Lifecycle 35 import androidx.lifecycle.LifecycleEventObserver 36 import androidx.lifecycle.LifecycleOwner 37 import androidx.lifecycle.lifecycleScope 38 import androidx.lifecycle.repeatOnLifecycle 39 import androidx.recyclerview.widget.LinearLayoutManager 40 import androidx.recyclerview.widget.RecyclerView 41 import com.android.customization.picker.clock.shared.ClockSize 42 import com.android.customization.picker.clock.ui.adapter.ClockSettingsTabAdapter 43 import com.android.customization.picker.clock.ui.view.ClockCarouselView 44 import com.android.customization.picker.clock.ui.view.ClockViewFactory 45 import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel 46 import com.android.customization.picker.color.ui.binder.ColorOptionIconBinder 47 import com.android.systemui.shared.Flags 48 import com.android.themepicker.R 49 import com.android.wallpaper.picker.common.ui.view.ItemSpacing 50 import com.android.wallpaper.picker.option.ui.binder.OptionItemBinder 51 import kotlinx.coroutines.flow.combine 52 import kotlinx.coroutines.flow.mapNotNull 53 import kotlinx.coroutines.launch 54 55 /** Bind between the clock settings screen and its view model. */ 56 object ClockSettingsBinder { 57 private const val SLIDER_ENABLED_ALPHA = 1f 58 private const val SLIDER_DISABLED_ALPHA = .3f 59 private const val COLOR_PICKER_ITEM_PREFIX_ID = 1234 60 61 fun bind( 62 view: View, 63 viewModel: ClockSettingsViewModel, 64 clockViewFactory: ClockViewFactory, 65 lifecycleOwner: LifecycleOwner, 66 ) { 67 if (Flags.newCustomizationPickerUi()) { 68 return 69 } 70 val clockHostView: ViewGroup = view.requireViewById(R.id.clock_host_view) 71 val tabView: RecyclerView = view.requireViewById(R.id.tabs) 72 val tabAdapter = ClockSettingsTabAdapter() 73 tabView.adapter = tabAdapter 74 tabView.layoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false) 75 tabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP)) 76 val colorOptionContainerListView: LinearLayout = view.requireViewById(R.id.color_options) 77 val slider: SeekBar = view.requireViewById(R.id.slider) 78 slider.setOnSeekBarChangeListener( 79 object : SeekBar.OnSeekBarChangeListener { 80 override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) { 81 if (fromUser) { 82 viewModel.onSliderProgressChanged(progress) 83 } 84 } 85 86 override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit 87 88 override fun onStopTrackingTouch(seekBar: SeekBar?) { 89 seekBar?.progress?.let { 90 lifecycleOwner.lifecycleScope.launch { viewModel.onSliderProgressStop(it) } 91 } 92 } 93 } 94 ) 95 96 val onCheckedChangeListener = OnCheckedChangeListener { _, id -> 97 when (id) { 98 R.id.radio_dynamic -> viewModel.setClockSize(ClockSize.DYNAMIC) 99 R.id.radio_small -> viewModel.setClockSize(ClockSize.SMALL) 100 } 101 } 102 val clockSizeRadioGroup = 103 view.requireViewById<RadioGroup>(R.id.clock_size_radio_button_group) 104 clockSizeRadioGroup.setOnCheckedChangeListener(onCheckedChangeListener) 105 view.requireViewById<RadioButton>(R.id.radio_dynamic).text = 106 getRadioText( 107 view.context.applicationContext, 108 view.resources.getString(R.string.clock_size_dynamic), 109 view.resources.getString(R.string.clock_size_dynamic_description), 110 ) 111 view.requireViewById<RadioButton>(R.id.radio_small).text = 112 getRadioText( 113 view.context.applicationContext, 114 view.resources.getString(R.string.clock_size_small), 115 view.resources.getString(R.string.clock_size_small_description), 116 ) 117 118 val colorOptionContainer = view.requireViewById<View>(R.id.color_picker_container) 119 lifecycleOwner.lifecycleScope.launch { 120 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 121 launch { 122 viewModel.seedColor.collect { seedColor -> 123 viewModel.selectedClockId.value?.let { selectedClockId -> 124 clockViewFactory.updateColor(selectedClockId, seedColor) 125 } 126 } 127 } 128 129 launch { viewModel.tabs.collect { tabAdapter.setItems(it) } } 130 131 launch { 132 viewModel.selectedTab.collect { tab -> 133 when (tab) { 134 ClockSettingsViewModel.Tab.COLOR -> { 135 colorOptionContainer.isVisible = true 136 clockSizeRadioGroup.isInvisible = true 137 } 138 ClockSettingsViewModel.Tab.SIZE -> { 139 colorOptionContainer.isInvisible = true 140 clockSizeRadioGroup.isVisible = true 141 } 142 } 143 } 144 } 145 146 launch { 147 viewModel.colorOptions.collect { colorOptions -> 148 colorOptionContainerListView.removeAllViews() 149 colorOptions.forEachIndexed { index, colorOption -> 150 colorOption.payload?.let { payload -> 151 val item = 152 LayoutInflater.from(view.context) 153 .inflate( 154 R.layout.clock_color_option, 155 colorOptionContainerListView, 156 false, 157 ) as LinearLayout 158 val darkMode = 159 (view.resources.configuration.uiMode and 160 Configuration.UI_MODE_NIGHT_MASK == 161 Configuration.UI_MODE_NIGHT_YES) 162 ColorOptionIconBinder.bind( 163 item.requireViewById(R.id.foreground), 164 payload, 165 darkMode, 166 ) 167 OptionItemBinder.bind( 168 view = item, 169 viewModel = colorOptions[index], 170 lifecycleOwner = lifecycleOwner, 171 foregroundTintSpec = null, 172 ) 173 174 val id = COLOR_PICKER_ITEM_PREFIX_ID + index 175 item.id = id 176 colorOptionContainerListView.addView(item) 177 } 178 } 179 } 180 } 181 182 launch { 183 viewModel.selectedColorOptionPosition.collect { selectedPosition -> 184 if (selectedPosition != -1) { 185 val colorOptionContainerListView: LinearLayout = 186 view.requireViewById(R.id.color_options) 187 188 val selectedView = 189 colorOptionContainerListView.findViewById<View>( 190 COLOR_PICKER_ITEM_PREFIX_ID + selectedPosition 191 ) 192 selectedView?.parent?.requestChildFocus(selectedView, selectedView) 193 } 194 } 195 } 196 197 launch { 198 combine( 199 viewModel.selectedClockId.mapNotNull { it }, 200 viewModel.selectedClockSize, 201 ::Pair, 202 ) 203 .collect { (clockId, size) -> 204 clockHostView.removeAllViews() 205 val clockView = 206 when (size) { 207 ClockSize.DYNAMIC -> clockViewFactory.getLargeView(clockId) 208 ClockSize.SMALL -> clockViewFactory.getSmallView(clockId) 209 } 210 // The clock view might still be attached to an existing parent. 211 // Detach before adding to another parent. 212 (clockView.parent as? ViewGroup)?.removeView(clockView) 213 clockHostView.addView(clockView) 214 215 when (size) { 216 ClockSize.DYNAMIC -> { 217 // When clock size data flow emits clock size signal, we want 218 // to update the view without triggering on checked change, 219 // which is supposed to be triggered by user interaction only. 220 clockSizeRadioGroup.setOnCheckedChangeListener(null) 221 clockSizeRadioGroup.check(R.id.radio_dynamic) 222 clockSizeRadioGroup.setOnCheckedChangeListener( 223 onCheckedChangeListener 224 ) 225 clockHostView.doOnPreDraw { 226 it.pivotX = it.width / 2F 227 it.pivotY = it.height / 2F 228 } 229 } 230 ClockSize.SMALL -> { 231 // When clock size data flow emits clock size signal, we want 232 // to update the view without triggering on checked change, 233 // which is supposed to be triggered by user interaction only. 234 clockSizeRadioGroup.setOnCheckedChangeListener(null) 235 clockSizeRadioGroup.check(R.id.radio_small) 236 clockSizeRadioGroup.setOnCheckedChangeListener( 237 onCheckedChangeListener 238 ) 239 clockHostView.doOnPreDraw { 240 it.pivotX = ClockCarouselView.getCenteredHostViewPivotX(it) 241 it.pivotY = 0F 242 } 243 } 244 } 245 } 246 } 247 248 launch { 249 viewModel.sliderProgress.collect { progress -> 250 slider.setProgress(progress, true) 251 } 252 } 253 254 launch { 255 viewModel.isSliderEnabled.collect { isEnabled -> 256 slider.isEnabled = isEnabled 257 slider.alpha = 258 if (isEnabled) SLIDER_ENABLED_ALPHA else SLIDER_DISABLED_ALPHA 259 } 260 } 261 } 262 } 263 264 lifecycleOwner.lifecycle.addObserver( 265 LifecycleEventObserver { source, event -> 266 when (event) { 267 Lifecycle.Event.ON_RESUME -> { 268 clockViewFactory.registerTimeTicker(source) 269 } 270 Lifecycle.Event.ON_PAUSE -> { 271 clockViewFactory.unregisterTimeTicker(source) 272 } 273 else -> {} 274 } 275 } 276 ) 277 } 278 279 private fun getRadioText( 280 context: Context, 281 title: String, 282 description: String, 283 ): SpannableString { 284 val text = SpannableString(title + "\n" + description) 285 text.setSpan( 286 TextAppearanceSpan(context, R.style.SectionTitleTextStyle), 287 0, 288 title.length, 289 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, 290 ) 291 text.setSpan( 292 TextAppearanceSpan(context, R.style.SectionSubtitleTextStyle), 293 title.length + 1, 294 title.length + 1 + description.length, 295 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, 296 ) 297 return text 298 } 299 } 300