How to synchronize two scrollview in Android? - android

I have two horizontal scrollview, and want to keep them always at the same position / distance. If user scrolls one, need to scroll programmatically the other. The challenge is, that an infinite loop will occur. One will raise the other, other will raise first. How can I set a state, indicate that a user initiated scroll is still in progress? So other scrollview should not execute the programmatic scroll.
One of them is a HorizontalScrollView other is a RecyclerView.
Tried solutions like below, without any success:
horizontalScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
if (programmaticScrollEnable) {
programmaticScrollEnable = false;
// do programmatic scrolling here
programmaticScrollEnable = true;
}
}
});
Tried to change state in onScrollStateChanged method:
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
/*if (newState == RecyclerView.SCROLL_STATE_IDLE) {
MainActivity.programmaticScrollEnable = true;
}*/
}

Try this...
horizontalScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
recyclerView.scrollTo(horizontalScrollView.scrollTo(horizontalScrollView.getScrollX(), 0);
}
});

The challenge is, that an infinite loop will occur.
You can avoid the infinite loop by registering the last touched ScrolledView in OnTouchListener. This touched ScrollView must not scroll programmatically; but the other(s) should be.
This can be tracked by setting a tag to each ScrollView and compare that tag during scroll event.
If you are API level 23 (Android M), you can register View.OnScrollChangeListener. And for APIs below that, you can register ViewTreeObserver.OnScrollChangedListener. But the latter is heavy on the resources; so you need to handle that.
For API levels 23+
// Tracking last touched SV
private var touchedSVTag = -1
// list of ScrollViews
val svs = arrayListOf(scrollview1, scrollview2)
val scrollListener =
View.OnScrollChangeListener { view, scrollX, _, _, _ ->
val currentSVTag = view!!.tag as Int
if (currentSVTag == touchedSVTag) {
for (svTag in 0 until svs.size)
if (svTag != currentSVTag)
svs[svTag].scrollTo(scrollX, 0)
}
}
val itemTouchListener = View.OnTouchListener { sv, _ ->
val svTag = sv.tag as Int
if (touchedSVTag != -1 && touchedSVTag != svTag) {
// stop scrollView in the middle of the scroll
svs[touchedSVTag].fling(0)
}
touchedSVTag = svTag
false
}
for (i in 0 until svs.size) {
val sv = svs[i]
sv.tag = i
sv.setOnScrollChangeListener(scrollListener)
sv.setOnTouchListener(itemTouchListener)
}
And for API levels below 23:
// Tracking last touched SV
private var touchedSVTag = -1
// list of ScrollViews
val svs = arrayListOf(scrollview1, scrollview2)
val scrollListener = {
val scrolledSV = svs[touchedSVTag]
for (svTag in 0 until svs.size)
if (svTag != touchedSVTag)
svs[svTag].scrollTo(scrolledSV.scrollX, 0)
}
val itemTouchListener = View.OnTouchListener { sv, _ ->
val svTag = sv.tag as Int
if (touchedSVTag != -1 && touchedSVTag != svTag) {
// stop scrollView in the middle of the scroll
svs[touchedSVTag].fling(0)
}
touchedSVTag = svTag
false
}
for (i in 0 until svs.size) {
val sv = svs[i]
sv.tag = i
sv.viewTreeObserver.addOnScrollChangedListener(scrollListener)
sv.setOnTouchListener(itemTouchListener)
}

Related

Make Endless RecycleView with Fixed data [duplicate]

