RecyclerView scrolls to top after adding items - android

I have a RecyclerView that I use to display items in an activity. When I scroll to the bottom, I make another API call to fetch an additional set of data. When the items load, my RecyclerView scrolls to the top. I have tried the other solutions listed without success. I have attempted many if not all of the suggestions mentioned in similar threads. None of them worked for me.
activity_see_more.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/see_more_frameLayout" />
</LinearLayout>
In the SeeMoreActivity.java I override the following method from the MvpView
#Override
public void loadPropertiesForCategory(List<Property> propertyList) {
Preferences sharedPref = new Preferences(this);
if (!properties.containsAll(propertyList)) {
properties.addAll(propertyList);
}
sharedPref.setFilterList(properties);
ResultListFragment fragment = ResultListFragment.setTag("see_more_filter");
getSupportFragmentManager().beginTransaction().add(R.id.see_more_frameLayout, fragment, "search").commit();
}
ResultListFragment.java
private void setListOfCategoryProperties() {
page = 2;
mSearchList = mPreferences.getFilterList();
setListAdapter(false, false, View.VISIBLE, View.GONE, false);
mResultRecycleView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (linearLayoutManager != null && linearLayoutManager.findLastVisibleItemPosition() == mSearchList.size() - 1) {
((SeeMoreActivity) getActivity()).setLoadPropertiesParamsAndCallApi(page);
if (mSearchList.size() != mPreferences.getFilterList().size()) {
page++;
mSearchList = mPreferences.getFilterList();
//mAdapter.notifyDataSetChanged();
mAdapter.notifyItemInserted(mSearchList.size() - 1);
}
}
}
});
}
public void listAdapter() {
mResultRecycleView.setLayoutManager(new LinearLayoutManager(getContext()));
mResultRecycleView.setItemAnimator(new DefaultItemAnimator());
mResultRecycleView.setNestedScrollingEnabled(true);
mResultRecycleView.setAdapter(mAdapter);
}
private void setListAdapter(boolean showDeleteButton, boolean changeVisibility, int showNoResult, int showNoSavedSearch, boolean reverseFavorite) {
if (mSearchList != null && mSearchList.size() != 0) {
if (changeVisibility) {
setVisibility(View.VISIBLE, View.GONE, View.GONE);
}
mAdapter = new SearchListAdapter(R.layout.item_single_element, mSearchList, showDeleteButton, this, reverseFavorite);
listAdapter();
} else {
setVisibility(View.GONE, showNoSavedSearch, showNoResult);
}
}
public void handleTags() {
switch (mTag) {
case "result_search":
setListAdapterForHomeSearch();
break;
case "see_more_filter":
setListOfCategoryProperties();
break;
case Constants.USER_FAVORITES_KEY:
setListOfUserFavoriteProperties();
break;
}
}

It's a bit hard to tell, but it looks like you're re-setting the adapter to a new adapter every time you receive more data. That resets a bunch of internal state in RecyclerView, so that's probably what's causing this.
To solve it, you should leave the existing adapter in place and instead call one of the Adapter.notify... methods (probably notifyItemRangeInserted) to inform the adapter that more data has been added. You could also use ListAdapter and its submitList method to automatically handle it for you.

Issue was that I was creating a new instance of my fragment every time I made an API call. Now that I create only one instance and update the list by just adding new elements to it, everything works correctly.

Related

Android pagination boundary callback clarification

