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.core
18
19 import androidx.compose.foundation.layout.Arrangement
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.Column
22 import androidx.compose.foundation.layout.Row
23 import androidx.compose.foundation.layout.WindowInsets
24 import androidx.compose.foundation.layout.WindowInsetsSides
25 import androidx.compose.foundation.layout.fillMaxHeight
26 import androidx.compose.foundation.layout.fillMaxSize
27 import androidx.compose.foundation.layout.fillMaxWidth
28 import androidx.compose.foundation.layout.offset
29 import androidx.compose.foundation.layout.only
30 import androidx.compose.foundation.layout.padding
31 import androidx.compose.foundation.layout.systemBars
32 import androidx.compose.foundation.layout.windowInsetsPadding
33 import androidx.compose.material3.ExperimentalMaterial3Api
34 import androidx.compose.material3.MaterialTheme
35 import androidx.compose.material3.ModalBottomSheet
36 import androidx.compose.material3.rememberModalBottomSheetState
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.CompositionLocalProvider
39 import androidx.compose.ui.Alignment
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.graphics.Color
42 import androidx.compose.ui.unit.IntOffset
43 import androidx.compose.ui.unit.dp
44 import androidx.navigation.compose.rememberNavController
45 import com.android.photopicker.core.features.LocalFeatureManager
46 import com.android.photopicker.core.features.Location
47 import com.android.photopicker.core.features.LocationParams
48 import com.android.photopicker.core.navigation.LocalNavController
49 import com.android.photopicker.core.navigation.PhotopickerNavGraph
50 import com.android.photopicker.data.model.Media
51 import kotlinx.coroutines.CompletableDeferred
52 import kotlinx.coroutines.flow.Flow
53
54 private val MEASUREMENT_BOTTOM_SHEET_EDGE_PADDING = 12.dp
55
56 /**
57 * This is an entrypoint of the Photopicker Compose UI. This is called from the MainActivity and is
58 * the top-most [@Composable] in the activity application. This should not be called except inside
59 * an Activity's [setContent] block.
60 *
61 * @param onDismissRequest handler for when the BottomSheet is dismissed.
62 */
63 @OptIn(ExperimentalMaterial3Api::class)
64 @Composable
PhotopickerAppWithBottomSheetnull65 fun PhotopickerAppWithBottomSheet(
66 onDismissRequest: () -> Unit,
67 onMediaSelectionConfirmed: () -> Unit,
68 preloadMedia: Flow<Set<Media>>,
69 obtainPreloaderDeferred: () -> CompletableDeferred<Boolean>,
70 ) {
71 // Initialize and remember the NavController. This needs to be provided before the call to
72 // the NavigationGraph, so this is done at the top.
73 val navController = rememberNavController()
74 val state = rememberModalBottomSheetState()
75 // Provide the NavController to the rest of the Compose stack.
76 CompositionLocalProvider(LocalNavController provides navController) {
77 Column(
78 modifier =
79 // Apply WindowInsets to this wrapping column to prevent the Bottom Sheet
80 // from drawing over the system bars.
81 Modifier.windowInsetsPadding(
82 WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
83 )
84 ) {
85 ModalBottomSheet(
86 sheetState = state,
87 onDismissRequest = onDismissRequest,
88 scrimColor = Color.Transparent,
89 containerColor = MaterialTheme.colorScheme.surfaceContainer,
90 contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
91 contentWindowInsets = { WindowInsets.systemBars },
92 ) {
93 Box(
94 modifier = Modifier.fillMaxHeight(),
95 contentAlignment = Alignment.BottomCenter
96 ) {
97 PhotopickerMain()
98 Column(
99 modifier =
100 // Some elements needs to be drawn over the UI inside of the
101 // BottomSheet A negative y offset will move it from the bottom of the
102 // content to the bottom of the onscreen BottomSheet.
103 Modifier.offset {
104 IntOffset(x = 0, y = -state.requireOffset().toInt())
105 },
106 ) {
107 LocalFeatureManager.current.composeLocation(
108 Location.SNACK_BAR,
109 maxSlots = 1,
110 )
111 LocalFeatureManager.current.composeLocation(
112 Location.SELECTION_BAR,
113 maxSlots = 1,
114 params = LocationParams.WithClickAction { onMediaSelectionConfirmed() }
115 )
116 }
117 }
118 // If a [MEDIA_PRELOADER] is configured in the current session, attach it
119 // to the compose UI here, so that any dialogs it shows are drawn overtop
120 // of the application.
121 LocalFeatureManager.current.composeLocation(
122 Location.MEDIA_PRELOADER,
123 maxSlots = 1,
124 params =
125 object : LocationParams.WithMediaPreloader {
126 override fun obtainDeferred(): CompletableDeferred<Boolean> {
127 return obtainPreloaderDeferred()
128 }
129
130 override val preloadMedia = preloadMedia
131 }
132 )
133 }
134 }
135 }
136 }
137
138 /**
139 * This is an entrypoint of the Photopicker Compose UI. This is called from a hosting View and is
140 * the top-most [@Composable] in the view based application. This should not be called by any
141 * Activity code, and should only be called inside of the ComposeView [setContent] block.
142 */
143 @OptIn(ExperimentalMaterial3Api::class)
144 @Composable
PhotopickerAppnull145 fun PhotopickerApp() {
146 // Initialize and remember the NavController. This needs to be provided before the call to
147 // the NavigationGraph, so this is done at the top.
148 val navController = rememberNavController()
149
150 // Provide the NavController to the rest of the Compose stack.
151 CompositionLocalProvider(LocalNavController provides navController) { PhotopickerMain() }
152 }
153
154 /**
155 * This is the shared entrypoint for the Photopicker compose-UI. Composables above this function
156 * must provide the required dependencies to the compose UI before calling this entrypoint.
157 *
158 * It is presumed after this composable the compose UI can either be running inside of a wrapped
159 * View or an Activity lifecycle.
160 *
161 * By this entrypoint, the expected CompositionLocals should already exist:
162 * - LocalEvents
163 * - LocalFeatureManager
164 * - LocalNavController
165 * - LocalPhotopickerConfiguration
166 * - LocalSelection
167 * - PhotopickerTheme
168 */
169 @Composable
PhotopickerMainnull170 fun PhotopickerMain() {
171 Box(modifier = Modifier.fillMaxSize()) {
172 Column {
173 // The navigation bar and profile switcher are drawn above the navigation graph
174 Row(
175 modifier =
176 Modifier.fillMaxWidth()
177 .padding(horizontal = MEASUREMENT_BOTTOM_SHEET_EDGE_PADDING),
178 horizontalArrangement = Arrangement.SpaceBetween,
179 verticalAlignment = Alignment.CenterVertically,
180 ) {
181 LocalFeatureManager.current.composeLocation(
182 Location.PROFILE_SELECTOR,
183 maxSlots = 1,
184 // Weight should match the overflow menu slot so they are the same size.
185 modifier = Modifier.weight(1f),
186 )
187 LocalFeatureManager.current.composeLocation(
188 Location.NAVIGATION_BAR,
189 maxSlots = 1,
190 modifier = Modifier,
191 )
192 LocalFeatureManager.current.composeLocation(
193 Location.OVERFLOW_MENU,
194 // Weight should match the profile switcher slot so they are the same size.
195 modifier = Modifier.weight(1f),
196 )
197 }
198 // Initialize the navigation graph.
199 PhotopickerNavGraph()
200 }
201 }
202 }
203