OnScrolled in LazyColumnFor ( Jetpack Compose ) - android

Has anyone found an equivalent of onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) in LazyColumnFor?

You can get the position of a first item by adding an onGloballyPositioned https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier option to the first item's modifier.
It returns:
an initial position after layout creation
an updated position every time during a scroll event
The LayoutCoordinates object inside the lambda let you get:
positionInParent()
positionInRoot()
positionInWindow()
To get an x or y from the LazyColumn/LazyRow you can use this part of code:
val initialLayoutY: MutableState<Float?> = rememberSaveable { mutableStateOf(null) }
Text(
text = "Lorem",
modifier = Modifier.onGloballyPositioned { layoutCoordinates ->
if (initialLayoutY.value == null) {
initialLayoutY.value = layoutCoordinates.positionInRoot().y
}
val currentTextLayoutY: Float = layoutCoordinates.positionInRoot().y
val currentLazyListY: Float = initialLayoutY.value!! - currentTextLayoutY
Log.e("Lazy Column y", currentLazyListY.toString())
}
)
Unfortunately, it skips a lot of values during fast scrolling. Here is a result:

It is not exactly onScrolled.
You can use LazyListState#isScrollInProgress to check if the list is currently scrolling by gesture, fling or programmatically or not.
val itemsList = (0..55).toList()
val state = rememberLazyListState()
val scrollInProgress: Boolean by remember {
derivedStateOf {
state.isScrollInProgress
}
}
if (scrollInProgress){
//....do something
}
LazyColumn(state = state) {
items(itemsList) {
Text(".....")
}
}

with 1.0.0-alpha12
It should be what you want:
val scrollCallback = object : ScrollCallback {
override fun onCancel() {
super.onCancel()
}
override fun onScroll(scrollDistance: Float): Float {
return super.onScroll(scrollDistance)
}
override fun onStart(downPosition: Offset) {
super.onStart(downPosition)
}
override fun onStop(velocity: Float) {
super.onStop(velocity)
}
}
LazyColumnFor(
modifier = Modifier.scrollGestureFilter(scrollCallback, Orientation.Vertical),
items = listOf("")
) { item ->
}

Related

How Can My Composable Function Know About His Childrens? (Jetpack Compose, Android, Kotlin)

