diff options
Diffstat (limited to 'src/com/android/customization/picker/color')
12 files changed, 478 insertions, 367 deletions
diff --git a/src/com/android/customization/picker/color/ColorPickerFragment.kt b/src/com/android/customization/picker/color/ColorPickerFragment.kt deleted file mode 100644 index c8ecb7f9..00000000 --- a/src/com/android/customization/picker/color/ColorPickerFragment.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.customization.picker.color - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.android.wallpaper.R -import com.android.wallpaper.picker.AppbarFragment - -// TODO (b/262924623): Color Picker Fragment -class ColorPickerFragment : AppbarFragment() { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val view = - inflater.inflate( - R.layout.fragment_color_picker, - container, - false, - ) - setUpToolbar(view) - return view - } -} diff --git a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt index 512a5007..41ef3a57 100644 --- a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt +++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt @@ -16,16 +16,15 @@ */ package com.android.customization.picker.color.data.repository -import android.app.WallpaperColors import android.util.Log import com.android.customization.model.CustomizationManager -import com.android.customization.model.color.ColorBundle import com.android.customization.model.color.ColorCustomizationManager import com.android.customization.model.color.ColorOption -import com.android.customization.model.color.ColorSeedOption +import com.android.customization.model.color.ColorOptionImpl import com.android.customization.picker.color.shared.model.ColorOptionModel import com.android.customization.picker.color.shared.model.ColorType import com.android.systemui.monet.Style +import com.android.wallpaper.model.WallpaperColorsModel import com.android.wallpaper.model.WallpaperColorsViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -40,9 +39,9 @@ class ColorPickerRepositoryImpl( private val colorManager: ColorCustomizationManager, ) : ColorPickerRepository { - private val homeWallpaperColors: StateFlow<WallpaperColors?> = + private val homeWallpaperColors: StateFlow<WallpaperColorsModel?> = wallpaperColorsViewModel.homeWallpaperColors - private val lockWallpaperColors: StateFlow<WallpaperColors?> = + private val lockWallpaperColors: StateFlow<WallpaperColorsModel?> = wallpaperColorsViewModel.lockWallpaperColors override val colorOptions: Flow<Map<ColorType, List<ColorOptionModel>>> = @@ -51,8 +50,27 @@ class ColorPickerRepositoryImpl( } .map { (homeColors, lockColors) -> suspendCancellableCoroutine { continuation -> - colorManager.setWallpaperColors(homeColors, lockColors) - colorManager.fetchOptions( + if ( + homeColors is WallpaperColorsModel.Loading || + lockColors is WallpaperColorsModel.Loading + ) { + continuation.resumeWith( + Result.success( + mapOf( + ColorType.WALLPAPER_COLOR to listOf(), + ColorType.PRESET_COLOR to listOf() + ) + ) + ) + return@suspendCancellableCoroutine + } + val homeColorsLoaded = homeColors as WallpaperColorsModel.Loaded + val lockColorsLoaded = lockColors as WallpaperColorsModel.Loaded + colorManager.setWallpaperColors( + homeColorsLoaded.colors, + lockColorsLoaded.colors + ) + colorManager.fetchRevampedUIOptions( object : CustomizationManager.OptionsFetchedListener<ColorOption?> { override fun onOptionsLoaded(options: MutableList<ColorOption?>?) { val wallpaperColorOptions: MutableList<ColorOptionModel> = @@ -60,10 +78,11 @@ class ColorPickerRepositoryImpl( val presetColorOptions: MutableList<ColorOptionModel> = mutableListOf() options?.forEach { option -> - when (option) { - is ColorSeedOption -> + when ((option as ColorOptionImpl).type) { + ColorType.WALLPAPER_COLOR -> wallpaperColorOptions.add(option.toModel()) - is ColorBundle -> presetColorOptions.add(option.toModel()) + ColorType.PRESET_COLOR -> + presetColorOptions.add(option.toModel()) } } continuation.resumeWith( @@ -113,16 +132,16 @@ class ColorPickerRepositoryImpl( val overlays = colorManager.currentOverlays val styleOrNull = colorManager.currentStyle val style = styleOrNull?.let { Style.valueOf(it) } ?: Style.TONAL_SPOT - val colorOptionBuilder = - // Does not matter whether ColorSeedOption or ColorBundle builder is used here - // because to apply the color, one just needs a generic ColorOption - ColorSeedOption.Builder().setSource(colorManager.currentColorSource).setStyle(style) + val source = colorManager.currentColorSource + val colorOptionBuilder = ColorOptionImpl.Builder() + colorOptionBuilder.source = source + colorOptionBuilder.style = style for (overlay in overlays) { colorOptionBuilder.addOverlayPackage(overlay.key, overlay.value) } val colorOption = colorOptionBuilder.build() return ColorOptionModel( - key = "${colorOption.style}::${colorOption.serializedPackages}", + key = "", colorOption = colorOption, isSelected = false, ) @@ -132,9 +151,9 @@ class ColorPickerRepositoryImpl( return colorManager.currentColorSource } - private fun ColorOption.toModel(): ColorOptionModel { + private fun ColorOptionImpl.toModel(): ColorOptionModel { return ColorOptionModel( - key = "${this.style}::${this.serializedPackages}", + key = "${this.type}::${this.style}::${this.serializedPackages}", colorOption = this, isSelected = isActive(colorManager), ) diff --git a/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt index edbf6dcf..714129df 100644 --- a/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt +++ b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt @@ -19,9 +19,8 @@ package com.android.customization.picker.color.data.repository import android.content.Context import android.graphics.Color import android.text.TextUtils -import com.android.customization.model.color.ColorBundle +import com.android.customization.model.color.ColorOptionImpl import com.android.customization.model.color.ColorOptionsProvider -import com.android.customization.model.color.ColorSeedOption import com.android.customization.picker.color.shared.model.ColorOptionModel import com.android.customization.picker.color.shared.model.ColorType import kotlinx.coroutines.flow.MutableStateFlow @@ -82,9 +81,7 @@ class FakeColorPickerRepository(private val context: Context) : ColorPickerRepos ColorOptionModel( key = "${ColorType.PRESET_COLOR}::$index", colorOption = buildPresetOption(index), - isSelected = - selectedColorOptionType == ColorType.PRESET_COLOR && - selectedColorOptionIndex == index, + isSelected = isSelected, ) if (isSelected) { selectedColorOption = colorOption @@ -95,36 +92,36 @@ class FakeColorPickerRepository(private val context: Context) : ColorPickerRepos ) } - private fun buildPresetOption(index: Int): ColorBundle { - return ColorBundle.Builder() + private fun buildPresetOption(index: Int): ColorOptionImpl { + val builder = ColorOptionImpl.Builder() + builder.lightColors = + intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT) + builder.darkColors = + intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT) + builder.index = index + builder.type = ColorType.PRESET_COLOR + builder.source = ColorOptionsProvider.COLOR_SOURCE_PRESET + builder.title = "Preset" + builder .addOverlayPackage("TEST_PACKAGE_TYPE", "preset_color") .addOverlayPackage("TEST_PACKAGE_INDEX", "$index") - .setIndex(index) - .build(context) + return builder.build() } - private fun buildWallpaperOption(index: Int): ColorSeedOption { - return ColorSeedOption.Builder() - .setLightColors( - intArrayOf( - Color.TRANSPARENT, - Color.TRANSPARENT, - Color.TRANSPARENT, - Color.TRANSPARENT - ) - ) - .setDarkColors( - intArrayOf( - Color.TRANSPARENT, - Color.TRANSPARENT, - Color.TRANSPARENT, - Color.TRANSPARENT - ) - ) + private fun buildWallpaperOption(index: Int): ColorOptionImpl { + val builder = ColorOptionImpl.Builder() + builder.lightColors = + intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT) + builder.darkColors = + intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT) + builder.index = index + builder.type = ColorType.WALLPAPER_COLOR + builder.source = ColorOptionsProvider.COLOR_SOURCE_HOME + builder.title = "Dynamic" + builder .addOverlayPackage("TEST_PACKAGE_TYPE", "wallpaper_color") .addOverlayPackage("TEST_PACKAGE_INDEX", "$index") - .setIndex(index) - .build() + return builder.build() } override suspend fun select(colorOptionModel: ColorOptionModel) { @@ -163,9 +160,9 @@ class FakeColorPickerRepository(private val context: Context) : ColorPickerRepos override fun getCurrentColorOption(): ColorOptionModel = selectedColorOption override fun getCurrentColorSource(): String? = - when (selectedColorOption.colorOption) { - is ColorSeedOption -> ColorOptionsProvider.COLOR_SOURCE_HOME - is ColorBundle -> ColorOptionsProvider.COLOR_SOURCE_PRESET + when ((selectedColorOption.colorOption as ColorOptionImpl).type) { + ColorType.WALLPAPER_COLOR -> ColorOptionsProvider.COLOR_SOURCE_HOME + ColorType.PRESET_COLOR -> ColorOptionsProvider.COLOR_SOURCE_PRESET else -> null } diff --git a/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt b/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt deleted file mode 100644 index 7aa390df..00000000 --- a/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.customization.picker.color.ui.adapter - -import android.graphics.BlendMode -import android.graphics.BlendModeColorFilter -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel -import com.android.wallpaper.R - -/** - * Adapts between color option items and views. - * - * TODO (b/272109171): Remove after clock settings is refactored to use OptionItemAdapter - */ -class ColorOptionAdapter : RecyclerView.Adapter<ColorOptionAdapter.ViewHolder>() { - - private val items = mutableListOf<ColorOptionViewModel>() - private var isTitleVisible = false - - fun setItems(items: List<ColorOptionViewModel>) { - this.items.clear() - this.items.addAll(items) - isTitleVisible = items.any { item -> item.title != null } - notifyDataSetChanged() - } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val borderView: View = itemView.requireViewById(R.id.selection_border) - val backgroundView: View = itemView.requireViewById(R.id.background) - val color0View: ImageView = itemView.requireViewById(R.id.color_preview_0) - val color1View: ImageView = itemView.requireViewById(R.id.color_preview_1) - val color2View: ImageView = itemView.requireViewById(R.id.color_preview_2) - val color3View: ImageView = itemView.requireViewById(R.id.color_preview_3) - val optionTitleView: TextView = itemView.requireViewById(R.id.option_title) - } - - override fun getItemCount(): Int { - return items.size - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder( - LayoutInflater.from(parent.context) - .inflate( - R.layout.color_option_with_background, - parent, - false, - ) - ) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val item = items[position] - - holder.itemView.setOnClickListener( - if (item.onClick != null) { - View.OnClickListener { item.onClick.invoke() } - } else { - null - } - ) - if (item.isSelected) { - holder.borderView.alpha = 1f - holder.borderView.scaleX = 1f - holder.borderView.scaleY = 1f - holder.backgroundView.scaleX = 0.86f - holder.backgroundView.scaleY = 0.86f - } else { - holder.borderView.alpha = 0f - holder.backgroundView.scaleX = 1f - holder.backgroundView.scaleY = 1f - } - holder.color0View.drawable.colorFilter = BlendModeColorFilter(item.color0, BlendMode.SRC) - holder.color1View.drawable.colorFilter = BlendModeColorFilter(item.color1, BlendMode.SRC) - holder.color2View.drawable.colorFilter = BlendModeColorFilter(item.color2, BlendMode.SRC) - holder.color3View.drawable.colorFilter = BlendModeColorFilter(item.color3, BlendMode.SRC) - holder.itemView.contentDescription = item.contentDescription - holder.optionTitleView.isVisible = isTitleVisible - holder.optionTitleView.text = item.title - } -} diff --git a/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt b/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt index bb9f0823..553f5869 100644 --- a/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt +++ b/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt @@ -55,7 +55,7 @@ class ColorTypeTabAdapter : RecyclerView.Adapter<ColorTypeTabAdapter.ViewHolder> val item = items[position] holder.itemView.isSelected = item.isSelected holder.textView.text = item.name - holder.textView.setOnClickListener( + holder.itemView.setOnClickListener( if (item.onClick != null) { View.OnClickListener { item.onClick.invoke() } } else { diff --git a/src/com/android/customization/picker/color/ui/binder/ColorOptionIconBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorOptionIconBinder.kt index 1478cc40..31d3dc81 100644 --- a/src/com/android/customization/picker/color/ui/binder/ColorOptionIconBinder.kt +++ b/src/com/android/customization/picker/color/ui/binder/ColorOptionIconBinder.kt @@ -17,25 +17,29 @@ package com.android.customization.picker.color.ui.binder -import android.graphics.BlendMode -import android.graphics.BlendModeColorFilter -import android.view.ViewGroup -import android.widget.ImageView +import com.android.customization.picker.color.ui.view.ColorOptionIconView import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel -import com.android.wallpaper.R object ColorOptionIconBinder { fun bind( - view: ViewGroup, + view: ColorOptionIconView, viewModel: ColorOptionIconViewModel, + darkTheme: Boolean, ) { - val color0View: ImageView = view.requireViewById(R.id.color_preview_0) - val color1View: ImageView = view.requireViewById(R.id.color_preview_1) - val color2View: ImageView = view.requireViewById(R.id.color_preview_2) - val color3View: ImageView = view.requireViewById(R.id.color_preview_3) - color0View.drawable.colorFilter = BlendModeColorFilter(viewModel.color0, BlendMode.SRC) - color1View.drawable.colorFilter = BlendModeColorFilter(viewModel.color1, BlendMode.SRC) - color2View.drawable.colorFilter = BlendModeColorFilter(viewModel.color2, BlendMode.SRC) - color3View.drawable.colorFilter = BlendModeColorFilter(viewModel.color3, BlendMode.SRC) + if (darkTheme) { + view.bindColor( + viewModel.darkThemeColor0, + viewModel.darkThemeColor1, + viewModel.darkThemeColor2, + viewModel.darkThemeColor3, + ) + } else { + view.bindColor( + viewModel.lightThemeColor0, + viewModel.lightThemeColor1, + viewModel.lightThemeColor2, + viewModel.lightThemeColor3, + ) + } } } diff --git a/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt index 7623048f..cd9dd540 100644 --- a/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt +++ b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt @@ -17,8 +17,10 @@ package com.android.customization.picker.color.ui.binder +import android.content.res.Configuration +import android.os.Bundle +import android.os.Parcelable import android.view.View -import android.view.ViewGroup import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -27,12 +29,12 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.customization.picker.color.ui.adapter.ColorTypeTabAdapter +import com.android.customization.picker.color.ui.view.ColorOptionIconView import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel import com.android.customization.picker.common.ui.view.ItemSpacing import com.android.wallpaper.R import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -48,7 +50,7 @@ object ColorPickerBinder { view: View, viewModel: ColorPickerViewModel, lifecycleOwner: LifecycleOwner, - ) { + ): Binding { val colorTypeTabView: RecyclerView = view.requireViewById(R.id.color_type_tabs) val colorTypeTabSubheaderView: TextView = view.requireViewById(R.id.color_type_tab_subhead) val colorOptionContainerView: RecyclerView = view.requireViewById(R.id.color_options) @@ -63,8 +65,11 @@ object ColorPickerBinder { layoutResourceId = R.layout.color_option_2, lifecycleOwner = lifecycleOwner, bindIcon = { foregroundView: View, colorIcon: ColorOptionIconViewModel -> - val viewGroup = foregroundView as? ViewGroup - viewGroup?.let { ColorOptionIconBinder.bind(viewGroup, colorIcon) } + val colorOptionIconView = foregroundView as? ColorOptionIconView + val night = + (view.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES) + colorOptionIconView?.let { ColorOptionIconBinder.bind(it, colorIcon, night) } } ) colorOptionContainerView.adapter = colorOptionAdapter @@ -89,9 +94,52 @@ object ColorPickerBinder { launch { viewModel.colorOptions.collect { colorOptions -> colorOptionAdapter.setItems(colorOptions) + // the same recycler view is used for different color types tabs + // the scroll state of each tab should be independent of others + if (layoutManagerSavedState != null) { + colorOptionContainerView.post { + (colorOptionContainerView.layoutManager as LinearLayoutManager) + .onRestoreInstanceState(layoutManagerSavedState) + layoutManagerSavedState = null + } + } else { + var indexToFocus = colorOptions.indexOfFirst { it.isSelected.value } + indexToFocus = if (indexToFocus < 0) 0 else indexToFocus + val linearLayoutManager = + object : LinearLayoutManager(view.context, HORIZONTAL, false) { + override fun onLayoutCompleted(state: RecyclerView.State?) { + super.onLayoutCompleted(state) + // scrollToPosition seems to be inconsistently moving + // selected + // color to different positions + scrollToPositionWithOffset(indexToFocus, 0) + } + } + colorOptionContainerView.layoutManager = linearLayoutManager + } } } } } + return object : Binding { + override fun saveInstanceState(savedState: Bundle) { + savedState.putParcelable( + LAYOUT_MANAGER_SAVED_STATE, + colorOptionContainerView.layoutManager?.onSaveInstanceState() + ) + } + + override fun restoreInstanceState(savedState: Bundle) { + layoutManagerSavedState = savedState.getParcelable(LAYOUT_MANAGER_SAVED_STATE) + } + } } + + interface Binding { + fun saveInstanceState(savedState: Bundle) + fun restoreInstanceState(savedState: Bundle) + } + + private val LAYOUT_MANAGER_SAVED_STATE: String = "layout_manager_state" + private var layoutManagerSavedState: Parcelable? = null } diff --git a/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt index 9e232d70..2daefe47 100644 --- a/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt +++ b/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt @@ -17,9 +17,9 @@ package com.android.customization.picker.color.ui.binder +import android.content.res.Configuration import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout import androidx.core.view.isVisible @@ -30,6 +30,7 @@ import androidx.lifecycle.repeatOnLifecycle import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel import com.android.wallpaper.R +import com.android.wallpaper.picker.common.icon.ui.viewbinder.ContentDescriptionViewBinder import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel import kotlinx.coroutines.launch @@ -91,10 +92,23 @@ object ColorSectionViewBinder { }) .let { if (it < 0) 0 else it } options.subList(0, colorOptionSlotSize).forEach { item -> + val night = + (view.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES) val itemView = LayoutInflater.from(view.context) .inflate(R.layout.color_option_no_background, view, false) - item.payload?.let { ColorOptionIconBinder.bind(itemView as ViewGroup, item.payload) } + item.payload?.let { + ColorOptionIconBinder.bind( + itemView.requireViewById(R.id.option_tile), + item.payload, + night + ) + ContentDescriptionViewBinder.bind( + view = itemView.requireViewById(R.id.option_tile), + viewModel = item.text, + ) + } val optionSelectedView = itemView.requireViewById<ImageView>(R.id.option_selected) lifecycleOwner.lifecycleScope.launch { diff --git a/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt b/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt index c6b2023e..78bfa43e 100644 --- a/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt +++ b/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt @@ -15,6 +15,7 @@ */ package com.android.customization.picker.color.ui.fragment +import android.app.WallpaperManager import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -23,21 +24,30 @@ import android.widget.FrameLayout import androidx.cardview.widget.CardView import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get +import androidx.lifecycle.lifecycleScope import com.android.customization.model.mode.DarkModeSectionController import com.android.customization.module.ThemePickerInjector import com.android.customization.picker.color.ui.binder.ColorPickerBinder import com.android.wallpaper.R +import com.android.wallpaper.model.WallpaperColorsModel +import com.android.wallpaper.model.WallpaperColorsViewModel +import com.android.wallpaper.module.CustomizationSections import com.android.wallpaper.module.InjectorProvider import com.android.wallpaper.picker.AppbarFragment import com.android.wallpaper.picker.customization.ui.binder.ScreenPreviewBinder import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel import com.android.wallpaper.util.DisplayUtils import com.android.wallpaper.util.PreviewUtils +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext @OptIn(ExperimentalCoroutinesApi::class) class ColorPickerFragment : AppbarFragment() { + private var binding: ColorPickerBinder.Binding? = null + companion object { @JvmStatic fun newInstance(): ColorPickerFragment { @@ -63,19 +73,25 @@ class ColorPickerFragment : AppbarFragment() { val wallpaperInfoFactory = injector.getCurrentWallpaperInfoFactory(requireContext()) val displayUtils: DisplayUtils = injector.getDisplayUtils(requireContext()) val wcViewModel = injector.getWallpaperColorsViewModel() - ColorPickerBinder.bind( - view = view, - viewModel = - ViewModelProvider( - requireActivity(), - injector.getColorPickerViewModelFactory( - context = requireContext(), - wallpaperColorsViewModel = wcViewModel, - ), - ) - .get(), - lifecycleOwner = this, - ) + val wallpaperManager = WallpaperManager.getInstance(requireContext()) + + binding = + ColorPickerBinder.bind( + view = view, + viewModel = + ViewModelProvider( + requireActivity(), + injector.getColorPickerViewModelFactory( + context = requireContext(), + wallpaperColorsViewModel = wcViewModel, + ), + ) + .get(), + lifecycleOwner = this, + ) + + savedInstanceState?.let { binding?.restoreInstanceState(it) } + ScreenPreviewBinder.bind( activity = requireActivity(), previewView = lockScreenView, @@ -90,23 +106,38 @@ class ColorPickerFragment : AppbarFragment() { R.string.lock_screen_preview_provider_authority, ), ), - wallpaperInfoProvider = { + wallpaperInfoProvider = { forceReload -> suspendCancellableCoroutine { continuation -> wallpaperInfoFactory.createCurrentWallpaperInfos( { homeWallpaper, lockWallpaper, _ -> + lifecycleScope.launch { + if ( + wcViewModel.lockWallpaperColors.value + is WallpaperColorsModel.Loading + ) { + loadInitialColors( + wallpaperManager, + wcViewModel, + CustomizationSections.Screen.LOCK_SCREEN + ) + } + } continuation.resume(lockWallpaper ?: homeWallpaper, null) }, - /* forceRefresh= */ true, + forceReload, ) } }, onWallpaperColorChanged = { colors -> wcViewModel.setLockWallpaperColors(colors) }, + wallpaperInteractor = injector.getWallpaperInteractor(requireContext()), + screen = CustomizationSections.Screen.LOCK_SCREEN, ), lifecycleOwner = this, offsetToStart = displayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(requireActivity()), + onWallpaperPreviewDirty = { activity?.recreate() }, ) ScreenPreviewBinder.bind( activity = requireActivity(), @@ -122,23 +153,38 @@ class ColorPickerFragment : AppbarFragment() { R.string.grid_control_metadata_name, ), ), - wallpaperInfoProvider = { + wallpaperInfoProvider = { forceReload -> suspendCancellableCoroutine { continuation -> wallpaperInfoFactory.createCurrentWallpaperInfos( { homeWallpaper, lockWallpaper, _ -> + lifecycleScope.launch { + if ( + wcViewModel.homeWallpaperColors.value + is WallpaperColorsModel.Loading + ) { + loadInitialColors( + wallpaperManager, + wcViewModel, + CustomizationSections.Screen.HOME_SCREEN + ) + } + } continuation.resume(homeWallpaper ?: lockWallpaper, null) }, - /* forceRefresh= */ true, + forceReload, ) } }, onWallpaperColorChanged = { colors -> - wcViewModel.setLockWallpaperColors(colors) + wcViewModel.setHomeWallpaperColors(colors) }, + wallpaperInteractor = injector.getWallpaperInteractor(requireContext()), + screen = CustomizationSections.Screen.HOME_SCREEN, ), lifecycleOwner = this, offsetToStart = displayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(requireActivity()), + onWallpaperPreviewDirty = { activity?.recreate() }, ) val darkModeToggleContainerView: FrameLayout = view.requireViewById(R.id.dark_mode_toggle_container) @@ -154,6 +200,35 @@ class ColorPickerFragment : AppbarFragment() { return view } + private suspend fun loadInitialColors( + wallpaperManager: WallpaperManager, + colorViewModel: WallpaperColorsViewModel, + screen: CustomizationSections.Screen, + ) { + withContext(Dispatchers.IO) { + val colors = + wallpaperManager.getWallpaperColors( + if (screen == CustomizationSections.Screen.LOCK_SCREEN) { + WallpaperManager.FLAG_LOCK + } else { + WallpaperManager.FLAG_SYSTEM + } + ) + withContext(Dispatchers.Main) { + if (screen == CustomizationSections.Screen.LOCK_SCREEN) { + colorViewModel.setLockWallpaperColors(colors) + } else { + colorViewModel.setHomeWallpaperColors(colors) + } + } + } + } + + override fun onSaveInstanceState(savedInstanceState: Bundle) { + super.onSaveInstanceState(savedInstanceState) + binding?.saveInstanceState(savedInstanceState) + } + override fun getDefaultTitle(): CharSequence { return requireContext().getString(R.string.color_picker_title) } diff --git a/src/com/android/customization/picker/color/ui/view/ColorOptionIconView.kt b/src/com/android/customization/picker/color/ui/view/ColorOptionIconView.kt new file mode 100644 index 00000000..257bffb2 --- /dev/null +++ b/src/com/android/customization/picker/color/ui/view/ColorOptionIconView.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.customization.picker.color.ui.view + +import android.annotation.ColorInt +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View + +/** + * Draw a color option icon, which is a quadrant circle that can show at most 4 different colors. + */ +class ColorOptionIconView( + context: Context, + attrs: AttributeSet, +) : View(context, attrs) { + + private val paint = Paint().apply { style = Paint.Style.FILL } + + private val oval = RectF() + + private var color0 = DEFAULT_PLACEHOLDER_COLOR + private var color1 = DEFAULT_PLACEHOLDER_COLOR + private var color2 = DEFAULT_PLACEHOLDER_COLOR + private var color3 = DEFAULT_PLACEHOLDER_COLOR + + private var w = 0 + private var h = 0 + + /** + * @param color0 the color in the top left quadrant + * @param color1 the color in the top right quadrant + * @param color2 the color in the bottom left quadrant + * @param color3 the color in the bottom right quadrant + */ + fun bindColor( + @ColorInt color0: Int, + @ColorInt color1: Int, + @ColorInt color2: Int, + @ColorInt color3: Int, + ) { + this.color0 = color0 + this.color1 = color1 + this.color2 = color2 + this.color3 = color3 + invalidate() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + this.w = w + this.h = h + super.onSizeChanged(w, h, oldw, oldh) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + // The w and h need to be an even number to avoid tiny pixel-level gaps between the pies + w = w.roundDownToEven() + h = h.roundDownToEven() + + val width = w.toFloat() + val height = h.toFloat() + + oval.set(0f, 0f, width, height) + canvas.apply { + paint.color = color3 + drawArc( + oval, + 0f, + 90f, + true, + paint, + ) + paint.color = color2 + drawArc( + oval, + 90f, + 90f, + true, + paint, + ) + paint.color = color0 + drawArc( + oval, + 180f, + 90f, + true, + paint, + ) + paint.color = color1 + drawArc( + oval, + 270f, + 90f, + true, + paint, + ) + } + } + + companion object { + const val DEFAULT_PLACEHOLDER_COLOR = Color.BLACK + + fun Int.roundDownToEven(): Int { + return if (this % 2 == 0) this else this - 1 + } + } +} diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionIconViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionIconViewModel.kt index d32538d8..8723c8c1 100644 --- a/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionIconViewModel.kt +++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionIconViewModel.kt @@ -20,8 +20,12 @@ package com.android.customization.picker.color.ui.viewmodel import android.annotation.ColorInt data class ColorOptionIconViewModel( - @ColorInt val color0: Int, - @ColorInt val color1: Int, - @ColorInt val color2: Int, - @ColorInt val color3: Int, + @ColorInt val lightThemeColor0: Int, + @ColorInt val lightThemeColor1: Int, + @ColorInt val lightThemeColor2: Int, + @ColorInt val lightThemeColor3: Int, + @ColorInt val darkThemeColor0: Int, + @ColorInt val darkThemeColor1: Int, + @ColorInt val darkThemeColor2: Int, + @ColorInt val darkThemeColor3: Int, ) diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt index 81a58107..67c68387 100644 --- a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt +++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt @@ -20,8 +20,7 @@ import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.android.customization.model.color.ColorBundle -import com.android.customization.model.color.ColorSeedOption +import com.android.customization.model.color.ColorOptionImpl import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor import com.android.customization.picker.color.shared.model.ColorType import com.android.wallpaper.R @@ -31,9 +30,11 @@ import kotlin.math.max import kotlin.math.min import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -92,146 +93,114 @@ private constructor( /** The list of all color options mapped by their color type */ private val allColorOptions: Flow<Map<ColorType, List<OptionItemViewModel<ColorOptionIconViewModel>>>> = - interactor.colorOptions.map { colorOptions -> - colorOptions - .map { colorOptionEntry -> - colorOptionEntry.key to - when (colorOptionEntry.key) { - ColorType.WALLPAPER_COLOR -> { - colorOptionEntry.value.map { colorOptionModel -> - val colorSeedOption: ColorSeedOption = - colorOptionModel.colorOption as ColorSeedOption - val colors = - colorSeedOption.previewInfo.resolveColors(context.resources) - val isSelectedFlow: StateFlow<Boolean> = - interactor.activeColorOption - .map { - it?.colorOption?.isEquivalent( - colorOptionModel.colorOption - ) - ?: colorOptionModel.isSelected - } - .stateIn(viewModelScope) - OptionItemViewModel<ColorOptionIconViewModel>( - key = - MutableStateFlow(colorOptionModel.key) - as StateFlow<String>, - payload = - ColorOptionIconViewModel( - colors[0], - colors[1], - colors[2], - colors[3] - ), - text = - Text.Loaded( - colorSeedOption - .getContentDescription(context) - .toString() - ), - isSelected = isSelectedFlow, - onClicked = - isSelectedFlow.map { isSelected -> - if (isSelected) { - null - } else { - { - viewModelScope.launch { - interactor.select(colorOptionModel) - } + interactor.colorOptions + .map { colorOptions -> + colorOptions + .map { colorOptionEntry -> + colorOptionEntry.key to + colorOptionEntry.value.map { colorOptionModel -> + val colorOption: ColorOptionImpl = + colorOptionModel.colorOption as ColorOptionImpl + val lightThemeColors = + colorOption.previewInfo.resolveColors(/* darkTheme= */ false) + val darkThemeColors = + colorOption.previewInfo.resolveColors(/* darkTheme= */ true) + val isSelectedFlow: StateFlow<Boolean> = + interactor.activeColorOption + .map { + it?.colorOption?.isEquivalent( + colorOptionModel.colorOption + ) + ?: colorOptionModel.isSelected + } + .stateIn(viewModelScope) + OptionItemViewModel<ColorOptionIconViewModel>( + key = + MutableStateFlow(colorOptionModel.key) as StateFlow<String>, + payload = + ColorOptionIconViewModel( + lightThemeColor0 = lightThemeColors[0], + lightThemeColor1 = lightThemeColors[1], + lightThemeColor2 = lightThemeColors[2], + lightThemeColor3 = lightThemeColors[3], + darkThemeColor0 = darkThemeColors[0], + darkThemeColor1 = darkThemeColors[1], + darkThemeColor2 = darkThemeColors[2], + darkThemeColor3 = darkThemeColors[3], + ), + text = + Text.Loaded( + colorOption.getContentDescription(context).toString() + ), + isTextUserVisible = false, + isSelected = isSelectedFlow, + onClicked = + isSelectedFlow.map { isSelected -> + if (isSelected) { + null + } else { + { + viewModelScope.launch { + interactor.select(colorOptionModel) } } - }, - ) - } - } - ColorType.PRESET_COLOR -> { - colorOptionEntry.value.map { colorOptionModel -> - val colorBundle: ColorBundle = - colorOptionModel.colorOption as ColorBundle - val primaryColor = - colorBundle.previewInfo.resolvePrimaryColor( - context.resources - ) - val secondaryColor = - colorBundle.previewInfo.resolveSecondaryColor( - context.resources - ) - val isSelectedFlow: StateFlow<Boolean> = - interactor.activeColorOption - .map { - it?.colorOption?.isEquivalent( - colorOptionModel.colorOption - ) - ?: colorOptionModel.isSelected } - .stateIn(viewModelScope) - OptionItemViewModel<ColorOptionIconViewModel>( - key = - MutableStateFlow(colorOptionModel.key) - as StateFlow<String>, - payload = - ColorOptionIconViewModel( - primaryColor, - secondaryColor, - primaryColor, - secondaryColor - ), - text = - Text.Loaded( - colorBundle - .getContentDescription(context) - .toString() - ), - isSelected = isSelectedFlow, - onClicked = - isSelectedFlow.map { isSelected -> - if (isSelected) { - null - } else { - { - viewModelScope.launch { - interactor.select(colorOptionModel) - } - } - } - }, - ) - } + }, + ) } - } - } - .toMap() - } + } + .toMap() + } + .shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) /** The list of all available color options for the selected Color Type. */ val colorOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> = combine(allColorOptions, selectedColorTypeTabId) { - allColorOptions: Map<ColorType, List<OptionItemViewModel<ColorOptionIconViewModel>>>, - selectedColorTypeIdOrNull -> - val selectedColorTypeId = selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR - allColorOptions[selectedColorTypeId]!! - } + allColorOptions: + Map<ColorType, List<OptionItemViewModel<ColorOptionIconViewModel>>>, + selectedColorTypeIdOrNull -> + val selectedColorTypeId = selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR + allColorOptions[selectedColorTypeId]!! + } + .shareIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + replay = 1, + ) /** The list of color options for the color section */ val colorSectionOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> = - allColorOptions.map { allColorOptions -> - val wallpaperOptions = allColorOptions[ColorType.WALLPAPER_COLOR] - val presetOptions = allColorOptions[ColorType.PRESET_COLOR] - val subOptions = - wallpaperOptions!!.subList(0, min(COLOR_SECTION_OPTION_SIZE, wallpaperOptions.size)) - // Add additional options based on preset colors if size of wallpaper color options is - // less than COLOR_SECTION_OPTION_SIZE - val additionalSubOptions = - presetOptions!!.subList( - 0, - min( - max(0, COLOR_SECTION_OPTION_SIZE - wallpaperOptions.size), - presetOptions.size, + allColorOptions + .map { allColorOptions -> + val wallpaperOptions = allColorOptions[ColorType.WALLPAPER_COLOR] + val presetOptions = allColorOptions[ColorType.PRESET_COLOR] + val subOptions = + wallpaperOptions!!.subList( + 0, + min(COLOR_SECTION_OPTION_SIZE, wallpaperOptions.size) ) - ) - subOptions + additionalSubOptions - } + // Add additional options based on preset colors if size of wallpaper color options + // is + // less than COLOR_SECTION_OPTION_SIZE + val additionalSubOptions = + presetOptions!!.subList( + 0, + min( + max(0, COLOR_SECTION_OPTION_SIZE - wallpaperOptions.size), + presetOptions.size, + ) + ) + subOptions + additionalSubOptions + } + .shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) class Factory( private val context: Context, |