1. Nimm jetzt an unserem Uhans - 3. ADVENT - Gewinnspiel teil - Alle Informationen findest Du hier!

Wie verhindere ich ruckeln beim Scrollen einer ListView mit ImageView-Elementen?

Dieses Thema im Forum "Android App Entwicklung" wurde erstellt von ChemDroid, 07.05.2011.

  1. ChemDroid, 07.05.2011 #1
    ChemDroid

    ChemDroid Threadstarter Gast

    Hallo zusammen,

    meine App beinhaltet eine ListActivity, die aus Elementen besteht, die wiederum 3 TextViews beinhalten für Interpret, Titel und Album, sowie eine ImageView zur Darstellung eines Albumarts.
    Die Liste ist quasi so aufgebaut wie die Tracklist in der Standard Musik App.
    Die Daten werden aus MediaStore per Cursor abgefragt und der Cursor mit einem modifizierten SimpleCursorAdapter an die ListView gebunden.

    Hier ist der Code:
    Code:
    void setListAdapterOverride() {
        String[] fromColumns = new String[] {
                AudioColumns.ARTIST,
                MediaColumns.TITLE,
                AudioColumns.ALBUM,
                AudioColumns.ALBUM_ID
        };
        int[] toColumns = new int[] {
                R.id.tv_artist,
                R.id.tv_title,
                R.id.tv_album,
                R.id.iv_albumArt
        };
        cursorAdapter = new customAdapter(getBaseContext(), R.layout.listviewitem, cursor, fromColumns, toColumns);
        setListAdapter(cursorAdapter);
        if (MyDebug.Log)
            Log.d("Activity", "ListAdapter gesetzt");
    }
    
    class customAdapter extends SimpleCursorAdapter {
    
        int layout;
        Cursor cursor;
        String[] from;
        int[] to;
    
        public customAdapter(Context context, int layout, Cursor c,
                String[] from, int[] to) {
            super(context, layout, c, from, to);
            this.layout = layout;
            this.cursor = c;
            this.from = from;
            this.to = to;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View row = convertView;
            if (row == null) {
                LayoutInflater inflater = getLayoutInflater();
                row = inflater.inflate(R.layout.listviewitem, parent, false);
                if (MyDebug.Log)
                    Log.d("Activity", "Inflate");
            }
            cursor.moveToPosition(position);
            TextView artist = (TextView) row.findViewById(to[0]);
            TextView title = (TextView) row.findViewById(to[1]);
            TextView album = (TextView) row.findViewById(to[2]);
            ImageView albumArt = (ImageView) row.findViewById(to[3]);
            artist.setText(cursor.getString(cursor.getColumnIndex(from[0])));
            title.setText(cursor.getString(cursor.getColumnIndex(from[1])));
            album.setText(cursor.getString(cursor.getColumnIndex(from[2])));
    
            Cursor albumArtCursor = contentResolver.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[] { MediaStore.Audio.Albums._ID, MediaStore.Audio.Albums.ALBUM_ART }, MediaStore.Audio.Albums._ID + "='" + cursor.getInt(cursor.getColumnIndex(from[3])) + "'", null, null);
            albumArtCursor.moveToFirst();
            String albumArtUri = albumArtCursor.getString(albumArtCursor.getColumnIndex(MediaStore.Audio.Albums.ALBUM_ART));
            if (albumArtUri == null) albumArtUri = "default";
            if (!imageCache.containsKey(albumArtUri)) {
                Bitmap albumArtBitmap;
                if (!albumArtUri.equals("default")) {
                    Options opts = new Options();
                    opts.inJustDecodeBounds = true;
                    BitmapFactory.decodeFile(albumArtUri, opts);
                    Integer[] bitmapSize = new Integer[] { opts.outWidth, opts.outHeight };
                    Integer scaleFactor = 1;
                    while ((bitmapSize[0]/2 > 50) && (bitmapSize[1]/2 > 50)) {
                        scaleFactor++;
                        bitmapSize[0] /= 2;
                        bitmapSize[1] /= 2;
                    }
                    opts = new Options();
                    opts.inSampleSize = scaleFactor;
                    albumArtBitmap = BitmapFactory.decodeFile(albumArtUri, opts);
                } else {
                    albumArtBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_mp_song_list);
                }
                imageCache.put(albumArtUri, albumArtBitmap);
            }
            albumArt.setImageBitmap(imageCache.get(albumArtUri));
            return row;
        }
    }
    imageCache ist eine HashMap<String, Bitmap> und dient als Cache für die Albumarts, die schon über decodeFile bearbeitet wurden, sodass sie nicht immer wieder neu decodiert werden müssen.

    In einer vorherigen Version des Codes fehlte dieser imageCache nocht, d.h. das Bild wurde jedes mal aus der Datei selbst decodiert. Das machte sich vor allem dadurch bemerkbar, dass die ListView beim scrollen extrem ruckelte.

    Nach Hinzufügen des imageCache ruckelt die ListView zwar nicht mehr so stark wie vorher, aber ein merkliches ruckeln ist dennoch sichtbar.

    Wenn ich die Tracklist in der Musik App anschaue fällt mir auf, dass sie eigentlich gar nicht ruckelt.

    Hat jemand von euch eine Idee wie ich den Code, bzw. das ganze Vorhaben noch weiter optimieren kann?

    Vielen Dank ;)
     
  2. sebastian, 07.05.2011 #2
    sebastian

    sebastian Fortgeschrittenes Mitglied

    Beiträge:
    271
    Erhaltene Danke:
    74
    Registriert seit:
    11.05.2009
    ChemDroid bedankt sich.
  3. swordi, 07.05.2011 #3
    swordi

    swordi Gewerbliches Mitglied

    Beiträge:
    3,389
    Erhaltene Danke:
    441
    Registriert seit:
    09.05.2009
    evtl das laden des bildes in einen thread auslagern?
     
  4. ChemDroid, 07.05.2011 #4
    ChemDroid

    ChemDroid Threadstarter Gast

    Hm also noch nen ViewHolder einbauen... Danke werde ich mal ausprobieren.

    Das laden des Bildes in nen externen Thread verlegen habe ich schon ausprobiert, aber leider kann nur der UI-Thread die ImageView ändern...
     
  5. Mort, 07.05.2011 #5
    Mort

    Mort Android-Lexikon

    Beiträge:
    960
    Erhaltene Danke:
    262
    Registriert seit:
    16.11.2009
    Handler.post() könnte helfen... Also "<visibility> Handler handler = new Handler();" als Member-Variable, dann im Ladethread handler.post( new Runnable(){....} ); aufrufen. Der Runnable-Code wird dann bei nächster Gelegenheit im UI-Thread ausgeführt.
    Allerdings bleibt noch das Problem, dass die Item-View ggf. schon gar nicht mehr existiert oder für ein anderes Item verwendet wird bis der Bildlade-Thread fertig ist, gerade bei schnellem Scrollen.
    Der Bild-Cache kann sich übrigens schnell als gelungener Schuss ins Knie entpuppen, v.a. auf etwas älteren/schwächeren Geräten, bei denen der Heap pro App auf 16MB begrenz ist. Bei 'ner etwas größeren Liste ist ein OutOfMemory vorprogrammiert...
     
  6. Haggy, 07.05.2011 #6
    Haggy

    Haggy Android-Experte

    Beiträge:
    562
    Erhaltene Danke:
    81
    Registriert seit:
    03.08.2009
    Phone:
    Nexus 4
    Tablet:
    Kindle Fire HDX7
    Wearable:
    LG G Watch
    Mort hat das ganze schon richtig erkannt. Möchte nur noch hinzufügen, dass du zu viel in deiner getView()-Methode machst. Zu viel was den UI-Thread beschäftigt. Um das richtig smooth zu bekommen, wirst du nicht drum rum kommen, das Suchen, Laden und Dekodieren des Bildes in einen Thread auszulagern und per MessageHandler den UI-Thread darüber zu informieren, dass das gewünschte Bild nun im imageCache zur Verfügung steht (als Bitmap gleich am besten). So bleibt die Liste auf Zack und die Bilder erscheinen wenn sie geladen sind (solange eben nichts oder ein statischer Platzhalter). Richtig ist aber, dass du auf jeden Fall prüfen musst, ob deine ImageView noch existent ist. Ich würde hierbei davon abraten, die ImageView in irgendeiner Form an den Cache durchzureichen. Das erzeugt nur Speicherlecks. Eleganter ist, eine eindeutige ID als Tag zu setzen und wenn das Bild geladen ist, zu überprüfen ob das Tag noch dasselbe ist. So kannst du erkennen ob du die richtige ImageView vor dir hast. Dann noch was Allgemeines zu Caches, vor Allem bei großen Daten wie Bilder - vermeide, wenn möglich, einfache HashMaps oder Arrays als Cache. Da geht dir, wie Mort schon meinte, schneller der Speicher aus als du scrollen kannst. Elegante Alternative sind Softreferences oder WeakReferences. Android hat dir bereits eine WeakHashmap gebaut, die sich prima für Caches eignet (Doku lesen nicht vergessen).
     
    ChemDroid bedankt sich.
  7. ChemDroid, 09.05.2011 #7
    ChemDroid

    ChemDroid Threadstarter Gast

    Vielen Dank für eure Antworten :)

    Ich habe es nun mit Hilfe der LazyList aus diesem Beitrag gelöst.

    Der Code ist ein wenig zu lang um ihn hier zu posten, also falls sich jemand für den Code interessiert, twittert mir einfach oder schreibt mir ne Nachricht, ich melde mich dann ;)
     

Diese Seite empfehlen