Firebaseui-android: RecyclerView automatically scrolls to the bottom when data is loaded the first time because of call to notifyItemInserted(index).

Created on 3 Sep 2017  路  18Comments  路  Source: firebase/FirebaseUI-Android

I was trying to implement a vertical recyclerview laid out from top to bottom with a firebase node as a data source. I implemented it, but when the data got loaded the list always scrolled to the bottom. I looked into the FirebaseUIDatabase code and discovered the following calls.

@Override
public void onChildChanged(ChangeEventListener.EventType type,
                            DataSnapshot snapshot,
                            int index,
                            int oldIndex) {
     switch (type) {
         case ADDED:
             notifyItemInserted(index); // This causes the list to scroll to the bottom
             break;
         case CHANGED:
             notifyItemChanged(index);
             break;
         case REMOVED:
             notifyItemRemoved(index);
             break;
         case MOVED:
             notifyItemMoved(oldIndex, index);
             break;
         default:
             throw new IllegalStateException("Incomplete case statement");
     }
}

In a top to bottom layout this isn't desired.

I have implemented a solution for this and wanted to share my code.

          // ....
          case ADDED:
              if(mClipTopFirstTime) notifyDataSetChanged(); else notifyItemInserted(index);
              break;
          // .....

The mClipTopFirstTime variable, if true initially, would allow children to be added without scrolling to them. After a certain delay it would be set to false, afterwards, the default behaviour of scrolling to newly added children would be restored.

Should I make a pull request?

Most helpful comment

No, there's something else wrong with your code: by default, the recyclerview doesn't scroll when new items are added. 馃槙 Did you copy the sample app code? It auto scrolls to the bottom because it's a chat app:
https://github.com/firebase/FirebaseUI-Android/blob/1e179c052bfabd9f3f776a76caf35068afbf3c29/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java#L129-L135

Hope this helps! 馃槂

All 18 comments

No, there's something else wrong with your code: by default, the recyclerview doesn't scroll when new items are added. 馃槙 Did you copy the sample app code? It auto scrolls to the bottom because it's a chat app:
https://github.com/firebase/FirebaseUI-Android/blob/1e179c052bfabd9f3f776a76caf35068afbf3c29/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java#L129-L135

Hope this helps! 馃槂

I didn't copy this code :-P

public abstract class ErunAdapter<T, VH extends RecyclerView.ViewHolder> 
extends FirebaseIndexRecyclerAdapter<T,VH>{
    AtomicBoolean isBinding;

    public ErunAdapter(
            Class<T> modelClass,
            @LayoutRes int modelLayout,
            Class<VH> viewHolderClass,
            Query keyQuery,
            DatabaseReference databaseRef
    ){
        super(
                modelClass,
                modelLayout,
                viewHolderClass,
                keyQuery,
                databaseRef
        );
    }

    public void onViewHolderBindingStarted() {
        if(isBinding == null){
            isBinding = new AtomicBoolean(true);
            return;
        }
        isBinding.set(true);
    }

    public void onViewHolderBound(){
        if(isBinding == null){
            isBinding = new AtomicBoolean(false);
            return;
        }
        isBinding.set(false);
    }

    /**
     * Safe notifyDataSetChanged implementation which can be
     * called even during scrolling or binding of the ViewHolder.
     */
    public void notifyDataSetChangedSafe(){
        if(isBinding == null){
            notifyDataSetChanged();
            return;
        }
        if(isBinding.get()){
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    notifyDataSetChangedSafe();
                }
            },0);
        }else{
            notifyDataSetChanged();
        }
    }

    public boolean isEmpty(){
        return getListCount() == 0;
    }

    public void onComplete(){}

    @Override
    public void onDataChanged() {
        super.onDataChanged();
        onComplete();
    }

    @Override
    public void onCancelled(DatabaseError error) {
        super.onCancelled(error);
        onComplete();
    }
}
public class EventAdapter<T, VH extends EventViewHolder> extends ErunAdapter<T, VH>{
    private static final String TAG = EventAdapter.class.getSimpleName();

    private List<VH> lstHolders;
    private Handler mHandler = new Handler();
    Context mContext;

