简体   繁体   中英

Jetpack Compose: Mimicking spinner.setSelection() inside of a DropDownMenu

The use case is that you have 10s or 100s of items inside of a dropdown menu, the dropdown options have some ordering - as with number values or alphabetical listing of words - and selections are made in succession.

When the user reopens the menu, you'd like for it to open in the same region as their last selection, so that for instance you don't jump from "car" to "apple" but rather from "car" to "cat". Or if they just opted to view order number 358, they can quickly view order number 359.

Using views, you could create a Spinner and put all of your items in an ArrayAdapter and then call spinner.setSelection() to scroll directly to the index you want.

DropdownMenu doesn't have anything like HorizontalPager 's scrollToPage() . So what solutions might exist to achieve this?

So far, I've tried adding verticalScroll() to the DropdownMenu 's modifier and trying to do arithmetic with the scrollState. But it crashes at runtime with an error saying the component has infinite height, the same error you get if you try to nest scrollable components like a LazyColumn inside of a Column with verticalScroll.

It's a known issue .

DropdownMenu has its own vertical scroll modifier inside, and there is no API to work with it.

Until this problem is fixed by providing a suitable API, the only workaround I can think of is to create your own view - you can take the source code of DropdownMenu as reference.

I'll post a more detailed answer here because I don't want to mislead anyone with my comment above.

If you're in Android Studio, click the three dots on the mouse-hover quick documentation box and select "edit source" to open the source for DropdownMenu in AndroidMenu.android.kt. Then observe that it uses a composable called DropdownMenuItemContent . Edit source again and you're in Menu.kt.

You'll see this:

@Composable
internal fun DropdownMenuContent(
...
...
...
{
        Column(
            modifier = modifier
                .padding(vertical = DropdownMenuVerticalPadding)
                .width(IntrinsicSize.Max)
                .verticalScroll(rememberScrollState()),//<-We want this
            content = content
        )
    }

So in your custom composable just replace that rememberScrollState() with your favorite variable name for a ScrollState.

And then chain that reference all the way back up to your original view.

Getting Access to the ScrollState

@Composable 
fun MyCustomDropdownMenu(
    expanded:Boolean,
    scrollStateProvidedByTopParent:ScrollState,
    ...
    ...
   )
   {...}

@Composable
fun MyCustomDropdownMenuContent(
    scrollStateProvidedByTopParent:ScrollState,
    ...
    ...
    )
    {...}


//And now for your actual content

@Composable
fun TopParent(){
    val scrollStateProvidedByTopParent=rememberScrollState()
    val spinnerExpanded by remember{mutableStateOf(false)}
    ...
    ...
    Box{
       Row(modifier=Modifier.clickable(onClick={spinnerExpanded=!spinnerExpanded}))//<-More about this line in the sequel
    {
      Text("Options")
      Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription = "")
      
      MyCustomDropdownMenu(
      expanded = spinnerExpanded,
      scrollStateProvidedByTopParent=scrollStateProvidedByTopParent,
      onDismissRequest = { spinnerExpanded = false }) 
    {//your spinner content}

       }
    }
}

The above only specifies how to access the ScrollState of the DropdownMenu . But once you have the ScrollState , you'll have to do some arithmetic to get the scroll position right when it opens. Here's one way that seems alright.

Calculating the scroll distance

Even after setting the contents of the menu items explicitly, the distance was never quite right if I relied on those values. So I used an onTextLayout callback inside the Text of my menu items in order to get the true Text height at the time of rendering. Then I use that value for the arithmetic. It looks like this:

@Composable
fun TopParent(){
    val scrollStateProvidedByTopParent=rememberScrollState()
    val spinnerExpanded by remember{mutableStateOf(false)}
    val chosenText:String by remember{mutableStateOf(myListOfSpinnerOptions[0])
    val height by remember{mutableStateOf(0)}
    val heightHasBeenChecked by remember{mutableStateOf(false)}
    val coroutineScope=rememberCoroutineScope()

    ...
    ...
    Box{
       Row(modifier=Modifier.clickable(onClick={spinnerExpanded=!spinnerExpanded
 coroutineScope.launch{scrollStateProvidedByTopParent.scrollTo(height*myListOfSpinnerOptions.indexOf[chosenText])}}))//<-This gets some arithmetic for scrolling distance
    {
      Text("Options")
      Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription = "")
          
      MyCustomDropdownMenu(
      expanded = spinnerExpanded,
      scrollStateProvidedByTopParent=scrollStateProvidedByTopParent,
      onDismissRequest = { spinnerExpanded = false }) {
        myListOfSpinnerOptions.forEach{option->  
        DropdownMenuItem(onClick={
            chosenText=option    
            spinnerExpanded=false
            }){
              Text(option,onTextLayout={layoutResult->
              if (!heightHasBeenChecked){
                 height=layoutResults.size.height
                 heightHasBeenChecked=true
                 }
              }
              )

              }
          }
      }  
   }
}  

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM