Android pagination boundary callback clarification - android

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

Related

RecyclerView scrolls to top after adding items

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.

How to add recyclerview item(s) remove animation

When I use this, it removes one element with animation
{
notificationItems.remove(0);
adapterForNotification.notifyItemRemoved(0);
adapterForNotification.notifyItemRangeRemoved(0,count-1);
}
But, when I use this, it removes all element without animation
count = adapter.getItemCount();
for(int i = 0 ; i < count; ++i){
notificationItems.remove(0);
adapterForNotification.notifyItemRemoved(0);
adapterForNotification.notifyItemRangeRemoved(0,count-1)
}
As I can understand, you are able to remove items, but you need to add sort of animation while removing.
It can be done by deleting a single item at a time with a single animation for each item.
For instance by simulating a swipe animation on an item at a time, and post a delay before deleting the next item, and so on to the way down to the last item of the RecyclerView
Steps:
Step No.1:
In your activity that holds the clear all button and the RecyclerView instance: Create a method of single item delete
private void deleteItem(View rowView, final int position) {
Animation anim = AnimationUtils.loadAnimation(requireContext(),
android.R.anim.slide_out_right);
anim.setDuration(300);
rowView.startAnimation(anim);
new Handler().postDelayed(new Runnable() {
public void run() {
if (myDataSource.size() == 0) {
addEmptyView(); // adding empty view instead of the RecyclerView
return;
}
myDataSource.remove(position); //Remove the current content from the array
myRVAdapter.notifyDataSetChanged(); //Refresh list
}
}, anim.getDuration());
}
Step No.2:
Create the method that will delete all RecyclerView list items >> call it in your button click callback.
boolean mStopHandler = false;
private void deleteAllItems() {
final Handler handler = new Handler();
Runnable runnable = new Runnable() {
#Override
public void run() {
if (myDataSource.size() == 0) {
mStopHandler = true;
}
if (!mStopHandler) {
View v = myRecyclerView.findViewHolderForAdapterPosition(0).itemView;
deleteItem(v, 0);
} else {
handler.removeCallbacksAndMessages(null);
}
handler.postDelayed(this, 250);
}
};
requireActivity().runOnUiThread(runnable);
}
Also it's important to handle configuration change in manifest, activity section, as if the configuration changes while clearing your recycler view list, an exception will be raised
<activity
android:name=".activities.MainActivity"
android:configChanges="orientation|screenSize|keyboard"
android:label="#string/app_name"
...
</activity>
You shouldn't be using both notifyItemRemoved() and notifyItemRangeRemoved(). Only use one at a time.
If you want to remove one item:
notificationItems.remove(index);
adapterForNotification.notifyItemRemoved(index);
If you want to remove all items:
int origCount = notificationItems.size();
notificationItems.clear();
adapterForNotification.notifyItemRangeRemoved(0, origCount - 1);
If you want to remove a range of items:
notificationItems.subList(startIndex, endIndex).clear();
adapterForNotification.notifyItemRangeRemoved(startIndex, endIndex);
EDIT:
If you want to remove each item one by one and show the removal animation for each, try this:
for (int i = 0; i < notificationItems.size(); i++) {
notificationItems.remove(i);
adapterForNotification.notifyItemRemoved(i);
}
count = adapter.getItemCount();
for (int i = 0 ; i < count; ++i){
notificationItems.remove(0);
}
adapterForNotification.notifyItemRangeRemoved(0, count-1)
I did this by accessing the views of the list items directly from the adapater of the recyclerview, animating them, and notifying the adapter after the last animation has played.
fun deleteMultipleAnimated(){
val oldSize = myList.size
myList.removeAt(3)
myList.removeAt(4)
val newSize = myList.size
for(i in newSize until oldSize){
val listItemView = myRecycler.findViewHolderForAdapterPosition(i) as MyAdapater.MyViewHolder
if(i == oldSize-1){
//only the last animation notifies the adapter
listItemView.myView.animate().scaleX(0f).setDuration(250).withEndAction{
myAdapter.notifyItemRangeRemoved(newSize, oldSize)}
}else{
//every other view is animated without an end action
listItemView.myView.animate().scaleX(0f).duration = 250
}
}
}

SharedElements undesirable effect in RecyclerView

I have one activity with one RecyclerView (SuperSlim library) and a detail activity, when I click a item of that list, detail activity will be open. The problem is when I go back, I'm trying to set the clicked element as the first visible element in the list but I get this horrible animation:
This is my onActivityReenter()
#Override
public void onActivityReenter(int resultCode, Intent data) {
super.onActivityReenter(resultCode, data);
Log.d(TAG, "onActivityReenter() called with: resultCode = [" + resultCode + "], data = [" + data + "]");
mTmpReenterState = new Bundle(data.getExtras());
int startingPosition = mTmpReenterState.getInt(EXTRA_STARTING_ITEM_POSITION);
int currentPosition = mTmpReenterState.getInt(EXTRA_CURRENT_ITEM_POSITION);
mRecycler.scrollToPosition(currentPosition);
postponeEnterTransition();
mRecycler.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
mRecycler.getViewTreeObserver().removeOnPreDrawListener(this);
// TODO: figure out why it is necessary to request layout here in order to get a smooth transition.
mRecycler.requestLayout();
startPostponedEnterTransition();
return true;
}
});
}
And my SharedElementCallback:
private final SharedElementCallback exitTransitionCallBack = new SharedElementCallback() {
#Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (mTmpReenterState == null) {
// If mTmpReenterState is null, then the activity is exiting.
View navigationBar = findViewById(android.R.id.navigationBarBackground);
View statusBar = findViewById(android.R.id.statusBarBackground);
if (navigationBar != null) {
names.add(navigationBar.getTransitionName());
sharedElements.put(navigationBar.getTransitionName(), navigationBar);
}
if (statusBar != null) {
names.add(statusBar.getTransitionName());
sharedElements.put(statusBar.getTransitionName(), statusBar);
}
} else {
int startingPosition = mTmpReenterState.getInt(EXTRA_STARTING_ITEM_POSITION);
int currentPosition = mTmpReenterState.getInt(EXTRA_CURRENT_ITEM_POSITION);
if (startingPosition != currentPosition) {
// If startingPosition != currentPosition the user must have swiped to a
// different page in the DetailsActivity. We must update the shared element
// so that the correct one falls into place.
sharedElements.clear();
sharedElements.put("number", mLayoutManager.findViewByPosition(currentPosition).findViewById(R.id.text_number));
sharedElements.put("day", mLayoutManager.findViewByPosition(currentPosition).findViewById(R.id.text_day));
sharedElements.put("recycler", mLayoutManager.findViewByPosition(currentPosition + 1).findViewById(R.id.recycler));
}
mTmpReenterState = null;
}
}
};
I think the problem is that the activity try to make an animation from the original item position to the top to the list, but I don't know how avoid that.
Does anyone know how to fix this??
Thanks in advance guys!!!
After a while I've realized that the problem that I was updating the Main Activity list with a notifyItemChanged(int) by LocalBroadcast and the standard recyclerView animation made that glitch. I solve the problem using:
RecyclerView.setItemAnimator(null)
because the standard didn't work
((SimpleItemAnimator) RecyclerView.getItemAnimator())
.setSupportsChangeAnimations(false)
So the problem is nothing to do with SharedElements.

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);

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