    public EventAdapter(
            Context context,
            Class<T> tClass,
            @LayoutRes int layoutId,
            Class<VH> viewHolderClass,
            Query query,
            DatabaseReference databaseRef
    ){
        super(tClass, layoutId, viewHolderClass, query, databaseRef);
        this.mContext = context;
        lstHolders = new ArrayList<>();
        startUpdateTimer();
    }

    private Runnable updateRemainingTimeRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (lstHolders) {
                long currentTime = System.currentTimeMillis();
                for (VH holder : lstHolders) {
                    holder.updateRemainingTime(currentTime);
                }
            }
        }
    };

    private void startUpdateTimer() {
        Timer tmr = new Timer();
        tmr.schedule(new TimerTask() {
            @Override
            public void run() {
                mHandler.post(updateRemainingTimeRunnable);
            }
        }, 1000, 1000);
    }

    @Override
    public void onBindViewHolder(VH viewHolder, int position) {
        int vt = viewHolder.getItemViewType();

        boolean isEmptyStub =
                vt == R.layout.joined_events_horizontal_empty;
        boolean isProgressView =
                vt == R.layout.joined_events_horizontal_progress;
        if(isEmptyStub){
            TextView tv = viewHolder.mRootView.findViewById(R.id.list_empty_message);
            tv.setText("No Events Joined");
        }
        if(isEmptyStub || isProgressView) return;

        boolean hasBackButton = vt == R.layout.joined_events_title_layout;
        if(hasBackButton){      viewHolder.mRootView.findViewById(R.id.back_button).setOnClickListener(mOnBackButtonPressedListener);
        }
        boolean isHeader = hasBackButton || vt == R.layout.your_events_title_layout;

        if(isHeader){return;}

        int index = position;
        boolean hasHeader = vt == R.layout.joined_events_item_vertical;
        if(hasHeader){index--;}
        super.onBindViewHolder(viewHolder, index);
    }

    @Override
    protected void populateViewHolder(final VH viewHolder, T model, int position) {
        onViewHolderBindingStarted();
        synchronized (lstHolders) {
            viewHolder.setEvent(event);
            if(lstHolders.size()< (getItemCount()-2)) lstHolders.add(viewHolder);
        }
        onViewHolderBound();
    }
}
public class JoinedEventsAdapter extends EventAdapter<EventBasics, EventViewHolder> {
    boolean mSeeAllEvents;
    View mProgressView;
    View mEmptyView;
    boolean mDataLoaded = false;

    public JoinedEventsAdapter(Context context){
        super(
                context,
                EventBasics.class,
                R.layout.joined_events_item,
                EventViewHolder.class,
                FirebaseDatabase.getInstance().getReference()
                        .child("demo")
                        .child("user_joined_events")
                        .child(ErunLogic.AuthTools.getUid()),
                FirebaseDatabase.getInstance().getReference()
                        .child("demo")
                        .child("events_basics")
        );
        this.mSeeAllEvents = false;
    }

    @Override
    public void onComplete(){
        mDataLoaded = true;
        if(mProgressView!=null) mProgressView.setVisibility(View.GONE);
        if(!mSeeAllEvents) return;
        int visibility = !isEmpty() ? View.GONE : View.VISIBLE;
        mEmptyView.setVisibility(visibility);
    }

    /**
       * I am using this adapter at two different places, One where the list has a preceding title and
       * another where there is no title layout. The tile layout is there so that it can scroll with recycler
       * view and I don't have to wrap it in as scroll-view with height set to match parent.
       *
       * There are also layouts which appear when the list is being loaded and when it is empty for the
       * case of the recycler view without the preceding title layout. As for the one with the title layout
       * I have made separate views in that fragment which are made visible in case of no items and 
       * loading respectively which are mProgressView and mEmptyView.
       */
    @Override
    public int getItemViewType(int position) {
        return mSeeAllEvents ?
                position == 0 ?
                        R.layout.joined_events_title_layout :
                        R.layout.joined_events_item_vertical
                :
                !isEmpty() ?
                        R.layout.joined_events_item :
                        mDataLoaded ?
                                R.layout.joined_events_horizontal_empty :
                                R.layout.joined_events_horizontal_progress;
    }

