RecyclerView doesn't mantain changes - android

I'm creating an application for Android, and in this moment i want just create a selectable list using a RecyclerView, for this, when a user clicks on an item, I put its id in an array and I change its icon.
The problem is that, when I scroll down and then I scroll up again or when I change fragment, the array still contains all the id of the items, but the icons comes back as before, they don't mantain the new image that I set.
This is the onBindViewHolder metod, this method is the only in the whole project which change the imageview :
List<Integer> selected = new LinkedList<>();
#Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.mBoundString = mValues.get(position);
holder.mTextView.setText(mValues.get(position));
final int pos = position;
holder.mView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final Context context = v.getContext();
Animation to_middle = AnimationUtils.loadAnimation(v.getContext(), R.anim.to_middle);
final Animation from_middle = AnimationUtils.loadAnimation(v.getContext(), R.anim.from_middle);
to_middle.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
holder.mImageView.setImageResource(R.drawable.ic_action_navigation_check);
holder.mImageView.clearAnimation();
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
holder.mImageView.setAnimation(to_middle);
holder.mImageView.startAnimation(to_middle);
selected.add(pos);
}
});
if(!selected.contains(position)) {
/* I checked and this method it's not called when
an item is selected, so the following instruction
isn't the cause of my problem, anyway the image
comes back as before */
Glide.with(holder.mImageView.getContext())
.load(R.drawable.ic_default)
.fitCenter()
.into(holder.mImageView);
}
}
I'm sure that R.drawable.ic_default isn't used elsewhere, so which method is changing the image?
Thank you

if(selected.contains(position)) { // Check if already selected
Glide.with(holder.mImageView.getContext())
.load(R.drawable.ic_action_navigation_check) // set selected drawable here.
.fitCenter()
.into(holder.mImageView);
}
You are checking that "if not selected set image to default" but you need to check "if selected set image to selected image".

Related

Toggle Button Recyclerview Issue

i've this issue made me crazy. When retrieved JSON data i will use this my adapter:
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof OriginalViewHolder) {
final DomandaQuizTipo1 domandaQuiz1 = items.get(position);
OriginalViewHolder vItem = (OriginalViewHolder) holder;
/* Getting each answer from array of answer */
Risposta1 risposta1 = domandaQuiz1.getRisposte().get(0);
Risposta1 risposta2 = domandaQuiz1.getRisposte().get(1);
Risposta1 risposta3 = domandaQuiz1.getRisposte().get(2);
/* Get and set text of question */
vItem.domanda.setTypeface(typefaceRegular);
vItem.domanda.setText(domandaQuiz1.getDomanda());
/* Get and set text of answer 1 */
vItem.risposta1.setText(risposta1.getRisposta());
vItem.risposta1.setTextOn(risposta1.getRisposta());
vItem.risposta1.setTextOff(risposta1.getRisposta());
/* Get and set text of answer 2 */
vItem.risposta2.setText(risposta2.getRisposta());
vItem.risposta2.setTextOn(risposta2.getRisposta());
vItem.risposta2.setTextOff(risposta2.getRisposta());
/* Get and set text of answer 3 */
vItem.risposta3.setText(risposta3.getRisposta());
vItem.risposta3.setTextOn(risposta3.getRisposta());
vItem.risposta3.setTextOff(risposta3.getRisposta());
/* When i click the first answer uncheck 2nd and 3rd */
vItem.risposta1.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MainActivity.punteggiDomandeQuiz1[position] = items.get(position).getRisposte().get(0).getPunteggio();
MainActivity.UpdatePunteggioTotale();
vItem.risposta2.setChecked(false);
vItem.risposta3.setChecked(false);
}
});
/* When i click the second answer uncheck 1st and 3rd */
vItem.risposta2.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MainActivity.punteggiDomandeQuiz1[position] = items.get(position).getRisposte().get(1).getPunteggio();
MainActivity.UpdatePunteggioTotale();
Log.d("adapterposition", "POS" + position);
vItem.risposta1.setChecked(false);
vItem.risposta3.setChecked(false);
}
});
/* When i click the third answer uncheck 1st and 2nd */
vItem.risposta3.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MainActivity.punteggiDomandeQuiz1[position] = items.get(position).getRisposte().get(2).getPunteggio();
MainActivity.UpdatePunteggioTotale();
vItem.risposta1.setChecked(false);
vItem.risposta2.setChecked(false);
}
});
} else {
((ProgressViewHolder) holder).progressBar.setIndeterminate(true);
}
}
My issue is this: in a recyclerview of each question that contains 3 answers to choose, when i click on one of answer (ex. first toggle button) is checked correctly but when i scroll recycler view i see other question with answer (ex. first toggle button) checked.
How can i avoid this issue? Take a look from this gif i putted below.
Check gif this please to understand
You should add a field like a selected (local not server) and check that's is selected and change item ui
if(vItem.selected==1)
vItem.risposta1.setChecked(true);
else
vItem.risposta1.setChecked(false);
if(vItem.selected==2)
vItem.risposta2.setChecked(true);
else
vItem.risposta2.setChecked(false);
if(vItem.selected==3)
vItem.risposta3.setChecked(true);
else
vItem.risposta3.setChecked(false);
vItem.risposta1.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MainActivity.punteggiDomandeQuiz1[position].selected=1;
notifyItemChange(adapterPosition);
}
});
vItem.risposta2.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MainActivity.punteggiDomandeQuiz1[position].selected=2;
notifyItemChange(adapterPosition);
}
});
vItem.risposta3.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MainActivity.punteggiDomandeQuiz1[position].selected=3;
notifyItemChange(adapterPosition);
}
});
You have to set the status of each toggle button in onBindViewHolder.
Something like:
vItem.risposta1.setText(risposta1.getRisposta());
//...
vItem.risposta1.setChecked(risposta1.isChecked());
Also when you update the data in the OnClickListener you have to notify the adapter:
adapter.notifyDataSetChanged();