Im trying to createa a #composable function that is able to keep track of all its children.
The first Parent TextExecutionOrder should be able to tell that it has 3 Children of the same composable function TestExecutionOrder("2"), TestExecutionOrder("3") and TestExecutionOrder("10").
#Preview
#Composable
fun test() {
TestExecutionOrder("1") {
TestExecutionOrder("2") {
TestExecutionOrder("15") {}
TestExecutionOrder("5") {
TestExecutionOrder("6") {}
TestExecutionOrder("7") {}
}
}
TestExecutionOrder("3") {}
TestExecutionOrder("10") {}
}
}
For Example the above Code could have a datastructure like a Stack, CircularArray or anything else where it stores
the following.
Parent{1} -> Childs{2,3,10}
Parent{2} -> Childs{15,5}
Parent{15} -> Childs{}
Parent{5} -> Childs{6,7}
Parent{6} -> Childs{}
Parent{7} -> Childs{}
Parent{3} -> Childs{}
Parent{10} -> Childs{}
v
data class Faaa(val name: String)//val size : IntSize,val pos: Offset)
#Composable
fun TestExecutionOrder(
text: String,
arr: CircularArray<Faaa>,
stack: Stack<CircularArray<Faaa>>,
content: #Composable () -> Unit,
) {
//TODO
content()
}
In QML I would be able to iterate through the children elements of a parent and then be able to add
all Items that are an instance of TestExecutionOrder inside my desired data structure.
I tried to use State-hoisting where my Stack data structure is at top of my test() function and then passed through
all children. Where each children will only get the stack().peek() reference of the current circular array but Kotlin
is pass by value so this also doesn't work.
Pass By Reference Solution that obv doesnt work :D
#Composable
fun TestExecutionOrder(
text: String,
arr: CircularArray<Faaa>,
stack: Stack<CircularArray<Faaa>>,
content: #Composable () -> Unit,
) {
arr.addLast(Faaa(text)) // Same reference for all children
stack.push(CircularArray<Faaa>()) // create new circularArray for new children
content()
}
data class Faaa(val name: String)//val size : IntSize,val pos: Offset)
#Preview
#Composable
fun test() {
val stack = Stack<CircularArray<Faaa>>()
stack.push(CircularArray<Faaa>())
TestExecutionOrder("1",stack.peek(),stack) {
var referenceCir = stack.peek()
TestExecutionOrder("2",referenceCir,stack) {
var referenceCir2 = stack.peek()
TestExecutionOrder("15",referenceCir2,stack) {}
TestExecutionOrder("5",referenceCir2,stack) {
var referenceCir3 = stack.peek()
TestExecutionOrder("6",referenceCir3,stack) {}
TestExecutionOrder("7",referenceCir3,stack) {}
}
}
TestExecutionOrder("3",referenceCir,stack) {}
TestExecutionOrder("10",referenceCir,stack) {}
}
}
I am assuming I am overthinking this stuff because I came from a QML/C++ Environment. How can one achieve this kind of stuff?
The Goal is to make this thing self managing I wrap my composable function around other functions and it automatically knows how many children of the same type it has without me explicitly passing it as a parameter.
EDIT1: Im aware that compose function can execute in any order
I am not sure that I understood your question correctly, but:
I would recommend you to think of a composable function as of a way to describe the UI, not an object.
So you should describe your UI in a way, that is not very tied up to execution order, since indeed it is a bit hard to predict.
Assuming your goal, I recommend you to create a single composable function that will draw all "children" and will also manage the movement of the box.
It is unlikely that parent composable will execute after children composables, since composable functions are being called. Therefore to call the child function the system needs to call the parent first.
I created a custom Tree DataStructure and used Kotlins "pass by reference" and it works. It is able to keep track of its children and also of its size (only the global/absolute position is not working(any suggestions are appreciated).
The basic Idea is to create a child at the beginning of the composable then continue with the passed Lambda-Scope(content) and after that go back to the parent.
Track Item Composable
class ToggleViewModel : ViewModel() { //View Model acts as a pass by reference #see todo add stackoverflow
var tree: TreeNode<Data> = TreeNode(Data("root"))
}
data class Data(
val name: String,
var size: IntSize = IntSize(0, 0),
var pos: Offset = Offset(0f, 0f)
)
#Composable
fun TrackItem(
text: String,
toggleViewModel: ToggleViewModel = viewModel(),
content: #Composable () -> Unit,
) {
//empty the list after a recomposition
if(toggleViewModel.tree.value.name == "root"){
toggleViewModel.tree.children = mutableListOf()
}
toggleViewModel.tree.addChild(Data(text))
//step 1 new tree is the child
val lastChildrenIndex = toggleViewModel.tree.children.size - 1
toggleViewModel.tree = toggleViewModel.tree.children[lastChildrenIndex]
var relativeToComposeRootPosition by remember { mutableStateOf(Offset(0f, 0f)) }
var size by remember { mutableStateOf(IntSize(0, 0)) }
//assign new children pos and size after recomposition
toggleViewModel.tree.value = Data(text, size, relativeToComposeRootPosition)
Box(modifier = Modifier
.onGloballyPositioned { coordinates ->
relativeToComposeRootPosition = coordinates.positionInRoot()
}
.onSizeChanged {
size = it
}) {
content()
}
//reverse step1
if (toggleViewModel.tree.parent != null) {
toggleViewModel.tree = toggleViewModel.tree.parent!!
}
}
Tree DataStructure
class TreeNode<T>(var value: T){
var parent:TreeNode<T>? = null
var children:MutableList<TreeNode<T>> = mutableListOf()
var currentChildren: TreeNode<T>? = null;
private var currentIndex = 0
fun addChild(nodeValue: T){
val node = TreeNode(nodeValue)
children.add(node)
node.parent = this
if(children.size == 1){ //change this once adding removeChild fun()
currentChildren = children[0]
}
}
fun nextChildren(){
currentIndex++
if(children.size == currentIndex +1){
currentIndex = 0
}
currentChildren = children[currentIndex]
}
fun previousChildren(){
currentIndex--
if(0 > currentIndex){
currentIndex = children.size - 1
}
currentChildren = children[currentIndex]
}
override fun toString(): String {
var s = "$value"
if (children.isNotEmpty()) {
s += " {" + children.map { it.toString() } + " }"
}
return s
}
}
Example Usage
#Preview
#Composable
fun ToggleFunctionality() {
TrackItem("0") {
Box(
modifier = Modifier
.width(PixelToDp(pixelSize = 200))
.offset(x = PixelToDp(pixelSize = 100), y = PixelToDp(pixelSize = 50))
.height(PixelToDp(pixelSize = 200))
.background(Color.Red)
) {
TrackItem("1") {
Column(
) {
TrackItem("2") {
Box(
Modifier
.size(PixelToDp(pixelSize = 20))
.background(Color.Green)
)
}
TrackItem("3") {
Box(
Modifier
.size(PixelToDp(pixelSize = 20))
.background(Color.Blue)
)
}
}
}
}
}
val toggleViewModel: ToggleViewModel = viewModel()
Text(text= "test", modifier = Modifier
.clickable {
log("TEST")
for (item in toggleViewModel.tree.children) {
log(item.toString())
}
})
}
which prints the following
Data(name=0, size=200 x 200, pos=Offset(0.0, 0.0)) {[Data(name=1, size=20 x 40, pos=Offset(100.0, 50.0)) {[Data(name=2, size=20 x 20, pos=Offset(100.0, 50.0)), Data(name=3, size=20 x 20, pos=Offset(100.0, 70.0))] }] }

Jetpack Compose Layout is there a way to determine measurable type or id when building custom components?

Is there a way to determine which measurable is what kind of component is Row, Column, Box, or a custom component?
#Composable
fun MyCustomComponent(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
Consider this as a chat layout with name on top but only in group chats when it's a row that sent by other participants, quoted text available only when a user quotes, image or message and some other type. And doing some operations based on availability of any of these components. And order is not fixed, you might not know the order of any component under every condition, it might change based on different situations. With Views this is possible by checking ids or checking View's instance type
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// some other stuff
val parent = parent as LinearLayout
val quotedMessageLayout = parent.findViewById<ViewGroup>(R.id.quoted_message_frame)
val userNameLayout = parent.findViewById<ViewGroup>(R.id.layoutUserName)
if (quotedMessageLayout != null && quotedMessageLayout.visibility == VISIBLE) {
if (widthFlexBox < quotedMessageLayout.measuredWidth) {
val quoteParams = quotedMessageLayout.layoutParams as LinearLayout.LayoutParams
val quoteWidth = quotedMessageLayout.measuredWidth + quoteParams.marginStart + quoteParams.marginEnd
val quoteMaxSize = min(parentWidth, quoteWidth)
widthFlexBox = max(widthFlexBox, quoteMaxSize)
}
}
if (userNameLayout != null && userNameLayout.visibility == VISIBLE) {
if (widthFlexBox < userNameLayout.measuredWidth) {
val userNameParams = userNameLayout.layoutParams as LinearLayout.LayoutParams
val userNameWidth = userNameLayout.measuredWidth + userNameParams.marginStart + userNameParams.marginEnd
val userNameMaxSize = min(parentWidth, userNameWidth)
widthFlexBox = max(widthFlexBox, userNameMaxSize)
}
}
setMeasuredDimension(widthFlexBox, heightFlexBox)
}
Checking out this article about creating custom modifiers, it makes me think can i add type modifiers to Composable functions or even better way to check out which Composable it is from measureables?
You can add layoutId to views inside your content using Modifier.layoutId, and then read these values from measurables.
enum class LayoutId {
Box,
Row,
}
#Composable
fun TestScreen(
) {
Layout(content = {
Box(Modifier.layoutId(LayoutId.Box)) {
}
Row(Modifier.layoutId(LayoutId.Row)) {
}
}) { measurables, constraints ->
measurables.forEach { measurable ->
when (val layoutId = measurable.layoutId as? LayoutId) {
LayoutId.Box -> {
println("box found")
}
LayoutId.Row -> {
println("row found")
}
null -> {
println("unknown layout id: ${measurable.layoutId}")
}
}
}
layout(0, 0) {
}
}
}

jetpack compose: scroll to bottom listener (end of list)

I am wondering if it is possible to get observer inside a #Compose function when the bottom of the list is reached (similar to recyclerView.canScrollVertically(1))
Thanks in advance.
you can use rememberLazyListState() and compare
scrollState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == scrollState.layoutInfo.totalItemsCount - 1
How to use example:
First add the above command as an extension (e.g., extensions.kt file):
fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
Then use it in the following code:
#Compose
fun PostsList() {
val scrollState = rememberLazyListState()
LazyColumn(
state = scrollState,),
) {
...
}
// observer when reached end of list
val endOfListReached by remember {
derivedStateOf {
scrollState.isScrolledToEnd()
}
}
// act when end of list reached
LaunchedEffect(endOfListReached) {
// do your stuff
}
}
For me the best and the simplest solution was to add LaunchedEffect as the last item in my LazyColumn:
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(someItemList) { item ->
MyItem(item = item)
}
item {
LaunchedEffect(true) {
//Do something when List end has been reached
}
}
}
I think, based on the other answer, that the best interpretation of recyclerView.canScrollVertically(1) referred to bottom scrolling is
fun LazyListState.isScrolledToTheEnd() : Boolean {
val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
return lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset
}
Starting from 1.4.0-alpha03 you can use LazyListState#canScrollForward to check if you are at the end of the list.
Something like:
val state = rememberLazyListState()
val isAtBottom = !state.canScrollForward
LaunchedEffect(isAtBottom){
if (isAtBottom) doSomething()
}
Before this release you can use the LazyListState#layoutInfo that contains information about the visible items. Note the you should use derivedStateOf to avoid redundant recompositions.
Use something:
#Composable
private fun LazyListState.isAtBottom(): Boolean {
return remember(this) {
derivedStateOf {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
false
} else {
val lastVisibleItem = visibleItemsInfo.last()
val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset
(lastVisibleItem.index + 1 == layoutInfo.totalItemsCount &&
lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)
}
}
}.value
}
The code above checks not only it the last visibile item == last index in the list but also if it is fully visible (lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight).
And then:
val state = rememberLazyListState()
var isAtBottom = state.isAtBottom()
LaunchedEffect(isAtBottom){
if (isAtBottom) doSomething()
}
LazyColumn(
state = state,
){
//...
}
Simply use the firstVisibleItemIndex and compare it to your last index. If it matches, you're at the end, else not. Use it as lazyListState.firstVisibleItemIndex
Found a much simplier solution than other answers. Get the last item index of list. Inside itemsIndexed of lazyColumn compare it to lastIndex. When the end of list is reached it triggers if statement. Code example:
LazyColumn(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
itemsIndexed(events) { i, event ->
if (lastIndex == i) {
Log.e("console log", "end of list reached $lastIndex")
}
}
}

