• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.picker.customization.ui.view.adapter
18 
19 import android.content.res.ColorStateList
20 import android.graphics.BlendMode
21 import android.graphics.BlendModeColorFilter
22 import android.view.LayoutInflater
23 import android.view.View
24 import android.view.ViewGroup
25 import android.widget.ImageView
26 import android.widget.TextView
27 import androidx.lifecycle.Lifecycle
28 import androidx.lifecycle.LifecycleOwner
29 import androidx.lifecycle.LifecycleRegistry
30 import androidx.recyclerview.widget.DiffUtil
31 import androidx.recyclerview.widget.ListAdapter
32 import androidx.recyclerview.widget.RecyclerView
33 import com.android.wallpaper.R
34 import com.android.wallpaper.picker.common.icon.ui.viewbinder.IconViewBinder
35 import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
36 import com.android.wallpaper.picker.customization.ui.binder.ColorUpdateBinder
37 import com.android.wallpaper.picker.customization.ui.view.animator.TabItemAnimator.Companion.BACKGROUND_ALPHA_MAX
38 import com.android.wallpaper.picker.customization.ui.view.animator.TabItemAnimator.Companion.SELECT_ITEM
39 import com.android.wallpaper.picker.customization.ui.view.animator.TabItemAnimator.Companion.UNSELECT_ITEM
40 import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel
41 import com.android.wallpaper.picker.customization.ui.viewmodel.FloatingToolbarTabViewModel
42 import java.lang.ref.WeakReference
43 
44 /** List adapter for the floating toolbar of tabs. */
45 class FloatingToolbarTabAdapter(
46     private val colorUpdateViewModel: WeakReference<ColorUpdateViewModel>,
47     private val shouldAnimateColor: () -> Boolean,
48 ) :
49     ListAdapter<FloatingToolbarTabViewModel, FloatingToolbarTabAdapter.TabViewHolder>(
50         ProductDiffCallback()
51     ) {
52 
53     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder {
54         val view =
55             LayoutInflater.from(parent.context)
56                 .inflate(R.layout.floating_toolbar_tab, parent, false)
57         val tabViewHolder = TabViewHolder(view)
58         return tabViewHolder
59     }
60 
61     override fun onBindViewHolder(
62         holder: TabViewHolder,
63         position: Int,
64         payloads: MutableList<Any>,
65     ) {
66         val payload = if (payloads.isNotEmpty()) payloads[0] as? Int else null
67         val item = getItem(position)
68         when (payload) {
69             SELECT_ITEM -> {
70                 // When transition from unselected to selected, initial state should be unselected
71                 bindViewHolder(holder, item.icon, item.text, false, item.onClick)
72             }
73             UNSELECT_ITEM -> {
74                 // When transition from selected to unselected, initial state should be selected
75                 bindViewHolder(holder, item.icon, item.text, true, item.onClick)
76             }
77             else -> super.onBindViewHolder(holder, position, payloads)
78         }
79     }
80 
81     override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
82         // Bind tab color in onBindViewHolder and destroy in onViewRecycled. Bind in this
83         // onBindViewHolder instead of the one with payload since this function is generally
84         // called when view holders are created or recycled, ensuring each view holder is only
85         // bound once, whereas the view holder with payload is called not only in the above cases,
86         // but also when the state is changed, which could result in multiple bindings.
87         colorUpdateViewModel.get()?.let {
88             ColorUpdateBinder.bind(
89                 setColor = { color ->
90                     holder.container.background.colorFilter =
91                         BlendModeColorFilter(color, BlendMode.SRC_ATOP)
92                 },
93                 color = it.colorSecondaryContainer,
94                 shouldAnimate = shouldAnimateColor,
95                 lifecycleOwner = holder,
96             )
97             ColorUpdateBinder.bind(
98                 setColor = { color ->
99                     holder.icon.imageTintList = ColorStateList.valueOf(color)
100                     holder.label.setTextColor(color)
101                 },
102                 color = it.colorOnSecondaryContainer,
103                 shouldAnimate = shouldAnimateColor,
104                 lifecycleOwner = holder,
105             )
106         }
107 
108         val item = getItem(position)
109         bindViewHolder(holder, item.icon, item.text, item.isSelected, item.onClick)
110     }
111 
112     private fun bindViewHolder(
113         holder: TabViewHolder,
114         icon: Icon,
115         text: String,
116         isSelected: Boolean,
117         onClick: (() -> Unit)?,
118     ) {
119         IconViewBinder.bind(holder.icon, icon)
120         holder.label.text = text
121         val iconSize =
122             holder.itemView.resources.getDimensionPixelSize(
123                 R.dimen.floating_tab_toolbar_tab_icon_size
124             )
125         holder.icon.layoutParams =
126             holder.icon.layoutParams.apply { width = if (isSelected) iconSize else 0 }
127         holder.container.background.alpha = if (isSelected) BACKGROUND_ALPHA_MAX else 0
128         holder.itemView.setOnClickListener { onClick?.invoke() }
129     }
130 
131     override fun onViewAttachedToWindow(holder: TabViewHolder) {
132         super.onViewAttachedToWindow(holder)
133         holder.onAttachToWindow()
134     }
135 
136     override fun onViewDetachedFromWindow(holder: TabViewHolder) {
137         super.onViewDetachedFromWindow(holder)
138         holder.onDetachFromWindow()
139     }
140 
141     override fun onViewRecycled(holder: TabViewHolder) {
142         super.onViewRecycled(holder)
143         holder.onRecycled()
144     }
145 
146     /**
147      * A [RecyclerView.ViewHolder] for the floating tabs recycler view, that also extends
148      * [LifecycleOwner] to enable binding flows and collecting based on lifecycle states. This
149      * optimizes the binding so that view holders that are not visible on screen will not be
150      * actively collecting and updating from a bound flow. The lifecycle state is created when the
151      * ViewHolder is created, then started and stopped in onViewAttachedToWindow and
152      * onViewDetachedFromWindow, and destroyed in onViewRecycled, where a new lifecycle is created.
153      */
154     class TabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), LifecycleOwner {
155         val container = itemView.requireViewById<ViewGroup>(R.id.tab_container)
156         val icon = itemView.requireViewById<ImageView>(R.id.tab_icon)
157         val label = itemView.requireViewById<TextView>(R.id.label_text)
158 
159         private lateinit var lifecycleRegistry: LifecycleRegistry
160         override val lifecycle: Lifecycle
161             get() = lifecycleRegistry
162 
163         init {
164             initializeRegistry()
165         }
166 
167         private fun initializeRegistry() {
168             lifecycleRegistry =
169                 LifecycleRegistry(this).also { it.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) }
170         }
171 
172         fun onAttachToWindow() {
173             lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
174         }
175 
176         fun onDetachFromWindow() {
177             lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
178         }
179 
180         fun onRecycled() {
181             lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
182             initializeRegistry()
183         }
184     }
185 
186     private class ProductDiffCallback : DiffUtil.ItemCallback<FloatingToolbarTabViewModel>() {
187 
188         override fun areItemsTheSame(
189             oldItem: FloatingToolbarTabViewModel,
190             newItem: FloatingToolbarTabViewModel,
191         ): Boolean {
192             return oldItem.text == newItem.text
193         }
194 
195         override fun areContentsTheSame(
196             oldItem: FloatingToolbarTabViewModel,
197             newItem: FloatingToolbarTabViewModel,
198         ): Boolean {
199             return oldItem.text == newItem.text &&
200                 oldItem.isSelected == newItem.isSelected &&
201                 oldItem.icon == newItem.icon
202         }
203 
204         override fun getChangePayload(
205             oldItem: FloatingToolbarTabViewModel,
206             newItem: FloatingToolbarTabViewModel,
207         ): Any? {
208             return when {
209                 !oldItem.isSelected && newItem.isSelected -> SELECT_ITEM
210                 oldItem.isSelected && !newItem.isSelected -> UNSELECT_ITEM
211                 else -> null
212             }
213         }
214     }
215 }
216