Change the background image of a RecyclerView item on click

I have a RecylerView which contains a textView as individual item. Based on the requirement, the background image of the textView will be changed on click after checking the present image.
In simple:
onClick textView TV--> check for the background image of the textview TV--> check if the background image of TV is a, then make it b and viceversa
Please find my code below:
viewHolder.addRemove.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(viewHolder.clicked==0)
{
viewHolder.addRemove.setBackgroundResource(R.mipmap.added);
viewHolder.clicked=1;
groupSlcArray.add(model.getName().trim());
Log.d("groupArr", Arrays.toString(groupSlcArray.toArray()));
}
else
{
viewHolder.addRemove.setBackgroundResource(R.mipmap.addslcgroup);
viewHolder.clicked=0;
groupSlcArray.remove(model.getName().trim());
Log.d("groupArr", Arrays.toString(groupSlcArray.toArray()));
}
}
});
I am unable to find anyway to get the background resource of the textView for checking.Need your help.This question might seem too simple for some. So please before Outvoting, try to give a solution because I have tried all the possible solutions that I found.Thanks in advance
Try following code :
holder.textview.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (holder.textview.getBackground() == getResources().getDrawable(R.drawable.a)){
holder.textview.setBackground(R.drawable.b);
}
else{
holder.textview.setBackground(R.drawable.a);
}
}
});
You have to understand about getView.
Sometimes If you scroll in ListView, This is not an exact Color
So I would like to recommand following ways like this:
1. Add Code on getView
if (groupSlcArray.contain(odel.getName().trim())) {
viewHolder.addRemove.setBackgroundResource(R.mipmap.addslcgroup);
} else {
viewHolder.addRemove.setBackgroundResource(R.mipmap.added);
}
2. Change OnClickListener Event Code
viewHolder.addRemove.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick (View v){
if (groupSlcArray.contain(odel.getName().trim())) {
viewHolder.addRemove.setBackgroundResource(R.mipmap.addslcgroup);
groupSlcArray.remove(model.getName().trim());
Log.d("groupArr", Arrays.toString(groupSlcArray.toArray()));
} else {
viewHolder.addRemove.setBackgroundResource(R.mipmap.added);
groupSlcArray.add(model.getName().trim());
Log.d("groupArr", Arrays.toString(groupSlcArray.toArray()));
}
}
});
Here is a solution that you can use for change background image of each RecyclerView item.
For example, currently, each row of your RecyclerView is a Item, you need to create 1 more field name textViewBackGround for hold the back ground of each row like this
public class Item{
...
public int textViewBackGround = R.drawable.a; // default image a
}
Then in your adapter
public class MyRecyclerViewAdapter extends RecyclerView.Adapter < MyRecyclerViewAdapter.CustomViewHolder > {
private List < Item > itemList;
private Context mContext;
public MyRecyclerViewAdapter(Context context, List < Item > itemList) {
this.itemList = itemList;
this.mContext = context;
}
#Override
public void onBindViewHolder(CustomViewHolder viewHolder, int i) {
Item item = itemList.get(i);
viewHolder.textView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// don't need to get the background of your TextView then change the background, just need to check the datasource. Later on, you absolutely will know that check the datasource for change background of item is a correct way not check the background resource
if (item.textViewBackGround == R.drawable.a) {
viewHolder.textView.setBackgroundResource(R.drawable.b);
item.textViewBackGround = R.drawable.b;
} else {
viewHolder.textView.setBackgroundResource(R.drawable.a);
item.textViewBackGround = R.drawable.a;
}
}
});
viewHolder.textView.setBackgroundResource(item.textViewBackGround); // need to set the background image for `TextView` everytime bind ViewHolder because RecyclerView item will recycle when you scroll (if you don't understand it, you can remove this line, run your app, scroll up/down and you will understand it)
...
}
class CustomViewHolder extends RecyclerView.ViewHolder {
protected TextView textView;
public CustomViewHolder(View view) {
super(view);
...
}
}
}

