I have a Recyclerview, im animating a view inside individual list item, but when I scroll the recyclerview the animation is stopping. Its because recyclerview removes the items form its view so when we scroll back it fetches it back! But now i want that animation to keep going as I would stop it only when i get data from server!
All I want is the animation that I start in the individual items inside the recylerview shouldn't stop even if the recyclerview is scrolled and the view is out of focus and comes back to focus! I need to stop the animation in the code when I get the server data! I have the code where to stop the animation and it works if the item is not scrolled off the view!
btn.onClick -- this button is the onClick for the recyclerview list
item 1 btn.startAnimation(anim.xml) -- starting the animation
onSuccess -- server returns success btn.clearAnimation();
but before the onSuccess if we scroll the list the animation is stopped!
Please help!
By inspiring from crymson's answer i have made little easy and useful solution using tag method of View instead setting a boolean in complicated logic of your custom adapter.
#Override
public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
super.onViewDetachedFromWindow(holder);
if (holder.getItemViewType() == TYPE_AD)
((ViewHolderForAd) holder).ivStory.setTag(false);
}
public class ViewHolderForAd extends RecyclerView.ViewHolder {
private ImageView ivStory;
TextView tvName;
public ViewHolderForAd(View view) {
super(view);
ivStory = (ImageView) view.findViewById(R.id.ivStoryImage);
tvName = (TextView) view.findViewById(R.id.tvAppName);
view.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
int pos = getAdapterPosition();
if (pos < 0) {
pos = (int) v.getTag();
}
customItemClickListener.onItemClicked(v, pos);
}
});
//ivStory.startAnimation(AnimationUtils.loadAnimation(context, R.anim.pulse_story));
ivStory.setTag(false); //Set default tag false to decrease risk of null
}
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
//...Your code...
if (!(boolean) holder1.ivStory.getTag()) {
holder1.ivStory.setTag(true);
holder1.ivStory.startAnimation(AnimationUtils.loadAnimation(context, R.anim.pulse_story));
}
//...Your code...//
}
You can use setTag(key, object) instead of setTag(object) if you already tagged something(like position) in your imageView.
Hope this helps someone.
Hard to give you a full solution but have you tried saving the animation state inside the ViewHolder that you are using? I'd recommend saving a boolean flag in the ViewHolder class you defined like isAnimating which is initially set to false and in your onBindViewHolder(...) method you can do something like
if (viewHolder.isAnimating) {
// start animation
} else {
// clear animation
}
viewHolder.btn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewHolder.isAnimating = true;
// start animation
}
});
Related
I have a RecylcerView holding cards with messages, each has a like and a dislike button. When the like button is clicked the user-ID is stored in my database and the user should not be able to like it again. If he clicks it again his action should reset his like. Same should hold true for dislikes.
In my Adapter in onBindViewHolder I check in my database if the user has already clicked the button. If so, I replace the white up-arrow image with a red arrow. I also set a tag to the ImageView by setTag. Where 0 represents the unvoted case and 1 if the like-button was pressed before :
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final String mUID = getUserID();
final String postID = getPostID(position);
final PostViewHolder holder1 = (PostViewHolder) holder;
//REFERENCE WHERE USER IDs ARE STORED WHEN THEY LIKE THE POST
mRefForUID = rootRef.child("likesUIDs").child(postID);
ValueEventListener valueEventListenerForUID = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
//IF USER HAS LIKED A REFERENCE IS STORED WITH "mUID+"
if (dataSnapshot.hasChild(mUID+"+")){
//SET RED ARROW IF UP IS CLICKED BEFORE AND SETTAG TO 1
holder1.mUp.setTag(1);
holder1.mUp.setImageResource(R.drawable.arrow_up_red);
} if(dataSnapshot.hasChild(mUID+"-")) {
//SAME AS ABOVE FOR THE DISLIKED CASE
//HERE I COULD SET THE TAG TO -1
} else {
//IF USER ID IS NOT KNOWN HE HAS NOT CLICKED
//SO I SET THE TAG TO 0 FOR THIS CASE
holder1.mUp.setTag(0);
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
};
mRefForUID.addListenerForSingleValueEvent(valueEventListenerForUID);
}
In my ViewHolder I define the onClick events of the buttons (in my case these are ImageViews). If the button was clicked before the onClick event should be different, so I distinguish between these states with the help of my previously initialized tag and a switch case algorithm:
public class PostViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
public ImageView mUp;
public ImageView mDown;
public PostViewHolder(View itemView, postClickListener listener){
super(itemView);
mUp = mView.findViewById(R.id.arrowUp);
mDown = mView.findViewById(R.id.arrowDown);
mUp.setOnClickListener(this);
mDown.setOnClickListener(this);
}
#Override
public void onClick(View v) {
if (v.getId() == mUp.getId()){
int tag = (int) mUp.getTag();
int position = getAdapterPosition();
switch(tag){
case 0:
//CASE 0 = NOT CLICKED BEFORE
//MAKE A NORMAL LIKE AND SET THE TAG TO 1 FROM HERE
case 1:
//CASE 1 = UPVOTE IS CLICKED BEFORE
//UNLIKE AND SET THE TAG TO 0 HERE
}
}
}
I leave out the case for the down-vote. So the assignment of the correct ImageView does work. When I open the app I get the red upvote button if it has been upvoted before. But the tag is always 0 when I start the app! So the onBindViewHolder does the following correctly holder1.mUp.setImageResource(R.drawable.arrow_up_red);, but does not set the tag holder1.mUp.setTag(1);.
It particularly does not work when opening the app for the first time. When clicking a couple of times I get the correct tags, since they will get set in the ViewHolder.
I hope to find the mistake or even a better solution for my intention.
I have created a horizontal recyclerview list. below is the image I have attached.
problem is that I have to change bulb state when everytime user clicked that row from off state to on state and on state to off state.
How do I implement this logic? please help me to find out the solution.
i want to change light color according to user clicks, if he clicks on then it should change its color for that row and vise versa
public void onClick(View view, int position) {
//toast("clicked" + position );
if (!lightClicked){
ImageView lightPopUp = view.findViewById(R.id.row_light_thumbnail);
DrawableCompat.setTint(lightPopUp.getDrawable().mutate(), ContextCompat.getColor(getContext(), R.color.white));
//toast("light on");
lightClicked = true;
}else {
ImageView lightPopUp = view.findViewById(R.id.row_light_thumbnail);
//toast("light off");
DrawableCompat.setTint(lightPopUp.getDrawable().mutate(), ContextCompat.getColor(getContext(), R.color.colorAccent));
lightClicked = false;
}
//View view1 = mLayoutManager.findViewByPosition(position);
}
Dont directly do this change as the recycler view's cells are reused and it wont work as expected, so instead apply the change in the list you are using. You can add a boolean variable in the model class of the list you use to populate the recycler view, and than on its click you can change the boolean's value and call notifydatasetchange on the adapter, and in bind view you should keep an If else condition based on that boolean for the Bulb's image
ie. if true set one image if false set another
declare this :
int selectedPosition=-1;
inside onBindViewHolder:
public void onBindViewHolder(FiltersAdapter.ViewHolder holder, int position) {
if(selectedPosition==position)
holder.itemView.setImageResource(R.drawable.higlihgt_image);
else
holder.itemView.setImageResource(R.drawable.normal_image);
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
selectedPosition=position;
notifyDataSetChanged();
}
});
}
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.
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);
}
I want to change the background view when it's pressed (its working). My problem is, If i press the other view (not the same one) in the list, i want to set my background to Black of the newly selected view and change the background to White of the previous selected view. Here is my Implementation
for(final TotalPlayers player : this.playerData){
final ArrayList<View> addedPlayerViews1 = getPlayerView(player);
dropPlayersListView.addView(addedPlayerViews1.get(0));
addedPlayerViews1.get(0).setOnClickListener(new OnClickListener() {
boolean highlight = false;
#Override
public void onClick(View v) {
if (!highlight)
{
addedPlayerViews1.get(0).setBackgroundColor(Color.BLACK);
highlight=true;
}
else {
addedPlayerViews1.get(0).setBackgroundColor(Color.WHITE);
highlight=false;
}
}
});
}
}
addedPlayerViews1.get(1).setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
addedPlayerViews1.get(0).setBackgroundColor(Color.WHITE);
highlight=false;
}
});
I assumed that you can get the other view by get(1).
The problem of your code was you only handled the onClick event of your view where you want to change the background. but you also need to handle the onClick of other view too.