I've integrated the pagination component in my app and it's working perfectly fine (almost).
I've Database + network model. Database initially has some items which are consumed by LivePagedListBuilder. I observe this LiveData<PagedList<InboxEntity>> and ultimately feed the list to PagedListAdapter#submitList(PagedList<T> pagedList), something like :
LiveData<PagedList<Entity>> entities;
PagedList.Config pagedListConfig =
(new PagedList.Config.Builder()).setEnablePlaceholders(true)
.setPrefetchDistance(5)
.setEnablePlaceholders(false)
.setPageSize(10)
.setInitialLoadSizeHint(10)
.build();
entities = new LivePagedListBuilder<>(DAO.getItemList(), pagedListConfig)
entities.observe(this, new Observer<PagedList<Entity>>() {
#Override
public void onChanged(#Nullable PagedList<Entity> inboxEntities) {
inboxAdapter.submitList(inboxEntities);
isFirstLoad = false;
}
});
DAO#getItemList returns DataSource.Factory<Integer, Entity>.
I am listening for the boundary callback and trigger a network call when it reaches the end of the paged list. That call populates the database again.
There's one more thing. I've registered AdapterDataObserver on recycler view because if an item has been inserted at the beginning, I've to scroll to the top position:
RecyclerView.AdapterDataObserver adapterDataObserver = new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
if (positionStart == 0) {
layoutManager.scrollToPositionWithOffset(positionStart, 0);
}
}
};
I am facing a problem in this model :
After making the network call, database is populated again and onChanged function is called with a new PagedList<Entity>. Now, does this paged list contains only the new items. I've confirmed this.
But onItemRangeInserted method is called with positionStart as 0 too, which suggests that items are being inserted at the beginning. But they are not. They are being inserted at the end, confirmed with stetho db inspector.
Then why is the onItemRangeInserted being called with positionStart as 0? This is making it difficult for me to distinguish when a fresh item is inserted at the beginning of the adapter and when items are inserted at the end.
Edit:
value of itemCount is 10 which is my page size.
In DiffCallback, I just compare the primary key column of the two entities in areItemsTheSame function.
This is how I'm doing it, not ideal but it works. I'm using a recycler view with reverse set to true as it's a chat app so you'll have to adjust for your use case.
In your fragment create a Timer field:
private Timer mTimer;
Add a scroll state listener that sets a "latest row id" that you can compare later.
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
//Get the latest id when we started scrolling
AppExecutors.getInstance().databaseIO().execute(() ->
mLatestMessageId = mHomeViewModel.getMessageViewModel().getLatestMessageId(mOurUsername, mChatname));
}
}});
Use the AdapterDataObserver to kick off a new timer when the insert is called.
private RecyclerView.AdapterDataObserver mAdapterDataObserver = new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
if (mTimer != null) {
mTimer.cancel();
}
mTimer = new Timer();
mTimer.schedule(new CheckNewMessageTimer(), 100);
}
};
Here's the timer class, if new items have been inserted the "latest row id" will have incremented in the database. I'm only scrolling to the bottom if I'm already at the bottom, otherwise I just change the color of a "scroll to bottom" FAB that I have.
class CheckNewMessageTimer extends TimerTask {
#Override
public void run() {
if (newItemsAdded()) {
int firstItemPosition = mLinearLayoutManager.findFirstVisibleItemPosition();
boolean atBottom = firstItemPosition == 1;
SurespotLog.d(TAG, "CheckNewMessageTimer, firstItemPosition: %d, atBottom: %b", firstItemPosition, atBottom);
AppExecutors.getInstance().mainThread().execute(() -> {
if (atBottom) {
SurespotLog.d(TAG, "CheckNewMessageTimer, scrolling to bottom");
mListView.scrollToPosition(0);
}
else {
if (mFab != null) {
SurespotLog.d(TAG, "CheckNewMessageTimer, new message received but we're not scrolled to the bottom, turn the scroll button blue");
mFab.setBackgroundTintList(ColorStateList.valueOf(getResources().getColor(R.color.surespotBlue)));
}
}
});
}
}
private boolean newItemsAdded() {
int dbLatestMessageID = mHomeViewModel.getMessageViewModel().getLatestMessageId(mOurUsername, mChatname);
if (mLatestMessageId > 0 && dbLatestMessageID > mLatestMessageId) {
mLatestMessageId = dbLatestMessageID;
return true;
}
return false;
}
}

SwipeListView setSwipeMode none is disabling onclick as well as swipe