recyclerview item changes button style after scroll

I have a recyclerview with simple items - an item has an image, title and a button. When the user clicks on the button it needs to change its layout -> indicating that button is clicked (similar to checkbox functionality).
Problem is that when I click on a button, for example the second item, it behaves weirdly when I scroll - multiple items are toggled or the original one is untoggled. You can check it out in image here:
GIF PREVIEW
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
holder.button.setOnClickListener(view -> {
toggleButtonStyle((Button)view);
});
}
public void toggleButtonStyle(Button toggle)
{
Context ctx = toggle.getContext();
if (toggle.isActivated()) {
toggle.setActivated(false);
toggle.setBackground(ContextCompat.getDrawable(ctx, R.drawable.btn_purple_corners));
toggle.setTextColor(ContextCompat.getColor(ctx, R.color.purple_light));
} else {
toggle.setActivated(true);
toggle.setBackground(ContextCompat.getDrawable(ctx, R.drawable.btn_purple));
toggle.setTextColor(ContextCompat.getColor(ctx, R.color.white));
}
}
You need to specify position for a onBindView method, because it's called multiple times. If you are using AutoValue immutable types, you can try to make an Array of booleans where you'll be storing toggled states (true/false). Then in onBindViewMethod check a value of current position and style a button.
private boolean[] toggledChoices = new boolean[yourListWithItems.size()];
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (toggledChoices[position]) {
toggleButtonStyle(true, holder.toggleButton);
} else {
toggleButtonStyle(false, holder.toggleButton;
}
holderVote.bToggleItem.setOnClickListener(v -> {
if (!toggledChoices[position]) {
toggledChoices[position] = true;
} else {
toggledChoices[position] = false;
}
});
}
I had some strange behavior too in the past.
For me it was solved by simply calling
toggle.invalidate();
after your if/else block. This forces the redraw. Try it, I was surprised too, that this fixed it for me.
Check Out my code
#Override
public void onBindViewHolder(GifGridAdapter.ViewHolder holder, final int position) {
final DataModel model = arrayList.get(position);
if (model.isToggle()) {
//Code for changing the button background color and text color
} else {
//Code for changing the button background color and text color
}
holder.ivGif.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
model.setToggle(!model.isToggle());
notifyDataSetChanged();
}
});
}

Hide the view in recyclerview's item

