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í