View flicking on slow scroll of Android RecyclerView - android

I have a horizontal linear layout at the top of recycler view and I want to hide that view if findFirstCompletelyVisibleItemPosition is > 2 and if findFirstCompletelyVisibleItemPosition <= 2, that view should be visible.
I was able to achieve that with scroll listener on recycler view . But there is one problem, when you are scrolling slow view is flickering (showing and hiding fast). However this works fine if we do a fast scroll on recycler view.
This is my code
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val findFirstCompletelyVisibleItemPosition =
layoutManager.findFirstCompletelyVisibleItemPosition()
if (findFirstCompletelyVisibleItemPosition > 2) {
layout.visibility = View.GONE
} else {
layout.visibility = View.VISIBLE
}
}
})

After tinkering a bit, here's what I came up with. It's not the prettiest solution due to slight performance implications but it should do the trick.
Because your LinearLayout should change its visibility during scrolling and not just when scrolling is completed, we'll have to stick with onScrolled. While this method does provide the amount of scrolling done (dx and dy), it's provided in pixels (seemingly, I did some testing). This means that we'd have to convert this to dp to actually get some use out of this value which is probably too expensive to be doing every time the method is called.
Instead, I decided to just save the last couple of values of findFirstCompletelyVisibleItemPosition in an array. This array is then checked each time onScrolled runs. It will look for items below your target value of 2. Your LinearLayout will only be shown if no values smaller than 2 are found.
This results in a more usable and more consistent true or false value that shouldn't cause flickering anymore.
Here's the code to achieve this behavior:
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager // TODO: unchecked cast
val prevValues = IntArray(5) // this can be changed to increase performance
var counter = 0
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
prevValues[counter++] = layoutManager.findFirstCompletelyVisibleItemPosition()
if(counter == prevValues.size)
counter = 0
var smallerValueFound = false
for(prevValue in prevValues.indices) {
if(prevValue < 2) {
smallerValueFound = true
break
}
}
if(smallerValueFound) layout.visibility = View.GONE
else layout.visibility = View.VISIBLE
}
})
To decrease the amount of performance lost, you could only access the array every couple of times onScrolled is run, though this could have issues on its own. You could also decrease the array's size.

Related

Item loses focus when scrolling up in RecyclerView

I'm facing a problem on recyclerview when developing an android app on TV
I have a list of reviews I'd like to scroll through
when scrolling to the second review, i have some logic to resize the layout to take the entire screen, however, the second review is not centered when scrolling down. all the other items after the second review are centered because im overriding
override fun calculateDtToFit(
viewStart: Int,
viewEnd: Int,
boxStart: Int,
boxEnd: Int,
snapPreference: Int
): Int {
return boxStart + (boxEnd - boxStart) / 2 - (viewStart + (viewEnd - viewStart) / 2)
}
Scrolling up works perfectly with all the reviews centered
Since only the scrolling down behavior was odd, I tried adding a scroll listener and using scrollToPositionWithOffset to scroll the second review to a specific position when scrolling down, and kept the scrolling up logic untouched. somehow it ended up messing with scrolling up anyway, when scrolling up to the second review, recycler view somehow loses focus
code to identify scroll direnction
var firstVisibleInListview = layoutManager.findFirstVisibleItemPosition()
var isScrollingUp = false
private val reviewsListScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
val currentFirstVisible = layoutManager.findFirstVisibleItemPosition()
isScrollingUp = currentFirstVisible > firstVisibleInListview
firstVisibleInListview = currentFirstVisible;
}
}
init {
listRecyclerView.addOnScrollListener(reviewsListScrollListener)
}
code to control scrolling behavior
private fun getScrollToPositionCallback(position: Int): (View, Boolean) -> Unit {
return { view, hasFocus ->
if (hasFocus) {
picksViewModel.selectReviewPosition(position)
if (position == 2) {
if (!isScrollingUp) {
listRecyclerView.setPadding(0,listRecyclerView.paddingBottom,
0, listRecyclerView.paddingBottom)
layoutManager.scrollToPositionWithOffset(position, 300)
}
} else if (position == 1) {
listRecyclerView.setPadding(0,0, 0, listRecyclerView.paddingBottom)
}
listRecyclerView.scrollToUserReviewPosition(position)
}
}
}
I then tried adding onKeyListner to the items and requireFocus to the second review when I'm pressing the up key on the third review, but it immediately skip past the second review and jumps to the first review.
So the problem seems to be the second review can't be focused on after I attached the onScrollListener, how do I fix that?

Add button slider like image slider in kotlin