i facing a problem in recyclerview's item.
My Adapter's code :
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
Profile item = mListChatting.get(position);
Log.d("TAG", "CEK : " + viewable);
if(viewable==true){
holder.mFormBookingan.setVisibility(View.GONE);
holder.mDetailBookingan.setVisibility(View.VISIBLE);
}else{
//assume that one way is show first as default
holder.mViewOneWay.setVisibility(View.VISIBLE);
holder.mViewRoundTrip.setVisibility(View.GONE);
holder.mOneOway.setBackgroundResource(R.drawable.round_just_left_white_focus);
holder.mRoundTrip.setBackgroundResource(R.drawable.state_pressed_booking_button_left);
holder.mSendBooking.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewable = true;
Log.d("TAG", "CEK 2 : " + viewable);
}
});
}
Like my code above, I want to hide mFormBookingan after mSendBooking has pressed. mFormBookingan never show anymore until user calls it again.
I have tried with a lot of ways but still can't find like what i need. After i press mSendBooking the form hide, but when i send new item to recyclerview, the from mFormBookingan that has been hide, appears again.
My Question, how to hide mFormBookingan forever? Until user call it again.
Thank in advance, i will appreciate anyone who help me for this one.
I not sure what the clear situation you want.
But if you want to set View invisible you can try this code then check it.
You need to add ismFormBookingVisible in viewHolder class as a boolean attribute.
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
Profile item = mListChatting.get(position);
Log.d("TAG", "CEK : " + viewable);
if(holder.ismFormBookingVisible==true){
holder.mFormBookingan.setVisibility(View.GONE);
holder.mDetailBookingan.setVisibility(View.VISIBLE);
}else{
//assume that one way is show first as default
holder.mViewOneWay.setVisibility(View.VISIBLE);
holder.mViewRoundTrip.setVisibility(View.GONE);
holder.mOneOway.setBackgroundResource(R.drawable.round_just_left_white_focus);
holder.mRoundTrip.setBackgroundResource(R.drawable.state_pressed_booking_button_left);
holder.mSendBooking.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
holder.ismFormBookingVisible = false;
Log.d("TAG", "CEK 2 : " + viewable);
}
Try this :
Create a boolean in your model class "Profile" to keep track of visibility of button : say boolean isBookingVisible;
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
Profile item = mListChatting.get(position);
if(!item.isBookingVisible){
holder.mFormBookingan.setVisibility(View.GONE);
holder.mDetailBookingan.setVisibility(View.VISIBLE);
}else{
//assume that one way is show first as default
holder.mViewOneWay.setVisibility(View.VISIBLE);
holder.mViewRoundTrip.setVisibility(View.GONE);
holder.mOneOway.setBackgroundResource(R.drawable.round_just_left_white_focus);
holder.mRoundTrip.setBackgroundResource(R.drawable.state_pressed_booking_button_left);
holder.mSendBooking.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
item.isBookingVisible = false;
//Use notiyItemChanged(position); or notifyDataSetChanged(); here as per your selection criterion
Log.d("TAG", "CEK 2 : " + viewable);
}
});
}
You may have to call notifyDataSetChanged() after changing the value of viewable.
onBindViewHolder will be called when you notify your data set.
So you need to save the viewable in mListChatting. When you click the button, change the viewable in the mListChatting.
And then, change the code in onBindViewHolder
holder.mFormBookingan.setVisibility(item.getViewable() ? View.VISIBLE : View.GONE);
On refresh android will destroy the view and create new view with new adapter data. So you have to track the current state (visibility) of mFormBookingan. You can use a simple visibility list. When mFormBookingan state (visibility) change update it in visibility list so that whenever the list is refreshed, you can use it to check and set the last state (visibility) of your mFormBookingan. Here is an example
private ArrayList<Boolean> isVisible;
public MyAdapter(ArrayList<Boolean> isVisible){
// initial state list of mFormBookingan for each row of list
this.isVisible = isVisible;
}
public void onBindViewHolder(final MyViewHolder holder, final int position) {
if (isVisible.get(position)) {
holder.mFormBookingan.setVisibility(View.VISIBLE);
}else {
holder.mFormBookingan.setVisibility(View.GONE);
}
holder.mSendBooking.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (holder.mFormBookingan.getVisibility() == View.GONE){
holder.mFormBookingan.setVisibility(View.VISIBLE);
isVisible.set(position, true);
}else {
holder.mFormBookingan.setVisibility(View.GONE);
isVisible.set(position, false);
}
}
});
}
when you click mSendBooking the mFormBookingan visibility will change and it will remain same after sending new item to recyclerview.

