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