Bilder von Server im ListView führen zu rucklern

  • Antworten:14
  • Bentwortet
Philip M.
  • Forum-Beiträge: 90

25.10.2011, 11:46:17 via Website

Hallo

Folgendes Problem ergibt sich in meiner App. Ich habe eine ListView welche ich mit Daten aus einer XML füttere welche ich online abrufe. Das funktioniert auch alles ganz gut und ohne größere Probleme. Nun ist in dieser XML auch immer ein link zu einem Bild mit drin, welches ich in einem ImageView darstelle.
Hier entsteht nun das Problem:
Das Bild wird vom Server gezogen, das Bild wird in ein Bitmap-Objekt gelegt, skalliert und dann ausgegeben.
Das verursacht den Effekt das wenn ich im ListView scrolle es immer zu rucklern kommt wenn ein Bild dargestellt werden soll. Gibt es eine Möglichkeit das zu umgehen bzw. hat da vielleicht jemand eine Idee zu? Kann man die Bilder vielleicht als Datei zwischenspeichern oder sollte man die aus der XML Datei direkt als Bild in ein Objekt schreiben und nicht nur als String-URL oder ist beides der falsche Ansatz? Wäre für jede Hilfe dankbar (:

Hierzu auch nochmal ein bisschen Quellcode wie ich skalliere und wie die betroffene Methode im Adapter aussieht

Methode zum skallieren der Bilder:
1public static Bitmap resizeProportionalMax(Bitmap bmp, int maxWidth , int maxHeight, Context c) {
2 if(bmp == null || bmp.getHeight() <= 0|| bmp.getWidth() <= 0) {
3 return null;
4 }
5
6
7
8 int hDiff = bmp.getHeight() - maxHeight;
9 int wDiff = bmp.getWidth() - maxWidth;
10
11 int size = (int)(((hDiff < wDiff)?maxWidth:maxHeight) * c.getResources().getDisplayMetrics().density + 0.5f);
12 float p = (float)size / (float)((hDiff < wDiff)?bmp.getWidth():bmp.getHeight());
13
14 Matrix matrix = new Matrix();
15 matrix.postScale(p, p);
16
17
18 Bitmap resizedBitmap = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), matrix, true);
19 bmp.recycle();
20
21 return resizedBitmap;
22 }

getView() Methode des Adapters:
1@Override
2 public View getView(int position, View convertView, ViewGroup parent) {
3
4
5 LayoutInflater vi = ( LayoutInflater )parent.getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE );
6 convertView = vi.inflate( R.layout.lv_list_item, null );
7
8 ImageView im = ( ImageView ) convertView.findViewById( R.id.pImage );
9 TextView name = ( TextView ) convertView.findViewById( R.id.sName );
10
11 if(name != null) {
12 name.setText(itemList.get( position ).getName());
13 }
14 if(im != null) {
15 String url = itemList.get( position ).getImage();
16
17 if(url != null && url.length() > 0) {
18 getraenkeImage.setImageBitmap( PictureManipulation.resizeProportionalMax( PictureManipulation.loadBitmap( url ), this.imageMaxWidth, this.imageMaxHeight, convertView.getContext() ) );
19 }
20 }
21 return convertView;
22 }

— geändert am 25.10.2011, 11:48:26

Antworten
Markus Gu
  • Forum-Beiträge: 2.644

25.10.2011, 11:48:21 via Website

das ganze in der getView zu resizen ist nicht ganz gut gelöst.

da wird die bildgröße bei jedem mal anzeigen wieder aufs neue verändert. das ist schon etwas umständlich.

du solltest die bildgröße zentral - einmal resizen. dann wird die liste auch etwas flüssiger laufen

swordiApps Blog - Website

Philip M.

Antworten
Rafael K.
  • Forum-Beiträge: 2.359

25.10.2011, 11:53:16 via Website

ACK @ Markus

+

1convertView = vi.inflate( R.layout.lv_list_item, null );

