0

I'm learning android programming, following a book's examples, but I'm still a beginner in this field.

The aim of the book is to build a progressively more sophisticated "note taking" app, starting from a very simple one then each time adding features.

I got a working app that allows adding notes, deleting notes: each note added becomes an item in a listview, and has a title, a text, and some boolean flag like "idea", "todo" and "important". Adding notes having the "important" boolean flag set, makes an "important" icon visible.

Internally, notes are stored in a List (see code later).

Each note, tapping its listitem, can be also opened in a modal showing its properties, which has a button to delete the shown note. Pressing that delete button, the modal is dismissed, the note is deleted, and the item in the listview is removed.

This works.

Now I'm following the next step: adding a "flashing" animation for items (notes) with the "Important" flag set. Regular notes' items are not animated, but "important" ones are constantly "flashing" when they appear in the listview.

This works.

Removing the notes, one by one, still works, and each item is removed.

But, if I remove all notes, and the last one is flashing - this last note is effectively "removed" from the list - but the listview still shows (only) its animation!?!

But the flashing animation, showing the last note (deleted) title and text in the listitem, cannot be clicked... inspecting the code it is effectively gone fro the List, but not its flashing animation.

Btw, the book at this point doesn't cover deleting notes, and that's a feature I added on my own.

the modal showing the note has, as said a btnDelete, that works like this:

btnDelete.setOnClickListener(
        new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                new AlertDialog.Builder(getContext())
                        .setMessage("Really delete this note?")
                        .setNegativeButton("Cancel",
                                new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialogInterface, int i) {
                                        //nothing
                                    }
                                }
                        )
                        .setPositiveButton("Ok",
                                new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialogInterface, int i) {

                                        MainActivity mainActivity = (MainActivity) getActivity();
                                        mainActivity.deleteNote(getNoteId());

                                        String msg = "Note " + getNoteId() + " deleted (" + mNote.getTitle() + ")";

                                        Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
                                        dismiss();
                                    }
                                })
                        .show();


            }
        }
);

I tried adding and deleting many notes, flashing and not, but the only way to get this "last" flashing ghost is to remove all notes but remain with one last flashing note. Then, after removing that last flashing note, the flashing ghost remains.

I can have any number of flashing notes, and maybe one normal note but if I delete all notes leaving a normal one, that is deleted just fine: leave a last flashing note and, even if it is deleted, the flashing ghost remains.

When this ghost flashing animation lasts forever, if I add a new note, I can see the new listitem note text and icons over the flashing ghost animation, since it is added at first place... there are two animation overimposed, one is the old "ghost" and the other is the "regular" new note...

Now, some more code:

The listview is

listNote = (ListView) findViewById(R.id.listView);

and is "ruled" by a mNoteAdapter object

listNote.setAdapter(mNoteAdapter);

which extends BaseAdapter, overriding needed methods and also has two methods to add and remove notes

public void addNote(Note n) {
    noteList.add(n);
    notifyDataSetChanged();
    saveNotes();

}

public void deleteNote(int i) {
    //(I tried getting the view and canceling animation, setting it to null.. nothing works)
    //View listItem = listNote.getAdapter().getView(i, null, listNote);

    noteList.remove(i);

    notifyDataSetChanged();
    saveNotes();
}

the added notes are stored in a list:

 List<Note> noteList;

the main overridden method is getView, which is "magically" called by the adapter when a node is added, and creates the listitem view, including the flashing animation if necessary.

@Override
public View getView(int i, View view, ViewGroup viewGroup) {

    if (view == null) {

        LayoutInflater inflater = (LayoutInflater)
                getSystemService(getApplicationContext().LAYOUT_INFLATER_SERVICE);

        view = inflater.inflate(R.layout.listitem, viewGroup, false);

    }

    TextView txtTitle = (TextView) view.findViewById(R.id.txtSettingsTitle);
    TextView txtDescription = (TextView) view.findViewById(R.id.txtDescription);
    ImageView ivImportant = (ImageView) view.findViewById(R.id.imageViewImportant);
    ImageView ivTodo = (ImageView) view.findViewById(R.id.imageViewTodo);
    ImageView ivIdea = (ImageView) view.findViewById(R.id.imageViewIdea);

    Note tmpNote = noteList.get(i);

    if (mAnimationSpeed != SettingsActivity.NONE && tmpNote.isImportant()) {
        view.setAnimation(mAnimFlash);
    } else {
        view.setAnimation(mAnimFadeIn);
    }

    if (!tmpNote.isIdea()) ivIdea.setVisibility(View.GONE);
    if (!tmpNote.isImportant()) ivImportant.setVisibility(View.GONE);
    if (!tmpNote.isTodo()) ivTodo.setVisibility(View.GONE);

    txtTitle.setText(tmpNote.getTitle());
    txtDescription.setText(tmpNote.getDescription());

    return view;
}

