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 */ 17 18 package com.android.customization.picker.color.ui.binder 19 20 import android.content.res.Configuration 21 import android.os.Bundle 22 import android.os.Parcelable 23 import android.view.View 24 import android.widget.TextView 25 import androidx.lifecycle.Lifecycle 26 import androidx.lifecycle.LifecycleOwner 27 import androidx.lifecycle.lifecycleScope 28 import androidx.lifecycle.repeatOnLifecycle 29 import androidx.recyclerview.widget.LinearLayoutManager 30 import androidx.recyclerview.widget.RecyclerView 31 import com.android.customization.picker.color.ui.adapter.ColorTypeTabAdapter 32 import com.android.customization.picker.color.ui.view.ColorOptionIconView 33 import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel 34 import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel 35 import com.android.themepicker.R 36 import com.android.wallpaper.picker.common.ui.view.ItemSpacing 37 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter 38 import kotlinx.coroutines.flow.map 39 import kotlinx.coroutines.launch 40 41 object ColorPickerBinder { 42 43 /** 44 * Binds view with view-model for a color picker experience. The view should include a Recycler 45 * View for color type tabs with id [R.id.color_type_tabs] and a Recycler View for color options 46 * with id [R.id.color_options] 47 */ 48 @JvmStatic 49 fun bind( 50 view: View, 51 viewModel: ColorPickerViewModel, 52 lifecycleOwner: LifecycleOwner, 53 ): Binding { 54 val colorTypeTabView: RecyclerView = view.requireViewById(R.id.color_type_tabs) 55 val colorTypeTabSubheaderView: TextView = view.requireViewById(R.id.color_type_tab_subhead) 56 val colorOptionContainerView: RecyclerView = view.requireViewById(R.id.color_options) 57 58 val colorTypeTabAdapter = ColorTypeTabAdapter() 59 colorTypeTabView.adapter = colorTypeTabAdapter 60 colorTypeTabView.layoutManager = 61 LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false) 62 colorTypeTabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP)) 63 val colorOptionAdapter = 64 OptionItemAdapter( 65 layoutResourceId = R.layout.color_option, 66 lifecycleOwner = lifecycleOwner, 67 bindIcon = { foregroundView: View, colorIcon: ColorOptionIconViewModel -> 68 val colorOptionIconView = foregroundView as? ColorOptionIconView 69 val night = 70 (view.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == 71 Configuration.UI_MODE_NIGHT_YES) 72 colorOptionIconView?.let { ColorOptionIconBinder.bind(it, colorIcon, night) } 73 } 74 ) 75 colorOptionContainerView.adapter = colorOptionAdapter 76 colorOptionContainerView.layoutManager = 77 LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false) 78 colorOptionContainerView.addItemDecoration(ItemSpacing(ItemSpacing.ITEM_SPACING_DP)) 79 80 lifecycleOwner.lifecycleScope.launch { 81 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 82 launch { 83 viewModel.colorTypeTabs 84 .map { colorTypeById -> colorTypeById.values } 85 .collect { colorTypes -> colorTypeTabAdapter.setItems(colorTypes.toList()) } 86 } 87 88 launch { 89 viewModel.colorTypeTabSubheader.collect { subhead -> 90 colorTypeTabSubheaderView.text = subhead 91 } 92 } 93 94 launch { 95 viewModel.colorOptions.collect { colorOptions -> 96 // only set or restore instance state on a recycler view once data binding 97 // is complete to ensure scroll position is reflected correctly 98 colorOptionAdapter.setItems(colorOptions) { 99 // the same recycler view is used for different color types tabs 100 // the scroll state of each tab should be independent of others 101 if (layoutManagerSavedState != null) { 102 (colorOptionContainerView.layoutManager as LinearLayoutManager) 103 .onRestoreInstanceState(layoutManagerSavedState) 104 layoutManagerSavedState = null 105 } else { 106 var indexToFocus = colorOptions.indexOfFirst { it.isSelected.value } 107 indexToFocus = if (indexToFocus < 0) 0 else indexToFocus 108 (colorOptionContainerView.layoutManager as LinearLayoutManager) 109 .scrollToPositionWithOffset(indexToFocus, 0) 110 } 111 } 112 } 113 } 114 } 115 } 116 return object : Binding { 117 override fun saveInstanceState(savedState: Bundle) { 118 // as a workaround for the picker restarting twice after a config change, if the 119 // picker restarts before the saved state was applied and set to null, 120 // re-use the same saved state 121 savedState.putParcelable( 122 LAYOUT_MANAGER_SAVED_STATE, 123 layoutManagerSavedState 124 ?: colorOptionContainerView.layoutManager?.onSaveInstanceState() 125 ) 126 } 127 128 override fun restoreInstanceState(savedState: Bundle) { 129 layoutManagerSavedState = savedState.getParcelable(LAYOUT_MANAGER_SAVED_STATE) 130 } 131 } 132 } 133 134 interface Binding { 135 fun saveInstanceState(savedState: Bundle) 136 137 fun restoreInstanceState(savedState: Bundle) 138 } 139 140 private const val LAYOUT_MANAGER_SAVED_STATE: String = "layout_manager_state" 141 private var layoutManagerSavedState: Parcelable? = null 142 } 143