Das ist auch unglücklich.

Der convertView wird ja grade darum übergeben, damit du nicht jedes mal das layout neu inflaten musst.
Wenn convertView != null ist, solltest du das Layout wiederverwenden und nur die Werte der enthaltenen Views anpassen.
Das spart jede Menge Zeit beim scrollen und es läuft deutlich flüssiger.

Philip M.

Antworten
Philip M.
  • Forum-Beiträge: 90

25.10.2011, 12:24:03 via Website

@Rafael K.
Vielen Dank für den Hinweis. Es ruckelt nun zwar überall weil ich überall natürlich das Bild setzen bzw. wieder entfernen muss, aber alles in allem läuft es tatsächlich schonmal deutlich besser. (:

Werde nun mal den Vorschlag von Markus noch umsetzen. Denke es sollte dann alles funktionieren, melde mich dann aber nochmal.
Eine Frage hab ich dazu dann allerdings nochmal: Wäre das die richtige Methode alle Bilder als Bitmap Objekt in einer Liste zu speichern damit ich diese dann dort einmal skallieren kann? Es könnten schon um die 100 Bilder werden.

Antworten
Markus Gu
  • Forum-Beiträge: 2.644

25.10.2011, 12:29:00 via Website

woher kommen die bilder?

kommen sie als download? oder werden sie direkt in der app mitgeliefert?

swordiApps Blog - Website

Antworten
Philip M.
  • Forum-Beiträge: 90

25.10.2011, 12:32:31 via Website

Die Bilder kommen direkt vom Server und werden so bei Bedarf geladen:
1public static Bitmap loadBitmap(String sURL) {
2 URL newurl = null;
3 Bitmap bm = null;
4 try {
5 newurl = new URL(sURL);
6 bm = BitmapFactory.decodeStream(newurl.openConnection().getInputStream());
7 } catch (MalformedURLException e) {
8 // TODO Auto-generated catch block
9 e.printStackTrace();
10 Log.e("loadBitmap", e.getMessage());
11 } catch (IOException e) {
12 e.printStackTrace();
13 Log.e("loadBitmap", e.getMessage());
14 }
15
16 return bm;
17 }

Da sich die Anzahl der Bilder oft ändert und auch die Bilder selber, können diese nicht direkt in der App mitgeliefert werden.

— geändert am 25.10.2011, 12:34:05

Antworten
Markus Gu
  • Forum-Beiträge: 2.644

25.10.2011, 12:47:26 via Website

dann musst du sie genau direkt nach dem laden in die richtige größe skalieren.

was genau heißt - bei bedarf ?

wenn die liste gezeigt wird, ist der bedarf schon zu spät. das muss auf jeden fall getan werden, bevor du die liste anzeigst.

edit: es gibt natürlich variante, bei denen du das bild echt erst dann lädst, wenn es am bildschirm auftauchen soll. das wird aber dann ein wenig komplizierter, das performant zu gestalten.

— geändert am 25.10.2011, 12:48:06

swordiApps Blog - Website

Antworten
Gelöschter Account
  • Forum-Beiträge: 294

25.10.2011, 13:23:15 via Website

Direkt vom Server? Ich hoffe in Deinem Sinne das Du das a.) in einem Thread machst und b.) so etwas wie einen LazyLoad mit Cache baust.

Mit allem anderen wird Deine App beim Kunden zu Problemen führen. Du glaubst nicht mit welchen Kategorien von Internet-Performance oder Device-Performance "da draußen" gearbeitet wird. Wenn es bei Dir läuft - und sich "nur" durch Ruckler bemerkbar macht - dann ist das zufällig und pures Glück.

