How do I know a `LazyColumn` can be navigated? - android

How do I know a LazyColumn can be navigated?
For example, a list contains 100 elements, then it is a lazy column that can be scroll and we show a fab button to scroll up, but when this list contains 5 elements, the lazy column is not scrollable and the FabButton is no longer needed.

The lazy list layout information can be obtained from state, which is passed to LazyColumn.
In my example, I use derivedStateOf to prevent redundant recompositions - this will cause a recomposition only when the value actually changes. Reading LazyColumn state information without derivedStateOf or side effect function can cause significant performance problems.
The most basic logic in such cases is to check if the first element has been scrolled:
val canScrollToTop by remember {
derivedStateOf {
state.layoutInfo.visibleItemsInfo.run {
isNotEmpty() && (first().index > 0 || first().offset < 0)
}
}
}
LazyColumn(state = state) {
// ...
}
if (canScrollToTop) {
// your button
}

Related

Derived state lagging

I have a Column with remember scroll state. I am using a derived state as follows.
val isHidden = remember {
derivedStateOf {
columnState.value > 400
}
}
I want to display a search bar whenever it scrolls beyond 400 pixels.
Another approach i used is to create a state based on offset of first item in Column using function onGloballyPositioned.
The modifier of first item is as follows:
Box(modifier = Modifier
.graphicsLayer { translationX = horizontalOffset.value }
.onGloballyPositioned { coordinates ->
isHidden.value = coordinates.boundsInRoot().top == 0f
}) {
The problem is no matter which approach i choose it always lags when the state is changed (when the value checks for < or > 400) or in case of onGloballyPositioned if the Compose reaches top of the screen i.e. y coordinate of view == 0.
In both of the approaches isHidden is going to be true. Changing is hidden in both cases causing lag at the point of change.
I can't use lazy row because of some restrictions. Is there any solution to implement this without the lag.

Compose SwipeToDismiss confirmStateChange applies only >= threshold

I have a SwipeToDismiss instance to delete items with dismissThresholds 75%.
If user swipes row too fast without reaching 75% threshold the row being deleted. How to prevent that?
Here is code where I execute an action:
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissValue.DismissedToStart) {
viewModel.deleteCity(city)
}
true
}
)
I ran into the same issue and found a fix that works for me. My theory is, that when the swipe is very fast but doesn't go very far (doesn't reach the set fractional threshold), the layout just immediately resets. In this case (DismissToStart), meaning the view snaps back to the right screen edge, giving us the value of 1.0f for the threshold and thus triggering the confirmStateChange because the fraction is per definition higher than our threshold. The problem being that our threshold is measured from the right screen edge, and this fracion (in my theory) is measured from the left screen edge.
So my solution is to track the current fractional value, and inside confirmStateChange check if the current value is higher than the threshold, BUT NOT 1.0f. In a real world scenario, I think it's impossible to reach 1.0 with actual swiping the finger from right to left, so the solution seems safe to me.
val dismissThreshold = 0.25f
val currentFraction = remember { mutableStateOf(0f) }
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissValue.DismissedToStart) {
if (currentFraction.value >= dismissThreshold && currentFraction.value < 1.0f) {
onSwiped(item)
}
}
dismissOnSwipe
}
)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.animateItemPlacement(),
directions = setOf(DismissDirection.EndToStart),
dismissThresholds = { direction ->
FractionalThreshold(dismissThreshold)
},
background = {
Box(...) {
currentFraction.value = dismissState.progress.fraction
...
}
}
dismissContent = {...}
)
I'm dealing with the same issue, it seems to be a feature, not a bug, but I did not find the way to disable it.
*If the user has dragged their finger across over 50% of the screen
width, the app should trigger the rest of the swipe back animation. If
it's less than that, the app should snap back to the full app view.
If the gesture is quick, ignore the 50% threshold rule and swipe back.*
https://developer.android.com/training/wearables/compose/swipe-to-dismiss

RecyclerView - Continuous Columns Layout