Remember LazyColumn Scroll Position - Jetpack Compose

I'm trying to save/remember LazyColumn scroll position when I navigate away from one composable screen to another. Even if I pass a rememberLazyListState to a LazyColumn the scroll position is not saved after I get back to my first composable screen. Can someone help me out?
#ExperimentalMaterialApi
#Composable
fun DisplayTasks(
tasks: List<Task>,
navigateToTaskScreen: (Int) -> Unit
) {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
itemsIndexed(
items = tasks,
key = { _, task ->
task.id
}
) { _, task ->
LazyColumnItem(
toDoTask = task,
navigateToTaskScreen = navigateToTaskScreen
)
}
}
}
Well if you literally want to save it, you must store it is something like a viewmodel where it remains preserved. The remembered stuff only lasts till the Composable gets destroyed. If you navigate to another screen, the previous Composables are destroyed and along with them, the scroll state
/**
* Static field, contains all scroll values
*/
private val SaveMap = mutableMapOf<String, KeyParams>()
private data class KeyParams(
val params: String = "",
val index: Int,
val scrollOffset: Int
)
/**
* Save scroll state on all time.
* #param key value for comparing screen
* #param params arguments for find different between equals screen
* #param initialFirstVisibleItemIndex see [LazyListState.firstVisibleItemIndex]
* #param initialFirstVisibleItemScrollOffset see [LazyListState.firstVisibleItemScrollOffset]
*/
#Composable
fun rememberForeverLazyListState(
key: String,
params: String = "",
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
val scrollState = rememberSaveable(saver = LazyListState.Saver) {
var savedValue = SaveMap[key]
if (savedValue?.params != params) savedValue = null
val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex
val savedOffset = savedValue?.scrollOffset ?: initialFirstVisibleItemScrollOffset
LazyListState(
savedIndex,
savedOffset
)
}
DisposableEffect(Unit) {
onDispose {
val lastIndex = scrollState.firstVisibleItemIndex
val lastOffset = scrollState.firstVisibleItemScrollOffset
SaveMap[key] = KeyParams(params, lastIndex, lastOffset)
}
}
return scrollState
}
example of use
LazyColumn(
state = rememberForeverLazyListState(key = "Overview")
)
#Composable
fun persistedLazyScrollState(viewModel: YourViewModel): LazyListState {
val scrollState = rememberLazyListState(viewModel.firstVisibleItemIdx, viewModel.firstVisibleItemOffset)
DisposableEffect(key1 = null) {
onDispose {
viewModel.firstVisibleItemIdx = scrollState.firstVisibleItemIndex
viewModel.firstVisibleItemOffset = scrollState.firstVisibleItemScrollOffset
}
}
return scrollState
}
}
Above I defined a helper function to persist scroll state when a composable is disposed of. All that is needed is a ViewModel with variables for firstVisibleItemIdx and firstVisibleItemOffet.
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(
persistedScrollState(viewModel = viewModel)
) {
//Your content here
}
The LazyColumn should save scroll position out of the box when navigating to next screen. If it doesn't work, this may be a bug described here (issue tracker). Basically check if the list becomes empty when changing screens, e.g. because you observe a cold Flow or LiveData (so the initial value is used).
#Composable
fun persistedScrollState(viewModel: ParentViewModel): ScrollState {
val scrollState = rememberScrollState(viewModel.scrollPosition)
DisposableEffect(key1 = null) {
onDispose {
viewModel.scrollPosition = scrollState.value
}
}
return scrollState
}
Above I defined a helper function to persist scroll state when a composable is disposed of. All that is needed is a ViewModel with a scroll position variable.
Hope this helps someone!
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(
persistedScrollState(viewModel = viewModel)
) {
//Your content here
}