I am trying to make my RecyclerView loop back to the start of my list.
I have searched all over the internet and have managed to detect when I have reached the end of my list, however I am unsure where to proceed from here.
This is what I am currently using to detect the end of the list (found here):
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
visibleItemCount = mLayoutManager.getChildCount();
totalItemCount = mLayoutManager.getItemCount();
pastVisiblesItems = mLayoutManager.findFirstVisibleItemPosition();
if (loading) {
if ( (visibleItemCount+pastVisiblesItems) >= totalItemCount) {
loading = false;
Log.v("...", ""+visibleItemCount);
}
}
}
When scrolled to the end, I would like to views to be visible while the displaying data from the top of the list or when scrolled to the top of the list I would display data from the bottom of the list.
For example:
View1 View2 View3 View4 View5
View5 View1 View2 View3 View4
There is no way of making it infinite, but there is a way to make it look like infinite.
in your adapter override getCount() to return something big like Integer.MAX_VALUE:
#Override
public int getCount() {
return Integer.MAX_VALUE;
}
in getItem() and getView() modulo divide (%) position by real item number:
#Override
public Fragment getItem(int position) {
int positionInList = position % fragmentList.size();
return fragmentList.get(positionInList);
}
at the end, set current item to something in the middle (or else, it would be endless only in downward direction).
// scroll to middle item
recyclerView.getLayoutManager().scrollToPosition(Integer.MAX_VALUE / 2);
The other solutions i found for this problem work well enough, but i think there might be some memory issues returning Integer.MAX_VALUE in getCount() method of recycler view.
To fix this, override getItemCount() method as below :
#Override
public int getItemCount() {
return itemList == null ? 0 : itemList.size() * 2;
}
Now wherever you are using the position to get the item from the list, use below
position % itemList.size()
Now add scrollListener to your recycler view
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstItemVisible = linearLayoutManager.findFirstVisibleItemPosition();
if (firstItemVisible != 0 && firstItemVisible % itemList.size() == 0) {
recyclerView.getLayoutManager().scrollToPosition(0);
}
}
});
Finally to start auto scrolling, call the method below
public void autoScroll() {
final Handler handler = new Handler();
final Runnable runnable = new Runnable() {
#Override
public void run() {
recyclerView.scrollBy(2, 0);
handler.postDelayed(this, 0);
}
};
handler.postDelayed(runnable, 0);
}
I have created a LoopingLayoutManager that fixes this issue.
It works without having to modify the adapter, which allows for greater flexibility and reusability.
It comes fully featured with support for:
Vertical and Horizontal Orientations
LTR and RTL
ReverseLayout for both orientations, as well as LTR, and RTL
Public functions for finding items and positions
Public functions for scrolling programmatically
Snap Helper support
Accessibility (TalkBack and Voice Access) support
And it is hosted on maven central, which means you just need to add it as a dependency in your build.gradle file:
dependencies {
implementation 'com.github.beksomega:loopinglayout:0.3.1'
}
and change your LinearLayoutManager to a LoopingLayoutManager.
It has a suite of 132 unit tests that make me confident it's stable, but if you find any bugs please put up an issue on the github!
I hope this helps!
In addition to solution above.
For endless recycler view in both sides you should add something like that:
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val firstItemVisible = linearLayoutManager.findFirstVisibleItemPosition()
if (firstItemVisible != 1 && firstItemVisible % songs.size == 1) {
linearLayoutManager.scrollToPosition(1)
}
val firstCompletelyItemVisible = linearLayoutManager.findFirstCompletelyVisibleItemPosition()
if (firstCompletelyItemVisible == 0) {
linearLayoutManager.scrollToPositionWithOffset(songs.size, 0)
}
}
})
And upgrade your getItemCount() method:
#Override
public int getItemCount() {
return itemList == null ? 0 : itemList.size() * 2 + 1;
}
It is work like unlimited down-scrolling, but in both directions. Glad to help!
Amended #afanit's solution to prevent the infinite scroll from momentarily halting when scrolling in the reverse direction (due to waiting for the 0th item to become completely visible, which allows the scrollable content to run out before scrollToPosition() is called):
val firstItemPosition = layoutManager.findFirstVisibleItemPosition()
if (firstItemPosition != 1 && firstItemPosition % items.size == 1) {
layoutManager.scrollToPosition(1)
} else if (firstItemPosition == 0) {
layoutManager.scrollToPositionWithOffset(items.size, -recyclerView.computeHorizontalScrollOffset())
}
Note the use of computeHorizontalScrollOffset() because my layout manager is horizontal.
Also, I found that the minimum return value from getItemCount() for this solution to work is items.size + 3. Items with position larger than this are never reached.
I was running into OOM issues with Glide and other APIs and created this Implementation using the Duplicate End Caps inspired by this post for an iOS build.
Might look intimidating but its literally just copying the RecyclerView class and updating two methods in your RecyclerView Adapter. All it is doing is that once it hits the end caps, it does a quick no-animation transition to either ends of the adapter's ViewHolders to allow continuous cycling transitions.
http://iosdevelopertips.com/user-interface/creating-circular-and-infinite-uiscrollviews.html
class CyclingRecyclerView(
context: Context,
attrs: AttributeSet?
) : RecyclerView(context, attrs) {
// --------------------- Instance Variables ------------------------
private val onScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
// The total number of items in our RecyclerView
val itemCount = adapter?.itemCount ?: 0
// Only continue if there are more than 1 item, otherwise, instantly return
if (itemCount <= 1) return
// Once the scroll state is idle, check what position we are in and scroll instantly without animation
if (newState == SCROLL_STATE_IDLE) {
// Get the current position
val pos = (layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
// If our current position is 0,
if (pos == 0) {
Log.d("AutoScrollingRV", "Current position is 0, moving to ${itemCount - 1} when item count is $itemCount")
scrollToPosition(itemCount - 2)
} else if (pos == itemCount - 1) {
Log.d("AutoScrollingRV", "Current position is ${itemCount - 1}, moving to 1 when item count is $itemCount")
scrollToPosition(1)
} else {
Log.d("AutoScrollingRV", "Curren position is $pos")
}
}
}
}
init {
addOnScrollListener(onScrollListener)
}
}
For the Adapter, just make sure to update 2 methods, in my case, viewModels is just my data structure that contains the data that I send over to my ViewHolders
override fun getItemCount(): Int = if (viewModels.size > 1) viewModels.size + 2 else viewModels.size
and on ViewHolder, you just retrieve the adjusted index's data
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val adjustedPos: Int =
if (viewModels.size > 1) {
when (position) {
0 -> viewModels.lastIndex
viewModels.size + 1 -> 0
else -> position - 1
}
} else {
position
}
holder.bind(viewModels[adjustedPos])
}
The previous implementation's hurt me haha, seemed way to hacky to just add a crazy amount of items, big problem when you run into Multiple cards with an Integer.MAX_VALUE nested RecyclerView. This approach fixed all the problems of OOM since it only necessarily creates 2 and ViewHolders.
Endless recyclerView in both sides
Add onScrollListener at your recyclerview
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener()
{
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstItemVisible = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
if (firstItemVisible != 1 && firstItemVisible % itemList.size() == 1) {
((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPosition(1);
}
int firstCompletelyItemVisible = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
if (firstCompletelyItemVisible == 0)
{}
if (firstItemVisible != RecyclerView.NO_POSITION
&& firstItemVisible== recyclerView.getAdapter().getItemCount()%itemList.size() - 1)
{
((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPositionWithOffset(itemList.size() + 1, 0);
}
}
});
In your adapter override the getItemCount method
#Override
public int getItemCount()
{
return itemList == null ? 0 : itemList.size() * 2 + 1;
}

How to determine the most visible view position after the RecyclerView adapter data change?

I want to determine the most visible item in my RecyclerView and so I use the following method:
public int getMostVisibleIndex() {
// try to figure which child is the most visible on screen
LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
mFirstVisibleIndex = layoutManager.findFirstVisibleItemPosition();
mLastVisibleIndex = layoutManager.findLastVisibleItemPosition();
// How do I use convertPreLayoutPositionToPostLayout() ?
VisibleIndex mostVisible = null;
if (mFirstVisibleIndex != -1|| mLastVisibleIndex != -1) {
// if it's not the same
if (mFirstVisibleIndex != mLastVisibleIndex) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) this.getLayoutManager();
// get the visible rect of the first item
Rect firstPercentageRect = new Rect();
linearLayoutManager.findViewByPosition(mFirstVisibleIndex).getGlobalVisibleRect(firstPercentageRect);
// get the visible rect of the last item
Rect lastPercentageRect = new Rect();
linearLayoutManager.findViewByPosition(mLastVisibleIndex).getGlobalVisibleRect(lastPercentageRect);
// since we're on a horizontal list
if (firstPercentageRect.width() > lastPercentageRect.width()) {
return mFirstVisibleIndex;
} else {
return mLastVisibleIndex;
}
} else {
return mFirstVisibleIndex;
}
}
return -1;
}
It works great, but after I change the data set, and call any notify*Changed methods in my adapter, if I try to use the above function, the item positions that findFirstVisibleItemPosition and findLastVisibleItemPosition return are wrong.
I noticed that they both use getlayoutposition behind the scenes, and I also noticed that on the documentation it says:
If LayoutManager needs to call an external method that requires the adapter position of the item, it can use getAdapterPosition() or convertPreLayoutPositionToPostLayout(int).
It sounds as if convertPreLayoutPositionToPostLayout is EXACTLY what I'm looking for, but I have no idea how to access it from within a RecyclerView.
Any ideas?
Thank you.
In my case I can only have maximum 2 visible views at a time in recyclerview and
I am using this method in onScrolled for always having the mostVisibleItemPosition
for playing video content when user scrolls.
So getTop() returns top position of this view relative to its parent and when user starts scrolling firstView is getting out of the screen and the firstView.getTop() will return minus value while secondView.getTop() is positive and when secondView top reaches nearly the center of the screen getting absolute value of firstView.getTop() we will determine how many pixels firstView top is above from parent view top and getting secondView.getTop() we will determine how many pixels secondView top is above parent bottom and this is where mostVisibleItemPosition will changed.
private void detectMostVisibleItem() {
int firstItemPosition = layoutManager.findFirstVisibleItemPosition();
int secondItemPosition = layoutManager.findLastVisibleItemPosition();
if (firstItemPosition == secondItemPosition) {
mostVisibleItemPosition = firstItemPosition;
} else {
View firstView = layoutManager.findViewByPosition(firstItemPosition);
View secondView = layoutManager.findViewByPosition(secondItemPosition);
if (Math.abs(firstView.getTop()) <= Math.abs(secondView.getTop())) {
if (mostVisibleItemPosition != firstItemPosition) {
mostVisibleItemPosition = firstItemPosition;
...
}
} else {
if (mostVisibleItemPosition != secondItemPosition) {
mostVisibleItemPosition = secondItemPosition;
...
}
}
}
}