    @Override
    protected void populateViewHolder(EventViewHolder viewHolder, EventBasics model, int position) {
        super.populateViewHolder(viewHolder, model, position);
        onViewHolderBindingStarted();

        /** Inbetween **/

        onViewHolderBound();

    }

    @Override
    public int getItemCount() {
        int count = super.getItemCount();
        return mSeeAllEvents ? count+1 : (count == 0) ? 1 : count;
    }
}
public class JoinedEventsFragment extends Fragment {
    RecyclerView mJoinedEventsList;

   // Other members ..........

    public JoinedEventsFragment() {
        // Required empty public constructor
    }

    public static JoinedEventsFragment newInstance() {
        JoinedEventsFragment fragment = new JoinedEventsFragment();
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_joined_events, container, false);
        mJoinedEventsList = rootView.findViewById(R.id.joined_events);
        mJoinedEventsList.setLayoutManager(new LinearLayoutManager(getActivity()));

       // Other code.........

        mJoinedEventsList.setAdapter(new JoinedEventsAdapter(getActivity(), mListProgress, mEmptyListLayout));
        return rootView;
    }
}

Maybe read from bottom to Top :P or as you like :P

@muddassir235 Nothing caught my eye right away so this kind of thing should probably be posted on Stack Overflow. However, you can check a few things: do you have the LinearLayoutManager reverse its order somewhere? Are views at the bottom of the recyclerview requesting focus? Are you doing something funky in XML? Are you using an old version of the support lib (latest is 26.0.1)?

This kind of thing is difficult to debug because there's so much code and non-conventional stuff going on. I would recommend saving your current code in a dev branch somewhere and then ripping everything out until you're left with only a fragment and the raw FirebaseRecyclerAdapter with just bind view overridden. If _that_ doesn't work, then something has gone terribly wrong somewhere else.

OK, will try that.

@muddassir235 I agree with @SUPERCILEX. This type of issue should be posted on StackOverflow since it is not caused by FirebaseUI but rather something in your a[['s code. Good luck!

please use this code

linearLayoutManager.setReverseLayout(true);

Hope this helps

@muddassir235
Hi, I have the same situation. Do you find solution?

Hi @Roxytsai, the solution proposed by @viramaham worked for me.

If this is happening when you are changing the visibility of the recycler view. The issue might be of focus. Try adding a dummy edit text view before recycler view and give 0 width and 0 height to it. Also set focusable to true for this editText and set focusable to false for recycler view

Although solution provided by @viramaham does work, it does not address situation where stackFromEnd = true is used: issue still presents

No, there's something else wrong with your code: by default, the recyclerview doesn't scroll when new items are added. 馃槙 Did you copy the sample app code? It auto scrolls to the bottom because it's a chat app:
FirebaseUI-Android/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java

Lines 129 to 135 in 1e179c0

// Scroll to bottom on new messages
mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
mManager.smoothScrollToPosition(mMessages, null, mAdapter.getItemCount());
}
});
Hope this helps! 馃槂

For those who want to have a better clue of what's going on here, please refer to this issue in epoxy repo. https://github.com/airbnb/epoxy/issues/224#issuecomment-305991898.
It has quite elaborated explanation on why and how.

@muddassir235 Nothing caught my eye right away so this kind of thing should probably be posted on Stack Overflow. However, you can check a few things: do you have the LinearLayoutManager reverse its order somewhere? Are views at the bottom of the recyclerview requesting focus? Are you doing something funky in XML? Are you using an old version of the support lib (latest is 26.0.1)?

This kind of thing is difficult to debug because there's so much code and non-conventional stuff going on. I would recommend saving your current code in a dev branch somewhere and then ripping everything out until you're left with only a fragment and the raw FirebaseRecyclerAdapter with just bind view overridden. If _that_ doesn't work, then something has gone terribly wrong somewhere else.

Thank You

No, there's something else wrong with your code: by default, the recyclerview doesn't scroll when new items are added. 馃槙 Did you copy the sample app code? It auto scrolls to the bottom because it's a chat app:
https://github.com/firebase/FirebaseUI-Android/blob/1e179c052bfabd9f3f776a76caf35068afbf3c29/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java#L129-L135

Hope this helps! 馃槂

where is mManager

Was this page helpful?
0 / 5 - 0 ratings