I don't need the flashing feature, of course, but I wish to understand what's happening and possibly solve it.

the "flashing" animation (mAnimFlash) xml is:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:repeatMode="reverse"
        android:repeatCount="infinite"/>
</set>

while the "fading in" (mAnimFadeIn, which is not causing any issue) animation xml is:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:duration="500"
        android:interpolator="@android:anim/accelerate_interpolator">
    </alpha>
</set>

Can anyone help me understand why that ghost animation remains, just in that case, and how can I better implement this kind of thing?

m.ardito
  • 361
  • 3
  • 13

1 Answers1

1

This is a bit speculation on my part, I'm not exactly sure but my guess is that this behavior is the result of the interaction between:

  1. The way ListView recycles and caches views for items that are no longer visible
  2. That the animation lasts 'forever'
  3. The property being animated is one of the properties used to 'hide' the cached views that ListView is saving for later.

A ListView is a container that arranges child views so that it 'seems' that all the items are visible. The items that would fall outside the ListView are not really there. This is done for performance reasons (speed and to save memory). When the user scrolls and a view falls out of view it is 'recycled' and put in stand-by until another item comes into view to be reused for that new item. It gets passed as the 2nd parameter of the getView() function.

This means that usually there are more views than visible items. There can be space for 10 items, and there will be 10 visible items and 1-3 hidden extra ones that will be used for items that come into view. (see How ListView's recycling mechanism works).

One drawback of ListView is that there is no easy way to detect when a view is being recycled (it becomes hidden). Usually we have to wait until it's reused for another item. RecyclerView does have events to handle when a view gets recycled.

In your case we can see this happening when you're setting the animation. If you set the mAnimFlash on an item, the only time it gets stopped is when the view is reused for another item that not 'important' and it gets the mAnimFadeIn animation (replacing the flashing animation).

If you commented out the call to view.setAnimation(mAnimFadeIn) you would notice that as you scroll down some items are blinking when they shouldn't. That's because there's no place where the view is told to stop blinking, even after being assigned to another item in the list.

Now to item 3 of the list above, the 'alpha' property. You are animating the 'alpha' property of the view. So if the ListView is making the view transparent to hide it from view, then after the ListView hides it by making it transparent, the animation is making it visible again by changing the values of the alpha transparency.

More thoughts on this:

If the animation worked on a property of a child item of the view, instead of the 'main' view, probably the view would remain hidden. Or the animation changed something else like the text color.

Additional info/topics:

Further investigation can be done with the Layout Inspector, which you can use to check where are the views and their properties. So you could for example check what children views are inside the ListView and what are their properties.

You can use different layouts for different items. See the getItemViewType() and related functions of the Adapter class. You can specify for example that there are 2 types of views: 'important' and 'not important' and the views passed as convertView will match the type for that item, and you won't have to make the choices in getView(), but isolate the decision of type in getItemViewType().

Also take a look at RecyclerView, since it has better handling for a few situations where ListView falls short. For example, the adapter for a RecyclerView has a method onViewRecycled() that lets you know that a view is no longer used, and you can stop animations there.

frozenkoi
  • 3,228
  • 22
  • 33
  • Thank you for this very good answer! I tried to comment out the call to view.setAnimation(mAnimFadeIn) and it behaves as you said. I also tried to use another way to make "important" notes stand out (change their background to red) and everything works as expected. I'll try what you suggest in the next days... I suspected the view recycling was involved in this (there is something about this in the book). – m.ardito Oct 18 '18 at 21:59