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