Spoiler component in Compose - android-jetpack-compose

Is there a something like a spoiler component in Compose (that doesn't look too much Android-ish)? Or can it be done fast using LazyVerticalGrid or something else? I'm looking for a box that opens and clothes by clicking on its header.

What you look for is AnimatedVisibility Composable.
var visible by remember {
mutableStateOf(true)
}
Column(modifier = Modifier.fillMaxSize()) {
Text("Spoiler", modifier = Modifier
.fillMaxWidth()
.clickable {
visible = !visible
}
)
AnimatedVisibility(visible = visible) {
Column {
Text("Lorem ipsum...Rest of the text")
}
}
}
https://developer.android.com/jetpack/compose/animation#animatedvisibility

Related

Material 3 Top Bar container color not change on scroll

The top app bar container color not change on scroll, the color keep the same. Working with XML views it works fine, but compose doesn't.
If is a bug or the featured was not ported for compose yet, but if ins't a bug, i'm miss something in the code?
I'm using version 1.0.1 of the material 3 library.
Sample code:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
val uiController = rememberSystemUiController()
uiController.setStatusBarColor(Color.Transparent)
MyApplication5Theme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("k") },
modifier = Modifier.statusBarsPadding(),
)
}
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(it)
) {
items((1..100).toList()) {
Text(it.toString(), modifier = Modifier.padding(8.dp))
}
}
}
}
}
}
}
}
I tried create a simple layout with a top app bar and a lazy column, i expected top app bar container color changes on scroll of the lazy column, but the color keep the same.
The TopAppBar composable has a scrollBehavior argument, which is null be default. If you leave it null, it will not react to scroll events, you have to connect it with your scrollable component. You can do that like this:
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { TopAppBar(scrollBehavior = scrollBehavior, ...) },
) { LazyColumn() {} }

Can't animate FAB visible in M3 Scaffold

I'm trying to get behaviour where I can toggle visibility of a FAB, with a bit of entry/exit animation, in the Compose Scaffold.
The issue
The FAB disappears fine. However, whatever I try, I can't get it to reappear - it's like it totally disappears from the tree, never to return!
This code reproduces the issue:
class TestActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var fabVisible by remember { mutableStateOf(true) }
Scaffold(
floatingActionButton = {
AnimatedVisibility(visible = fabVisible) {
FloatingActionButton(onClick = {}) {
Icon(Icons.Default.Star, contentDescription = null)
}
}
}
) {
Button(onClick = { fabVisible = !fabVisible }) {
Text("Click to toggle FAB")
}
}
}
}
}
Here's a demo - the FAB does a sort of wipe out, but then never returns:
Workarounds I've attempted
If I do any of the following, the FAB will toggle:
Put the FAB in another area, such as the Scaffold content or topbar area.
Remove the AnimatedVisibility.
Put the FAB inside an explicitly-sized Box:
…
Scaffold(
floatingActionButton = {
Box(
modifier = Modifier
.width(56.0.dp)
.height(56.0.dp)
) {
AnimatedVisibility(visible = fabVisible) {
FloatingActionButton(onClick = {}) {
Icon(Icons.Default.Star, contentDescription = null)
}
}
}
}
)
…
Obviously solutions 1 and 2 don't achieve what I want, and solution 3 (the Box solution) isn't ideal - I have to know the size of the FAB to do this (I imagine there's probably a way to remember its intrinsic size when first visible?) and the animation is odd - it goes from the corner from the middle rather than from the centre, and the elevation suddenly 'pops' right at the end:
What should I do to get the FAB entering and exiting as expected?
Try with scaleIn and scaleOut animations.
AnimatedVisibility(
visible = fabVisible,
enter = scaleIn(),
exit = scaleOut(),
) {
FloatingActionButton(onClick = {}) {
Icon(Icons.Default.Star, contentDescription = null)
}
}

How to properly use Jetpack Compose inside BottomSheetDialogFragment?

For example, I have MyBottomSheetDialogFragment with Compose LazyColumn code in the application:
class MyBottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Header", color = Color.Black)
LazyColumn(
Modifier
.weight(1f)
.fillMaxWidth()) {
items(100) {
Text("Item $it", Modifier.fillMaxWidth(), Color.Black)
}
}
}
}
}
}
}
And show it using this code:
MyBottomSheetDialogFragment().show(activity.supportFragmentManager, null)
That's what we have:
MyBottomSheetDialogFragment screen image.jpg
Now if to scroll LazyColumn list DOWN then everything works as it should, but if to scroll LazyColumn list UP then Bottom Sheet Dialog scrolls instead of LazyColumn list.
How to properly implement LazyColumn inside BottomSheetDialogFragment?
When we used the XML RecyclerView list, to fix this issue we had to wrap the RecyclerView list with NestedScrollView like described here, but how to fix it with Jetpack Compose?
You should use a BottomSheetScaffold and set the sheetContent with your bottom sheet content. Don't use the BottomSheetDialogFragment.
Here's an example of basic structure of a BottomSheetScaffold:
val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberBottomSheetState(BottomSheetValue.Expanded)
)
BottomSheetScaffold(
modifier = Modifier.navigationBarsPadding(),
scaffoldState = scaffoldState,
topBar = {
// Your topBar
},
sheetShape = BottomSheetShape,
sheetPeekHeight = MaterialTheme.spacing.large,
sheetContent = {
// Your sheet content
}
) { innerPadding ->
// You content
}
You can use the new rememberNestedScrollInteropConnection() on compose 1.2.0 which will allow compose to interrupt View's scrolling and enable nested scrolling.
In your case it will be
setContent {
Column(modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()), horizontalAlignment = Alignment.CenterHorizontally) {
Text("Header", color = Color.Black)
LazyColumn(
Modifier
.weight(1f)
.fillMaxWidth()) {
items(100) {
Text("Item $it", Modifier.fillMaxWidth(), Color.Black)
}
}
}
}