Ein typischer LazyLoad ist ein Singleton das einen oder mehrere Caches vorhält (FetchQueue, ImageCache, ...). In Deinem Custom List Adapter wird die Abfrage der Bilder über das Singleton durchgeführt. Dieses prüft ob das Bild bereits im Cache vorliegt und liefert dieses im positiven Fall zurück. Im negativen Fall bekommst Du ein Defaultbild und der Thread startet um die Daten zu besorgen. Ist das Bild geladen dann setzt das Singleton das Bild. Dazu benötigst Du nun nur noch eine extended ImageView in der der URL gespeichert wird. Das Singleton prüft dann vor dem Setzen des geladenen Bildes ob noch das ursprüngliche Bild angezeigt werden kann oder ob das ImageView bereits durch Scrollen recycled wurde und somit auf ein anderes Bild wartet.

Philip M.

Antworten
Philip M.
  • Forum-Beiträge: 90

25.10.2011, 13:59:38 via Website

@Markus Gu
Okay, dann ist mein "bei Bedarf" wohl eindeutig zu spät, denn genau so wie du es beschrieben hast, mach ich es.

Danke auch an Harald. All diese Dinge hatte ich bisher nicht bedacht da es bei mir mit ein paar kleineren Performance-Problemen lief. Ich werde mich mit dem was du erläutert hast ein wenig mehr auseinander setzen und mir auch eine Art "Loader" zusammen basteln, denn ab dann sollte es ja auch funktionieren. Melde mich dann aber wie gesagt nochmal und poste meine Lösung (:

Antworten
Philip M.
  • Forum-Beiträge: 90

26.10.2011, 09:26:30 via Website

So, ich habe mit jetzt solch einen Loader geschrieben und bin damit auch ganz glücklich. Schon allein deswegen weil die Bilder alle nicht immer und immer wieder zum neu zeichnen abgerufen werden.
Allerdings ergibt sich nun ein anderes Problem, denn wenn ich wie Rafael sagte das Layout nicht immer neu inflate, dann werden mir die Bilder wahelos an irgendwelchen anderen ImageViews im ListView angezeigt. Teilweise auch doppelt oder dreifach oder so das die ganze Liste voller Bilder ist (auch wenn im Web von der Stelle gar kein Bild kommt).

Als Beispiel: Ich habe eine ListView mit 10 Einträgen, wo laut XML aber nur zu 3 Einträgen Bilder hinterlegt sind. Demnach sollen auch nur zu den 3 Einträgen Bilder angezeigt werden. Hier werden die Bilder aber nun völlig wahelos überall angezeigt. Noch dazu kommt das beim scrollen sich die Bilder teilweise vertauschen und auch in andere ImageVies innerhalb des ListViews setzen bis ich irgendwann bei allen 10 Einträgen auch Bilder hab. Das soll so nicht sein.

Wenn ich jetzt wieder jedesmal das Layout neu inflate, funktioniert alles wunderbar, aber ich erhalte im Gegenzug wieder kleine Ruckler beim scrollen des ListViews.

Anbei wieder der Quellcode den ich geschrieben habe:

Mein "LazyLoader"
1public class LazyImageLoader {
2
3 private Map<String, Bitmap> bitmapMap;
4 private static LazyImageLoader llm = null;
5
6 public static LazyImageLoader getInstance() {
7 if(llm == null) {
8 llm = new LazyImageLoader();
9 }
10 return llm;
11 }
12
13 private LazyImageLoader() {
14 bitmapMap = new HashMap<String, Bitmap>();
15 }
16
17 public void clearCache() {
18 this.bitmapMap.clear();
19 }
20
21
22 public Bitmap fetchBitmap(String urlString) {
23 if (bitmapMap.containsKey(urlString)) {
24 return bitmapMap.get(urlString);
25 }
26
27 try {
28
29 Bitmap bmp = BitmapFactory.decodeStream(new URL(urlString).openStream());
30
31 if (bmp != null) {
32 bitmapMap.put(urlString, bmp);
33 } else {
34 Log.w(this.getClass().getSimpleName(), "Fehler beim Abrufen des Bildes");
35 }
36
37 return bmp;
38 } catch (MalformedURLException e) {
39 Log.e(this.getClass().getSimpleName(), "fetchBitmap failed", e);
40 return null;
41 } catch (IOException e) {
42 Log.e(this.getClass().getSimpleName(), "fetchBitmap failed", e);
43 return null;
44 }
45 }
46
47 // Diese Methode wird in diesem Falle vom Adapter aus aufgerufen
48 public void fetchScaledBitmapOnThread(String urlString, ImageView imageView, int maxWidth, int maxHeight) {
49
50 FetchingHandler handler = new FetchingHandler(imageView);
51 FetchingThread thread = new FetchingThread(urlString, handler, imageView.getContext(), maxWidth, maxHeight);
52 thread.start();
53 }
54
55 public class FetchingHandler extends Handler {
56
57 private ImageView imageView;
58
59 public FetchingHandler(ImageView imageView) {
60 this.imageView = imageView;
61 }
62
63 @Override
64 public void handleMessage(Message message) {
65 if(message.obj != null)
66 imageView.setImageBitmap((Bitmap) message.obj);
67 }
68 }
69
70 public class FetchingThread extends Thread {
71
72 private String urlString;
73 private Handler handler;
74 private Context c;
75 private int maxWidth;
76 private int maxHeight;
77
78 public FetchingThread( String urlString, Handler handler, Context c, int maxWidth, int maxHeight ) {
79 this.urlString = urlString;
80 this.handler = handler;
81 this.c = c;
82 this.maxHeight = maxHeight;
83 this.maxWidth = maxWidth;
84 }
85
86 public void run() {
87
88 Bitmap bmp = fetchBitmap( urlString );
89 if(bmp != null)
90 bmp = PictureManipulation.resizeProportionalMax( bmp, maxWidth, maxHeight, c );
91 Message message = handler.obtainMessage( 1, bmp );
92 handler.sendMessage( message );
93 }
94 }
95
96}

Hier ist meine aktuelle getView() Methode des Adapters
1@Override
2 public View getView(int position, View convertView, ViewGroup parent) {
3
4 if(convertView == null) {
5 LayoutInflater vi = (LayoutInflater)parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
6 convertView = vi.inflate(R.layout.getraenk_list_item, null);
7 }
8
9 ImageView imageView = (ImageView) convertView.findViewById(R.id.imageView);
10 TextView textViewName = (TextView) convertView.findViewById(R.id.itemName);
11
12
13 if(textViewName != null) {
14 textViewName.setText(itemList.get(position).getName());
15 }
16 if(imageView != null) {
17 if(itemList.get(position).getImage() != null && itemList.get(position).getImage().length() > 0) {
18 String url = itemList.get(position).getImage();
19 LazyImageLoader.getInstance().fetchScaledBitmapOnThread(url, imageView, this.imageMaxWidth, this.imageMaxHeight);
20 }
21
22 }
23
24 return convertView;
25 }


In dem Moment indem ich die Methode
1LazyImageLoader.getInstance().fetchScaledBitmapOnThread(url, imageView, this.imageMaxWidth, this.imageMaxHeight);
aufrufe, und ich mit dem debugger dort mittels Breakpoint unerbreche um zu gucken ob die richtigen Werte übergeben werden, ist auch alles so wie es sein soll. Der Wert von "position" beträgt 4, der Name und das Image des Items passen beide zusammen so wie es laut der ausgelesenen XML Datei auch sein soll, nur wird es in einem falschen ImageView eingetragen. Der Name allerdings wird mit
1textViewName.setText(itemList.get(position).getName());
an der richtigen Position im ListView eingetragen.

Wisst ihr vielleicht wo hier der Fehler liegen könnte oder was ich falsch gemacht habe?

Antworten
Markus Gu
  • Forum-Beiträge: 2.644

26.10.2011, 10:37:40 via Website

naja da deine listview die zeilen immer wieder benutzt, bleiben die alten werte da drin enthalten.

wenn du jetzt ein bild anzeigst, weiterscrollst und die view wieder verwendet wird, hat sie noch die alten werte. gibts dann aber kein bild, dann musst es zumindest auf null setzen. weil sonst bleibt alles wie es ist.

also im getview noch den fall behandeln, dass es auch kein bild geben kann.

swordiApps Blog - Website

Philip M.

Antworten
Philip M.
  • Forum-Beiträge: 90

26.10.2011, 10:44:57 via Website

Da sah ich wohl tatsächlich den Wald vor lauter Bäumen nicht mehr :grin:
Dachte das wird automatisch erledigt weil der Loader ja null zurückgibt falls das Bild nicht im Cache oder auf dem Server existiert. Das der Loader bei mir garnicht aufgerufen wird wenn es keine URL zu einem Bild gibt, hatte ich völlig übersehen.

Vielen Dank auf jedenfall an alle, nun funktioniert alles einwandfrei. (:

//Edit: Okay, leider doch nicht gelöst. Nun das meiste schon, aber wenn ich jetzt sehr sehr schnell durch scrolle, passiert es ab und an, das ein Bild wieder falsch gesetzt wird. =/ Mal schauen woran das liegt und gebe dann hier wieder Rückmeldung.

— geändert am 26.10.2011, 10:53:56

Antworten
Gelöschter Account
  • Forum-Beiträge: 294

26.10.2011, 11:01:49 via Website

Das Problem ist Zeile 66. Du setzt dort das Bild ins ImageView ohne zu überprüfen ob dieses ImageView nicht bereits durchs Scrollen recycled wurde und auf ein anderes Bild wartet.

Deshalb schrieb ich weiter oben das Du einen extended ImageView benötigst. Dieser UrlImageView hat als zusätzliches Member einen String mit dem Url das für diesen ImageView angefordert wurde. Diesen Url, der auch gleichzeitig der Key für den Cache ist, füllst Du in Deinem Adapter. Und vor dem setImageBitmap im ImageCache prüfst Du ob der Url des UrlImageView noch dem Url des Images im Cache entspricht. Und nur dann füllst Du das Bild.

Philip M.

Antworten
Philip M.
  • Forum-Beiträge: 90

26.10.2011, 11:28:25 via Website

Wunderbar, das war der Fehler :grin:

Ich habe jetzt wie Harald sagte, mir ein extend ImageView gemacht mit der URL als string welche ich vor dem setzen des Bildes abfrage ob diese gleich der URL des bildes ist. Bevor ich den ImageView in den Thread gebe setz ich natürlich die URL im ImageView. Davor allerdings muss ich die URL per Hand wieder auf null setzen, weil die URL von vorher ja sonst weiter drin steht und nicht überschrieben wird wenn kein Bild angezeigt werden soll. (:

Klappt alles wunderbar, keine Ruckler mehr, alle Bilder an der richtigen Stelle, werden alle in einem extra Thread geladen usw.
Nun muss ich mich nur noch um die Cache-Größe kümmern damit die Anzahl an Bilder welche ich zwischenspeicher nicht überhand nimmt, aber das ist ein Problem mit dem ich wohl auch wieder alleine klar komme.

Danke nochmal an alle die geholfen haben: :grin:

Antworten
Gelöschter Account
  • Forum-Beiträge: 294

26.10.2011, 12:28:15 via Website

Füge Deinem ImageCache noch Methoden zum kompletten Leeren des Caches hinzu - oder einen Algorithmus um nur eine bestimmte Anzahl vorzuhalten. Dann extendest Du Application:

1public class MyApplication extends Application {
2 @Override
3 public void onLowMemory() {
4 DeinImageCache cache = ...
5 cache.clearCache();
6 }
7}

Und diese gibst Du noch im Manifest bekannt:

1<application
2 ...
3 android:name="MyApplication" >

Antworten