/* * Copyright (C) 2025 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.wallpaper.picker.customization.ui.binder import android.content.res.ColorStateList import android.widget.TextView import androidx.compose.ui.platform.ComposeView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isVisible import androidx.core.widget.TextViewCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.RecyclerView import com.android.wallpaper.R import com.android.wallpaper.config.BaseFlags import com.android.wallpaper.model.Screen import com.android.wallpaper.module.logging.UserEventLogger import com.android.wallpaper.picker.category.ui.view.adapter.CuratedPhotosAdapter import com.android.wallpaper.picker.category.ui.view.adapter.LoadingAnimationAdapter import com.android.wallpaper.picker.customization.shared.model.CategoryType import com.android.wallpaper.picker.customization.ui.compose.desktop.WallpaperCarouselDesktop import com.android.wallpaper.picker.customization.ui.view.WallpaperPickerEntry import com.android.wallpaper.picker.customization.ui.view.listener.CarouselHorizontalScrollEnforcer import com.android.wallpaper.picker.customization.ui.view.listener.WallpaperTitleScrollListener import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel2 import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperCarouselViewModel import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperCarouselViewModel.NavigationEvent.NavigateToExtendedWallpaperEffects import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperCarouselViewModel.NavigationEvent.NavigateToPreviewScreen import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperCarouselViewModel.NavigationEvent.NavigateToWallpaperCollection import com.android.wallpaper.picker.data.WallpaperModel import com.android.wallpaper.util.CuratedPhotosTimeUtil import com.google.android.material.carousel.CarouselLayoutManager import com.google.android.material.carousel.CarouselSnapHelper import kotlinx.coroutines.launch object WallpaperPickerEntryBinder { private const val DESKTOP_CAROUSEL_ITEMS_COUNT = 5 private class CustomScrollableCarouselLayoutManager : CarouselLayoutManager() { private var isScrollable = false override fun canScrollHorizontally(): Boolean { return if (isScrollable) super.canScrollHorizontally() else false } override fun canScrollVertically(): Boolean { return false } fun setIsScrollable(isScrollable: Boolean) { this.isScrollable = isScrollable } } fun bind( view: WallpaperPickerEntry, viewModel: CustomizationPickerViewModel2, colorUpdateViewModel: ColorUpdateViewModel, lifecycleOwner: LifecycleOwner, navigateToWallpaperCategoriesScreen: (screen: Screen) -> Unit, navigateToPreviewScreen: ((wallpaperModel: WallpaperModel, setWallpaperEntryPoint: Int) -> Unit)?, navigateToWallpaperCollectionScreen: ((collectionId: String, categoryType: CategoryType) -> Unit)?, navigateToExtendedWallpaperEffects: (() -> Unit)?, curatedPhotosTimeUtil: CuratedPhotosTimeUtil, userEventLogger: UserEventLogger, ) { val isOnMainScreen = { viewModel.customizationOptionsViewModel.selectedOption.value == null } val wallpaperCarouselViewModel = viewModel.customizationOptionsViewModel.wallpaperCarouselViewModel bindNavigationEvents( viewModel = wallpaperCarouselViewModel, lifecycleOwner = lifecycleOwner, navigateToPreviewScreen = navigateToPreviewScreen, navigateToWallpaperCollectionScreen = navigateToWallpaperCollectionScreen, navigateToExtendedWallpaperEffects = navigateToExtendedWallpaperEffects, ) val shouldShowDesktopUi = BaseFlags.get().shouldShowDesktopUi(view.context) if (shouldShowDesktopUi) { bindWallpaperCarouselDesktop( wallpaperPickerEntryView = view, viewModel = wallpaperCarouselViewModel, lifecycleOwner = lifecycleOwner, curatedPhotosTimeUtil = curatedPhotosTimeUtil, userEventLogger = userEventLogger, ) } else { bindWallpaperCarousel( wallpaperPickerEntryView = view, viewModel = wallpaperCarouselViewModel, colorUpdateViewModel = colorUpdateViewModel, shouldAnimateColor = isOnMainScreen, lifecycleOwner = lifecycleOwner, curatedPhotosTimeUtil = curatedPhotosTimeUtil, userEventLogger = userEventLogger, ) bindWallpaperPickerEntryLabels( container = view.requireViewById( R.id.wallpaper_picker_entry_expanded_container ), suggestedPhotosLabel = view.suggestedPhotosText, viewModel = viewModel.customizationOptionsViewModel.wallpaperCarouselViewModel, lifecycleOwner = lifecycleOwner, ) } lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.selectedPreviewScreen.collect { previewScreen -> view.collapsedButton.setOnClickListener { navigateToWallpaperCategoriesScreen.invoke(previewScreen) } view.moreWallpapersButton.setOnClickListener { navigateToWallpaperCategoriesScreen.invoke(previewScreen) } if (shouldShowDesktopUi) { view.suggestedPhotosText.setOnClickListener { navigateToWallpaperCategoriesScreen.invoke(previewScreen) } } } } } ColorUpdateBinder.bind( setColor = { color -> DrawableCompat.setTint(DrawableCompat.wrap(view.background), color) }, color = colorUpdateViewModel.colorSurfaceBright, shouldAnimate = isOnMainScreen, lifecycleOwner = lifecycleOwner, ) ColorUpdateBinder.bind( setColor = { color -> TextViewCompat.setCompoundDrawableTintList( view.moreWallpapersButton, ColorStateList.valueOf(color), ) TextViewCompat.setCompoundDrawableTintList( view.collapsedButton, ColorStateList.valueOf(color), ) view.collapsedButton.setTextColor(color) }, color = colorUpdateViewModel.colorPrimary, shouldAnimate = isOnMainScreen, lifecycleOwner = lifecycleOwner, ) ColorUpdateBinder.bind( setColor = { color -> view.suggestedPhotosText.setTextColor(color) }, color = colorUpdateViewModel.colorSecondary, shouldAnimate = isOnMainScreen, lifecycleOwner = lifecycleOwner, ) } /** * Sets up the RecyclerView's LayoutManager and attaches mode-specific listeners based on * whether the application is in desktop mode. */ private fun setupWallpaperCarouselRecyclerView(recyclerView: RecyclerView) { // Mobile/Tablet mode: Use CustomScrollableCarouselLayoutManager to enable carousel // experience. recyclerView.layoutManager = CustomScrollableCarouselLayoutManager() if (recyclerView.onFlingListener == null) { CarouselSnapHelper().attachToRecyclerView(recyclerView) } val horizontalScrollEnforcer = CarouselHorizontalScrollEnforcer(recyclerView.context) recyclerView.addOnScrollListener(horizontalScrollEnforcer) recyclerView.addOnItemTouchListener(horizontalScrollEnforcer) recyclerView.isNestedScrollingEnabled = false } private fun bindNavigationEvents( viewModel: WallpaperCarouselViewModel, lifecycleOwner: LifecycleOwner, navigateToPreviewScreen: ((wallpaperModel: WallpaperModel, setWallpaperEntryPoint: Int) -> Unit)?, navigateToWallpaperCollectionScreen: ((collectionId: String, categoryType: CategoryType) -> Unit)?, navigateToExtendedWallpaperEffects: (() -> Unit)?, ) { lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { // Collect and handle navigation events once. viewModel.navigationEvents.collect { navigationEvent: WallpaperCarouselViewModel.NavigationEvent -> when (navigationEvent) { is NavigateToWallpaperCollection -> { navigateToWallpaperCollectionScreen?.invoke( navigationEvent.categoryId, navigationEvent.categoryType, ) } is NavigateToPreviewScreen -> { navigateToPreviewScreen?.invoke( navigationEvent.wallpaperModel, navigationEvent.entryPoint, ) } is NavigateToExtendedWallpaperEffects -> { navigateToExtendedWallpaperEffects?.invoke() } } } } } } private fun bindWallpaperCarousel( wallpaperPickerEntryView: WallpaperPickerEntry, viewModel: WallpaperCarouselViewModel, colorUpdateViewModel: ColorUpdateViewModel, shouldAnimateColor: () -> Boolean, lifecycleOwner: LifecycleOwner, curatedPhotosTimeUtil: CuratedPhotosTimeUtil, userEventLogger: UserEventLogger, ) { val wallpaperCarousel: RecyclerView = wallpaperPickerEntryView.wallpaperCarousel ?: return lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { val loadingAnimationAdapter = LoadingAnimationAdapter( size = 3, colorUpdateViewModel = colorUpdateViewModel, shouldAnimateColor = shouldAnimateColor, lifecycleOwner = lifecycleOwner, curatedPhotosTimeUtil = curatedPhotosTimeUtil, userEventLogger = userEventLogger, ) wallpaperCarousel.adapter = loadingAnimationAdapter setupWallpaperCarouselRecyclerView(wallpaperCarousel) viewModel.wallpaperCarouselItems.collect { wallpaperPickerEntryView.post { if (it.isEmpty()) { wallpaperPickerEntryView.animateToCollapsed() } } wallpaperCarousel.swapAdapter( CuratedPhotosAdapter(it, curatedPhotosTimeUtil, userEventLogger), /** removeAndRecycleExistingViews= */ false, ) // Enable scrolling for the carousel only for mobile/tablet mode. if (!it.isEmpty() && it.get(0).showTitle) { wallpaperCarousel.addOnScrollListener(WallpaperTitleScrollListener()) } (wallpaperCarousel.layoutManager as? CustomScrollableCarouselLayoutManager) ?.setIsScrollable(true) } } } } } private fun bindWallpaperCarouselDesktop( wallpaperPickerEntryView: WallpaperPickerEntry, viewModel: WallpaperCarouselViewModel, lifecycleOwner: LifecycleOwner, curatedPhotosTimeUtil: CuratedPhotosTimeUtil, userEventLogger: UserEventLogger, ) { val wallpaperCarouselDesktop: ComposeView = wallpaperPickerEntryView.wallpaperCarouselDesktop ?: return lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { viewModel.wallpaperCarouselItems.collect { items -> wallpaperPickerEntryView.post { if (items.isEmpty()) { wallpaperPickerEntryView.animateToCollapsed() } } wallpaperCarouselDesktop.setContent { WallpaperCarouselDesktop( items = items, curatedPhotosTimeUtil = curatedPhotosTimeUtil, userEventLogger = userEventLogger, ) } } } } } } private fun bindWallpaperPickerEntryLabels( container: ConstraintLayout, suggestedPhotosLabel: TextView, viewModel: WallpaperCarouselViewModel, lifecycleOwner: LifecycleOwner, ) { lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { viewModel.shouldShowSuggestedPhotosLabel.collect { suggestedPhotosLabel.isVisible = it if (it) { applyEndAlignedConstraints(container) } else { applyCenteredConstraints(container) } } } } } } private fun applyEndAlignedConstraints( wallpaperPickerEntryExpandedContainer: ConstraintLayout ) { val constraintSet = ConstraintSet() constraintSet.clone(wallpaperPickerEntryExpandedContainer) constraintSet.clear(R.id.more_wallpapers_button, ConstraintSet.START) constraintSet.connect( R.id.more_wallpapers_button, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, ) constraintSet.applyTo(wallpaperPickerEntryExpandedContainer) } private fun applyCenteredConstraints(wallpaperPickerEntryExpandedContainer: ConstraintLayout) { val constraintSet = ConstraintSet() constraintSet.clone(wallpaperPickerEntryExpandedContainer) constraintSet.clear(R.id.more_wallpapers_button, ConstraintSet.END) constraintSet.connect( R.id.more_wallpapers_button, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, ) constraintSet.connect( R.id.more_wallpapers_button, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, ) constraintSet.setHorizontalBias(R.id.more_wallpapers_button, 0.5f) constraintSet.applyTo(wallpaperPickerEntryExpandedContainer) } }