1 /* 2 * Copyright 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.photopicker.features.albumgrid 18 19 import androidx.compose.animation.AnimatedContentTransitionScope 20 import androidx.compose.animation.EnterTransition 21 import androidx.compose.animation.ExitTransition 22 import androidx.compose.animation.slideInHorizontally 23 import androidx.compose.animation.slideOutHorizontally 24 import androidx.compose.runtime.Composable 25 import androidx.compose.ui.Modifier 26 import androidx.navigation.NamedNavArgument 27 import androidx.navigation.NavBackStackEntry 28 import androidx.navigation.NavDeepLink 29 import com.android.photopicker.core.animations.springDefaultEffectOffset 30 import com.android.photopicker.core.configuration.PhotopickerConfiguration 31 import com.android.photopicker.core.events.Event 32 import com.android.photopicker.core.events.RegisteredEventClass 33 import com.android.photopicker.core.features.FeatureManager 34 import com.android.photopicker.core.features.FeatureRegistration 35 import com.android.photopicker.core.features.FeatureToken 36 import com.android.photopicker.core.features.Location 37 import com.android.photopicker.core.features.LocationParams 38 import com.android.photopicker.core.features.PhotopickerUiFeature 39 import com.android.photopicker.core.features.PrefetchResultKey 40 import com.android.photopicker.core.features.Priority 41 import com.android.photopicker.core.navigation.PhotopickerDestinations.ALBUM_GRID 42 import com.android.photopicker.core.navigation.PhotopickerDestinations.ALBUM_MEDIA_GRID 43 import com.android.photopicker.core.navigation.Route 44 import com.android.photopicker.data.model.Group 45 import kotlinx.coroutines.Deferred 46 import kotlinx.coroutines.flow.StateFlow 47 48 /** 49 * Feature class for the Photopicker's primary album grid. 50 * 51 * This feature adds the [ALBUM_GRID] route and [ALBUM_MEDIA_GRID] route. 52 */ 53 class AlbumGridFeature : PhotopickerUiFeature { 54 companion object Registration : FeatureRegistration { 55 override val TAG: String = "PhotopickerAlbumGridFeature" 56 isEnablednull57 override fun isEnabled( 58 config: PhotopickerConfiguration, 59 deferredPrefetchResultsMap: Map<PrefetchResultKey, Deferred<Any?>>, 60 ) = !config.flags.PICKER_SEARCH_ENABLED 61 62 override fun build(featureManager: FeatureManager) = AlbumGridFeature() 63 64 const val ALBUM_KEY = "selected_album" 65 } 66 67 override val token = FeatureToken.ALBUM_GRID.token 68 69 /** Events consumed by the Album grid */ 70 override val eventsConsumed = emptySet<RegisteredEventClass>() 71 72 /** Events produced by the Album grid */ 73 override val eventsProduced = 74 setOf( 75 Event.ShowSnackbarMessage::class.java, 76 Event.LogPhotopickerUIEvent::class.java, 77 Event.LogPhotopickerAlbumOpenedUIEvent::class.java, 78 ) 79 80 override fun registerLocations(): List<Pair<Location, Int>> { 81 return listOf(Pair(Location.NAVIGATION_BAR_NAV_BUTTON, Priority.HIGH.priority)) 82 } 83 registerNavigationRoutesnull84 override fun registerNavigationRoutes(): Set<Route> { 85 return setOf( 86 // The main grid of the user's albums. 87 object : Route { 88 override val route = ALBUM_GRID.route 89 override val initialRoutePriority = Priority.MEDIUM.priority 90 override val arguments = emptyList<NamedNavArgument>() 91 override val deepLinks = emptyList<NavDeepLink>() 92 override val isDialog = false 93 override val dialogProperties = null 94 95 /* 96 Animations for ALBUM_GRID 97 - When navigating directly, content will slide IN from the left edge. 98 - When navigating away, content will slide OUT towards the left edge. 99 - When returning from the backstack, content will slide IN from the right edge. 100 - When popping to another route on the backstack, content will slide OUT towards 101 the left edge. 102 */ 103 override val enterTransition: 104 (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? = 105 { 106 if (initialState.destination.route == ALBUM_MEDIA_GRID.route) { 107 // Negative value to slide right-to-left 108 // if previous route was from ALBUM_MEDIA_GRID. 109 slideInHorizontally(animationSpec = springDefaultEffectOffset) { -it } 110 } else { 111 // Positive value to slide left-to-right 112 slideInHorizontally(animationSpec = springDefaultEffectOffset) { it } 113 } 114 } 115 override val exitTransition: 116 (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? = 117 { 118 if (targetState.destination.route == ALBUM_MEDIA_GRID.route) { 119 // Negative value to slide right-to-left 120 // if target route is ALBUM_MEDIA_GRID 121 slideOutHorizontally(animationSpec = springDefaultEffectOffset) { -it } 122 } else { 123 // Positive value to slide left-to-right 124 slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it } 125 } 126 } 127 override val popEnterTransition: 128 (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? = 129 { 130 if (initialState.destination.route == ALBUM_MEDIA_GRID.route) { 131 // Negative value to slide right-to-left 132 // if previous route was from ALBUM_MEDIA_GRID. 133 slideInHorizontally(animationSpec = springDefaultEffectOffset) { -it } 134 } else { 135 // Positive value to slide left-to-right 136 slideInHorizontally(animationSpec = springDefaultEffectOffset) { it } 137 } 138 } 139 override val popExitTransition: 140 (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? = 141 { 142 if (targetState.destination.route == ALBUM_MEDIA_GRID.route) { 143 // Negative value to slide right-to-left 144 // if target route is ALBUM_MEDIA_GRID 145 slideOutHorizontally(animationSpec = springDefaultEffectOffset) { -it } 146 } else { 147 // Positive value to slide left-to-right 148 slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it } 149 } 150 } 151 152 @Composable 153 override fun composable(navBackStackEntry: NavBackStackEntry?) { 154 AlbumGrid() 155 } 156 }, 157 // Grid to show the album content for the album selected by the user. 158 object : Route { 159 override val route = ALBUM_MEDIA_GRID.route 160 override val initialRoutePriority = Priority.MEDIUM.priority 161 override val arguments = emptyList<NamedNavArgument>() 162 override val deepLinks = emptyList<NavDeepLink>() 163 override val isDialog = false 164 override val dialogProperties = null 165 166 /** 167 * Animations for ALBUM_CONTENT_GRID are by default [EnterTransition.None] for 168 * entering into view and [ExitTransition.None] while exiting. 169 */ 170 override val enterTransition: 171 (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? = 172 { 173 // Positive value to slide left-to-right 174 slideInHorizontally(animationSpec = springDefaultEffectOffset) { it } 175 } 176 override val exitTransition: 177 (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? = 178 { 179 slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it } 180 } 181 override val popEnterTransition: 182 (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition)? = 183 { 184 slideInHorizontally(animationSpec = springDefaultEffectOffset) { it } 185 } 186 override val popExitTransition: 187 (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition)? = 188 { 189 slideOutHorizontally(animationSpec = springDefaultEffectOffset) { it } 190 } 191 192 @Composable 193 override fun composable(navBackStackEntry: NavBackStackEntry?) { 194 val flow: StateFlow<Group.Album?> = 195 checkNotNull( 196 navBackStackEntry 197 ?.savedStateHandle 198 ?.getStateFlow<Group.Album?>(ALBUM_KEY, null) 199 ) { 200 "Unable to get a savedStateHandle for album content grid" 201 } 202 AlbumMediaGrid(flow) 203 } 204 }, 205 ) 206 } 207 208 @Composable composenull209 override fun compose(location: Location, modifier: Modifier, params: LocationParams) { 210 when (location) { 211 Location.NAVIGATION_BAR_NAV_BUTTON -> AlbumGridNavButton(modifier) 212 else -> {} 213 } 214 } 215 } 216