Smoothy scroll to Child in NestedScrollView

I have expandable views inside CardView thats parent is NestedScrollView. I'm trying to create smooth scroll to child when expand animation ended. But I found only one solution:
scrollView.requestChildFocus(someView, someView);
This code works fine, but, when call requestChildFocus it scrolls immediately, and that annoying me a little bit. Is it possible to scroll to child smoothly?
The childView, to which I wanted to scroll, has CardView parrent, so childView.getTop() returns the value relative to the CardView not to the ScrollView. So, to get top relative to ScrollView I should get childView.getParent().getParent() then cast it to View and call getTop().
Scroll position calculates like
int scrollTo = ((View) childView.getParent().getParent()).getTop() + childView.getTop();
nestedScrollView.smoothScrollTo(0, scrollTo);
more to the answer from #jerry-sha
fun NestedScrollView.smoothScrollTo(view: View) {
var distance = view.top
var viewParent = view.parent
//traverses 10 times
for (i in 0..9) {
if ((viewParent as View) === this) break
distance += (viewParent as View).top
viewParent = viewParent.getParent()
}
smoothScrollTo(0, distance)
}
I had child views at different levels to the scrollview so made this function based off the accepted answer to calculate the distance and scroll
private int findDistanceToScroll(View view){
int distance = view.getTop();
ViewParent viewParent = view.getParent();
//traverses 10 times
for(int i = 0; i < 10; i++) {
if (((View) viewParent).getId() == R.id.journal_scrollview) {
return distance;
}
distance += ((View) viewParent).getTop();
viewParent = viewParent.getParent();
}
Timber.w("view not found");
return 0;
}
Then scroll using
journal_scrollview.smoothScrollTo(0, distance);
You can use my library ViewPropertyObjectAnimator for that.
Assuming mNestedScrollView is your NestedScrollView and mChildView is the child View you want to scroll to, you can do the following:
ViewPropertyObjectAnimator.animate(mNestedScrollView).scrollY(mChildView.getTop()).start();
Just make sure mChildView.getTop() is not 0 at the moment of calling .animate(...).
Edit:
As I said: make sure your View's top is non-zero when CALL .animate(...). In other words: call .animate(...) only when your child View already has dimensions. How can you determine that? For example like this:
mChildView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int width = mChildView.getWidth();
int height = mChildView.getHeight();
if (width > 0 && height > 0) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
mChildView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mChildView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
ViewPropertyObjectAnimator.animate(mNestedScrollView)
.scrollY(mChildView.getTop())
.start();
}
}
});
Try to read the source code.
svMain.setSmoothScrollingEnabled(true);
Rect rect = new Rect();
rect.top = 0;
rect.left = 0;
rect.right = tv4.getWidth();
rect.bottom =tv4.getHeight();
svMain.requestChildRectangleOnScreen(tv4,rect,false);
rect is the place u want the view to be shown on screen.
The main issue is to calculate the correct x/y position relative to the nestedscrollview. However, most of the answers here try to propose a solution by navigating in the hierarchy and hardcoding the hierarchy-level of the desired view. Imho, this is very error prone, if you change your viewgroup hierarchy.
Therefore, I would suggest to use a much more cleaner approach, which computes the relative position based on a Rect. Then, you can use the nestedscrollview's smoothScrollTo(..) methods to scroll to the desired position.
This should get the work done, where childView is the view you want to scroll to
public static void scrollToView(final NestedScrollView nestedScrollView, final View viewToScrollTo) {
final int[] xYPos = new int[2];
viewToScrollTo.getLocationOnScreen(xYPos);
final int[] scrollxYPos = new int[2];
nestedScrollView.getLocationOnScreen(scrollxYPos);
int yPosition = xYPos[1];
if (yPosition < 0) {
yPosition = 0;
}
nestedScrollView.scrollTo(0, scrollxYPos[1] - yPosition);
}
I find the accepted answer to work, but it is specific to their view structure as of now and it could not be used as a static method to use for all similar scrolls with different view structures. I made a variety of it in a utility class that measures until it find it's ScrollView or NestedScrollView parent. I made it so that the scrollToView(View,View) method should work with both ScrollView and NestedScrollView in case I would update which I use later on or whatever. You could of course call the right "child method" directly.
public static void scrollToView(View scrollView, View scrollToView) {
if (scrollToView == null) {
Log.d(TAG, "scrollToView() failed due to scrollToView == NULL!");
return;
}
if (scrollView instanceof NestedScrollView) {
scrollToInNestedView((NestedScrollView) scrollView, scrollToView);
} else if (scrollView instanceof ScrollView) {
scrollToInScrollView((ScrollView) scrollView, scrollToView);
} else {
Log.d(TAG, "scrollToView() failed due to scrollView not appearing to be any kind of scroll view!");
}
}
public static void scrollToInNestedView(NestedScrollView scrollView, View scrollToView) {
if (scrollView == null || scrollToView == null) {
return;
}
scrollView.post(() -> {
int startY = scrollView.getScrollY();
int requiredY = scrollToView.getTop();
View parent = (View) scrollToView.getParent();
while (parent != null && !(parent instanceof NestedScrollView)) {
requiredY += parent.getTop();
parent = (View) parent.getParent();
}
if (requiredY != startY) {
scrollView.smoothScrollTo(0, requiredY);
}
});
}
public static void scrollToInScrollView(ScrollView scrollView, View scrollToView) {
if (scrollView == null || scrollToView == null) {
return;
}
scrollView.post(() -> {
int startY = scrollView.getScrollY();
int requiredY = scrollToView.getTop();
View parent = (View) scrollToView.getParent();
while (parent != null && !(parent instanceof ScrollView)) {
requiredY += parent.getTop();
parent = (View) parent.getParent();
}
if (requiredY != startY) {
scrollView.smoothScrollTo(0, requiredY);
}
});
}

