Jetpack Compose - 在Column中滚动到聚焦的组合部件

24

我有如下的UI界面:

val scrollState = rememberScrollState()
        Column(
            modifier = Modifier
                .fillMaxSize(1F)
                .padding(horizontal = 16.dp)
                .verticalScroll(scrollState)
        ) {

            TextField(...)
 // multiple textfields
             TextField(
                        //...
                        modifier = Modifier.focusOrder(countryFocus).onFocusChanged {
                            if(it == FocusState.Active) {
                               // scroll to this textfield
                            }
                        },
                    )
         }

我在这一列中有多个文本字段,当其中一个字段获得焦点时,我想滚动到该列。scrollState中有一个方法scrollState.smoothScrollTo(0f),但我不知道如何获取已聚焦的TextField的位置。

更新:

看起来我已经找到了一个可行的解决方案。我使用了onGloballyPositioned,它有效了。但我不确定这是否是解决此问题的最佳方法。

var scrollToPosition = 0.0F

TextField(
   modifier = Modifier
    .focusOrder(countryFocus)
    .onGloballyPositioned { coordinates ->
        scrollToPosition = scrollState.value + coordinates.positionInRoot().y
    }
    .onFocusChanged {
    if (it == FocusState.Active) {
        scope.launch {
            scrollState.smoothScrollTo(scrollToPosition)
        }
    }
}
)
5个回答

3

Compose 中有一个叫做 RelocationRequester 的新功能,它为我解决了问题。我在自定义的 TextField 内使用了类似以下的代码:

val focused = source.collectIsFocusedAsState()
val relocationRequester = remember { RelocationRequester() }
val ime = LocalWindowInsets.current.ime
if (ime.isVisible && focused.value) {
    relocationRequester.bringIntoView()
}

1
这对我不起作用。我的意思是View被带入屏幕高度(frame),但它仍然在输入法之后。我认为输入法背后的空间仍然被视为可见空间。我有什么遗漏吗? - rafa
bringIntoView() 的行为很奇怪... 在 One Plus 和 Pixel 设备上工作正常,但在 Redmi 设备上不起作用。 - Ali_Waris
2
你能否更清楚地回答并解释一下“源代码”是什么? - alfietap
bringIntoView() 之前添加 awaitFrame() 解决了我的问题。 - OscarCreator

3
同时,你可以使用BringIntoViewRequester。
//
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val coroutineScope = rememberCoroutineScope()
//--------
TextField( ..., modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent {
                        if (it.isFocused) {
                            coroutineScope.launch {
                                bringIntoViewRequester.bringIntoView()
                            }
                        }
                    }

1
这是正确的方式。 - deviant

1

0
这是我用来确保表单字段不被键盘遮挡的代码:
来源:stack overflow - detect when keyboard is open
enum class Keyboard {
Opened, Closed
}

@Composable
fun keyboardAsState(): State<Keyboard> {
    val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
    val view = LocalView.current
    DisposableEffect(view) {
    val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
        val rect = Rect()
        view.getWindowVisibleDisplayFrame(rect)
        val screenHeight = view.rootView.height
        val keypadHeight = screenHeight - rect.bottom
        keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
            Keyboard.Opened
        } else {
            Keyboard.Closed
        }
    }
    view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)

    onDispose {
        
    view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
         }
    }

    return keyboardState
}

然后在我的可组合函数中:

val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()

val isKeyboardOpen by keyboardAsState()

if (isKeyboardOpen == Keyboard.Opened) {
    val view = LocalView.current
    val screenHeight = view.rootView.height
    scope.launch { scrollState.scrollTo((screenHeight * 2)) }
}

Surface(modifier = Modifier
    .fillMaxHeight()
    .verticalScroll(scrollState),
    
)  {
  //Rest of your Composables, Columns, Rows, TextFields, Buttons

  //add this so the screen can scroll up and keyboard cannot cover the form fields - Important!
 /*************************************************/
  if (isKeyboardOpen == Keyboard.Opened) {
     Spacer(modifier = Modifier.height(140.dp))
  }

}

希望能对某些人有所帮助。我正在使用:
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scope = rememberCoroutineScope()
val view = LocalView.current
DisposableEffect(view) {
    val listener = ViewTreeObserver.OnGlobalLayoutListener {
        scope.launch { bringIntoViewRequester.bringIntoView() }
    }
    view.viewTreeObserver.addOnGlobalLayoutListener(listener)
    onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
Surface(modifier.bringIntoViewRequester(bringIntoViewRequester)) {

///////////rest of my composables
}

但是这并没有起作用。


0
对于常规的Column,您可以使用以下扩展函数:
这里是完整的Gist源代码。
fun Modifier.bringIntoView(
    scrollState: ScrollState
): Modifier = composed {
    var scrollToPosition by remember {
        mutableStateOf(0f)
    }
    val coroutineScope = rememberCoroutineScope()
    this
        .onGloballyPositioned { coordinates ->
            scrollToPosition = scrollState.value + coordinates.positionInRoot().y
        }
        .onFocusEvent {
            if (it.isFocused) {
                coroutineScope.launch {
                    scrollState.animateScrollTo(scrollToPosition.toInt())
                }
            }
        }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接