I'm developing a palyer app using jetpack compose and I want to play/pause programatically.
here is my code:
val exoPlayer = ExoPlayer.Builder(context).build()
.also { exoPlayer ->
val mediaItem = MediaItem.Builder()
.setUri("www.mp4")
.build()
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.playWhenReady = true
}
DisposableEffect(
key1 = AndroidView(
factory = {
StyledPlayerView(context).apply {
hideController()
useController = false
player = exoPlayer
exoPlayer.videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT
exoPlayer.playWhenReady = true
}
}
),
effect = {
onDispose { exoPlayer.release() }
}
)
also I created a LaunchedEffect which makes the player stop after 5 seconds
here is my LaunchedEffect:
LaunchedEffect(
key1 = shouldShowValidator,
block = {
if (shouldShowValidator) {
exoPlayer.playWhenReady = false
exoPlayer.pause()
}
}
)
I expect to pause the player but i dont get that!
what is the problem ?
I managed to pause the video after 5 seconds using the following code
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri("<Enter video url>"))
prepare()
playWhenReady = true
}
}
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
delay(5000)
withContext(Dispatchers.Main) {
exoPlayer.playWhenReady = false
Toast.makeText(context, "Paused", Toast.LENGTH_SHORT).show()
}
}
}
DisposableEffect(
key1 = AndroidView(
factory = {
StyledPlayerView(context).apply {
player = exoPlayer
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
}, modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(8.dp)
)
) {
onDispose {
exoPlayer.release()
}
}
PS: shouldShowValidator could be the reason the video is not being paused in your case.
the problem is you should use Dispatchers.Main for pausing the player
LaunchedEffect(
key1 = shouldShowValidator,
block = {
if (shouldShowValidator) {
withContext(
context = Dispatchers.Main,
block = {
exoPlayer.playWhenReady = false
exoPlayer.pause()
}
)
}
}
)
Related
Could you pelase help me?
I want to use swipe to delete and tried a lot of things, but nothing works good. The best way is:
items(listactivity.size) { index ->
val activityItem = listactivity.getOrNull(index)
if (activityItem != null) {
key(activityItem) {
val dismissState = rememberDismissState()
LaunchedEffect(dismissState.currentValue) {
if (dismissState.currentValue == DismissValue.DismissedToStart) {
dashboardViewModel.activityForDelete = activityItem
println("ACT ITEM " + activityItem.activityType?.activityTypeName)
visibleDeleteDialog.value = true
dismissState.snapTo(DismissValue.Default)
}
}
if (visibleDeleteDialog.value) {
BaseAlertDialog(
onExit = {
visibleDeleteDialog.value = false
},
onSuccess = {
removeActivity()
visibleDeleteDialog.value = false
},
disclaimerFirst = R.string.confirm_delete_activity,
disclaimerSecond = R.string.confirm_delete_activity_text,
successName = R.string.delete_session
)
}
SwipeToDismiss(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
background = {
val direction =
dismissState.dismissDirection ?: return#SwipeToDismiss
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> CenterEnd
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Black)
.padding(horizontal = Spacing_12),
contentAlignment = alignment
) {
Text(
text = stringResource(id = R.string.delete),
color = Color.Red,
fontSize = Text_16,
fontWeight = FontWeight.Bold
)
}
},
dismissContent = {
GameCard(activityItem)
}
)
}
}
}
However th lisitem do not update items after delete immidiately (only after scroll i can see what item was deleted).
What am i doing wrong?
I tried SwipeToDismiss and other internet things.
I know google has a bug in this function items() some times,
but please try to replace your function with itemsIndexed and add another parameter
itemsIndexed(listactivity.size) { index , item ->
something like this
// import this at the top
import androidx.compose.foundation.lazy.itemsIndexed
//code
//*
//*
//*
itemsIndexed(listactivity.size) { index , item ->
val activityItem = listactivity.getOrNull(index)
if (activityItem != null) {
key(activityItem) {
val dismissState = rememberDismissState()
LaunchedEffect(dismissState.currentValue) {
if (dismissState.currentValue == DismissValue.DismissedToStart) {
dashboardViewModel.activityForDelete = activityItem
println("ACT ITEM " + activityItem.activityType?.activityTypeName)
visibleDeleteDialog.value = true
dismissState.snapTo(DismissValue.Default)
}
}
if (visibleDeleteDialog.value) {
BaseAlertDialog(
onExit = {
visibleDeleteDialog.value = false
},
onSuccess = {
removeActivity()
visibleDeleteDialog.value = false
},
disclaimerFirst = R.string.confirm_delete_activity,
disclaimerSecond = R.string.confirm_delete_activity_text,
successName = R.string.delete_session
)
}
SwipeToDismiss(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
background = {
val direction =
dismissState.dismissDirection ?: return#SwipeToDismiss
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> CenterEnd
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Black)
.padding(horizontal = Spacing_12),
contentAlignment = alignment
) {
Text(
text = stringResource(id = R.string.delete),
color = Color.Red,
fontSize = Text_16,
fontWeight = FontWeight.Bold
)
}
},
dismissContent = {
GameCard(activityItem)
}
)
}
}
}
I don't have the time to go over all of your code to make more changes that maybe will make it a better fit for your code and needs. but I am sure you are able to do so!
you can see my code when I had a similar problem maybe it will help to solve your problem.
itemsIndexed(celebsSearchList) { index, item ->
if (celebsSearchList.isEmpty()) {
println("no celebs")
} else {
val celebie = item
// celebsSearchList.forEach { celebie ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 10.dp),
elevation = 8.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = celebie.FirstName + " " + celebie.LastName,
style = MaterialTheme.typography.h6
)
Button(
onClick = {
CelebForCelebProfile = celebie
navController.navigate(Screen.CelebProfileScreen.route)
}, colors = ButtonDefaults.buttonColors(
backgroundColor = Color.White, contentColor = Color.Gray
)
) {
Text(text = "View Profile")
}
}
}
}
}```
if you like me please upvote me :) help me gain more points on stack over flow and comment if you have any questions.
I made LazyColumn with vertical scroll bar and it's work good, but when i scroll mouse, column just jumping(not smooth), but when I scroll vert. bar, it's smooth
#ExperimentalFoundationApi
#OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
#Composable
fun App() {
Box(modifier = Modifier.fillMaxSize().padding(10.dp)
) {
val animatedpr = remember { androidx.compose.animation.core.Animatable(initialValue = 0.8f) }
val stateVertical = rememberLazyListState(0)
LaunchedEffect(Unit){animatedpr.animateTo(targetValue = 1f, animationSpec = tween(300, easing = LinearEasing))}
LazyColumn(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, state = stateVertical) {
items(1100) {
OutlinedCard(modifier = Modifier.size(500.dp, 100.dp).padding(20.dp).animateItemPlacement().graphicsLayer(scaleY = animatedpr.value, scaleX = animatedpr.value)) {
}
}
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
adapter = rememberScrollbarAdapter(
scrollState = stateVertical
)
)
}
}
better sollution
if(it.changes.first().scrollDelta.y == 1.0f){
scope.launch { stateVertical.animateScrollBy(200.0f) }
}
else{
scope.launch {
scope.launch { stateVertical.animateScrollBy(-200.0f) }
}
}
The problem is that you use a single scroll state for two different scroll views. This is causing the jumping while recomposing.
I fixed it by adding "scrollhandler"(onPointerEvent(PointerEventType.Scroll))
val scope = rememberCoroutineScope() // coroutine for scroling(idk, i know coroutines very bad)
val stateVertical = rememberLazyListState(0)
.....
LazyColumn(modifier = Modifier.fillMaxSize().onPointerEvent(PointerEventType.Scroll){
var currentItem = stateVertical.layoutInfo.visibleItemsInfo[0].index
val itemsToScrolling = stateVertical.layoutInfo.visibleItemsInfo.size/2 // how many items we scrolling
if(it.changes.first().scrollDelta.y == 1.0f){ // scroll down
scope.launch { stateVertical.animateScrollToItem(currentItem+itemsToScrolling) }
}
else{ // scroll up
if(currentItem < itemsToScrolling){currentItem = itemsToScrolling} // because we cannot animate to negative number
scope.launch { stateVertical.animateScrollToItem(currentItem-itemsToScrolling) }
}
}, state = stateVertical){
*items*
}
I've got a project that is built on the serverless Twilio Voice JavaScript quickstart. Here's a link to the quickstart.
https://www.twilio.com/docs/voice/sdks/javascript/get-started#information
Below is the chunk of code where it allows the user to select an audio output device. Is it possible to select more than one audio output device, so my users can hear their phones ring through their speakers AND their headsets at the same time?
function updateDevices(selectEl, selectedDevices) {
selectEl.innerHTML = '';
device.audio.availableOutputDevices.forEach(function (device, id) {
let isActive = selectedDevices.size === 0 && id === 'default';
selectedDevices.forEach(function (device) {
if (device.deviceId === id) {
isActive = true;
}
});
const option = document.createElement('option');
option.label = device.label;
option.setAttribute('data-id', id);
if (isActive) {
option.setAttribute('selected', 'selected');
}
selectEl.appendChild(option);
});
}
function updateAllAudioDevices() {
if (device) {
updateDevices(speakerDevices, device.audio.speakerDevices.get());
updateDevices(ringtoneDevices, device.audio.ringtoneDevices.get());
}
}
async function getAudioDevices() {
await navigator.mediaDevices.getUserMedia({ audio: true });
updateAllAudioDevices.bind(device);
}
function updateOutputDevice() {
const selectedDevices = Array.from(speakerDevices.children)
.filter((node) => node.selected)
.map((node) => node.getAttribute('data-id'));
device.audio.speakerDevices.set(selectedDevices);
}
function updateRingtoneDevice() {
const selectedDevices = Array.from(ringtoneDevices.children)
.filter((node) => node.selected)
.map((node) => node.getAttribute('data-id'));
device.audio.ringtoneDevices.set(selectedDevices);
}
function bindVolumeIndicators(call) {
call.on('volume', function (inputVolume, outputVolume) {
let inputColor = 'red';
if (inputVolume < 0.5) {
inputColor = 'green';
} else if (inputVolume < 0.75) {
inputColor = 'yellow';
}
inputVolumeBar.style.width = `${Math.floor(inputVolume * 300)}px`;
inputVolumeBar.style.background = inputColor;
let outputColor = 'red';
if (outputVolume < 0.5) {
outputColor = 'green';
} else if (outputVolume < 0.75) {
outputColor = 'yellow';
}
outputVolumeBar.style.width = `${Math.floor(outputVolume * 300)}px`;
outputVolumeBar.style.background = outputColor;
});
}
Sometimes the app crashes with exception "java.lang.IllegalArgumentException: View=androidx.compose.ui.window.PopupLayout{...} not attached to window manager".
I'm only able to reproduce it with testcase and it happens only sometimes.
My test case: list with 3 items, let's call them A, B, C; and test case has basically 3 steps:
Update item A text and remove item B
Update item A text and remove item C
Try to add item C back to list (and sometimes this causes a crash). (The problem only happens when adding item with the same key back that removed one was)
My hypothesis why it happens:
It seems to be happen only when update text A causes to go A text to more lines than it was before. So the recomposition redrew the item A and it will have more height than before
If I run fetchSemanticsNode before step3, then it seems it crashes only when item C is still cached (the node exists but it's not displayed)
Seems like these both conditions needs to be true to error to happen. So I was wondering am I using LazyColumn somehow wrongly or there seems to be bug in Jetpack Compose code?
Below is the full exception and my code and test case to reproduce it.
App is crashing with following exception:
java.lang.IllegalArgumentException: View=androidx.compose.ui.window.PopupLayout{7744d0c V.E...... ......ID 0,0-62,75 #1020002 android:id/content} not attached to window manager
at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:544)
at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:433)
at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:116)
at androidx.compose.ui.window.PopupLayoutHelperImpl.updateViewLayout(AndroidPopup.android.kt:776)
at androidx.compose.ui.window.PopupLayout.updatePosition(AndroidPopup.android.kt:659)
at androidx.compose.ui.window.PopupLayout.updateParentBounds$ui_release(AndroidPopup.android.kt:626)
at androidx.compose.ui.window.PopupLayout.updateParentLayoutCoordinates(AndroidPopup.android.kt:581)
at androidx.compose.ui.window.AndroidPopup_androidKt$Popup$7.invoke(AndroidPopup.android.kt:316)
at androidx.compose.ui.window.AndroidPopup_androidKt$Popup$7.invoke(AndroidPopup.android.kt:310)
at androidx.compose.ui.layout.OnGloballyPositionedModifierImpl.onGloballyPositioned(OnGloballyPositionedModifier.kt:59)
at androidx.compose.ui.node.LayoutNode.dispatchOnPositionedCallbacks$ui_release(LayoutNode.kt:1149)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:51)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatch(OnPositionedDispatcher.kt:44)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.dispatchOnPositionedCallbacks(MeasureAndLayoutDelegate.kt:348)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.dispatchOnPositionedCallbacks$default(MeasureAndLayoutDelegate.kt:344)
at androidx.compose.ui.platform.AndroidComposeView.measureAndLayout(AndroidComposeView.android.kt:761)
at androidx.compose.ui.node.Owner.measureAndLayout$default(Owner.kt:196)
at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:954)
at android.view.View.draw(View.java:22353)
at android.view.View.updateDisplayListIfDirty(View.java:21226)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:559)
at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:565)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:642)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:4101)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3828)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3099)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1952)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8171)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:731)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
UI
const val LAZY_COLUMN_TEST_TAG = "lazy_column_tag"
const val UNDO_TEST_TAG = "undo_tag"
const val ITEM_FIELD_TEST_TAG = "item_field_tag"
#Composable
fun ScreenInitWrapper(viewModel: MyViewModel = hiltViewModel()) {
viewModel.initModel()
if (viewModel.isLoaded) {
Screen()
}
}
#Composable
fun Screen(viewModel: MyViewModel = hiltViewModel()) {
val items = viewModel.items
val scrollState = rememberLazyListState()
LazyColumn(
state = scrollState,
modifier = Modifier.testTag(LAZY_COLUMN_TEST_TAG)
) {
item {
UndoButton(viewModel)
}
items(items = items, key = { item -> "ITEM_${item.id}" }) { item ->
ItemText(item, viewModel)
}
}
}
#Composable
fun UndoButton(viewModel: MyViewModel) {
IconButton(
onClick = { viewModel.undo() }, enabled = true,
modifier = Modifier.testTag(UNDO_TEST_TAG)
) {
Icon(
imageVector = Icons.Filled.Undo, contentDescription = "undo",
modifier = Modifier.padding(start = 16.dp, end = 16.dp)
)
}
}
#Composable
fun ItemText(item: MyItem, viewModel: MyViewModel) {
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = item.text, selection = item.selection)) }
val textFieldValue = textFieldValueState.copy(text = item.text, selection = item.selection)
TextField(
value = textFieldValue,
onValueChange = {
textFieldValueState = it
if (item.selection != it.selection) {
item.selection = it.selection
}
if (item.text != it.text) {
item.text = it.text
}
},
modifier = Modifier
.onPreviewKeyEvent {
if (isBackspaceClickedAndCursorIsBeginningOfLine(it, textFieldValue)) {
if (item.id != 1) {
viewModel.removeItemAndUpdatePreviousItemText(item)
}
true
} else {
false
}
}
.testTag("${ITEM_FIELD_TEST_TAG}_${item.id}"),
)
}
#OptIn(ExperimentalComposeUiApi::class)
private fun isBackspaceClickedAndCursorIsBeginningOfLine(it: KeyEvent, textFieldValue: TextFieldValue): Boolean {
if (it.key == Key.Backspace && it.type == KeyEventType.KeyDown) {
val currentPosition = textFieldValue.selection.end
if (currentPosition == 0) {
return true
}
}
return false
}
ViewModel
data class MyItem (val id: Int, var text: String, var selection: TextRange = TextRange.Zero)
#HiltViewModel
class MyViewModel : ViewModel() {
var isLoaded: Boolean by mutableStateOf(false)
private set
private var _items = emptyList<MyItem>().toMutableStateList()
var items: List<MyItem> = _items
private set
private val undoStack = ArrayDeque<MyItem>()
fun initModel() {
if (!isLoaded) {
// The names matter
val itemsTemp = listOf(
MyItem(id = 1, text = "some text long enough for item 1"),
MyItem(id = 2, text = "item 2"),
MyItem(id = 3, text = "item 3"),
).toMutableList()
_items.addAll(itemsTemp)
isLoaded = true
}
}
fun removeItemAndUpdatePreviousItemText(deletedItem: MyItem) {
// Update previous item text
val index = items.indexOfFirst { it.id == deletedItem.id }
val previousItemText = items[index - 1]
// I was able to get this exception only when the amount of lines of the previous text changed
previousItemText.text = previousItemText.text + "Some new text to make it more lines"
// Undo
undoStack.addLast(deletedItem)
// Remove value from list
val index = items.indexOfFirst { it.id == item.id }
_items.removeAt(index)
}
fun undo() = viewModelScope.launch {
val deletedItem = undoStack.removeLastOrNull()
if (deletedItem != null) {
_items.add(deletedItem)
}
}
}
InstrumentedTest
#RunWith(AndroidJUnit4::class)
#FixMethodOrder(MethodSorters.NAME_ASCENDING)
#LargeTest
#HiltAndroidTest
class UndoRemoveItemInstrumentedTest {
#get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
#get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()
#Test
undoRemoveItemTest() {
removeItem(2)
removeItem(3)
// First undo causes sometimes failure
composeRule.onNodeWithTag(UNDO_TEST_TAG).performClick()
composeRule.onNodeWithTag(UNDO_TEST_TAG).performClick()
}
#OptIn(ExperimentalTestApi::class)
private fun removeItem(id: Int) {
composeRule.onNodeWithTag(LAZY_COLUMN_TEST_TAG)
.performScrollToKey("ITEM_${id}")
composeRule.onNodeWithTag("${ITEM_FIELD_TEST_TAG}_${id}").performClick()
composeRule.onNodeWithTag("${ITEM_FIELD_TEST_TAG}_${id}")
.performTextInputSelection(TextRange(0))
performBackspaceKeyPress(id)
}
private fun performBackspaceKeyPress(itemId: Int) {
val backspaceDown = android.view.KeyEvent(NativeKeyEvent.ACTION_DOWN, NativeKeyEvent.KEYCODE_DEL)
val backspaceUp = android.view.KeyEvent(NativeKeyEvent.ACTION_UP, NativeKeyEvent.KEYCODE_DEL)
composeRule.onNodeWithTag("${ITEM_FIELD_TEST_TAG}_${itemId}").performKeyPress(KeyEvent(backspaceUp))
composeRule.onNodeWithTag("${ITEM_FIELD_TEST_TAG}_${itemId}").performKeyPress(KeyEvent(backspaceDown))
}
}
Seems to be fixed in compose version 1.3.0. At least I'm not able to reproduce it in this version
I have UI like this:
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
}
},
)
}
I have multiple TextFields in this column and when one of them is focused I want to scroll Column to it. There is a method in scrollState scrollState.smoothScrollTo(0f) but I have no idea how to get a focused TextField position.
Update:
It seems that I've found a working solution. I've used onGloballyPositioned and it works. But I'm not sure if it the best way of solving this.
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)
}
}
}
)
There is a new thing in compose called RelocationRequester. That solved the problem for me. I have something like this inside of my custom TextField.
val focused = source.collectIsFocusedAsState()
val relocationRequester = remember { RelocationRequester() }
val ime = LocalWindowInsets.current.ime
if (ime.isVisible && focused.value) {
relocationRequester.bringIntoView()
}
Also you can use BringIntoViewRequester
//
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val coroutineScope = rememberCoroutineScope()
//--------
TextField( ..., modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent {
if (it.isFocused) {
coroutineScope.launch {
bringIntoViewRequester.bringIntoView()
}
}
}
It seems that using LazyColumn and LazyListState.animateScrollToItem() instead of Column could be a good option for your case.
Reference: https://developer.android.com/jetpack/compose/lists#control-scroll-position
By the way, thank you for the information about onGloballyPositioned() modifier. I was finding a solution for normal Column case. It saved me a lot of time!
Here's some code I used to make sure that the fields in my form were not cut off by the keyboard:
From: 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
}
and then in my composable:
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))
}
}
Hope it helps someone. I was using:
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
}
But this did not work.