I am trying to create a layout where items would follow one another in columns (see image below) but I am not getting there yet. I have tried GridLayoutManager and StaggeredGridLayoutManager - the problem with both neither provides the feature of item flowing into another column and following each other this way. With my current attempt I am trying FlexboxLayoutManager but the result I am getting is always columns with single items instead of the items flowing one after another.
The desired behavior is that the items are located one after another and when the high of the recycler doesn't allow for the full item view it should be broken down to the next column.
Here is what I am trying right now:
mBinding?.activeRecycler?.layoutManager = FlexboxLayoutManager(context).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.STRETCH
}
And this is getting me one item per column.
Trying to achieve this:
I highly doubt this is possible.
The RecyclerView, its adapters and its layout managers all are not designed to alter the fundamental form of a view.
Meaning that "splitting" one would not be possible.
The RecyclerView is designed to understand how many views are in sight at the same time, create that many views only and then bind the underlying objects to the views respectively.
Meaning the RecyclerView doesn't "Cut a View in half and displays its halves in different places".
The only way in which a constellation like yours would be possible, was if the layout manager is specifically designed to display one item in multiple views and thereby multiple positions. Which would then allow it to be displayed as you described. However, as I said, that would mean the view 3 in the middle and the view 3 in the last column would be two views being bound to the same object or a copy of it. (Or someone went completely crazy and actually split the view, which I doubt).
I don't believe that any of the standard layout managers are capable of it and I doubt that you can even achieve this without also altering the adapter accordingly, at the very least. Because the adapter basically does the binding so without its help the standard layout managers wouldn't be able to do the double binding as described above.
That being said, this is just a very good guess, going by the principles of the view and its components. I have not read the source code or full description of every layout manager.
The way I understand your problem is like this: You have your current list of data that contains the text fields and you want to show them on the normal way, one list item one view item in recycler view.
But based on your design requirements this is not possible.
My idea to achieve that is like this:
You have to create a new list which will separate one item of the previous list into 2,3 or more items to fit in your columns.
private fun demo() {
val originalList = listOf<String>()
val newScreenSpecificList = mutableListOf<String>()
val columnHeight = 3//example number of lines
val columnWidth = 10//example number of chars
var columnsIndex = 0//index of column
var currentColumnHeight = 0 // current column filled height
originalList.forEach {
if (currentColumnHeight + getTextHeight(it, columnWidth) <= columnHeight) {
newScreenSpecificList.add(it)
currentColumnHeight = currentColumnHeight + getTextHeight(it, columnWidth)
} else {
//here is the part where your text is bigger then your column height so you need to divide it
val textForSpaceLeft = getTextForSpaceLeft(it, columnHeight - currentColumnHeight)
newScreenSpecificList.add(textForSpaceLeft)
currentColumnHeight = currentColumnHeight + getTextHeight(textForSpaceLeft, columnWidth)
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
if (getTextForNewSpaceLeft(it, columnHeight - currentColumnHeight)){
//continue to repeat logic for new column
//...
}
}
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
}
}
private fun getTextForSpaceLeft(it: String, spaceLeft: Int): String {
return "it"// return text for the available space
}
private fun getTextForNewSpaceLeft(it: String, spaceLeft: Int): String {
return "new column also"// return text left for the new available space
}
private fun getTextHeight(text: String, columnWidth: Int): Int {
return 2//todo your logic to convert text length to number of lines needed for a specific width of the column
}
Now you need to continue this logic it is not complete, I hope it helps you.
I guess your problem is with the LayoutParams of items which are being created in your adapter. probably the height is set to match_parent in items. You can try to change the LayoutParams of itemViews in your adapter's onCreateViewHolder/onBindViewHolder. Or if the items' heights are kinda tricky to calculate, you can create a customView and try calculate the height in onMeasure and set the height to wrap_content
try to set items' height to wrap_content or if you want to do it in code, something like this:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlexItemViewHolder {
val infatedView = ...
infatedView.layoutParams = FlexboxLayoutManager.LayoutParams(FlexboxLayoutManager.LayoutParams.WRAP_CONTENT, FlexboxLayoutManager.LayoutParams.WRAP_CONTENT)
infatedView.addView(textView)
return FlexItemViewHolder(f)
}

doOnPreDraw method not getting called for some items in a recyclerView

I'm having troubles with some animation in a recycler view. I do the relevant measurements in onViewAttachedToWindow:
override fun onViewAttachedToWindow(holder: PairingViewHolder) {
super.onViewAttachedToWindow(holder)
// get originalHeight & expandedHeight if not gotten before
if (holder.expandedHeight < 0) {
// Execute pending bindings, otherwise the measurement will be wrong.
holder.itemViewDataBinding.executePendingBindings()
holder.cardContainer.layoutParams.width = expandedWidth
holder.expandedHeight = 0 // so that this block is only called once
holder.cardContainer.doOnLayout { view ->
holder.originalHeight = view.height
holder.expandView.isVisible = true
// show expandView and record expandedHeight in next layout pass
// (doOnPreDraw) and hide it immediately.
view.doOnPreDraw {
holder.expandedHeight = view.height
holder.expandView.isVisible = false
holder.cardContainer.layoutParams.width = originalWidth
}
}
}
}
The problem is that doOnPreDraw gets called just for some views. It is something related to the visibility of the views I guess, since the smaller the items (expanded) are, the highest the count of the ones on which onPreDraw gets called.
My guess is that since I'm expanding them in onLayout, the recyclerView consider visible only the ones that when expanded are actually visible on screen. In onPreDraw I collapse them, resulting in some views being able to animate correctly and some not.
How would you solve this?
Thanks in advance.