Hi am using a SwipeListView that has two buttons on its back view i'm trying to programatically set the SwipeListView so that it doesn't swipe by setting the swipe mode to none. the problem i am having is the front view is now not registering the click. Does any one know why???
Heres what i have tried so far
final SwipeListView messagesList = (SwipeListView)v.findViewById(R.id.list);
if(r_deleteMessages == false || r_markMessages == false) messagesList.setSwipeMode(SwipeListView.SWIPE_MODE_NONE);
if(messageData != null){
ViewGroup.LayoutParams params = messagesList.getLayoutParams();
int size;
size = messageData.size();
params.height = (SM_Global.pxFromDp(context, 80) * size) +3;
messagesList.setLayoutParams(params);
messagesList.requestLayout();
messagesList.setFocusable(false);
final MessagesAdapter messagesAdapter = new MessagesAdapter(context, R.layout.layout_message_item, messageData,messagesList,"profile");
messagesList.setAdapter(messagesAdapter);
Log.v("Auth","CAN READ MESSAGES | " + r_readMessages);
messagesList.setSwipeListViewListener(new BaseSwipeListViewListener() {
#Override
public void onClickFrontView(int position) {
super.onClickFrontView(position);
Log.v("Auth","CLICKED ");
}
});
You could add a Listener to the list view and override onChangeSwipeMode
if(r_deleteMessages == false || r_markMessages == false){
mList.setSwipeListViewListener(new BaseSwipeListViewListener() {
#Override
public int onChangeSwipeMode(int position) {
return SwipeListView.SWIPE_MODE_NONE;
}
});
}
This way you will still get touch events.
You may also need to do mList.setSwipeOpenOnLongPress(false); to disablr the long click
see: https://github.com/47deg/android-swipelistview/issues/9
I tried the suggested ways but didn't work for me. So I made changes in my List Adapter & handled the click event there. Now it is working fine.

Scroll a ListView to the top without any screen flickering

I have implemented a ListView that has the functionality that you see in many apps, where user scrolls to bottom and it loads more, that OnScrollListener is this:
public class OnScrolledToEndListener implements AbsListView.OnScrollListener
{
private int prevLast;
#Override
public void onScrollStateChanged(AbsListView absListView, int i)
{
}
#Override
public void onScroll(AbsListView absListView, int first, int visible, int total)
{
int last = first + visible;
if (last == total)
{
if (prevLast != last)
{
prevLast = last;
onScrolledToEnd();
}
}
}
public void onScrolledToEnd()
{
}
}
Now the problem is that when a user has scrolled to the bottom of a list, and hits the refresh button in my app, I want it to start over at the top of the list, because if it stays at the bottom of the list, then the scroll listener will immediately trigger. The best way I've found to solve this is by doing the following before executing the refresh:
mListView.setSelection(0);
mListView.post(
new Runnable()
{
#Override
public void run()
{
mListView.setVisibility(View.GONE);
mLoadingLayout.setVisibility(View.VISIBLE); //this is basically a progressbar
// do the refresh
}
}
);
But there is a slight flicker when the list scrolls to the top. Any ideas on how to make it look better?
I figured out the solution. Apparently setting the ListView to View.GONE makes it not update its layout, so I set it to View.INVISIBLE instead and it worked. I didn't even have to use a Runnable.
mListView.setSelection(0);
mListView.setVisibility(View.INVISIBLE);
mLoadingLayout.setVisibility(View.VISIBLE);

How to handle clicks on the rows of listView, while notifyDataSetChanged might be called?

Background
I have a complex adapter for a listView.
Each row has some inner views that need to be clickable (and hande the clicks), and they also have selectors (to show the effect of touching).
on some cases, notifyDataSetChanged() needs to be called quite frequently (for example once/twice a second), to show some changes on the listView's items.
As an example, consider seeing a list of downloading files, where you show the user the progress of each file being downloaded.
The problem
Each time notifyDataSetChanged is called, the touch event is lost on the touched view, so the user can miss clicking on it , and especially miss long clicking on it.
Not only that, but the selector also loses its state, so if you touch it and see the effect, when the notifyDataSetChanged is called, the view loses its state and you see it as if it isn't get touched.
This happens even for views that have nothing in them being updated (meaning I just return the convertView for them) .
Sample code
The code below demonstrates the problem. It is not the original code but a super short sample to make it clear what I'm talking about.
Again, this is not the original code, so I've removed the ViewHolder usage and taking care of the clicking to do some operations, in order to make it simple to read and understand. But it's still the same logic.
Here's the code:
MainActivity.java
public class MainActivity extends ActionBarActivity {
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ListView listView = (ListView) findViewById(R.id.listView);
final BaseAdapter adapter = new BaseAdapter() {
#Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
TextView tv = (TextView) convertView;
if (tv == null) {
tv = new TextView(MainActivity.this);
tv.setBackgroundDrawable(getResources().getDrawable(R.drawable.item_background_selector));
tv.setOnClickListener(new OnClickListener() {
#Override
public void onClick(final View v) {
android.util.Log.d("AppLog", "click");
}
});
}
//NOTE: putting the setOnClickListener here won't help either.
final int itemViewType = getItemViewType(position);
tv.setText((itemViewType == 0 ? "A " : "B ") + System.currentTimeMillis());
return tv;
}
#Override
public int getItemViewType(final int position) {
return position % 2;
}
#Override
public long getItemId(final int position) {
return position;
}
#Override
public Object getItem(final int position) {
return null;
}
#Override
public int getCount() {
return 100;
}
#Override
public boolean areAllItemsEnabled() {
return false;
}
#Override
public boolean isEnabled(final int position) {
return false;
}
};
listView.setAdapter(adapter);
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
// fake notifying
adapter.notifyDataSetChanged();
android.util.Log.d("AppLog", "notifyDataSetChanged");
handler.postDelayed(this, 1000);
}
}, 1000);
}
}
item_background_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"><shape>
<solid android:color="#android:color/holo_blue_light" />
</shape></item>
<item android:state_focused="true"><shape>
<solid android:color="#android:color/holo_blue_light" />
</shape></item>
<item android:drawable="#android:color/transparent"/>
</selector>
activity_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.test.MainActivity"
tools:ignore="MergeRootFrame" >
<ListView
android:id="#+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>
</FrameLayout>
Partial solution
It's possible to update only the needed views, by finding the view and then calling getView on it, but this is a workaround.
Also, it won't work in the case of adding/removing items from the listView, which needs to have notifyDataSetChangedbeing called. Plus it also makes the updated view to lose its touching state.
EDIT: even the partial solution doesn't work. Maybe it's causing a layout of the entire listView, which causes the other views to lose their states.
The question
How can I let views stay "in sync" with the touch events after calling notifyDataSetChanged() ?
You are not updating click listener of "Recycled" views.
Place tv.setOnClickListener() out of the if (tv == null) check.
Also, the properties you want to "stay synced" should be in the model backing the ListView. Never trust Views to hold important data, they should only reflect data from model.
class Item{
String name;
boolean enabled;
boolean checked
}
class ItemAdapter extends ArrayAdapter<Item>{
#Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
if(convertView == null){
// create new instance
}
// remove all event listeners
Item item = getItem(position);
// set view properties from item (some times, old event listeners will fire when changing view properties , so we have cleared event listeners above)
// setup new event listeners to update properties of view and item
}
}
As mentioned you can use the View.setOnTouchListener() and catch the ACTION_DOWN and the ACTION_UP Event.
For the Selector Animation you can use your Custom Color Animation. Here is an example for changing the backgroundColor with an Animation.
Integer colorFrom = getResources().getColor(R.color.red);
Integer colorTo = getResources().getColor(R.color.blue);
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
colorAnimation.addUpdateListener(new AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animator) {
view.setBackgroundColor((Integer)animator.getAnimatedValue());
}
});
colorAnimation.start();
Alternative solution
EDIT (by the OP, meaning the thread-creator): An alternative solution, based on the above, is to use the touchListener to set the state of the background of the view.
The disadvantage of it (though it should be fine for most cases) is that if the list gets more/less items, the touch is lost (by ACTION_CANCEL) , so, if you wish to handle it too, you could use this event and handle it yourself using your own logic.
Even though the solution is a bit weird and doesn't handle all possible states, it works fine.
Here's a sample code:
public static abstract class StateTouchListener implements OnTouchListener,OnClickListener
{
#Override
public boolean onTouch(final View v,final MotionEvent event)
{
final Drawable background=v.getBackground();
// TODO use GestureDetectorCompat if needed
switch(event.getAction())
{
case MotionEvent.ACTION_CANCEL:
background.setState(new int[] {});
v.invalidate();
break;
case MotionEvent.ACTION_DOWN:
background.setState(new int[] {android.R.attr.state_pressed});
v.invalidate();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
background.setState(new int[] {});
v.invalidate();
v.performClick();
onClick(v);
break;
}
return true;
}
}
and the fix in my code:
tv.setOnTouchListener(new StateTouchListener()
{
#Override
public void onClick(final View v)
{
android.util.Log.d("Applog","click!");
};
});
This should replace the setOnClickListener I used.