I am making a mobile app in kotlin in which I need to run different Machine Learning models by clicking buttons. The idea is simple there will only be one button at a time on the screen and the user can slide left or right to get the next button just like an image slider where you can access the next or prev image by sliding.
This effect can be achieved in kotlin by Horizontal Scroll View but the problem is if we do an incomplete scroll there can be 2 buttons on the screen but I want an autocomplete scroll effect where when you scroll only the next button should stay on screen.
Edited
So far I have used RecyclerView to implement my buttons but I can't seem to have a good startSmoothScroll. I embedded startSmoothScroll in onScrolled to trigger it for first time and to find the position to smoothScroll but it starts to jiggle between 2 items to and fro.
binding.recyclerView.setHasFixedSize(true)
val smoothScroller: SmoothScroller =
object : LinearSmoothScroller(binding.recyclerView.context) {
override fun getHorizontalSnapPreference(): Int {
return SNAP_TO_START
}
}
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!(recyclerView.layoutManager as LinearLayoutManager).isSmoothScrolling) {
val firstPosition =
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val lastPosition =
(recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
if ((targetPosition == 0) || (targetPosition < lastPosition))
targetPosition = lastPosition
else if (firstPosition != lastPosition)
targetPosition = firstPosition
smoothScroller.targetPosition = targetPosition
(recyclerView.layoutManager as LinearLayoutManager).startSmoothScroll(
smoothScroller
)
}
}
})
Without having much code to go by, here's a general idea you could implement.
First try to set a setOnScrollChangeListener on your Horizontal Scroll View and override the onScrollChange.
Depending on the direction, calculated by the current X and old X position, you can figure out the direction and appropriately move the view left or right: yourHorizontalScrollView.pageScroll(View.FOCUS_LEFT) //or View.FOCUS_RIGHT
If you use a RecyclerView, you could have a few more options with it's OnScrollListener and the recyclerView's smoothScrollToPosition()

Android RecyclerView Negative Offset

I'm trying to implement a timeline similar to Google Nest Camera timeline, I'm using a recyclerview which I want all rows to be the same height, therefore, for a row with an image, I'm using offset, however, when scrolling, since it is a negative scroll, the image appears/disappear of all of the sudden, here is a gif
https://media.giphy.com/media/3DZApAZd284aVPWVGu/giphy.gif
Is there a way to make the row load before appearing on the screen?
recyclerview initialize
timelineRecyclerView.apply {
addItemDecoration(OverlapDecoration(-15.toDp()))
adapter = timelineAdapter
val lm = layoutManager as LinearLayoutManager
lm.reverseLayout = true
lm.stackFromEnd = true
layoutManager = lm
}
ItemDecoration for offset:
class OverlapDecoration(val offset: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, itemPosition: Int, parent: RecyclerView) {
Log.d("MainActivity", "${parent.adapter?.getItemViewType(itemPosition) }")
when( parent.adapter?.getItemViewType(itemPosition) ) {
1 -> {}
2 -> { outRect.set(0, offset, 0, offset) }
}
}
Since you want all rows to have the same height, you can use
recyclerView.setHasFixedSize(true)
for optimization.
To pre-load items and keep them in cache, you can use
recyclerView.setItemViewCacheSize(3) // example: 3 items will be held in cache off-screen.
If this does not work, you can implement a custom LayoutManager to pre-cache items. This example is a bit older, but it should still work. Not tested though.

How to get the offset of scroll position relative to the current text view's start?

