how to keep RecyclerView always scroll bottom - android

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
}
}
}

Related

Recyclerview: update view elements outside the screen

I have a recycler view that looks like this:
At the start, these circle icons are empty. I need to update every icon of my recycler to be from empty to full within an interval of 5 seconds (see the image above).
I actually can update these icons, but my problem is:
If I have 20 items, I'll need to scroll the recycler in order to see every item. Whenever I scroll the recycler, the last 4-5 items don't get updated from empty to full.
I just need to update the UI, I don't need to remove or add anything to the recyclerview. I've already tried to use notifyDataSetChanged(), notifyItemChanged(), but nothing worked so far.
What's your suggestion? Thank you in advance
Here's one strategy. Have one function in your adapter to start/reset the animation. You can call it when you set the data list. In onBindViewHolder you calculate when relative to now the icon should change to filled (could be in the past). The ViewHolder class either immediately shows the filled icon if the time is negative, or else it posts a delayed runnable to change it in the future. You'll need to cancel any previous delayed runnable so when views get recycled, they always get updated to the correct state.
//Inside your ViewHolder class:
private val setIconRunnable = Runnable { setFilledIcon() }
fun fillIconAt(timeFromNowMillis: Long) {
itemView.removeCallbacks(setIconRunnable)
if (timeFromNowMillis <= 0L) {
setFilledIcon()
} else {
setEmptyIcon()
itemView.postDelayed(setIconRunnable, timeFromNowMillis)
}
}
// In your adapter class:
companion object {
private const val ANIMATION_DURATION = 5000L
}
private var animationStartTime = 0L
fun initiateIconAnimation() {
animationStartTime = System.currentTimeMillis()
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: YourViewHolderType, position: Int) {
//...
val iconChangeTime = (
ANIMATION_DURATION * (position + 1).toFloat() / yourDataList.size
).roundToLong() + animationStartTime
holder.fillIconAt(iconChangeTime - System.currentTimeMillis())
}

Changing TextView width in RecyclerView CardView

