Patnáctka podruhé - GridView s obrázky

Implementace dialogu pro výběr obrázku přinesla několik zajímavých problémů. A protože si problémy žádaly komplexní řešení, rozhodl jsem se zveřejnit vlastní snippet.

Problémy byly následující a přišly přibližně v tomto pořadí:

  • OutOfMemoryError při spouštění dialogu - nutnost zmenšovat obrázky
  • Dlouhá odezva při spouštění dialogu a sekání při rolování - nutnost načítat obrázky mimo GUI vlákno a implementace obrázkové cache
  • Špatné pořadí obrázků v dialogu (obrázek byl po načtení umístěn na políčko, kam nepatříl, patrné zejména na starém HTC Wildfire) - nutnost hlídat jednotlivá políčka v GridView.

Dílčí řešení jsem již popsal v předchozím článku - práci s obrázky a práci s vlákny. Na snippetu ale chci ukázat, jak jsem to všechno dal dohromady, abych vytvořil funkční řešení. Inspirací pro mne bylo i řešení zveřejněné tady.

package eu.cranion.h15;

import java.lang.reflect.Field;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class ChooseImageDialogAdapter extends BaseAdapter {
	
	private Context context;
	private LayoutInflater inflater;
	private Bitmap base;
	private ImageCache imageCache;

	public ChooseImageDialogAdapter(Context c) {
		this.context = c;
		inflater = LayoutInflater.from(context);
		base = BitmapFactory.decodeResource(context.getResources(), R.drawable.loading);
		imageCache = ImageCache.getInstance();
	}

	@Override
	public int getCount() {
		return context.getResources().getInteger(R.integer.max_pics);
	}

	@Override
	public Object getItem(int arg0) {
		return null;
	}

	@Override
	public long getItemId(int arg0) {
		return 0;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view;
		
		if(convertView == null) {
			view = inflater.inflate(R.layout.image_tile, parent, false);
		} else {
			view = convertView;
		}
		
		View v = view.findViewById(R.id.image_tile_text);
		if(v instanceof TextView) {
			TextView textView = (TextView) v;
			String[] texts = context.getResources().getStringArray(R.array.images);
			textView.setText(texts[position]);
		}
		
		v = view.findViewById(R.id.image_tile_image);
		if(v instanceof ImageView) {
			ImageView imageView = (ImageView) v;
			try {
				int picId = position + 1;
				Class<?> drawables = R.drawable.class; 
				Field f = drawables.getField(Gameboard.PICTURE_PREFIX + picId);
				fillImage(imageView, f.getInt(null));				
			} catch(Throwable e) {
				// this should not happen
				imageView.setImageBitmap(base);
			}
		}
		return view;
	}

	private void fillImage(ImageView imageView, int resId) {
		imageView.setTag(resId);
		Bitmap bmp = imageCache.getThumb(resId);
		if(bmp != null) {
			imageView.setImageBitmap(bmp);
		} else {
			imageView.setImageBitmap(base);
			ImageLoadTask task = new ImageLoadTask(imageView);
			task.execute(resId);
		}
	}

	public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
		final int height = options.outHeight;
		final int width = options.outWidth;
		int inSampleSize = 1;

		if (height > reqHeight || width > reqWidth) {

			final int halfHeight = height / 2;
			final int halfWidth = width / 2;

			while ((halfHeight / inSampleSize) > reqHeight
					&& (halfWidth / inSampleSize) > reqWidth) {
				inSampleSize *= 2;
			}
		}

		return inSampleSize;
	}
	
	private class ImageLoadTask extends AsyncTask<Integer, Void, Bitmap> {
		
		private ImageView imageView;
		private int imgWidth;
		private int imgHeight;
		private int resId;
		
		public ImageLoadTask(ImageView imageView) {
			this.imageView = imageView;
			if(imageView.getMeasuredWidth() < 10) {
				imgWidth = (int) imageView.getContext().getResources().getDimension(R.dimen.image_tile_size);
			} else {
				imgWidth = imageView.getMeasuredWidth();
			}
			int maxSize = (int) imageView.getContext().getResources().getDimension(R.dimen.image_tile_max_size);
			imgWidth = imgHeight = Math.min(imgWidth, maxSize);
		}

		@Override
		protected Bitmap doInBackground(Integer... arg0) {
			resId = arg0[0];
			BitmapFactory.Options options = new BitmapFactory.Options();
			options.inJustDecodeBounds = true;
			BitmapFactory.decodeResource(context.getResources(), resId, options);

			options.inSampleSize = calculateInSampleSize(options, imgWidth, imgHeight);

			options.inJustDecodeBounds = false;
			Bitmap bmp = BitmapFactory.decodeResource(context.getResources(), resId, options);
			return bmp;
		}
		
		@Override
		protected void onPostExecute(Bitmap result) {
			imageCache.putThumb(resId, result);
			Object o = imageView.getTag();
			if(o instanceof Integer && ((int)o) == resId) {
				imageView.setImageBitmap(result);
			}
		}
	}
}

