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