I have a RecyclerView made of CardView with several TextViews
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
<TextView...
<TextView...
...
</LinearLayout>
</androidx.cardview.widget.CardView>
I'm trying to change the width of a TextView to be equal in each RecyclerView row and to fit the widest content, so it looks like a table with equal columns. To do that I made a function:
private fun optimizeLayout() {
var maxWidth = 100
val recyclerView = myRecyclerViewLayout
recyclerView.doOnLayout {
// Get max width
for (i in 0 until recyclerView.childCount) {
val v = recyclerView.layoutManager?.findViewByPosition(i)
val tv = v?.findViewById<TextView>(R.id.txtDest)
if (tv != null) {
tv.measure(0, 0)
if (tv.measuredWidth > maxWidth) maxWidth = tv.measuredWidth
println(i.toString() + " " + tv.measuredWidth)
}
}
// Set width
for (i in 0 until recyclerView.childCount) {
val v = recyclerView.layoutManager?.findViewByPosition(i)
val tv = v?.findViewById<TextView>(R.id.txtDest)
if (tv != null) {
tv.width = maxWidth
println("set $i")
}
}
// Header width
txtHeader.doOnLayout { txtHeader.width = maxWidth }
}
}
This function was created after reading many other posts on a similar topic on the Internet. I call it from onViewCreated of the Fragment that contains the RecyclerView and it works fine beside that I get warnings:
requestLayout() improperly called by com.google.android.material.textview.MaterialTextView{b1b06f0 V.ED..... ......ID 0,5-143,110 #7f0a0225 app:id/txtDest} during layout: running second layout pass
I also have a dialog for editing items. The problem starts when I try to change an item - for example I enter wider text and want all the rows in the RecyclerView to have a new width. When the dialog closes I call the function. It works, but not for all elements(?!). For example, I have 10 rows and the function stops in fourth like recyclerView.childCount only returned 4 out of 10. When I close and open Fragment all columns are again even for all elements. I tried to run the function in thread and from onLayoutCompleted:
recyclerView.layoutManager = object : LinearLayoutManager(this.context) {
override fun onLayoutCompleted(state: RecyclerView.State?) {
optimizeLayout()
super.onLayoutCompleted(state)
}
}
val runnable = Runnable {
while (true) {
optimizeLayout()
Thread.sleep(1000)
}
}
Every time with the same result. Why is this happening?
You should never reference recycled views outside of onBindViewHolder.
The reason all of the recycler views are not updating is due to the views not being drawn yet when you call optimizeLayout(). A RecyclerView recycles views and only draws them when they become visible.
I suggest following google design guidelines for list patterns
Which would make the view match parent or keep a consistent width across all views.
If that is not an option I would loop through the string list and find the string with the greatest length and calculate the width and pass it to the adapter before setting the adapter list.

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.

Parallax header effect with RecyclerView

I want to change my ListView I currently have over to use RecyclerView so I can make use of StaggeredGridLayoutManager but RecyclerView does not have the ability to add a header like ListView.
Usually with a ListView I set an empty view in the header and put the image below the listview and translate the bottom image with the scrolling of the list to create the Parallax effect.
So with out a header how can I create the same parallax effect with RecyclerView?
the easiest way to do it, is using below onScrollListener without relying on any library.
View view = recyclerView.getChildAt(0);
if(view != null && recyclerView.getChildAdapterPosition(view) == 0) {
view.setTranslationY(-view.getTop() / 2);// or use view.animate().translateY();
}
make sure your second viewHolder item has a background color to match the drawer/activity background. so the scrolling looks parallax.
So today I tried to archive that effect on a RecyclerView. I was able to do it but since the code is too much I will paste here my github project and I will explain some of the key points of the project.
https://github.com/kanytu/android-parallax-recyclerview
First we need to look at getItemViewType on the RecyclerView.Adapter class. This methods defines what type of view we're dealing with. That type will be passed on to onCreateViewHolder and there we can inflate different views. So what I did was: check if the position is the first one. If so then inflate the header, if not inflate a normal row.
I've added also a CustomRelativeLayout that clips the view so we don't have any trouble with the dividers and with the rows getting on top of the header.
From this point you seem to know the rest of the logic behind it.
The final result was:
EDIT:
If you need to insert something in adapter make sure you notify the correct position by adding 1 in the notifyItemChanged/Inserted method. For example:
public void addItem(String item, int position) {
mData.add(position, item);
notifyItemInserted(position + 1); //we have to add 1 to the notification position since we don't want to mess with the header
}
Another important edit I've done is the scroll logic. The mCurrentOffset system I was using didn't work with the item insertion since the offset will change if you add an item. So what I did was:
ViewHolder holder = findViewHolderForPosition(0);
if (holder != null)
((ParallaxRecyclerAdapter) getAdapter()).translateHeader(-holder.itemView.getTop() * 0.5f);
To test this I added a postDelayed Runnable, started the app, scrolled to the end, add the item in position 0, and scroll up again. The result was:
If anyone is looking for other parallax effects they can check my other repo:
https://github.com/kanytu/android-parallax-listview
For kotlin, you may config the recycler view as below
//setting parallax effects
recyclerView.addOnScrollListener(object :RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val view = recyclerView?.getChildAt(0)
if (view != null && recyclerView?.getChildAdapterPosition(view) === 0) {
val imageView = view.findViewById(R.id.parallaxImage)
imageView.translationY = -view.top / 2f
}
}
})
This answer is for those curious about adding a parallax header to a GridLayoutManager or a StaggeredGridLayoutManager
You'll want to add the following code to your adapter in either onBindViewHolder or onCreateViewHolder
StaggeredGridLayoutManager.LayoutParams layoutParams =
(StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
layoutParams.setFullSpan(true);

Check if ScrollView is higher than screen / scrollable

How can I check if a ScrollView is higher than the screen? When the content of a ScrollView fits on the screen, the ScrollView isn't scrollable, when it's contents exceed the screen height it's scrollable.
How can I check the condition of a ScrollView in that regard?
This is the code from ScrollView, which is private, but can be adapted to be used outside of the class itself
/**
* #return Returns true this ScrollView can be scrolled
*/
private boolean canScroll() {
View child = getChildAt(0);
if (child != null) {
int childHeight = child.getHeight();
return getHeight() < childHeight + mPaddingTop + mPaddingBottom;
}
return false;
}
Too late, but I'm using the following code and it looks more safe for me:
if (view.canScrollVertically(1) || view.canScrollVertically(-1)) {
// you code here
}
A ScrollView always has 1 child. All you need to do is get the height of the child
int scrollViewHeight = scrollView.getChildAt(0).getHeight();
and Calculate Height of Your Screen
if both are equal(or scrollView Height is more) then it fits on your screen.
In my case, I was checking to see if my scrollView(which contained text) was scrollable vertically when the activity was created. On phones, it would scroll but on tablets it couldn't. canScrollVertically was returning me incorrect value because it couldn't be determined yet. I fixed this issue by calling it in the OnGlobalLayoutListener.
(Kotlin)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Must use onGlobalLayout or else canScrollVertically will not return the correct value because the layout hasn't been made yet
scrollView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
// If the scrollView can scroll, disable the accept menu item button
if ( scrollView.canScrollVertically(1) || scrollView.canScrollVertically(-1) )
acceptMenuItem?.isEnabled = false
// Remove itself after onGlobalLayout is first called or else it would be called about a million times per second
scrollView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
My use case was displaying terms of use. I didn't want the accept button to be enabled until the user scrolled to the bottom. I know this is late but i hope this resolves some confusion about canScrollVertically

Categories

Resources