how to keep RecyclerView always scroll bottom

I Use Recyclerview Replace with list view
I want to keep Recyclerview always scroll bottom.
ListView can use this method setTranscriptMode(AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL)
RecyclerView I use method smoothScrollToPosition(myAdapter.getItemCount() - 1)
but when Soft keyboard Pop ,its replace RecyclerView content.
If you want to keep the scroll position anchored to the bottom of the RecyclerView, it's useful in chat apps. just call setStackFromEnd(true) to on the LinearLayoutManager to make the keyboard keep the list items anchored on the bottom (the keyboard) and not the top.
This is because RV thinks its reference point is TOP and when keyboard comes up, RV's size is updated by the parent and RV keeps its reference point stable. (thus keeps the top position at the same location)
You can set LayoutManager#ReverseLayout to true in which case RV will layout items from the end of the adapter.
e.g. adapter position 0 is at the bottom, 1 is above it etc...
This will of course require you to reverse the order of your adapter.
I'm not sure but setting stack from end may also give you the same result w/o reordering your adapter.
recyclerView.scrollToPosition(getAdapter().getItemCount()-1);
I have faced the same problem and I solved it using the approach mentioned here. It is used to detect whether soft keyboard is open or not and if it is open, just call the smoothScrollToPosition() method.
A much simpler solution is to give your activity's root view a known ID, say '#+id/activityRoot', hook a GlobalLayoutListener into the ViewTreeObserver, and from there calculate the size diff between your activity's view root and the window size:
final View activityRootView = findViewById(R.id.activityRoot);
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();
if (heightDiff > 100) {
recyclerView.smoothScrollToPosition(myAdapter.getItemCount() - 1);
}
}
});
Easy!
I have also faced same problem. But following code help me. I hope this is useful.
In this staus is arraylist.
recyclerView.scrollToPosition(staus.size()-1);
next one is:-
In This you can use adapter class
recyclerView.scrollToPosition(showAdapter.getItemCount()-1);
I ran into this problem myself and I ended up creating my own LayoutManager to solve it. It's a pretty straightforward solution that can be broken down into three steps:
Set stackFromEnd to true.
Determine whether forceTranscriptScroll should be set to true whenever onItemsChanged is called. Per the documentation, onItemsChanged gets called whenever the contents of the adapter changes. If transcriptMode is set to Disabled, forceTranscriptScroll will always be false, if it's set to AlwaysScroll, it will always be true, and if it's set to Normal, it will only be true if the last item in the adapter is completely visible.
In onLayoutCompleted, scroll to the last item in the list if forceTranscriptScroll is set to true and the last item in the list isn't already completely visible.
Below is the code that accomplishes these three steps:
import android.content.Context
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class TranscriptEnabledLinearLayoutManager(context: Context, transcriptMode: TranscriptMode = TranscriptMode.Normal) :
LinearLayoutManager(context) {
enum class TranscriptMode {
Disabled, Normal, AlwaysScroll
}
private var transcriptMode: TranscriptMode = TranscriptMode.Disabled
set(value) {
field = value
// Step 1
stackFromEnd = field != TranscriptMode.Disabled
}
private var forceTranscriptScroll = false
init {
this.transcriptMode = transcriptMode
}
// Step 2
override fun onItemsChanged(recyclerView: RecyclerView) {
super.onItemsChanged(recyclerView)
forceTranscriptScroll = when (transcriptMode) {
TranscriptMode.Disabled -> false
TranscriptMode.Normal -> {
findLastCompletelyVisibleItemPosition() == itemCount - 1
}
TranscriptMode.AlwaysScroll -> true
}
}
// Step 3
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
val recyclerViewState = state ?: return
if (!recyclerViewState.isPreLayout && forceTranscriptScroll) {
// gets the position of the last item in the list. returns if list is empty
val lastAdapterItemPosition = recyclerViewState.itemCount.takeIf { it > 0 }
?.minus(1) ?: return
val lastCompletelyVisibleItem = findLastCompletelyVisibleItemPosition()
if (lastCompletelyVisibleItem != lastAdapterItemPosition ||
recyclerViewState.targetScrollPosition != lastAdapterItemPosition) {
scrollToPositionWithOffset(lastAdapterItemPosition, 0)
}
forceTranscriptScroll = false
}
}
}

Categories

Resources