Jetpack Compose LazyRow scroll with snap only to start of next or previous element

Is there a way to horizontally scroll only to start or specified position of previous or next element with Jetpack Compose?
Snappy scrolling in RecyclerView
You can check the scrolling direction like so
#Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
Of course, you will need to create a rememberLazyListState(), and then pass it to the list as a parameter.
Then, based upon the scrolling direction, you can call lazyListState.scrollTo(lazyListState.firstVisibleItemIndex + 1) in a coroutine (if the user is scrolling right), and appropriate calls for the other direction.
(Example for a horizontal LazyRow)
You could do a scroll to the next or previous item to create a snap effect. Check the offset of the first visible item to see which item of the list takes up more screen space and then scroll left or right to the most visible one.
#Composable
fun SnappyLazyRow() {
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyRow(
state = listState,
modifier = Modifier.fillMaxSize(),
content = {
items(/*list of items*/) { index ->
/* Item view */
if(!listState.isScrollInProgress){
if(listState.isHalfPastItemLeft())
coroutineScope.scrollBasic(listState, left = true)
else
coroutineScope.scrollBasic(listState)
if(listState.isHalfPastItemRight())
coroutineScope.scrollBasic(listState)
else
coroutineScope.scrollBasic(listState, left = true)
}
}
})
}
private fun CoroutineScope.scrollBasic(listState: LazyListState, left: Boolean = false){
launch {
val pos = if(left) listState.firstVisibleItemIndex else listState.firstVisibleItemIndex+1
listState.animateScrollToItem(pos)
}
}
#Composable
private fun LazyListState.isHalfPastItemRight(): Boolean {
return firstVisibleItemScrollOffset > 500
}
#Composable
private fun LazyListState.isHalfPastItemLeft(): Boolean {
return firstVisibleItemScrollOffset <= 500
}

Categories

Resources