I have a RecyclerView that contains TextViews. The number of TextViews can vary and the size of them vary as well and can be dynamically changed.
When the user scrolls to a certain position within the list and exits app, I want to be able to return to that exact position in the next session.
To do this, I need to know how many pixels have scrolled past from where the current TextView in view started and where the current position of the scroll is. For example, if the user has the 3rd TextView in view and scrolls 100 pixels down from where that TextView started, I will be able to return to this spot with scrollToPositionWithOffset(2, 100). If the TextView changes size (due to font changes), I can also return to the same spot by calculating the percentage of offset using the TextView's height.
Problem is, I cannot get the offset value in any accurate manor.
I know I can keep a running calculation on the Y value scrolled using get scroll Y of RecyclerView or Webview, but this does not give me where the TextView actually started. I can listen to when the user scrolled past the start of any TextView and record the Y position there but this will be inaccurate on fast scrolling.
Is there a better way?
Don't use position in pixels, use the index of the view. Using layout manager's findFirstVisibleItemPosition or findFirstCompletelyVisibleItemPosition.
That's a very popular question, although it may be intuitive to think and search for pixels not index.
Get visible items in RecyclerView
Find if the first visible item in the recycler view is the first item of the list or not
how to get current visible item in Recycler View
A good reason to not trust pixels is that it's not useful on some situations where index is, like rotating the screen, resizeing/splitting the app size to fit other apps side by side, foldable phones, and changing text / screen resolution.
I solved this by converting to a ListView:
lateinit var adapterRead: AdapterRead // Custom Adapter
lateinit var itemListView: ListView
/*=======================================================================================================*/
// OnViewCreated
itemListView = view.findViewById(R.id.read_listview)
setListView(itemListView)
// Upon entering this Fragment, will automatically scroll to saved position:
itemListView.afterMeasured {
scrollToPosition(itemListView, getPosition(), getOffset())
}
itemListView.setOnScrollListener(object : AbsListView.OnScrollListener {
private var currentFirstVisibleItem = 0
var offset = 0
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// When scrolling stops, will save the current position and offset:
if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
offset = if(itemListView.getChildAt(0) == null) 0 else itemListView.getChildAt(0).top - itemListView.paddingTop
saveReadPosition(getReadPosition(itemListView), offset)
}
}
override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
currentFirstVisibleItem = firstVisibleItem
}
})
/*=======================================================================================================*/
// Thanks to https://antonioleiva.com/kotlin-ongloballayoutlistener/ for this:
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if(measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
/*=======================================================================================================*/
fun setListView(lv: ListView) {
adapterRead = AdapterRead(list, context!!)
lv.apply {this.adapter = adapterRead}
}
/*=======================================================================================================*/
fun scrollToPosition(lv: ListView, position: Int, offset: Int) {
lv.post { lv.setSelectionFromTop(position, offset) }
}
/*=======================================================================================================*/
fun saveReadPosition(position: Int, offset: Int) {
// Persist your data to database here
}
/*=======================================================================================================*/
fun getPosition() {
// Get your saved position here
}
/*=======================================================================================================*/
fun getOffse() {
// Get your saved offset here
}

Endless scroll kotlin recycling view/ListView

I am desperately trying to implement endless scrolling on an android app using kotlin. All the tutorials are either useless since they dont explain things properly. So for example:
https://github.com/chetdeva/recyclerview-bindings
it looks promising but the author uses phrases like
"put this in your BindingAdapter" so i look what this BindingAdapter is, I found a java file but if you insert anything in there I get errors. Its like anything I try fails directly.
The other tutorials are written in java and even with "translate to kotlin" option its useless since the translated code throws 100 errors.
I tried things like :
setContentView(R.layout.activity_main)
list.layoutManager = LinearLayoutManager(this)
list.hasFixedSize()
list.adapter = ListAdapter(this, getLists())
val list_view: RecyclerView = findViewById(R.id.list)
fun setRecyclerViewScrollListener() {
list_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
val height = list_view.getHeight()
val diff = height-dy
if (diff < 1000){
/*load next list */
}
}
})
}
setRecyclerViewScrollListener()
}
or this
val inflater = LayoutInflater.from(this#MainActivity)
val layout = inflater.inflate(R.layout.append_list, null, false)
button.setOnClickListener{screen.addView(layout)}
Is there a bullet proof method where you can simply append elemets like with html and js? I wrote this snippet in 2 min. Is there a similar "easy" way in Android/Kotlin?
$("#next").click(function(){
$(".append_text").append("new text <img src='http://static.webshopapp.com/shops/015426/files/005031634/560x625x2/kek-amsterdam-wandtattoo-hase-forest-friends-braun.jpg'/>")
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="next">Load</button>
<span class="append_text"> </span>
In general I recive a lot of errors for choosing the wrong Layout. I tried Listview and contrainlayout and recycling Layout and Vertical Scrolling layout and so on. Is there a simple body tag where you can simply append a xml file?
I think I go the wrong way the whole time because I see everything though the eyes of a Web. Dev. while android does not have the classical DOM. Can anybody explain it to me with an minimal example on how to append a xml file to the main xml file on button click/on scroll?
I use this method for adding endless scroll functionality to a recyclerview in Kotlin:
private fun setRecyclerViewScrollListener() {
scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val totalItemCount = recyclerView!!.layoutManager.itemCount
if (totalItemCount == lastVisibleItemPosition + 1) {
Log.d("MyTAG", "Load new list")
recycler.removeOnScrollListener(scrollListener)
}
}
}
recycler.addOnScrollListener(scrollListener)
}
the variable lastVisibleItemPosition is declared as follows:
private val lastVisibleItemPosition: Int
get() = linearLayoutManager.findLastVisibleItemPosition()
private lateinit var scrollListener: RecyclerView.OnScrollListener
Just call the setRecyclerViewScrollListener() method every time you nedd to add this functionality to the recyclerView.
Hope it helps,
Leonardo
Hmm i don't now if this solve your problem but i use this to implement an recycler view that added new data to my recycler view when the scroll come to the end:
productsListActivityBinding.recyclerViewProducts.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (!recyclerView.canScrollVertically(1)){
//function that add new elements to my recycler view
}
}
})
set in recycler view in scroll listener
recycler.addOnScrollListener(your listener)

Categories

Resources