I would like to achieve a "snap" effect on an Android ListView. Specifically, when the ListView stops scrolling I would like it to stop at certain positions so that the first visible item is completely shown. To do that, I need to be able to estimate the final position that the ListView will stop at when the user stops dragging or flinging.
In the case of dragging, when we receive the ACTION_UP or ACTION_CANCEL event, I can simply read off the current Y offset as the final position.
In the case of flinging, however, when we receive the ACTION_UP or ACTION_CANCEL event, I will need some extra information to determine the final position that the ListView will stop scrolling at because of the deceleration period.
iOS provides such information through the targetContentOffset parameter in willEndDragging. Is there any equivalent information available in Android?
This is related to another question I asked but got no response: In Android, how to achieve snap effect on a listview while respecting acceleration caused by fling?
I would override the OnScroll event of your TestListView like:
#Override
public void onScroll(AbsListView lw, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) {
// Determine when the last row item is fully visible.
final int lastItem = firstVisibleItem + visibleItemCount;
if(lastItem >= totalItemCount) {
Log.d("onScroll", "Reached last item");
}
}
}
Notice this listener gives you the first visible position/item, and the number of row items in the Listview.
I would use this listener because sometimes getFirstVisiblePosition() is not accurate, especially on a virtual ListView like what we're using.
Another advantage of onScroll override method over onTouchEvent is that onScroll covers less triggers than onTouchEvent. And it is fast in my experience with it.
Keep us posted and I am interested on this issue. Regards, Tommy Kwee
Related
I'm trying to implement an autoplay feature for video items in a RecyclerView (linear vertical layout). I can't figure out how to know when a certain item is currently on/off the screen so I can autoplay/pause the video. If I put the code in onBindViewHolder method all videos start playing simultaniously. Couldn't find a solution by googling it either. Help, please!
For Recyclerview you should rely on your layout manager to give you this information.
https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager
or
https://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager
Assign a layout to the RecyclerView when you assign the adapter. Then use it to see what is visible.
Let me help a bit more with some Psedo Kotlin code for you to help with the player aspect.
Let's pretend you have some object that is bound into each row that can trigger the playing and has a unique ID. Let's call that an ActiveRowPlayer for this example.
NOTE*
If you are using databinding, it is simple to bind your video player playing content to a property in your model that is populating the row, but that's different story for a different post.
You can make an interface like:
interface IActivePlayerUpdater{
fun onUpdateCurrentPlayer
}
You can make a helper method like:
in your activity, you can implement an interface like :IActivePlayerUpdater and override the methods for it.
override fun onUpdateCurrentPlayer(){
var activeRowPlayer = recyclerView.layoutManager.findFirstCompletelyVisibleItemPosition()
if(activeRowplayer.someID != currentRowPlayer.someID){
currentRowPlayer.stopPlaying
currentRowPlayer = activeRowPlayer
activeRowPlayer.startPlaying
}
}
Then pass it into your adapter and just monitor your onBind method and anytime a new onBind is called that means the content has moved enough to trigger a new row item.
MyAdapterConstructor(IActivePlayerUpdater myCallback)
fun recyclerView.onBindMethod(stuffThatComesHere){
//do normal stuff
myCallback.onUpdateCurrentPlayer()
}
Keep in mind, this is just pseudo to help you on your journey. not intended to be direct copy and paste.
----NOTE* REQUESTED FROM COMMENT TO SUPPLY HOW TO TOUCH VIEWMODEL FROM OUTSIDE OF ADAPTER---
#Goran, this example I had setup a long time ago to avoid
notifyDataSetChanged on selection changed to toggle a checkbox for
each item. I eventually moved to a better option, but you asked how do
you get the viewmodel, here is a sample. rvCameraRoll was my
recyclerView, I was using it to display camera media, but that is not
relevant, just focus on getting the viewModel piece.
The only part you should care about is getting the ViewHolder, I just left the rest there in case it helps you with anything else.
int count = rvCameraRoll.getChildCount();
for (int i = 0; i < count; i++) {
MediaModelGridAdapter.ViewHolder childRow = (MediaModelGridAdapter.ViewHolder)rvCameraRoll.getChildViewHolder(rvCameraRoll.getChildAt(i));
if(isVisible) {
childRow.imgCellSelected.setVisibility(View.VISIBLE);
if(getMediaModelList().get(i).getIsSelected()){
childRow.imgCellSelected.setBackgroundResource(R.drawable.ic_ap_selected_on);
}
}else{
//check that exists, because after fresh delete list may be short while updating cells
if(getMediaModelList().size() > i) {
getMediaModelList().get(i).setIsSelected(false);
}
childRow.imgCellSelected.setBackgroundResource(R.drawable.ic_ap_selected_off);
childRow.imgCellSelected.setVisibility(View.GONE);
}
}
OLD
leaving this for anyone using a ListView
There is a OnScrollListener for ListView
You can override the
onScrollStateChanged(AbsListView view, int scrollState)
and
the
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
this will give you the ListView item that is visible. So by using the onScroll you can detect which items are visible and determine which one to play and stop playing.
if you use with recyclerview
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.OnScrollListener
The RecyclerView is only holding the items and the LayoutManager is responsible for displaying the items, so in order to get the ones that are visible to the user, assuming that you use LinearLayoutManager, you should call :
((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
or
((LinearLayoutManager)recyclerView.getLayoutManager()).findLastVisibleItemPosition();
I need to synchronize the scrolling positions of two ScrollViews.
Both ScrollViews contain their individual RecyclerView (I can't put them inside same ScrollView due to design requirements).
How can I achieve that?
I highly recommend that you CHANGE YOUR DESIGN but there is a way to achieve this
for first scroll view, define two variable x,y and get current scroll position
x = firstScrollView.getScrollX();
y = firstScrollView.getScrollY();
and pass these values to other scrollView "scrollTo" method.
like:
ScrollView secondScrollView = (ScrollView)findViewById(R.id.scrollView);
secondScrollView.scrollTo(x, y);
if didn't work try:
secondScrollView.post(new Runnable() {
#Override
public void run() {
secondScrollView.scrollTo(x, y);
}
});
First of all, use NestedScrollView from Support library since it enables us to listen scroll events easily. Then, set onScrollChange listeners for both NestedScrollView you have. When you receive scroll change for scrollView1 for instance, call scrollTo(...) for scrollView2:
scrollView1.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
#Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
scrollView2.setOnScrollChangeListener(null);
scrollView2.scrollTo(scrollX, scrollY);
scrollView2.setOnScrollChangeListener(...); //SET SCROLL LISTENER AGAIN
}
});
Before calling scrollTo(..) for scrollView2, remove its listener, then add it again. Otherwise, scrollTo(..) you call can cause infinite calls in both listeners of NestedScrollViews.
And of course, you need to write similar above code for your scrollView2.
Attach an RecyclerView.OnScrollListener to both RecyclerViews via addOnScrollListener(RecyclerView.OnScrollListener listener).
Update the position of the other one when the event is fired via: scrollTo(int x, int y).
Remember to somehow distinguish between a user-triggered scroll and a programmtic scroll (from the other RecyclerViews event).
One way would be, like Ugurcan Yildirim suggested, to detach the listener of the ScrollView you want to update.
Another option is a boolean flag isUserTriggered. A third way would be to determine which ScrollView currently has the focus to distinguish (see here).
Doesn't sound like a problem, right?) Let me explain the use case. Let say at the end of every scroll event you want to animate/highlight the target item:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener(){
#Override
public void onScrollStateChanged(final RecyclerView recyclerView,final int newState){
super.onScrollStateChanged(recyclerView,newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) { // on scroll stop
blink();
}
}
}
...
recyclerView.scrollToPosition(targetPosition);
Works as expected, as long as current position is different from the target one and there is something to scroll actually. What is also very possible is that the list is already in the right position and calling scrollToPosition() has no effect, onScrollStateChanged() is not triggered, as well as our animation logic. As a workaround I can think about this:
final int firstVisibleItemPosition = layoutManager.findFirstCompletelyVisibleItemPosition();
if (firstVisibleItemPosition == targetPosition){
blink();
} else {
[previous code block]
}
but is it really the best possible solution? It sounds natural to me to have some consistent onScrollStop() callback after every scrollToPosition() call, regardless of the visual list movement, because it's none of my business to predict what is going to happen on the screen (will it move, or not?) and try to hack that. All I know, as an API user is that scrollToPosition() was called and I expect the appropriate callback to be fired.
Fun fact: if you extend RecyclerView.SmoothScroller and override onStop() method then you're actually notified all the time. The issue with this approach is that it's triggered slightly before the list stop scrolling ¯\_(ツ)_/¯.
I have a list that displays a fairly complex layout for each list item and there is noticable lag when I scroll. Yes, I have already put it in AsyncTask and used a View Holder, as suggested here http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/ and http://developer.android.com/training/improving-layouts/smooth-scrolling.html
I think the next thing to try is to load all items in the list when the page is loading (even those that will be off screen), but I cannot seem to find a way to do this. And, yes, I understand that this is a very bad idea and goes against the point of a ListView in a way, if you are expecting many list items. But I should never have more than 20 items in my case and I cannot think of what else to try to make it faster when the user scrolls. So, I would like to be able to control what gets loaded into my ListView adapter, not just take the default of "whatever is visible on the screen at the time".
I think I understand from http://android.amberfog.com/?p=296 and How ListView's recycling mechanism works how the ListView recycling works. But I cannot see a way to load the items that are not currently appearing on the screen, so that it does not have to create a new view when the user scrolls. So my question is: is it possible to load off screen items in the list view so they are not created when the user scrolls? If so, how?
Thanks in advance.
Use this listener object to detect if visibleThreshold number of items are still to be rendered in the list. And based on that condition fire your Asynk task to load the data in advance for the rest of elements in list. This kind of technique is used in adding Footer view also.
listenerObject = new AbsListView.OnScrollListener() {
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
int visibleThreshold = 3;
if (loading == false && (totalItemCount - visibleItemCount) <= (firstVisibleItem + visibleThreshold)) {
loading = true;
//DO YOUR JOB HERE i.e. your async task
// once your async Task is complete make loading as false
}
}
};
lv.setOnScrollListener(listenerObject);
Here lv is the listview.
And in case your data is already loaded then you don't have to worry about view creation because it is already taken care of by Android's mechanism to optimise performance.
I'd like to have my application do some on-screen changes (outside the ListView) based on what the first visible item in my list is.
What I've done is override the onScroll handler. When this handler is invoked, I set the position of my Cursor to the first visible position so I can look at the data for that row:
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
mCursor.moveToPosition(getListView().getFirstVisiblePosition());
...
I then reference the data in the Cursor to make my display changes.
Here's the thing: This working is spotty. Most of the time it works perfectly. I can't make it screw up.
However, sometimes getListView().getFirstVisiblePosition() does not return 1 until the 3rd item (index 2) is really the first visible item. It displays 0 when the 0 index is showing. It displays 0 when the 1 index is showing. It displays 1 when the 2 index is showing and so on. Additionally, it only does this when the user is scrolling down the list. If the user is scrolling backwards (up), getFirstVisiblePosition() returns the correct value.
I'm wondering if this isn't related to some efficiency problem. This handler is going to execute over and over and over as the user scrolls.
Does anyone have any ideas on what could be the problem? Does anyone have any better solutions?
Update:
Do I remember hearing that getFirstVisiblePosition() is really only a guess; and that Android essentially supposes what index might be available by rows' probable height? If this is so, then I have a problem. Some rows in my list are bigger than others. If this is the case, is there any way around it?
I would actually perform your call in onScrollStateChanged and only execute when scrolling is idle. You will drastically cut down on number of executed requests. Something like this:
public void onScrollStateChanged(final AbsListView view, final int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
// do yer stuff
}
}