How to provide custom animation during sorting (notifyDataSetChanged) on RecyclerView

Currently, by using the default animator android.support.v7.widget.DefaultItemAnimator, here's the outcome I'm having during sorting
DefaultItemAnimator animation video : https://youtu.be/EccI7RUcdbg
public void sortAndNotifyDataSetChanged() {
int i0 = 0;
int i1 = models.size() - 1;
while (i0 < i1) {
DemoModel o0 = models.get(i0);
DemoModel o1 = models.get(i1);
models.set(i0, o1);
models.set(i1, o0);
i0++;
i1--;
//break;
}
// adapter is created via adapter = new RecyclerViewDemoAdapter(models, mRecyclerView, this);
adapter.notifyDataSetChanged();
}
However, instead of the default animation during sorting (notifyDataSetChanged), I prefer to provide custom animation as follow. Old item will slide out via right side, and new item will slide up.
Expected animation video : https://youtu.be/9aQTyM7K4B0
How I achieve such animation without RecylerView
Few years ago, I achieve this effect by using LinearLayout + View, as that time, we don't have RecyclerView yet.
This is how the animation is being setup
PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f);
PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", 0f, (float) width);
ObjectAnimator animOut = ObjectAnimator.ofPropertyValuesHolder(this, alpha, translationX);
animOut.setDuration(duration);
animOut.setInterpolator(accelerateInterpolator);
animOut.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator anim) {
final View view = (View) ((ObjectAnimator) anim).getTarget();
Message message = (Message)view.getTag(R.id.TAG_MESSAGE_ID);
if (message == null) {
return;
}
view.setAlpha(0f);
view.setTranslationX(0);
NewsListFragment.this.refreshUI(view, message);
final Animation animation = AnimationUtils.loadAnimation(NewsListFragment.this.getActivity(),
R.anim.slide_up);
animation.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
view.setVisibility(View.VISIBLE);
view.setTag(R.id.TAG_MESSAGE_ID, null);
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
view.startAnimation(animation);
}
});
layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animOut);
this.nowLinearLayout.setLayoutTransition(layoutTransition);
and, this is how the animation is being triggered.
// messageView is view being added earlier in nowLinearLayout
for (int i = 0, ei = messageViews.size(); i < ei; i++) {
View messageView = messageViews.get(i);
messageView.setTag(R.id.TAG_MESSAGE_ID, messages.get(i));
messageView.setVisibility(View.INVISIBLE);
}
I was wondering, how I can achieve the same effect in RecylerView?
Here is one more direction you can look at, if you don't want your scroll to reset on each sort (GITHUB demo project):
Use some kind of RecyclerView.ItemAnimator, but instead of rewriting animateAdd() and animateRemove() functions, you can implement animateChange() and animateChangeImpl(). After sort you can call adapter.notifyItemRangeChanged(0, mItems.size()); to triger animation.
So code to trigger animation will look pretty simple:
for (int i = 0, j = mItems.size() - 1; i < j; i++, j--)
Collections.swap(mItems, i, j);
adapter.notifyItemRangeChanged(0, mItems.size());
For animation code you can use android.support.v7.widget.DefaultItemAnimator, but this class has private animateChangeImpl() so you will have to copy-pasted code and changed this method or use reflection. Or you can create your own ItemAnimator class like #Andreas Wenger did in his example of SlidingAnimator. The point here is to implement animateChangeImpl Simmilar to your code there are 2 animations:
1) Slide old view to the right
private void animateChangeImpl(final ChangeInfo changeInfo) {
final RecyclerView.ViewHolder oldHolder = changeInfo.oldHolder;
final View view = oldHolder == null ? null : oldHolder.itemView;
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view == null) return;
mChangeAnimations.add(oldHolder);
final ViewPropertyAnimatorCompat animOut = ViewCompat.animate(view)
.setDuration(getChangeDuration())
.setInterpolator(interpolator)
.translationX(view.getRootView().getWidth())
.alpha(0);
animOut.setListener(new VpaListenerAdapter() {
#Override
public void onAnimationStart(View view) {
dispatchChangeStarting(oldHolder, true);
}
#Override
public void onAnimationEnd(View view) {
animOut.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
dispatchChangeFinished(oldHolder, true);
mChangeAnimations.remove(oldHolder);
dispatchFinishedWhenDone();
// starting 2-nd (Slide Up) animation
if (newView != null)
animateChangeInImpl(newHolder, newView);
}
}).start();
}
2) Slide up new view
private void animateChangeInImpl(final RecyclerView.ViewHolder newHolder,
final View newView) {
// setting starting pre-animation params for view
ViewCompat.setTranslationY(newView, newView.getHeight());
ViewCompat.setAlpha(newView, 0);
mChangeAnimations.add(newHolder);
final ViewPropertyAnimatorCompat animIn = ViewCompat.animate(newView)
.setDuration(getChangeDuration())
.translationY(0)
.alpha(1);
animIn.setListener(new VpaListenerAdapter() {
#Override
public void onAnimationStart(View view) {
dispatchChangeStarting(newHolder, false);
}
#Override
public void onAnimationEnd(View view) {
animIn.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationY(newView, 0);
dispatchChangeFinished(newHolder, false);
mChangeAnimations.remove(newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
Here is demo image with working scroll and kinda similar animation
https://i.gyazo.com/04f4b767ea61569c00d3b4a4a86795ce.gif
https://i.gyazo.com/57a52b8477a361c383d44664392db0be.gif
Edit:
To speed up RecyclerView preformance, instead of adapter.notifyItemRangeChanged(0, mItems.size()); you probably would want to use something like:
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisible = layoutManager.findFirstVisibleItemPosition();
int lastVisible = layoutManager.findLastVisibleItemPosition();
int itemsChanged = lastVisible - firstVisible + 1;
// + 1 because we start count items from 0
adapter.notifyItemRangeChanged(firstVisible, itemsChanged);
First of all:
This solution assumes that items that are still visible, after the dataset changed, also slide out to the right and later slide in from the bottom again (This is at least what I understood you are asking for)
Because of this requirement I couldn't find an easy and nice solution for this problem (At least during the first iteration). The only way I found was to trick the adapter - and fight the framework to do something that it was not intended for. This is why the first part (How it normally works) describes how to achieve nice animations with the RecyclerView the default way. The second part describes the solution how to enforce the slide out/slide in animation for all items after the dataset changed.
Later on I found a better solution that doesn't require to trick the adapter with random ids (jump to the bottom for the updated version).
How it normally works
To enable animations you need to tell the RecyclerView how the dataset changed (So that it knows what kind of animations should be run). This can be done in two ways:
1) Simple Version:
We need to set adapter.setHasStableIds(true); and providing the ids of your items via public long getItemId(int position) in your Adapter to the RecyclerView. The RecyclerView utilizes these ids to figure out which items were removed/added/moved during the call to adapter.notifyDataSetChanged();
2) Advanced Version: Instead of calling adapter.notifyDataSetChanged(); you can also explicitly state how the dataset changed. The Adapter provides several methods, like adapter.notifyItemChanged(int position),adapter.notifyItemInserted(int position),... to describe the changes in the dataset
The animations that are triggered to reflect the changes in the dataset are managed by the ItemAnimator. The RecyclerView is already equipped with a nice default DefaultItemAnimator. Furthermore it is possible to define custom animation behavior with a custom ItemAnimator.
Strategy to implement the slide out (right), slide in (bottom)
The slide to the right is the animation that should be played if items are removed from the dataset. The slide from bottom animation should be played for items that were added to the dataset. As mentioned at the beginning I assume that it is desired that all elements slide out to the right and slide in from the bottom. Even if they are visible before and after the dataset change. Normally RecyclerView would play to change/move animation for such items that stay visible. However, because we want to utilize the remove/add animation for all items we need to trick the adapter into thinking that there are only new elements after the change and all previously available items were removed. This can be achieved by providing a random id for each item in the adapter:
#Override
public long getItemId(int position) {
return Math.round(Math.random() * Long.MAX_VALUE);
}
Now we need to provide a custom ItemAnimator that manages the animations for the added/removed items. The structure of the presented SlidingAnimator is very similar to theandroid.support.v7.widget.DefaultItemAnimator that is provided with the RecyclerView. Also Notice this is a prove of concept and should be adjusted before used in any app:
public class SlidingAnimator extends SimpleItemAnimator {
List<RecyclerView.ViewHolder> pendingAdditions = new ArrayList<>();
List<RecyclerView.ViewHolder> pendingRemovals = new ArrayList<>();
#Override
public void runPendingAnimations() {
final List<RecyclerView.ViewHolder> additionsTmp = pendingAdditions;
List<RecyclerView.ViewHolder> removalsTmp = pendingRemovals;
pendingAdditions = new ArrayList<>();
pendingRemovals = new ArrayList<>();
for (RecyclerView.ViewHolder removal : removalsTmp) {
// run the pending remove animation
animateRemoveImpl(removal);
}
removalsTmp.clear();
if (!additionsTmp.isEmpty()) {
Runnable adder = new Runnable() {
public void run() {
for (RecyclerView.ViewHolder addition : additionsTmp) {
// run the pending add animation
animateAddImpl(addition);
}
additionsTmp.clear();
}
};
// play the add animation after the remove animation finished
ViewCompat.postOnAnimationDelayed(additionsTmp.get(0).itemView, adder, getRemoveDuration());
}
}
#Override
public boolean animateAdd(RecyclerView.ViewHolder holder) {
pendingAdditions.add(holder);
// translate the new items vertically so that they later slide in from the bottom
holder.itemView.setTranslationY(300);
// also make them invisible
holder.itemView.setAlpha(0);
// this requests the execution of runPendingAnimations()
return true;
}
#Override
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
pendingRemovals.add(holder);
// this requests the execution of runPendingAnimations()
return true;
}
private void animateAddImpl(final RecyclerView.ViewHolder holder) {
View view = holder.itemView;
final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
anim
// undo the translation we applied in animateAdd
.translationY(0)
// undo the alpha we applied in animateAdd
.alpha(1)
.setDuration(getAddDuration())
.setInterpolator(new DecelerateInterpolator())
.setListener(new ViewPropertyAnimatorListener() {
#Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
#Override
public void onAnimationEnd(View view) {
anim.setListener(null);
dispatchAddFinished(holder);
// cleanup
view.setTranslationY(0);
view.setAlpha(1);
}
#Override
public void onAnimationCancel(View view) {
}
}).start();
}
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
View view = holder.itemView;
final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
anim
// translate horizontally to provide slide out to right
.translationX(view.getWidth())
// fade out
.alpha(0)
.setDuration(getRemoveDuration())
.setInterpolator(new AccelerateInterpolator())
.setListener(new ViewPropertyAnimatorListener() {
#Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
#Override
public void onAnimationEnd(View view) {
anim.setListener(null);
dispatchRemoveFinished(holder);
// cleanup
view.setTranslationX(0);
view.setAlpha(1);
}
#Override
public void onAnimationCancel(View view) {
}
}).start();
}
#Override
public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
// don't handle animateMove because there should only be add/remove animations
dispatchMoveFinished(holder);
return false;
}
#Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
// don't handle animateChange because there should only be add/remove animations
if (newHolder != null) {
dispatchChangeFinished(newHolder, false);
}
dispatchChangeFinished(oldHolder, true);
return false;
}
#Override
public void endAnimation(RecyclerView.ViewHolder item) { }
#Override
public void endAnimations() { }
#Override
public boolean isRunning() { return false; }
}
This is the final result:
Update: While Reading the post again I figured out a better solution
This updated solution doesn't require to trick the adapter with random ids into thinking all items were removed and only new items were added. If we apply the 2) Advanced Version - how to notify the adapter about dataset changes, we can just tell the adapter that all previous items were removed and all the new items were added:
int oldSize = oldItems.size();
oldItems.clear();
// Notify the adapter all previous items were removed
notifyItemRangeRemoved(0, oldSize);
oldItems.addAll(items);
// Notify the adapter all the new items were added
notifyItemRangeInserted(0, items.size());
// don't call notifyDataSetChanged
//notifyDataSetChanged();
The previously presented SlidingAnimator is still necessary to animate the changes.

Categories

Resources