I am trying to use RecyclerView to create a chat application. I am using a LinearLayoutManager with setReverseLayout(true).
When I am scrolled all the way to the bottom (which is the dataset start = newest message) and a new message is inserted into the dataset, the item appears at the bottom of the list as expected (the view is scrolled up to make room for the new item).
The problem I have is when I have scrolled up to see the older messages. When a new message is inserted to the beginning of the dataset, the view is scrolled up by approximately one message height, even though that message isn't even rendered since it's out of the viewport's range.
How can I keep the scrolling behavior, when the view is scrolled to the bottom, but disable it when I have scrolled to the older messages?
UPDATE:
I also made a small app, where this problem is reproduced:
https://github.com/ehehhh/RecyclerViewProblem
UPDATE 2: I committed the fix that worked to the repo I made as well.
Relevant code (hopefully):
compile 'com.android.support:recyclerview-v7:24.2.0'
XML:
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="8dp"
android:paddingTop="8dp"
app:layout_behavior="#string/appbar_scrolling_view_behavior"/>
RecyclerView init code:
layoutManager = new LinearLayoutManager(context);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
layoutManager.setReverseLayout(true);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setScrollContainer(true);
recyclerView.setLayoutAnimation(null);
recyclerView.setItemAnimator(null);
adapter = new ChatAdapter(...);
recyclerView.setAdapter(adapter);
Adapter:
public class ChatAdapter extends RecyclerView.Adapter<ChatViewHolder> {
private List<MessageWrapper> dataset;
public ChatAdapter(List<MessageWrapper> dataset, ...) {
this.dataset = dataset;
setHasStableIds(true);
}
...
#Override
public long getItemId(int position) {
return dataset.get(position).getId();
}
#Override
public int getItemCount() {
return dataset.size();
}
public void datasetChanged(List<MessageWrapper> dataset) {
this.dataset = dataset;
notifyDataSetChanged();
}
}
When a new item is added to the dataset, I just call the datasetChanged method in the adapter.
in Recycler view using notifyDataSetChanged is redundant if you know the items changed
you can use
notifyItemInserted(position)
in this particular case what worked was
notifyItemInserted(0);
or
notifyItemRangeInserted(positionStart, newItems.size() - 1)
this will only rebind the views in this range
check
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#notifyItemInserted(int)
I had a similar issue. I was making chat application, and my RecyclerView was always displaying rows from the top, and I wanted it to go to the bottom to display last inserted message. This is what worked for me, recyclerView.scrollToPosition(messages.size()-1) added in ChildEventListener:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MessageAdapter mMessageAdapter;
private List<Message> messages;
private FirebaseDatabase mFirebaseDatabase;
private DatabaseReference mMessagesDatabaseRef;
....
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mFirebaseDatabase = FirebaseDatabase.getInstance();
mMessagesDatabaseRef = mFirebaseDatabase.getReference().child("messages");
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
// Initialize message ListView and its adapter
messages = new ArrayList<>();
mMessageAdapter = new MessageAdapter(messages);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setAdapter(mMessageAdapter);
.....
mMessagesDatabaseRef.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
messages.add(dataSnapshot.getValue(Message.class));
recyclerView.scrollToPosition(messages.size()-1);
mMessageAdapter.notifyDataSetChanged();
}
......
});
}
}
Related
i hope that you could help me.
i want to use a expandablerecyclerview but i dont need to expand a list of other elements like the lib says
library example
I just need that the item expand and show more info of the item inside the recycler
You can use advanced recyclerview's expandable future
public class ExpandableWithHeaderFooterExampleActivity extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo_minimal);
OnListItemClickMessageListener clickListener = new OnListItemClickMessageListener() {
#Override
public void onItemClicked(String message) {
View container = findViewById(R.id.container);
Snackbar.make(container, message, Snackbar.LENGTH_SHORT).show();
}
};
RecyclerView recyclerView = findViewById(R.id.recycler_view);
// Setup expandable feature and RecyclerView
RecyclerViewExpandableItemManager expMgr = new RecyclerViewExpandableItemManager(null);
// Create wrapped adapter: MyItemAdapter -> expMgr.createWrappedAdapter -> MyHeaderFooterAdapter
RecyclerView.Adapter adapter;
adapter = new SimpleDemoExpandableItemAdapter(expMgr, clickListener);
adapter = expMgr.createWrappedAdapter(adapter);
adapter = new DemoHeaderFooterAdapter(adapter, clickListener);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// NOTE: need to disable change animations to ripple effect work properly
((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
expMgr.attachRecyclerView(recyclerView);
}
}
In my Activity, I use a RecyclerView that uses a FirebaseRecyclerAdapter to draw a list:
mChatDetailsAdapter = new ChatDetailsAdapter(
this,
MessageData.class,
R.layout.layout_chat_item,
ChatDetailsAdapter.MessageViewHolder.class,
query);
mChatListView.setHasFixedSize(true);
LinearLayoutManager lLinearLayoutManager = new LinearLayoutManager(ChatDetailsActivity.this, LinearLayoutManager.VERTICAL, false);
lLinearLayoutManager.setStackFromEnd(true);
lLinearLayoutManager.setReverseLayout(true);
mChatListView.setLayoutManager(lLinearLayoutManager);
mChatListView.addItemDecoration(new VerticalSpaceItemDecoration(UIUtils.dpToPx(ChatDetailsActivity.this, 8)));
mChatListView.setAdapter(mChatDetailsAdapter);
The Adapter takes care about the views:
#Override
protected void populateViewHolder(final MessageViewHolder pViewHolder, final MessageData pMessageData, int pPosition) {
pViewHolder.mMessageTextView.setText(pMessageData.getMessage());
pViewHolder.mSelfLinearLayout.setVisibility(View.VISIBLE);
pViewHolder.mTimeTExtView.setText(StringUtils.toMessageTimeString(mContext, pMessageData.getTime()));
}
In the onBindViewHolder() method I basically do the same, be assigning the data from the model (works correctly after looking at the log messages). Now when scrolling the list the RecyclerView changes its values and doesn't recycle correctly and I have no idea why. Any idea?
I'm using a FirebaseIndexRecyclerAdapter as described on https://github.com/firebase/FirebaseUI-Android/tree/master/database. But my list has many items and I want to only load a few items at first and then lazy load the rest when the user scrolls up.
My first thought was that I could use mKeyRef.limitToLast(5) but then updating this requires recreating the Adapter, right?
How do I achieve this lazy-loading mechanic?
RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler);
recycler.setHasFixedSize(true);
recycler.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new FirebaseIndexRecyclerAdapter<Chat, ChatHolder>(
Chat.class,
android.R.layout.two_line_list_item,
ChatHolder.class, mIndexRef,
mDataRef) {
#Override
public void populateViewHolder(ChatHolder chatMessageViewHolder, Chat chatMessage, int position) {
chatMessageViewHolder.setName(chatMessage.getName());
chatMessageViewHolder.setText(chatMessage.getText());
}
};
recycler.setAdapter(mAdapter);
I'm using RecyclerView with databindings but when I run the app the first time nothing is showing up then after update some content or update the app via instant Run the content appears.
my ViewHolder:
class MyViewHolder extends RecyclerView.ViewHolder {
private ItemBinding mBinding;
public MyViewHolder(View itemView) {
super(itemView);
mBinding = DataBindingUtil.bind(itemView);
}
ItemBinding getBinding() {
return mBinding;
}
}
my Adapter:
public class MyAdapter extends FirebaseRecyclerAdapter<MyModel, MyViewHolder> {
public MyAdapter(Query ref) {
super(MyModel.class, R.layout.my_item, MyViewHolder.class, ref);
}
#Override
protected void populateViewHolder(MyViewHolder viewHolder, MyModel model, int position) {
ItemBinding binding = viewHolder.getBinding();
binding.setMyModel(model);
binding.executePendingBindings();
}
}
I found in some other question I need call binding.executePendingBindings() that was I did without success.
Edit
I just added a log call:
Log.d(BuildConfig.TAG, "called populateViewHolder " + position);
on the populateViewHolder method. The log is never printed.
Edit 2
The way how I'm initializing my recyclerView:
// onCreate
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view)
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setLayoutManager(llm);
mRef = FirebaseDatabase.getInstance().getReference().child(CHILD_TREE)
// onStart
mAdapter = new MyAdapter(mref.orderByChild("date"));
mRecyclerView.setAdapter(mAdapter);
From my comment:
Have you already looked at the firebase/FirebaseUI-Android issue..? Setting the RecyclerView height from wrap_contentto match_parent solved the problem there.
Apparently, there really is a problem with the height set to wrap_content in the RecyclerView when using the FirebaseRecyclerAdapter with DataBinding.
Change it to
android:layout_height="match_parent"
and it should work.
The FirebaseUI-Android Team thinks that this is a problem with the RecyclerView itself and has closed the issue. This is the crosspost from the Github Issue.
Store your data in an ArrayList of your model.Then try implementing the onBindViewHolder method in your adapter. onBindViewHolder has two attributes-holder and position. The holder will be an object of your MyViewHolder class, and you can use the position to get the object of your model at that position from the array list.
Edited
You are calling given statements
mAdapter = new MyAdapter(mref.orderByChild("date"));
mRecyclerView.setAdapter(mAdapter);
in onStart . onStart calls after onCreate. you have set adapter to recyclerView before it's initialized and reference of Database isn't yet created .Put the above two statements in onCreate after
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view)
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setLayoutManager(llm);
mRef = FirebaseDatabase.getInstance().getReference().child(CHILD_TREE) ;
// adapter must be set after getting mRef
mAdapter = new MyAdapter(mref.orderByChild("date"));
mRecyclerView.setAdapter(mAdapter);
I have got CardView with Views inside a RecyclerView. I have created adapter in which assets are attached to Views and everything works. Now, I would like to change these Views from my activity. Is it any simple way to do that?
public class MainActivity extends AppCompatActivity {
RecyclerView recyclerView;
private RecyclerView.LayoutManager layoutManager;
private List<Offer>offers;
TextView timer; //timer inside CardView
private void getViewReferences() {
recyclerView = (RecyclerView)findViewById(R.id.mainRecyclerView);
timer = (TextView)findViewById(R.id.timer);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getViewReferences();
layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
initializeData();
initializeAdapter();
timer.setText("13:16"); //NullPointerException here
}
private void initializeData(){
offers = new ArrayList<>();
offers.add(new Offer("godzina", R.drawable.zdj, 200));
offers.add(new Offer("godzina", R.drawable.zdj));
}
private void initializeAdapter() {
RecyclerViewAdapter adapter = new RecyclerViewAdapter(offers);
recyclerView.setAdapter(adapter);
}
}
So you don't want to change the Views but you want to update the text values of the views in your list.
You can't directly do that by trying to find the Views in the recyclerView and change the text. The adapter is responsible for giving values to your list. So you need to update the dataset in the adapter and call one of the notifyDataSetChanged methods on the adapter to update the recyclerView.
The big advantage of this design is that it places Views in Cache in order to reuse them multiple times without having to inflate and construct them again. This is a huge performance improvement.
You need to read the docs and some tutorials to understand the adapter design pattern better.