How do I create a circular (endless) RecyclerView?

I am trying to make my RecyclerView loop back to the start of my list.
I have searched all over the internet and have managed to detect when I have reached the end of my list, however I am unsure where to proceed from here.
This is what I am currently using to detect the end of the list (found here):
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
visibleItemCount = mLayoutManager.getChildCount();
totalItemCount = mLayoutManager.getItemCount();
pastVisiblesItems = mLayoutManager.findFirstVisibleItemPosition();
if (loading) {
if ( (visibleItemCount+pastVisiblesItems) >= totalItemCount) {
loading = false;
Log.v("...", ""+visibleItemCount);
}
}
}
When scrolled to the end, I would like to views to be visible while the displaying data from the top of the list or when scrolled to the top of the list I would display data from the bottom of the list.
For example:
View1 View2 View3 View4 View5
View5 View1 View2 View3 View4
There is no way of making it infinite, but there is a way to make it look like infinite.
in your adapter override getCount() to return something big like Integer.MAX_VALUE:
#Override
public int getCount() {
return Integer.MAX_VALUE;
}
in getItem() and getView() modulo divide (%) position by real item number:
#Override
public Fragment getItem(int position) {
int positionInList = position % fragmentList.size();
return fragmentList.get(positionInList);
}
at the end, set current item to something in the middle (or else, it would be endless only in downward direction).
// scroll to middle item
recyclerView.getLayoutManager().scrollToPosition(Integer.MAX_VALUE / 2);
The other solutions i found for this problem work well enough, but i think there might be some memory issues returning Integer.MAX_VALUE in getCount() method of recycler view.
To fix this, override getItemCount() method as below :
#Override
public int getItemCount() {
return itemList == null ? 0 : itemList.size() * 2;
}
Now wherever you are using the position to get the item from the list, use below
position % itemList.size()
Now add scrollListener to your recycler view
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstItemVisible = linearLayoutManager.findFirstVisibleItemPosition();
if (firstItemVisible != 0 && firstItemVisible % itemList.size() == 0) {
recyclerView.getLayoutManager().scrollToPosition(0);
}
}
});
Finally to start auto scrolling, call the method below
public void autoScroll() {
final Handler handler = new Handler();
final Runnable runnable = new Runnable() {
#Override
public void run() {
recyclerView.scrollBy(2, 0);
handler.postDelayed(this, 0);
}
};
handler.postDelayed(runnable, 0);
}
I have created a LoopingLayoutManager that fixes this issue.
It works without having to modify the adapter, which allows for greater flexibility and reusability.
It comes fully featured with support for:
Vertical and Horizontal Orientations
LTR and RTL
ReverseLayout for both orientations, as well as LTR, and RTL
Public functions for finding items and positions
Public functions for scrolling programmatically
Snap Helper support
Accessibility (TalkBack and Voice Access) support
And it is hosted on maven central, which means you just need to add it as a dependency in your build.gradle file:
dependencies {
implementation 'com.github.beksomega:loopinglayout:0.3.1'
}
and change your LinearLayoutManager to a LoopingLayoutManager.
It has a suite of 132 unit tests that make me confident it's stable, but if you find any bugs please put up an issue on the github!
I hope this helps!
In addition to solution above.
For endless recycler view in both sides you should add something like that:
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val firstItemVisible = linearLayoutManager.findFirstVisibleItemPosition()
if (firstItemVisible != 1 && firstItemVisible % songs.size == 1) {
linearLayoutManager.scrollToPosition(1)
}
val firstCompletelyItemVisible = linearLayoutManager.findFirstCompletelyVisibleItemPosition()
if (firstCompletelyItemVisible == 0) {
linearLayoutManager.scrollToPositionWithOffset(songs.size, 0)
}
}
})
And upgrade your getItemCount() method:
#Override
public int getItemCount() {
return itemList == null ? 0 : itemList.size() * 2 + 1;
}
It is work like unlimited down-scrolling, but in both directions. Glad to help!
Amended #afanit's solution to prevent the infinite scroll from momentarily halting when scrolling in the reverse direction (due to waiting for the 0th item to become completely visible, which allows the scrollable content to run out before scrollToPosition() is called):
val firstItemPosition = layoutManager.findFirstVisibleItemPosition()
if (firstItemPosition != 1 && firstItemPosition % items.size == 1) {
layoutManager.scrollToPosition(1)
} else if (firstItemPosition == 0) {
layoutManager.scrollToPositionWithOffset(items.size, -recyclerView.computeHorizontalScrollOffset())
}
Note the use of computeHorizontalScrollOffset() because my layout manager is horizontal.
Also, I found that the minimum return value from getItemCount() for this solution to work is items.size + 3. Items with position larger than this are never reached.
I was running into OOM issues with Glide and other APIs and created this Implementation using the Duplicate End Caps inspired by this post for an iOS build.
Might look intimidating but its literally just copying the RecyclerView class and updating two methods in your RecyclerView Adapter. All it is doing is that once it hits the end caps, it does a quick no-animation transition to either ends of the adapter's ViewHolders to allow continuous cycling transitions.
http://iosdevelopertips.com/user-interface/creating-circular-and-infinite-uiscrollviews.html
class CyclingRecyclerView(
context: Context,
attrs: AttributeSet?
) : RecyclerView(context, attrs) {
// --------------------- Instance Variables ------------------------
private val onScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
// The total number of items in our RecyclerView
val itemCount = adapter?.itemCount ?: 0
// Only continue if there are more than 1 item, otherwise, instantly return
if (itemCount <= 1) return
// Once the scroll state is idle, check what position we are in and scroll instantly without animation
if (newState == SCROLL_STATE_IDLE) {
// Get the current position
val pos = (layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
// If our current position is 0,
if (pos == 0) {
Log.d("AutoScrollingRV", "Current position is 0, moving to ${itemCount - 1} when item count is $itemCount")
scrollToPosition(itemCount - 2)
} else if (pos == itemCount - 1) {
Log.d("AutoScrollingRV", "Current position is ${itemCount - 1}, moving to 1 when item count is $itemCount")
scrollToPosition(1)
} else {
Log.d("AutoScrollingRV", "Curren position is $pos")
}
}
}
}
init {
addOnScrollListener(onScrollListener)
}
}
For the Adapter, just make sure to update 2 methods, in my case, viewModels is just my data structure that contains the data that I send over to my ViewHolders
override fun getItemCount(): Int = if (viewModels.size > 1) viewModels.size + 2 else viewModels.size
and on ViewHolder, you just retrieve the adjusted index's data
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val adjustedPos: Int =
if (viewModels.size > 1) {
when (position) {
0 -> viewModels.lastIndex
viewModels.size + 1 -> 0
else -> position - 1
}
} else {
position
}
holder.bind(viewModels[adjustedPos])
}
The previous implementation's hurt me haha, seemed way to hacky to just add a crazy amount of items, big problem when you run into Multiple cards with an Integer.MAX_VALUE nested RecyclerView. This approach fixed all the problems of OOM since it only necessarily creates 2 and ViewHolders.
Endless recyclerView in both sides
Add onScrollListener at your recyclerview
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener()
{
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstItemVisible = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
if (firstItemVisible != 1 && firstItemVisible % itemList.size() == 1) {
((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPosition(1);
}
int firstCompletelyItemVisible = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
if (firstCompletelyItemVisible == 0)
{}
if (firstItemVisible != RecyclerView.NO_POSITION
&& firstItemVisible== recyclerView.getAdapter().getItemCount()%itemList.size() - 1)
{
((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPositionWithOffset(itemList.size() + 1, 0);
}
}
});
In your adapter override the getItemCount method
#Override
public int getItemCount()
{
return itemList == null ? 0 : itemList.size() * 2 + 1;
}

Possible to jump straight to second view in ViewAnimator

I have a ViewAnimator (ViewSwitcher to be precise) with fade in/out animations associated to it and two Views which I transition between using the ViewAnimator.showNext() and ViewAnimator.showPrevious() methods.
There is a requirement in my app where sometimes I need to start straight from the second View, without showing a fade animation in going from the first View to the second. Anyone know if there is a straightforward way to accomplish this without having to mess around with the in/out animations associated to my ViewAnimator?
Note: I have tried calling ViewAnimator.setDisplayedChild(1) but that also animates the transition.
I looked around and waited for a response but I'm guessing it's not possible. So, in the absence of a better solution, set your ViewAnimator's in/out animations to null when you want to jump straight to a particular child View of your ViewAnimator, as follows:
myViewAnimator.setInAnimation(null);
myViewAnimator.setOutAnimation(null);
myViewAnimator.setDisplayedChild(childIndex);
If anyone knows a better way - rather than playing with the ViewAnimator's in/out animations - please share!
Unfortunately, there is no built-in function to do that, but you can add this function, which does the job:
public static void setDisplayedChildNoAnim(ViewAnimator viewAnimator, int whichChild) {
Animation inAnimation = viewAnimator.getInAnimation();
Animation outAnimation = viewAnimator.getOutAnimation();
viewAnimator.setInAnimation(null);
viewAnimator.setOutAnimation(null);
viewAnimator.setDisplayedChild(whichChild);
viewAnimator.setInAnimation(inAnimation);
viewAnimator.setOutAnimation(outAnimation);
}
Here's a more comprehensive way, which let you choose either an id or a view, and with/without animation:
fun ViewAnimator.setViewToSwitchTo(viewToSwitchTo: View, animate: Boolean = true): Boolean {
if (currentView === viewToSwitchTo)
return false
for (i in 0 until childCount) {
if (getChildAt(i) !== viewToSwitchTo)
continue
if (animate)
displayedChild = i
else {
val outAnimation = this.outAnimation
val inAnimation = this.inAnimation
this.inAnimation = null
this.outAnimation = null
displayedChild = i
this.inAnimation = inAnimation
this.outAnimation = outAnimation
}
return true
}
return false
}
fun ViewAnimator.setViewToSwitchTo(#IdRes viewIdToSwitchTo: Int, animate: Boolean = true): Boolean {
if (currentView.id == viewIdToSwitchTo)
return false
for (i in 0 until childCount) {
if (getChildAt(i).id != viewIdToSwitchTo)
continue
if (animate)
displayedChild = i
else {
val outAnimation = this.outAnimation
val inAnimation = this.inAnimation
this.inAnimation = null
this.outAnimation = null
displayedChild = i
this.inAnimation = inAnimation
this.outAnimation = outAnimation
}
return true
}
return false
}
Example of usage:
viewSwitcher.setViewToSwitchTo(someView,false)

Categories

Resources