Snippet sám o sobě je adaptér pro plnění GridView. Třída ImageCache, která je ve snippetu odkazována a která je inicializována v konstruktoru, je implementací obrázkové cache a byla popsána již dříve. Konstruktor rovněž inicializuje obrázek-placeholder zobrazovaný místo obrázku, který ještě není načtený.

Inicializaci políčka s obrázkem zajišťuje metoda getView(int position, View convertView, ViewGroup parent). Ta nejprve ověří, zda je potřeba vytvořit nový View, nebo zda se bude recyklovat již vytvořený. Metoda pak nastaví text popisku obrázku a načte resourceId obrázku. Tento resourceId pak bude využíván dále.

Důležitou metodou je také metoda fillImage(ImageView imageView, int resId). Ta napřed podle resourceId ověří, zda obrázek již není načtený v cache. Pokud ano, je použit obrázek z cache. Pokud ne, pak je do daného ImageView umístěn obrázek-placeholder a je spuštěno načítání obrázku na pozadí. Daný ImageView je označen tagem - resourceId obrázku. Podle něj je později určeno, jestli nedošlo k recyklaci daného View.

K načítání obrázků na pozadí je určena třída ImageLoadTask. Ta je inicializována jednak ImageView, do kterého se má obrázek vyplnit, a potom resourceId obrázku. Třída pak na pozadí provede načtení a zmenšení obrázku (postup zmenšení jsem převzal z Android tutoriálu). Když je obrázek načten, je umístěn do cache. Při umísťování obrázku do ImageView je nejprve ověřen tag - resourceId, jestli odpovídá resourceId načteného obrázku. Pokud ano, znamená to, že ImageView je zobrazen na displeji a načtený obrázek do něj patří. Pokud ne, znamená to že ImageView byl zrecyklován, tudíž má obsahovat jiný obrázek. Touto kontrolou jsem vyřešil problém špatného pořadí obrázků v dialogu.

Metoda calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) je standardní implementací zmenšováíní dle Android tutoriálu. Implementoval jsem ji jako statickou, abych ji mohl použít ještě jinde.

Aby nedocházelo k problémům se synchronizací, přistupuje k ImageCache pouze GUI vlákno. Tak je zaručeno, že nedojde k aktualizaci cache v průběhu čtení z ní. Teoreticky se může stát, že stejný obrázek může začít načítat více vláken najednou. Načítání je však rychlé, takže k tomu zřejmě nedochází (anebo to prostě nevadí).

Tagy: H15, Android, Java, Programování

Tento web bude tebe a tvůj počítač krmit piškotkami, jelikož a protože je to slušný web a jako takový ví, že je potřeba návštěvu řádně pohostit, aby se u nás cítila dobře. Užíváním tohoto webu potvrzuješ, že netrpíš mentální anorexií, nedržíš žádnou obskurní dietu a že můžeš piškotki do sebe cpát kdykoli a v jakémkoli množství. Více informací...