I have a dialogue that has a button to open bottom sheet in that bottom sheet I have an image picker that first item of it is camera preview I want to make this preview full screen when I tap on it. this is my bottom sheet:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomSheetImagePicker(onDismiss: () -> Unit) {
val context = LocalContext.current
val imageUris = getAllImageUris(context)
val selected = remember { mutableStateOf(false) }
ModalBottomSheet(onDismiss) {
LazyVerticalGrid(
columns = GridCells.Fixed(3), // 3 columns
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
item(span = { GridItemSpan(2) }) {
CameraPreviewScreen(selected = selected.value) {
selected.value = true
}
}
item() {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
ImageItem(imageUris[0])
ImageItem(imageUris[1])
}
}
val otherImages = imageUris.drop(2)
items(otherImages) { uri ->
ImageItem(uri)
}
}
}
}
I tried to make CameraPreviewScreen
bigger with changing its size and scale but it put camera under other items like this:
now my question is how I can bring it on top of other things in first layer?
I want to make an image picker like telegram there
This is not that hard to implement with SharedElementTransition
api except when you use ModalBottomSheet
because it's a Dialog
, and creates a new window layout and draws above anything on screen. If you try to have shared element transition it crashes with exception with
E FATAL EXCEPTION: main java.lang.IllegalArgumentException: layouts are not part of the same hierarchy
There are 2 ways to implement this with shared transitions, one without and another with ModalBottomSheet
to create another Dialog
and do shared element transitions in it with a workaround by getting bounds and resource of clicked Image and doing transition in new Dialog.
Telegram also doesn't use a Dialog but a custom scrollable that does snap in specific positions and change TopAppbar.
You can substitute custom Composable if you don't want to implement it yourself with BottomSheetScaffold
, my initial choice was BottomDrawer
but it crashes inside SharedTransitionLayout, when it's fixed you can use it instead of BottomSheetScaffold
.
@Preview
@Composable
private fun SharedElementsample() {
SharedTransitionLayout(modifier = Modifier.fillMaxSize()) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home",
) {
composable("home") {
BottomSheetImagePicker(
sharedTransitionScope = this@SharedTransitionLayout,
animatedContentScope = this@composable,
onClick = {
navController.navigate("details/$it")
},
onDismiss = {}
)
}
composable(
"details/{item}",
arguments = listOf(navArgument("item") { type = NavType.IntType })
) { backStackEntry ->
val item = backStackEntry.arguments?.getInt("item") ?: 0
Column(
modifier = Modifier.fillMaxSize().background(Color.Black),
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(item),
modifier = Modifier
.sharedElement(
state = rememberSharedContentState(key = item),
animatedVisibilityScope = this@composable,
)
.fillMaxWidth(),
contentScale = ContentScale.Crop,
alignment = Alignment.CenterStart,
contentDescription = null
)
}
}
}
}
}
@Composable
private fun BottomSheetImagePicker(
sharedTransitionScope: SharedTransitionScope,
animatedContentScope: AnimatedContentScope,
onDismiss: () -> Unit,
onClick: (Int) -> Unit,
) {
val imageUris = remember {
listOf(
R.drawable.landscape1,
R.drawable.landscape2,
R.drawable.landscape3,
R.drawable.landscape4,
R.drawable.landscape5,
R.drawable.landscape6,
R.drawable.landscape7,
R.drawable.landscape8,
R.drawable.landscape9,
R.drawable.landscape10
)
}
val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberStandardBottomSheetState(
initialValue = SheetValue.Hidden,
skipHiddenState = false
)
)
BottomSheetScaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState,
sheetPeekHeight = 400.dp,
content = {
val bottomState = scaffoldState.bottomSheetState
val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
Spacer(Modifier.weight(1f))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
coroutineScope.launch {
bottomState.partialExpand()
}
}
) {
Text("Expand")
}
}
if (bottomState.isVisible) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = .3f))
)
}
},
sheetContent = {
LazyVerticalGrid(
columns = GridCells.Fixed(3), // 3 columns
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp)
) {
item(span = { GridItemSpan(2) }) {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red, RoundedCornerShape(16.dp))
)
}
item {
Column(
verticalArrangement = Arrangement
.spacedBy(8.dp)
) {
ImageItem(
sharedTransitionScope,
animatedContentScope,
imageUris[0],
onClick
)
ImageItem(
sharedTransitionScope,
animatedContentScope,
imageUris[1],
onClick
)
}
}
val otherImages = imageUris.drop(2)
items(otherImages) { uri ->
ImageItem(
sharedTransitionScope,
animatedContentScope,
uri,
onClick
)
}
}
}
)
}
@Composable
private fun ImageItem(
sharedTransitionScope: SharedTransitionScope,
animatedContentScope: AnimatedContentScope,
@DrawableRes uri: Int,
onClick: (Int) -> Unit,
) {
with(sharedTransitionScope) {
Image(
modifier = Modifier.sharedElement(
state = rememberSharedContentState(key = uri),
animatedVisibilityScope = animatedContentScope,
boundsTransform = gridBoundsTransform
).clickable {
onClick(uri)
},
painter = painterResource(uri),
contentScale = ContentScale.Crop,
contentDescription = null
)
}
}
val gridBoundsTransform = BoundsTransform { initialBounds, targetBounds ->
keyframes {
durationMillis = 500
initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing
targetBounds at 500
}
}
To way to implement this with ModalBottomSheet
is to use another Dialog
to draw above ModalBottomSheet
.
Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile, especially when offering a long list of action items, or when items require longer descriptions and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other app functionality when they appear, and remaining on screen until confirmed, dismissed, or a required action has been taken.
With this approach you need to handle position and size of image as Rect
you get from onGloballyPositioned
and back press
as well as in answer below.
private sealed class AnimationScreen {
data object List : AnimationScreen()
data class Details(val item: Int, val rect: Rect) : AnimationScreen()
}
@Preview
@Composable
fun SharedElementsample2() {
BottomSheetImagePicker()
}
@Composable
fun BottomSheetImagePicker() {
var state by remember {
mutableStateOf<AnimationScreen>(AnimationScreen.List)
}
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
val bottomSheetState = rememberModalBottomSheetState()
// App Content
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
Spacer(Modifier.weight(1f))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
openBottomSheet = openBottomSheet.not()
}
) {
Text("Expand")
}
}
if (openBottomSheet) {
ModalBottomSheet(
sheetState = bottomSheetState,
modifier = Modifier.fillMaxSize().systemBarsPadding(),
onDismissRequest = {
openBottomSheet = false
}
) {
BottomSheetPickerContent { uri, rect ->
state = AnimationScreen.Details(uri, rect)
}
}
}
if (state is AnimationScreen.Details) {
BasicAlertDialog(
modifier = Modifier.fillMaxSize().background(Color.Black),
onDismissRequest = {
state = AnimationScreen.List
},
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
var visible by remember {
mutableStateOf(false)
}
var expanded by remember {
mutableStateOf(false)
}
val dispatcher = LocalOnBackPressedDispatcherOwner.current
BackHandler(visible) {
visible = false
}
SharedTransitionLayout(modifier = Modifier.fillMaxSize()) {
LaunchedEffect(Unit) {
awaitFrame()
visible = true
}
AnimatedContent(
modifier = Modifier.fillMaxSize(),
targetState = visible,
label = "",
) { visibleState ->
if (expanded.not()) {
expanded = visibleState && isTransitionActive.not()
}
val item = (state as? AnimationScreen.Details)?.item ?: R.drawable.landscape2
val rect = (state as? AnimationScreen.Details)?.rect ?: Rect.Zero
if (visibleState) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(item),
modifier = Modifier
.sharedElement(
state = rememberSharedContentState(key = item),
animatedVisibilityScope = this@AnimatedContent,
boundsTransform = gridBoundsTransform
)
.fillMaxWidth(),
contentScale = ContentScale.Crop,
contentDescription = null
)
}
} else {
LaunchedEffect(expanded, isTransitionActive) {
if (expanded && isTransitionActive.not()) {
dispatcher?.onBackPressedDispatcher?.onBackPressed()
}
}
Box(modifier = Modifier.fillMaxSize()) {
val density = LocalDensity.current
val width: Dp
val height: Dp
with(density) {
width = rect.width.toDp()
height = rect.height.toDp()
}
Image(
painter = painterResource(item),
modifier = Modifier
.offset {
rect.topLeft.round()
}
.size(width, height)
.sharedElement(
state = rememberSharedContentState(key = item),
animatedVisibilityScope = this@AnimatedContent,
boundsTransform = gridBoundsTransform
),
contentScale = ContentScale.Crop,
contentDescription = null
)
}
}
}
}
}
}
}
@Composable
private fun BottomSheetPickerContent(
onClick: (Int, Rect) -> Unit,
) {
val imageUris = remember {
listOf(
R.drawable.landscape1,
R.drawable.landscape2,
R.drawable.landscape3,
R.drawable.landscape4,
R.drawable.landscape5,
R.drawable.landscape6,
R.drawable.landscape7,
R.drawable.landscape8,
R.drawable.landscape9,
R.drawable.landscape10
)
}
val imageMap = remember {
mutableStateMapOf<Int, Rect>()
}
val density = LocalDensity.current
val statusBarHeight = WindowInsets.statusBars.getTop(density)
LazyVerticalGrid(
columns = GridCells.Fixed(3), // 3 columns
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp)
) {
item(span = { GridItemSpan(2) }) {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red, RoundedCornerShape(16.dp))
)
}
item {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ImageItem(
modifier = Modifier.onGloballyPositioned {
imageMap[imageUris[0]] = Rect(
offset = Offset(
x = it.positionOnScreen().x,
y = it.positionOnScreen().y - statusBarHeight
),
size = it.size.toSize()
)
},
uri = imageUris[0],
onClick = {
onClick(imageUris[0], imageMap[imageUris[0]] ?: Rect.Zero)
}
)
ImageItem(
modifier = Modifier.onGloballyPositioned {
imageMap[imageUris[1]] = Rect(
offset = Offset(
x = it.positionOnScreen().x,
y = it.positionOnScreen().y - statusBarHeight
),
size = it.size.toSize()
)
},
uri = imageUris[1],
onClick = {
onClick(imageUris[1], imageMap[imageUris[1]] ?: Rect.Zero)
}
)
}
}
val otherImages = imageUris.drop(2)
items(otherImages) { uri ->
ImageItem(
modifier = Modifier.onGloballyPositioned {
imageMap[uri] = Rect(
offset = Offset(
x = it.positionOnScreen().x,
y = it.positionOnScreen().y - statusBarHeight
),
size = it.size.toSize()
)
},
uri = uri,
onClick = {
onClick(uri, imageMap[uri] ?: Rect.Zero)
}
)
}
}
}
@Composable
private fun ImageItem(
modifier: Modifier = Modifier,
@DrawableRes uri: Int,
onClick: (Int) -> Unit,
) {
Image(
modifier = modifier
.clickable {
onClick(uri)
},
painter = painterResource(uri),
contentScale = ContentScale.Crop,
contentDescription = null
)
}