Is there a jetpack compose issue in 1.1.0-beta3 when clicking outside a specific area (focus loss)?

I encounter a strange behavior when using the jetpack compose version 1.1.0-beta3 using the following simple code snippet (two buttons that trigger an adjustment/update of a text field accordingly):
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var text by remember { mutableStateOf("No text") }
Column(
modifier = Modifier
.fillMaxSize()
.border(4.dp, Color.Red)
) {
Text(text)
Row {
TextButton({ text = "Test button on the left clicked" }) {
Text("Test 1")
}
TextButton({ text = "Test button on the right clicked" }) {
Text("Test 2")
}
}
}
}
}
}
So it seems to work unless I click on the space outside of the two buttons (e.g. the complete screen occupied by the Column area (that fills the complete screen with the .fillMaxSize() modifier)). I do NOT have this behavior with an older version e.g. compose 1.0.5.
I should be able to change the version I am using, just wanted to know whether this is a known issue and yet to be reported or whether I am using this feature wrong.
There was probably a bugfix to the latest version of Compose that deals with how click events are propagated through the hierarchy for Columns (and other composables). In your code sample above, you are only using a Column. Any clicks on the column outside of the buttons will cause the click events to pass to the parent and it seems that the parent of the Column ends up obtaining the focus on these events. This prevents your buttons from regaining focus. To get around this, the Column needs to be placed inside a Surface which specifically is designed to prevent propagating the clicks. Try this:
var text by remember { mutableStateOf("No text") }
Surface(
modifier = Modifier
.fillMaxSize()
.border(4.dp, Color.Red)
) {
Column(modifier = Modifier.fillMaxSize()) {
Text(text)
Row {
TextButton({ text = "Test button on the left clicked" }) {
Text("Test 1")
}
TextButton({ text = "Test button on the right clicked" }) {
Text("Test 2")
}
}
}
}
Check out the documentation on Surface as it describes this in detail:
Surface
You don't need to use a Surface in this case, as this bug is no longer reproduced in 1.1.0-beta4

Is ScrollableTabRow lazy

If i create like 1000 Tabs within the ScrollableTabRow will it only compose the Tabs which are visible to the screen or`will compose everytime non-visible Tabs if something changes?
Like same as a LazyColumn composes only visible items.
I've written a minimal example in which i'm just "repeat"ing the same composable a lot of times - with no item{}-like DSL available like the lazy-components have.
#Composable
#Preview
fun MinimalTabExample() {
var selectedTabIndex by remember { mutableStateOf(0) }
ScrollableTabRow(selectedTabIndex = selectedTabIndex) {
repeat(60) { tabNumber ->
Tab(
selected = selectedTabIndex == tabNumber,
onClick = { selectedTabIndex = tabNumber },
text = { Text(text = "Tab #$tabNumber") }
)
}
}
}
Also, if you look into the ScrollableTabRow component you'll find that all the tabs are measured right at the beginning: (tabs are the compose-tabs created above)
val tabPlaceables = subcompose(TabSlots.Tabs, tabs)
.map { it.measure(tabConstraints) }
I guess this is implemented this way due to the height-calculation: "Make the height as high as the highest component"
tabPlaceables.forEach {
layoutWidth += it.width
layoutHeight = maxOf(layoutHeight, it.height)
}

Resources