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.wallpaper.picker.customization.ui.binder 19 20 import android.view.LayoutInflater 21 import android.view.View 22 import android.view.ViewGroup 23 import android.widget.TextView 24 import androidx.annotation.DimenRes 25 import androidx.core.view.doOnLayout 26 import androidx.lifecycle.Lifecycle 27 import androidx.lifecycle.LifecycleOwner 28 import androidx.lifecycle.lifecycleScope 29 import androidx.lifecycle.repeatOnLifecycle 30 import com.android.wallpaper.R 31 import com.android.wallpaper.picker.common.text.ui.viewbinder.TextViewBinder 32 import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperQuickSwitchViewModel 33 import kotlinx.coroutines.launch 34 35 /** Binds between the view and view-model for the wallpaper quick switch section. */ 36 object WallpaperQuickSwitchSectionBinder { 37 fun bind( 38 view: View, 39 viewModel: WallpaperQuickSwitchViewModel, 40 lifecycleOwner: LifecycleOwner, 41 isThumbnailFadeAnimationEnabled: Boolean, 42 onNavigateToFullWallpaperSelector: () -> Unit, 43 ) { 44 val moreWallpapers: TextView = view.requireViewById(R.id.more_wallpapers) 45 moreWallpapers.setOnClickListener { onNavigateToFullWallpaperSelector() } 46 47 TextViewBinder.bind(moreWallpapers, viewModel.actionText) 48 49 val optionContainer: ViewGroup = view.requireViewById(R.id.options) 50 51 if (!viewModel.areRecentsAvailable) { 52 optionContainer.visibility = View.GONE 53 } else { 54 optionContainer.visibility = View.VISIBLE 55 // We have to wait for the container to be laid out before we can bind it because we 56 // need 57 // its size to calculate the sizes of the option items. 58 optionContainer.doOnLayout { 59 lifecycleOwner.lifecycleScope.launch { 60 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 61 launch { 62 bindOptions( 63 parent = optionContainer, 64 viewModel = viewModel, 65 lifecycleOwner = lifecycleOwner, 66 isThumbnailFadeAnimationEnabled = isThumbnailFadeAnimationEnabled, 67 ) 68 } 69 } 70 } 71 } 72 } 73 } 74 75 /** Binds the option items to the given parent. */ 76 private suspend fun bindOptions( 77 parent: ViewGroup, 78 viewModel: WallpaperQuickSwitchViewModel, 79 lifecycleOwner: LifecycleOwner, 80 isThumbnailFadeAnimationEnabled: Boolean, 81 ) { 82 viewModel.options.collect { options -> 83 // Remove all views from a previous update. 84 parent.removeAllViews() 85 86 // Calculate the sizes that views should have. 87 val (largeOptionWidth, smallOptionWidth) = calculateSizes(parent, options.size) 88 89 val titleMap = mutableMapOf<String, Int>() 90 91 // Create, add, and bind a view for each option. 92 options.forEachIndexed { index, option -> 93 val optionView = 94 createOptionView( 95 parent = parent, 96 ) 97 parent.addView(optionView) 98 WallpaperQuickSwitchOptionBinder.bind( 99 view = optionView, 100 viewModel = option, 101 lifecycleOwner = lifecycleOwner, 102 smallOptionWidthPx = smallOptionWidth, 103 largeOptionWidthPx = largeOptionWidth, 104 isThumbnailFadeAnimationEnabled = isThumbnailFadeAnimationEnabled, 105 position = index, 106 titleMap = titleMap, 107 ) 108 } 109 } 110 } 111 112 /** 113 * Returns a pair where the first value is the width that we should use for the large/selected 114 * option and the second value is the width for the small/non-selected options. 115 */ 116 private fun calculateSizes( 117 parent: View, 118 optionCount: Int, 119 ): Pair<Int, Int> { 120 // The large/selected option is a square. Its size should be equal to the height of its 121 // container (with padding removed). 122 val largeOptionWidth = parent.height - parent.paddingTop - parent.paddingBottom 123 // We'll use the total (non-padded) width of the container to figure out the widths of the 124 // small/non-selected options. 125 val optionContainerWidthWithoutPadding = 126 parent.width - parent.paddingStart - parent.paddingEnd 127 // First, we will need the total of the widths of all the spacings between the options. 128 val spacingWidth = parent.dimensionResource(R.dimen.spacing_8dp) 129 val totalMarginWidths = (optionCount - 1) * spacingWidth 130 131 val remainingSpaceForSmallOptions = 132 optionContainerWidthWithoutPadding - largeOptionWidth - totalMarginWidths 133 // One option is always large, the rest are small. 134 val numberOfSmallOptions = optionCount - 1 135 val smallOptionWidth = 136 if (numberOfSmallOptions != 0) { 137 (remainingSpaceForSmallOptions / numberOfSmallOptions).coerceAtMost( 138 parent.dimensionResource(R.dimen.wallpaper_quick_switch_max_option_width) 139 ) 140 } else { 141 0 142 } 143 144 return Pair(largeOptionWidth, smallOptionWidth) 145 } 146 147 /** Returns a new [View] for an option, without attaching it to the view-tree. */ 148 private fun createOptionView( 149 parent: ViewGroup, 150 ): View { 151 return LayoutInflater.from(parent.context) 152 .inflate( 153 R.layout.wallpaper_quick_switch_option, 154 parent, 155 false, 156 ) 157 } 158 159 /** Compose-inspired convenience alias for getting a dimension in pixels. */ 160 private fun View.dimensionResource( 161 @DimenRes res: Int, 162 ): Int { 163 return context.resources.getDimensionPixelSize(res) 164 } 165 } 166