I want to know the visible item on screen so that if the exoplayer of that item is running I can stop it. Here I have tried many ways to stop already running exoplayer but I've not found any appropriate solution. Can anyone tell me how can I achieve this.
Using all these four methods is not giving me visible items of recyclerview. It's only giving me the last item and the first item visible on the screen as their name suggests
rvTips.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0) {
// Scrolling up
} else {
// Scrolling down
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
// First Method Tried
firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
firstCompletelyVisiblePosition = layoutManager.findFirstCompletelyVisibleItemPosition()
lastCompletelyVisiblePosition = layoutManager.findLastCompletelyVisibleItemPosition()
for (i in 0 until tipList.size) {
if (i != firstVisiblePosition
&& i != lastVisiblePosition
&& i != firstCompletelyVisiblePosition
&& i != lastCompletelyVisiblePosition
&& tipList[i].exoplayer != null
) {
tipList[i].exoplayer!!.playWhenReady = false
tipList[i].exoplayer!!.stop()
tipList[i].exoplayer!!.seekTo(0)
}}
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
}
// Second Method Tried
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
firstCompletelyVisiblePosition = layoutManager.findFirstCompletelyVisibleItemPosition()
lastCompletelyVisiblePosition = layoutManager.findLastCompletelyVisibleItemPosition()
for (i in 0 until tipList.size) {
if ((i < firstVisiblePosition || i > lastVisiblePosition)
&& tipList[i].exoplayer != null
) {
tipList[i].exoplayer!!.playWhenReady = false
tipList[i].exoplayer!!.stop()
tipList[i].exoplayer!!.seekTo(0)
}}
}
}
})
your for loop should look like this
for (i in 0 until tipList.size) {
if ((i < firstVisiblePosition || i > lastVisiblePosition) &&
tipList[i].exoplayer != null) {
tipList[i].exoplayer!!.playWhenReady = false
tipList[i].exoplayer!!.stop()
tipList[i].exoplayer!!.seekTo(0)
}
}
pause all non-visible items - position lower than first visible or higher than last visible - assuming you've collected all ExoPlayers in tipList properly
players pausing code should fire only when state change to AbsListView.OnScrollListener.SCROLL_STATE_IDLE (list stopped scrolling, by touch or by fling), so above should be wrapped in some if statement
additionaly you can pause player inside adapter in onViewDetachedFromWindow or onViewRecycled method
Using the RecyclerView LayoutManager, you can find firstVisibleItemPosition & firstCompletelyVisibleItemPosition and similarly the last ones.
When an item starts playing the video (assuming you've written the implementation in ViewHolder class), you can find the playing item position by getAdapterPosition in there. Use a global variable to maintain this currentlyPlaying item position (probably in your fragment/activity class from where you are setting the adapter), you can send the position to view class through an interface listener.
Now whenever you scroll, you can check whether currently playing item's position is in visible items range. If yes, you can call adapter.notifyItemChanged(currentlyPlaying, true) to pause/stop the player. true is custom boolean value you can send to your adapter, which can be intercepted inside OnBindViewHolder with payloads argument, to toggle the playing state of video.
Related
StackOverflow contains a lot of questions like this one, but so far absolutely no solution works 100%.
I tried the solutions for these:
RecyclerView ItemDecoration - How to draw a different width divider for every viewHolder?
first item center aligns in SnapHelper in RecyclerView
Horizontally center first item of RecyclerView
LinearSnapHelper doesn't snap on edge items of RecyclerView
Android Centering Item in RecyclerView
How to make recycler view start adding items from center?
How to have RecyclerView snapped to center and yet be able to scroll to all items, while the center is "selected"?
How to snap to particular position of LinearSnapHelper in horizontal RecyclerView?
And every time the first item fails to be centered correctly.
Sample code of what I am using
recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
#Override
public void getItemOffsets(
#NonNull Rect outRect,
#NonNull View view,
#NonNull RecyclerView parent,
#NonNull RecyclerView.State state
) {
super.getItemOffsets(outRect, view, parent, state);
final int count = state.getItemCount();
final int position = parent.getChildAdapterPosition(view);
if (position == 0 || position == count - 1) {
int offset = (int) (parent.getWidth() * 0.5f - view.getWidth() * 0.5f);
if (position == 0) {
setupOutRect(outRect, offset, true);
} else if (position == count - 1) {
setupOutRect(outRect, offset, false);
}
}
}
private void setupOutRect(Rect rect, int offset, boolean start) {
if (start) {
rect.left = offset;
} else {
rect.right = offset;
}
}
});
After investigating I discovered that is because at the time of the getItemOffsets the view.getWidth is 0, it hasn't been measured yet.
I tried to force it to be measured, but every single time it gives an incorrect size, nothing like the actual size it occupies, it is smaller.
I also tried to use the addOnGlobalLayoutListener trick, but by the time it is called and has the correct width, the outRect was already consumed, so it is lost.
I do not want to set any fixed sizes because the items in the RecyclerView can have different sizes, so setting its padding in advance is not an option.
I also do not want to add "ghost" items to fill the space and those also don't work well for the scrolling experience.
How can I get this working properly?
Ideally the ItemDecorator method looks to be the best, but it falls flat for the first item right away.
I have been using the CenterLinearLayoutManager from Pawel's answer and so far it has been working almost perfectly. I say almost, not because it is not working straight away as it is, but because I ended up using a LinearSnapHelper in the same RecyclerView that makes use of the mentioned layout manager.
Because the snap helper depends on knowing the RecyclerView paddings to calculate its correct center, setting just the padding for the first item throws off this process, causing the first item (and subsequent items until the last one actually shows) to be offset from the center.
My solution was to ensure that both paddings are set right from the start when the first item is shown.
So this:
if (!reverseLayout) {
if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding)
if (lp == itemCount - 1) recyclerView.updatePaddingRelative(end = hPadding)
} else {
if (lp == 0) recyclerView.updatePaddingRelative(end = hPadding)
if (lp == itemCount - 1) recyclerView.updatePaddingRelative(start = hPadding)
}
becomes this
if (!reverseLayout) {
if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding, end = hPadding) // here we set the same padding for both sides
if (lp == itemCount - 1) recyclerView.updatePaddingRelative(end = hPadding)
} else {
if (lp == 0) recyclerView.updatePaddingRelative(end = hPadding, start = hPadding) // here we set the same padding for both sides
if (lp == itemCount - 1) recyclerView.updatePaddingRelative(start = hPadding)
}
And I assume the same logic must be applied to the vertical block as well.
I am sure this can be now optimized even further, so the above final block would look like this:
if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding, end = hPadding) // here we set the same padding for both sides
if (lp == itemCount - 1) {
if (!reverseLayout) recyclerView.updatePaddingRelative(end = hPadding)
if (reverseLayout) recyclerView.updatePaddingRelative(start = hPadding)
}
NOTE: I ended up finding that if I use items with significant width differences between them the same issue I refer here also occurs when the last item loads, which makes sense since at that point we are setting a padding on one side different than the other one, again throwing off the native center calculation.
The solution for this one is to set the same padding for both sides whenever the first item loads and the last one as well like so
recyclerView.updatePaddingRelative(start = hPadding, end = hPadding)
That's literally it, no ifs like in the previous sample.
This, of course, still not solves the issue for when there are too few items showing in such a way that the first and last items are visible, but when I manage to find a solution for that specific case I will update it here.
You can alter padding of RecyclerView itself to get this effect too (as long as clipToPadding is disabled). We can intercept first layout phase in LayoutManager so it can use updated padding even when laying out items for the first time:
Add this layout manager:
open class CenterLinearLayoutManager : LinearLayoutManager {
constructor(context: Context) : super(context)
constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
private lateinit var recyclerView: RecyclerView
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
// always measure first item, its size determines starting offset
// this must be done before super.onLayoutChildren
if (childCount == 0 && state.itemCount > 0) {
val firstChild = recycler.getViewForPosition(0)
measureChildWithMargins(firstChild, 0, 0)
recycler.recycleView(firstChild)
}
super.onLayoutChildren(recycler, state)
}
override fun measureChildWithMargins(child: View, widthUsed: Int, heightUsed: Int) {
val lp = (child.layoutParams as RecyclerView.LayoutParams).absoluteAdapterPosition
super.measureChildWithMargins(child, widthUsed, heightUsed)
if (lp != 0 && lp != itemCount - 1) return
// after determining first and/or last items size use it to alter host padding
when (orientation) {
HORIZONTAL -> {
val hPadding = ((width - child.measuredWidth) / 2).coerceAtLeast(0)
if (!reverseLayout) {
if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding)
if (lp == itemCount - 1) recyclerView.updatePaddingRelative(end = hPadding)
} else {
if (lp == 0) recyclerView.updatePaddingRelative(end = hPadding)
if (lp == itemCount - 1) recyclerView.updatePaddingRelative(start = hPadding)
}
}
VERTICAL -> {
val vPadding = ((height - child.measuredHeight) / 2).coerceAtLeast(0)
if (!reverseLayout) {
if (lp == 0) recyclerView.updatePaddingRelative(top = vPadding)
if (lp == itemCount - 1) recyclerView.updatePaddingRelative(bottom = vPadding)
} else {
if (lp == 0) recyclerView.updatePaddingRelative(bottom = vPadding)
if (lp == itemCount - 1) recyclerView.updatePaddingRelative(top = vPadding)
}
}
}
}
// capture host recyclerview
override fun onAttachedToWindow(view: RecyclerView) {
recyclerView = view
super.onAttachedToWindow(view)
}
}
Then use it for your RecyclerView:
recyclerView.layoutManager = CenterLinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recyclerView.clipToPadding = false // disabling clip to padding is critical
From what I get, you want your first and last items be in the center of your recyclerview. If so, I would recommend a much simpler workaround.
public class OverlaysAdapter extends RecyclerView.Adapter<OverlaysAdapter2.CategoryViewHolder> {
private int fullWidth;//gets the recyclerview full width in constructor. In my case it is full display width.
#Override
public CategoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
FrameLayout fr = (FrameLayout) inflater.inflate(R.layout.item_sticker, parent, false);
if (viewType==TYPE_LEFT_ITEM) {
int marginLeft = (fullWidth-leftItemWidth)/2;
((RecyclerView.LayoutParams)fr.getLayoutParams()).setMargins(marginLeft, 0,0,0);
} else if (viewType==TYPE_RIGHT_ITEM) {
int marginRight = (fullWidth-rightItemWidth)/2;
((RecyclerView.LayoutParams)fr.getLayoutParams()).setMargins(0, 0,marginRight,0);
} else if (viewType==TYPE_MIDDLE_ITEM) {
((RecyclerView.LayoutParams)fr.getLayoutParams()).setMargins(0, 0,0,0);
}
return new CategoryViewHolder(fr);
}
private final int TYPE_LEFT_ITEM = 1;
private final int TYPE_MIDDLE_ITEM = 2;
private final int TYPE_RIGHT_ITEM = 3;
#Override
public int getItemViewType(int position) {
if (position==0)
return TYPE_RIGHT_ITEM;
else if (position==items.size()-1)
return TYPE_LEFT_ITEM;
else
return TYPE_MIDDLE_ITEM;
}
}
The idea is simple. Define three view types, for first, middle and last items. Mind the item view type and calculate the needed margins and set the margins.
Note that in my case, the left margin goes for the last item and the right margin goes for the first item, as the layout is always RTL. You may want to use the reverse order.
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;
}
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)
}
I have a recyclerview in my android project which displays media contents within each view. What I'm trying to achieve is that I'm able to play/pause media as I scroll up and down. I need to get the adapter position of the completely visible view. I'm doing something like this.
In my activity fragment I have this:
layoutmanager = new LinearLayoutManager(Activity);
adapter = new FeedAdapter(vid, userName, this.Context);
feeditem.SetLayoutManager(layoutmanager);
feeditem.SetAdapter(adapter);
var onScrollListener = new XamarinRecyclerViewOnScrollListener(Activity, layoutmanager, adapter);
The scroll listener event looks like this:
public override void OnScrollStateChanged(RecyclerView recyclerView, int newState)
{
base.OnScrollStateChanged(recyclerView, newState);
if (newState == (int)ScrollState.Idle)
{
layoutmanager = (LinearLayoutManager)recyclerView.GetLayoutManager();
int firstVisiblePosition = layoutmanager.FindFirstCompletelyVisibleItemPosition();
int visible = layoutmanager.FindFirstVisibleItemPosition();
int last = layoutmanager.FindLastVisibleItemPosition();
if (firstVisiblePosition >= 0)
{
if (oldFocusedLayout != null)
{
Toast.MakeText(ctx, "Stop Video", ToastLength.Long).Show();
}
}
currentFocusedLayout = layoutmanager.FindViewByPosition(firstVisiblePosition);
Toast.MakeText(ctx, "Play video", ToastLength.Long).Show();
oldFocusedLayout = currentFocusedLayout;
}
}
feeditem.AddOnScrollListener(onScrollListener);
The issue is that the linearlayout manager method FindFirstCompletelyVisibleItemPosition always returns -1 even when the view is completely visible. Other methods like FindFirstVisibleItemPosition and FindLastVisibleItemPosition gives the correct position of the view.
Any idea what might be the issue here?
layoutManager.findFirstCompletelyVisibleItemPosition()
FROM DOCUMENT
Returns the adapter position of the FIRST FULLY VISIBLE view. This position does not include adapter changes that were dispatched after the last layout pass.
It mean that, at least one listitem view should be fully visible otherwise, it give -1 (NO_POSITION)
FROM TESTING
This will work and give correct position...
This won't work and give -1 (NO_POSITION), because two ListItem view is not fully visible.
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;
}