Remove Item + Scroll to Next in Android ViewPager

I have an Activity containing a ViewPager that displays N fragments. Each fragment is showing the properties of an object from an ArrayList in my ViewPager's custom adapter (extends FragmentStatePagerAdapter).
The fragment has (among other things) a button that should remove the currently displayed fragment and scroll to the next one with setCurrentItem(position, true) so that if the user scrolls back, the previous item is gone. I do so by using something like this (simplified):
deleteButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
MyActivity parentActivity = (MyActivity)getActivity();
// First, scroll to next item (smoothly)
parentActivity.pager.setCurrentItem(parentActivity.pager.getCurrentItem()+1, true);
// Database stuff...
doSomeDBOperations();
// Method in Activity that removes the current object (I believe this method is working fine and yes, it calls notifyDataSetChanged())
parent.removeObject(currentObject);
}
});
This has the desired behavior as the object represented by the fragment whose delete button was pressed gets removed and the viewpager goes to the next page.
My problem is that the ViewPager doesn't scroll smoothly but rather "jumps instantly" to the next fragment. If I comment the removeObject() call, the smooth scroll works (but the item isn't removed). I believe it's has something to do with the removeObject() being called before the setCurrentItem() has finished the smooth scrolling animation?
Any ideas on how to fix this and achieve item removal + smooth scroll? If my assumption is correct, how can I make sure I get the smooth scroll to finish before removing the object?
EDIT 1:
My assumption seems correct. If I put the parent.removeObject(currentObject) inside
// ...inside the previously shown public void onClick(View v)...
confirm.postDelayed(new Runnable() {
#Override
public void run() {
// Method in Activity that removes the current object (I believe this method is working fine and yes, it calls notifyDataSetChanged())
parent.removeObject(currentObject);
}
}, 1000);
so that the removeObject() call waits for a second, it works as expected: scroll to the next item, remove the previous. But this is a very ugly workaround so I'd still like a better approach.
EDIT 2:
I figured out a possible solution (see below).
I ended up overriding the
public void onPageScrollStateChanged(int state)
method:
Whenever the user presses the delete button in the fragment, the listener sets a bool in the current item (flagging it for deletion) and scrolls to the next one.
When the onPageScrollStateChanged detects that the scroll state changed to ViewPager.SCROLL_STATE_IDLE (which happens when the smooth scroll ends) it checks if the previous item was marked for deletion and, if so, removes it from the ArrayList and calls notifyDataSetChanged().
By doing so, I've managed to get the ViewPager to smoothly scroll to the next position and delete the previous item when the "delete" button is pressed.
EDIT: Code snippet.
#Override
public void onPageScrollStateChanged(int state)
{
switch(state)
{
case ViewPager.SCROLL_STATE_DRAGGING:
break;
case ViewPager.SCROLL_STATE_IDLE:
int previousPosition = currentPosition - 1;
if(previousPosition < 0){
previousPosition = 0;
}
MyItem previousItem = itemList.get(previousPosition);
if(previousItem.isDeleted())
{
deleteItem(previousItem);
// deleteItem() Does some DB operations, then calls itemList.remove(position) and notifyDataSetChanged()
}
break;
case ViewPager.SCROLL_STATE_SETTLING:
break;
}
}
Have you tried ViewPager.OnPageChangeListener?
I would call removeObject(n) method in OnPageChangeListener.onPageSelected(n+1) method.
I did something different that works smoothly. The idea is to to remove the current item with animation (setting its alpha to 0), then translating horizontally the left or right item (with animation) to the now invisible item position.
After the animation is complete, I do the actual data removal and notfyDataSetChanged() call.
This remove() method I put inside a subclass of ViewPager
public void remove(int position, OnViewRemovedListener onViewRemovedListener) {
final int childCount = getChildCount();
if (childCount > 0) {
View toRemove = getChildAt(position);
int to = toRemove.getLeft();
final PagerAdapter adapter = getAdapter();
toRemove.animate()
.alpha(0)
.setDuration(getResources().getInteger(android.R.integer.config_shortAnimTime))
.setListener(new SimpleAnimatorListener() {
#Override
public void onAnimationEnd(Animator animation) {
if (childCount == 1) {
if (onViewRemovedListener != null) onViewRemovedListener.onRemoved(position, -1);
if (adapter!= null) adapter.notifyDataSetChanged();
}
}
})
.start();
if (childCount > 1) {
int newPosition = position + 1 <= childCount - 1 ? position + 1 : position - 1;
View replacement = getChildAt(newPosition);
int from = replacement.getLeft();
replacement.animate()
.setInterpolator(new DecelerateInterpolator())
.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime))
.translationX(to - from)
.setListener(new SimpleAnimatorListener() {
#Override
public void onAnimationEnd(Animator animation) {
if (onViewRemovedListener != null) onViewRemovedListener.onRemoved(position, newPosition);
if (adapter!= null) adapter.notifyDataSetChanged();
}
})
.start();
}
}
}
public interface OnViewRemovedListener {
void onRemoved(int position, int newPosition);
}

Categories

Resources