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.content.Context 21 import android.graphics.drawable.Drawable 22 import android.view.View 23 import android.view.ViewGroup 24 import android.view.ViewTreeObserver.OnGlobalLayoutListener 25 import android.widget.ImageView 26 import androidx.core.graphics.drawable.DrawableCompat 27 import androidx.core.view.isVisible 28 import androidx.lifecycle.Lifecycle 29 import androidx.lifecycle.LifecycleOwner 30 import androidx.lifecycle.lifecycleScope 31 import androidx.lifecycle.repeatOnLifecycle 32 import androidx.recyclerview.widget.LinearLayoutManager 33 import androidx.recyclerview.widget.RecyclerView 34 import com.android.customization.picker.common.ui.view.SingleRowListItemSpacing 35 import com.android.customization.picker.grid.ui.viewmodel.ShapeIconViewModel 36 import com.android.themepicker.R 37 import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption.APP_SHAPE_GRID 38 import com.android.wallpaper.customization.ui.viewmodel.ShapeGridFloatingSheetHeightsViewModel 39 import com.android.wallpaper.customization.ui.viewmodel.ShapeGridPickerViewModel.Tab.GRID 40 import com.android.wallpaper.customization.ui.viewmodel.ShapeGridPickerViewModel.Tab.SHAPE 41 import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel 42 import com.android.wallpaper.picker.customization.ui.binder.ColorUpdateBinder 43 import com.android.wallpaper.picker.customization.ui.view.FloatingToolbar 44 import com.android.wallpaper.picker.customization.ui.view.adapter.FloatingToolbarTabAdapter 45 import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel 46 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter2 47 import java.lang.ref.WeakReference 48 import kotlinx.coroutines.CoroutineDispatcher 49 import kotlinx.coroutines.flow.Flow 50 import kotlinx.coroutines.flow.MutableStateFlow 51 import kotlinx.coroutines.flow.asStateFlow 52 import kotlinx.coroutines.flow.combine 53 import kotlinx.coroutines.flow.filter 54 import kotlinx.coroutines.flow.filterNotNull 55 import kotlinx.coroutines.launch 56 57 object ShapeGridFloatingSheetBinder { 58 private const val ANIMATION_DURATION = 200L 59 60 private val _shapeGridFloatingSheetHeights: 61 MutableStateFlow<ShapeGridFloatingSheetHeightsViewModel?> = 62 MutableStateFlow(null) 63 private val shapeGridFloatingSheetHeights: Flow<ShapeGridFloatingSheetHeightsViewModel> = 64 _shapeGridFloatingSheetHeights.asStateFlow().filterNotNull().filter { 65 it.shapeContentHeight != null && it.gridContentHeight != null 66 } 67 68 fun bind( 69 view: View, 70 optionsViewModel: ThemePickerCustomizationOptionsViewModel, 71 colorUpdateViewModel: ColorUpdateViewModel, 72 lifecycleOwner: LifecycleOwner, 73 backgroundDispatcher: CoroutineDispatcher, 74 ) { 75 val floatingSheetContentVerticalPadding = 76 view.resources.getDimensionPixelSize(R.dimen.floating_sheet_content_vertical_padding) 77 val viewModel = optionsViewModel.shapeGridPickerViewModel 78 val isFloatingSheetActive = { optionsViewModel.selectedOption.value == APP_SHAPE_GRID } 79 80 val tabs = view.requireViewById<FloatingToolbar>(R.id.floating_toolbar) 81 val tabContainer = 82 tabs.findViewById<ViewGroup>(com.android.wallpaper.R.id.floating_toolbar_tab_container) 83 ColorUpdateBinder.bind( 84 setColor = { color -> 85 DrawableCompat.setTint(DrawableCompat.wrap(tabContainer.background), color) 86 }, 87 color = colorUpdateViewModel.floatingToolbarBackground, 88 shouldAnimate = isFloatingSheetActive, 89 lifecycleOwner = lifecycleOwner, 90 ) 91 val tabAdapter = 92 FloatingToolbarTabAdapter( 93 colorUpdateViewModel = WeakReference(colorUpdateViewModel), 94 shouldAnimateColor = isFloatingSheetActive, 95 ) 96 .also { tabs.setAdapter(it) } 97 98 val floatingSheetContainer = 99 view.requireViewById<ViewGroup>(R.id.floating_sheet_content_container) 100 ColorUpdateBinder.bind( 101 setColor = { color -> 102 DrawableCompat.setTint( 103 DrawableCompat.wrap(floatingSheetContainer.background), 104 color, 105 ) 106 }, 107 color = colorUpdateViewModel.colorSurfaceBright, 108 shouldAnimate = isFloatingSheetActive, 109 lifecycleOwner = lifecycleOwner, 110 ) 111 112 val shapeContent = view.requireViewById<View>(R.id.app_shape_container) 113 val shapeOptionListAdapter = 114 createShapeOptionItemAdapter( 115 colorUpdateViewModel = colorUpdateViewModel, 116 shouldAnimateColor = isFloatingSheetActive, 117 lifecycleOwner = lifecycleOwner, 118 backgroundDispatcher = backgroundDispatcher, 119 ) 120 val shapeOptionList = 121 view.requireViewById<RecyclerView>(R.id.shape_options).also { 122 it.initShapeOptionList(view.context, shapeOptionListAdapter) 123 } 124 125 val gridContent = view.requireViewById<View>(R.id.app_grid_container) 126 val gridOptionListAdapter = 127 createGridOptionItemAdapter( 128 colorUpdateViewModel = colorUpdateViewModel, 129 shouldAnimateColor = isFloatingSheetActive, 130 lifecycleOwner = lifecycleOwner, 131 backgroundDispatcher = backgroundDispatcher, 132 ) 133 val gridOptionList = 134 view.requireViewById<RecyclerView>(R.id.grid_options).also { 135 it.initGridOptionList(view.context, gridOptionListAdapter) 136 } 137 138 // Get the shape content height when it is ready 139 shapeContent.viewTreeObserver.addOnGlobalLayoutListener( 140 object : OnGlobalLayoutListener { 141 override fun onGlobalLayout() { 142 if (shapeContent.height != 0) { 143 _shapeGridFloatingSheetHeights.value = 144 _shapeGridFloatingSheetHeights.value?.copy( 145 shapeContentHeight = shapeContent.height 146 ) 147 ?: ShapeGridFloatingSheetHeightsViewModel( 148 shapeContentHeight = shapeContent.height 149 ) 150 } 151 shapeContent.viewTreeObserver.removeOnGlobalLayoutListener(this) 152 } 153 } 154 ) 155 // Get the grid content height when it is ready 156 gridContent.viewTreeObserver.addOnGlobalLayoutListener( 157 object : OnGlobalLayoutListener { 158 override fun onGlobalLayout() { 159 if (gridContent.height != 0) { 160 _shapeGridFloatingSheetHeights.value = 161 _shapeGridFloatingSheetHeights.value?.copy( 162 gridContentHeight = gridContent.height 163 ) 164 ?: ShapeGridFloatingSheetHeightsViewModel( 165 gridContentHeight = shapeContent.height 166 ) 167 } 168 shapeContent.viewTreeObserver.removeOnGlobalLayoutListener(this) 169 } 170 } 171 ) 172 173 lifecycleOwner.lifecycleScope.launch { 174 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 175 launch { viewModel.tabs.collect { tabAdapter.submitList(it) } } 176 177 launch { 178 combine(shapeGridFloatingSheetHeights, viewModel.selectedTab) { 179 heights, 180 selectedTab -> 181 heights to selectedTab 182 } 183 .collect { (heights, selectedTab) -> 184 val (shapeContentHeight, gridContentHeight) = heights 185 shapeContentHeight ?: return@collect 186 gridContentHeight ?: return@collect 187 // Make sure the recycler view height is the same as its parent. It's 188 // possible that the recycler view is shorter than expected. 189 gridOptionList.layoutParams = 190 gridOptionList.layoutParams.apply { height = gridContentHeight } 191 val targetHeight = 192 when (selectedTab) { 193 SHAPE -> shapeContentHeight 194 GRID -> gridContentHeight 195 } + floatingSheetContentVerticalPadding * 2 196 197 ValueAnimator.ofInt(floatingSheetContainer.height, targetHeight) 198 .apply { 199 addUpdateListener { valueAnimator -> 200 val value = valueAnimator.animatedValue as Int 201 floatingSheetContainer.layoutParams = 202 floatingSheetContainer.layoutParams.apply { 203 height = value 204 } 205 } 206 duration = ANIMATION_DURATION 207 } 208 .start() 209 210 shapeContent.isVisible = selectedTab == SHAPE 211 gridContent.isVisible = selectedTab == GRID 212 } 213 } 214 215 launch { 216 viewModel.gridOptions.collect { options -> 217 gridOptionListAdapter.setItems(options) { 218 val indexToFocus = 219 options.indexOfFirst { it.isSelected.value }.coerceAtLeast(0) 220 (gridOptionList.layoutManager as LinearLayoutManager).scrollToPosition( 221 indexToFocus 222 ) 223 } 224 } 225 } 226 227 launch { 228 viewModel.shapeOptions.collect { options -> 229 shapeOptionListAdapter.setItems(options) { 230 val indexToFocus = 231 options.indexOfFirst { it.isSelected.value }.coerceAtLeast(0) 232 (shapeOptionList.layoutManager as LinearLayoutManager).scrollToPosition( 233 indexToFocus 234 ) 235 } 236 } 237 } 238 } 239 } 240 } 241 242 private fun createShapeOptionItemAdapter( 243 colorUpdateViewModel: ColorUpdateViewModel, 244 shouldAnimateColor: () -> Boolean, 245 lifecycleOwner: LifecycleOwner, 246 backgroundDispatcher: CoroutineDispatcher, 247 ): OptionItemAdapter2<ShapeIconViewModel> = 248 OptionItemAdapter2( 249 layoutResourceId = R.layout.shape_option2, 250 lifecycleOwner = lifecycleOwner, 251 backgroundDispatcher = backgroundDispatcher, 252 bindPayload = { view: View, shapeIcon: ShapeIconViewModel -> 253 val imageView = view.findViewById(R.id.foreground) as? ImageView 254 imageView?.let { ShapeIconViewBinder.bind(imageView, shapeIcon) } 255 return@OptionItemAdapter2 null 256 }, 257 colorUpdateViewModel = WeakReference(colorUpdateViewModel), 258 shouldAnimateColor = shouldAnimateColor, 259 ) 260 261 private fun RecyclerView.initShapeOptionList( 262 context: Context, 263 adapter: OptionItemAdapter2<ShapeIconViewModel>, 264 ) { 265 apply { 266 this.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) 267 addItemDecoration( 268 SingleRowListItemSpacing( 269 edgeItemSpacePx = 270 context.resources.getDimensionPixelSize( 271 R.dimen.floating_sheet_content_horizontal_padding 272 ), 273 itemHorizontalSpacePx = 274 context.resources.getDimensionPixelSize( 275 R.dimen.floating_sheet_list_item_horizontal_space 276 ), 277 ) 278 ) 279 this.adapter = adapter 280 } 281 } 282 283 private fun createGridOptionItemAdapter( 284 colorUpdateViewModel: ColorUpdateViewModel, 285 shouldAnimateColor: () -> Boolean, 286 lifecycleOwner: LifecycleOwner, 287 backgroundDispatcher: CoroutineDispatcher, 288 ): OptionItemAdapter2<Drawable> = 289 OptionItemAdapter2( 290 layoutResourceId = R.layout.grid_option2, 291 lifecycleOwner = lifecycleOwner, 292 backgroundDispatcher = backgroundDispatcher, 293 bindPayload = { view: View, gridIcon: Drawable -> 294 val imageView = view.findViewById(R.id.foreground) as? ImageView 295 imageView?.setImageDrawable(gridIcon) 296 return@OptionItemAdapter2 null 297 }, 298 colorUpdateViewModel = WeakReference(colorUpdateViewModel), 299 shouldAnimateColor = shouldAnimateColor, 300 ) 301 302 private fun RecyclerView.initGridOptionList( 303 context: Context, 304 adapter: OptionItemAdapter2<Drawable>, 305 ) { 306 apply { 307 this.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) 308 addItemDecoration( 309 SingleRowListItemSpacing( 310 edgeItemSpacePx = 311 context.resources.getDimensionPixelSize( 312 R.dimen.floating_sheet_content_horizontal_padding 313 ), 314 itemHorizontalSpacePx = 315 context.resources.getDimensionPixelSize( 316 R.dimen.floating_sheet_grid_list_item_horizontal_space 317 ), 318 ) 319 ) 320 this.adapter = adapter 321 } 322 } 323 } 324