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.ValueAnimator 20 import android.view.ViewTreeObserver.OnGlobalLayoutListener 21 import android.widget.Button 22 import android.widget.FrameLayout 23 import android.widget.Toolbar 24 import androidx.core.graphics.ColorUtils 25 import androidx.core.graphics.drawable.DrawableCompat 26 import androidx.core.view.isInvisible 27 import androidx.lifecycle.Lifecycle 28 import androidx.lifecycle.LifecycleOwner 29 import androidx.lifecycle.lifecycleScope 30 import androidx.lifecycle.repeatOnLifecycle 31 import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel 32 import com.android.wallpaper.customization.ui.viewmodel.ToolbarHeightsViewModel 33 import com.android.wallpaper.picker.customization.ui.binder.ColorUpdateBinder 34 import com.android.wallpaper.picker.customization.ui.binder.DefaultToolbarBinder 35 import com.android.wallpaper.picker.customization.ui.binder.ToolbarBinder 36 import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel 37 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationOptionsViewModel 38 import javax.inject.Inject 39 import javax.inject.Singleton 40 import kotlinx.coroutines.flow.MutableStateFlow 41 import kotlinx.coroutines.flow.asStateFlow 42 import kotlinx.coroutines.flow.combine 43 import kotlinx.coroutines.flow.filterNotNull 44 import kotlinx.coroutines.flow.map 45 import kotlinx.coroutines.launch 46 47 @Singleton 48 class ThemePickerToolbarBinder 49 @Inject 50 constructor(private val defaultToolbarBinder: DefaultToolbarBinder) : ToolbarBinder { 51 52 private val _toolbarHeights: MutableStateFlow<ToolbarHeightsViewModel?> = MutableStateFlow(null) 53 private val toolbarHeights = _toolbarHeights.asStateFlow().filterNotNull() 54 55 override fun bind( 56 navButton: FrameLayout, 57 toolbar: Toolbar, 58 applyButton: Button, 59 viewModel: CustomizationOptionsViewModel, 60 colorUpdateViewModel: ColorUpdateViewModel, 61 lifecycleOwner: LifecycleOwner, 62 onNavBack: () -> Unit, 63 ) { 64 defaultToolbarBinder.bind( 65 navButton, 66 toolbar, 67 applyButton, 68 viewModel, 69 colorUpdateViewModel, 70 lifecycleOwner, 71 onNavBack, 72 ) 73 74 if (viewModel !is ThemePickerCustomizationOptionsViewModel) { 75 throw IllegalArgumentException( 76 "viewModel $viewModel is not a ThemePickerCustomizationOptionsViewModel." 77 ) 78 } 79 80 navButton.viewTreeObserver.addOnGlobalLayoutListener( 81 object : OnGlobalLayoutListener { 82 override fun onGlobalLayout() { 83 if (navButton.height != 0) { 84 _toolbarHeights.value = 85 _toolbarHeights.value?.copy(navButtonHeight = navButton.height) 86 ?: ToolbarHeightsViewModel(navButtonHeight = navButton.height) 87 } 88 navButton.viewTreeObserver.removeOnGlobalLayoutListener(this) 89 } 90 } 91 ) 92 93 toolbar.viewTreeObserver.addOnGlobalLayoutListener( 94 object : OnGlobalLayoutListener { 95 override fun onGlobalLayout() { 96 if (toolbar.height != 0) { 97 _toolbarHeights.value = 98 _toolbarHeights.value?.copy(toolbarHeight = toolbar.height) 99 ?: ToolbarHeightsViewModel(toolbarHeight = toolbar.height) 100 } 101 navButton.viewTreeObserver.removeOnGlobalLayoutListener(this) 102 } 103 } 104 ) 105 106 applyButton.viewTreeObserver.addOnGlobalLayoutListener( 107 object : OnGlobalLayoutListener { 108 override fun onGlobalLayout() { 109 if (applyButton.height != 0) { 110 _toolbarHeights.value = 111 _toolbarHeights.value?.copy(applyButtonHeight = applyButton.height) 112 ?: ToolbarHeightsViewModel(applyButtonHeight = applyButton.height) 113 } 114 applyButton.viewTreeObserver.removeOnGlobalLayoutListener(this) 115 } 116 } 117 ) 118 119 ColorUpdateBinder.bind( 120 setColor = { color -> 121 DrawableCompat.setTint(DrawableCompat.wrap(applyButton.background), color) 122 }, 123 color = colorUpdateViewModel.colorPrimary, 124 shouldAnimate = { true }, 125 lifecycleOwner = lifecycleOwner, 126 ) 127 128 lifecycleOwner.lifecycleScope.launch { 129 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 130 launch { 131 viewModel.onApplyButtonClicked.collect { onApplyButtonClicked -> 132 applyButton.setOnClickListener { onApplyButtonClicked?.invoke(onNavBack) } 133 } 134 } 135 136 launch { viewModel.isApplyButtonVisible.collect { applyButton.isInvisible = !it } } 137 138 launch { 139 viewModel.isApplyButtonEnabled.collect { 140 applyButton.isEnabled = it 141 applyButton.background.alpha = 142 if (it) 255 else 31 // 255 for 100%, 31 for 12% transparent 143 ColorUpdateBinder.bind( 144 setColor = { color -> applyButton.setTextColor(color) }, 145 color = 146 if (it) { 147 colorUpdateViewModel.colorOnPrimary 148 } else { 149 colorUpdateViewModel.colorOnSurface.map { color: Int -> 150 ColorUtils.setAlphaComponent( 151 color, 152 97, 153 ) // 97 for 38% transparent 154 } 155 }, 156 shouldAnimate = { true }, 157 lifecycleOwner = lifecycleOwner, 158 ) 159 } 160 } 161 162 launch { 163 combine(toolbarHeights, viewModel.isToolbarCollapsed, ::Pair).collect { 164 (toolbarHeights, isToolbarCollapsed) -> 165 val (navButtonHeight, toolbarHeight, applyButtonHeight) = toolbarHeights 166 navButtonHeight ?: return@collect 167 toolbarHeight ?: return@collect 168 applyButtonHeight ?: return@collect 169 170 val navButtonToHeight = if (isToolbarCollapsed) 0 else navButtonHeight 171 val toolbarToHeight = if (isToolbarCollapsed) 0 else toolbarHeight 172 val applyButtonToHeight = if (isToolbarCollapsed) 0 else applyButtonHeight 173 ValueAnimator.ofInt(navButton.height, navButtonToHeight) 174 .apply { 175 addUpdateListener { valueAnimator -> 176 val value = valueAnimator.animatedValue as Int 177 navButton.layoutParams = 178 navButton.layoutParams.apply { height = value } 179 } 180 duration = ANIMATION_DURATION 181 } 182 .start() 183 184 ValueAnimator.ofInt(toolbar.height, toolbarToHeight) 185 .apply { 186 addUpdateListener { valueAnimator -> 187 val value = valueAnimator.animatedValue as Int 188 toolbar.layoutParams = 189 toolbar.layoutParams.apply { height = value } 190 } 191 duration = ANIMATION_DURATION 192 } 193 .start() 194 195 ValueAnimator.ofInt(applyButton.height, applyButtonToHeight) 196 .apply { 197 addUpdateListener { valueAnimator -> 198 val value = valueAnimator.animatedValue as Int 199 applyButton.layoutParams = 200 applyButton.layoutParams.apply { height = value } 201 } 202 duration = ANIMATION_DURATION 203 } 204 .start() 205 } 206 } 207 } 208 } 209 } 210 211 companion object { 212 private const val ANIMATION_DURATION = 200L 213 } 214 } 215