do zrobienia tego programu nie wystarczy kawa i tutorialu typu "Hello world"
trzeb zaznajomić się z pojęciami wielowątkowości, dziedziczenia klas i referencji
galeria w formie listy przewijanej pionowo ale można zastosować zamiast ListView kontrolkę Gallery a BaseAdapter można użyć ten sam
najpierw trzeba stworzyć klasę nowej kontrolki np: GalleryView, kontrolka funkcjonuje jako samodzielny element można go łatwo zdefiniować w Layout i używać jak Widget z API
public class GalleryView extends LinearLayout {
private ListView mlist;
private ListAdapter mAdapter;
private Miniature mMiniature;
public GalleryView(Context context, AttributeSet attrs) {
super(context, attrs);
//usługa dzięki której podmienimy Layout tej kontrolki
final LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//teraz podmieniamy całą hierarchię wyglądu na swoją pobraną z pliku XML, podpinając do tej kontrolki
final View view = layoutInflater.inflate(R.layout.gallery, GalleryView.this);
//uchwyt do kontrolki ListView, ta kontrolka służy do generowania listy przewijanej w pionie jest częścią API Android
mlist = (ListView) view.findViewById(R.id.galleryList);
//klasa która zajmie się generowaniem miniatur
mMiniature = new Miniature(context);
//rozszerzenie BaseAdapter, konieczny do wygenerowania własnej zawartości ListView i zachowania kontroli nad procesem generowania
mAdapter = new ListAdapter(context, mMiniature);
//ustawienie własnego BaseAdapter
mlist.setAdapter(mAdapter);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
//tu ładuje listę obrazków do Adaptera i odświeża zawartość ListView
public void loadImages(File[] images) {
mAdapter.updateList(images);
}
//tu czyści listę obrazków w Adapterze i zawartość ListView
public void clearImages() {
mAdapter.clearList();
}
}
teraz kolej na wypełnieni ListView zawartością, do tego służy BaseAdapter, aby można było użyć właściwości BaseAdapter oraz dodać coś od siebie koniecznie jest stworzenie klasy rozszerzającej np: ListAdapter
ListView jest dobrze opisana w sieci wiec pominę wyjaśnienia, ale dobrze jest przeczytać bo bez pełnego zrozumienia zasady działania nie zrozumie się co tak naprawdę dalej się dzieje
public class ListAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private Miniature mMiniature;
private File[] mImages;
private int count = 0;
public ListAdapter(Context context, Miniature miniature) {
mInflater = LayoutInflater.from(context);
mMiniature = miniature;
}
public void updateList(File[] images) {
mImages = images;
count = mImages.length;
notifyDataSetChanged();
}
public void clearList() {
mImages = null;
count = 0;
notifyDataSetChanged();
}
public int getCount() {
return count;
}
public File getItem(int position) {
return mImages[position];
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item, null);
holder = new ViewHolder();
holder.image = (ImageView) convertView.findViewById(R.id.image);
holder.image.setMaxHeight(mMiniature.getMaxSize());
holder.image.setMaxWidth(mMiniature.getMaxSize());
holder.image.setMinimumHeight(mMiniature.getMaxSize());
holder.image.setMinimumWidth(mMiniature.getMaxSize());
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
//w tym momencie rozpoczyna się generowanie miniatury dla obrazka z galerii na pozycji "position", to jest kluczowy element ponieważ w danym momencie jedynie widoczne elementy ListView istnieją pozostałe zostają stworzone na bieżąco podczas przewijania, generowania miniatury chwile potrwa i nie ma pewności ze w momencie zakończenia lista się nie przewinie, konieczne jest "przywiązanie" konkretnego widoku konkretnej pozycji na liście do zadania generowania miniatury
mMiniature.createWorker(holder.image, mImages[position].getPath());
return convertView;
}
private final class ViewHolder {
ImageView image;
}
}
przyszła pora na właściwe generowanie miniatury i wstawienie lub nie do odpowiedniej pozycji na liście, do tego posłuży klasa: Miniature
public class Miniature {
private static int mSize;
public Miniature(Context context) {
mSize = context.getResources().getDisplayMetrics().widthPixels;
}
public int getMaxSize() {
return mSize;
}
public void createWorker(ImageView image, String file) {
try {
cały proces generowani jest dość nudny i nie ma o czym pisać, natomiast następne 4 linijki są kluczowe do działania całej galerii, najpierw twory się pracownika który w osobnym wątku zmniejszy obrazek
final Worker mWorker = new Worker(image, file);
//teraz trzeba przywiązać pracownika to obrazka na danej pozycji na liście, robi się to korzystając z faktu ze niektóre klasy można rozszerzyć zachowując ich oryginalną funkcjonalność dodając coś od siebie np: metodę lub pole danych, w tym przypadku jest to BitmapDrawable do której dodajemy własny konstruktor pode danych i metodę wydobycia pola
final Linker mLinker = new Linker(mWorker);
//tutaj ustawiamy spreparowany BitmapDrawable jako obrazek wyświetlany na liście, BitmapDrawable posiada własciwisci Drawable wieć może posłurzy jako obrazek wyświetlany w UI "premycając" coś przyokazji
image.setImageDrawable(mLinker);
//czas na start procesu generowania miniatury
mWorker.execute();
} catch (RejectedExecutionException e) {
Log.i("Info", "Runtime Exception");
}
}
moje rozwiązanie korzysta z specjalnie stworzonej klasy kolejkowania zadań ale można użyć w tym celu AsyncTask
public class Worker extends SingleTask {
//żeby umożliwić kontrole zbieżności miedzy procesem generowania miniatury i przewijaniem listy, koniecznie jest stworzenia specjalnej więzi miedzy procesem generowania a miejscem/elemente na liście obrazków, do tego najlepiej użyć WeakReference, tu należy poczytać co to jest referencja
private WeakReference<ImageView> mImage;
private String mFile;
public Worker(ImageView image, String file) {
mImage = new WeakReference<ImageView>(image);
mFile = file;
}
@Override
protected Object background() {
Bitmap thumb = null;
try {
thumb = getMiniature(mFile);
} catch (RuntimeException e) {
Log.w("Warn", "Runtime Exception", e);
} catch (OutOfMemoryError e) {
Log.w("Warn", "Out of memory");
} catch (FileNotFoundException e) {
Log.w("Warn", "File not found");
} catch (IOException e) {
Log.w("Warn", "IO Exception");
}
return thumb;
}
@Override
protected void foreground(Object result) {
if (result != null) {
final ImageView imageView = isConcurrence();
if (imageView != null) {
imageView.setImageBitmap((Bitmap) result);
}
}
}
//ta metoda sprawdza czy bieżący pracownik jest tym samym co "przywiązany" do elementu/pozycji na liście obrazków dzięki temu wiadomo czy pracownik ma jeszcze dla kogo generować miniaturę
private ImageView isConcurrence() {
if (mImage != null) {
final ImageView imageView = mImage.get();
if (imageView != null) {
if (fetchWorker(imageView) == this) {
return imageView;
}
}
}
return null;
}
//ta metoda "wydobywa" pracownika z BitmapDrawable
public Worker fetchWorker(ImageView imageView) {
if (imageView != null) {
final Drawable mDrawable = imageView.getDrawable();
if (mDrawable instanceof Linker) {
final Linker mLinker = (Linker) mDrawable;
return mLinker.fetchWorker();
}
}
return null;
}
}
//ta metoda oblicza o ile trzeba zmniejszyć obrazek żeby zmieścił się na ekranie
static public int getSampleSize(String file) throws FileNotFoundException, IOException, OutOfMemoryError {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file, options);
int size = Math.max(options.outHeight, options.outWidth) / mSize;
if (size % 2 != 0) size++;
return size;
}
//ta metoda zmniejsza obrazek i "wypluwa" miniaturę
static public Bitmap getMiniature(String file) throws FileNotFoundException, IOException, OutOfMemoryError {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = getSampleSize(file);
return BitmapFactory.decodeFile(file, options);
}
}
kolejna klasa to spreparowany BitmapDrawable, wykorzystana w "podpięciu" pracownika do elementu/pozycji na liście obrazków tu również użyta jest właściwość WeakReference, zasada jest podobne jak wyżej inny jest jedynie przedmiot
public class Linker extends BitmapDrawable {
private WeakReference<Worker> mWorker;
public Linker(Worker worker) {
super();
mWorker = new WeakReference<Worker>(worker);
}
public Worker fetchWorker() {
return mWorker.get();
}
}
oraz klasa do kolejkowania zadań, można użyć AsyncTask ale do tego celu nie nadaje za bardzo, więc lepiej użyć czegoś innego, ważne żeby nie używać zwyczajnych Thread ze względy na wydajność
public abstract class SingleTask implements Runnable {
public static final int FOREGROUND = 0x1;
public static enum Running {YES, CANCEL, NO};
private static final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final ForegroundHandler mHandler = new ForegroundHandler();
private volatile Running mRunning = Running.NO;
public void execute() {
if (running()) {
throw new RejectedExecutionException("Task already running.");
}
mRunning = Running.YES;
mExecutor.execute(this);
}
public final boolean running() {
return mRunning == Running.YES || mRunning == Running.CANCEL;
}
public final boolean cancel() {
return mRunning == Running.CANCEL;
}
public void abort() {
mRunning = Running.CANCEL;
}
public void run() {
mHandler.sendMessage(mHandler.obtainMessage(FOREGROUND, background()));
}
public void end(Object result) {
mRunning = Running.NO;
foreground(result);
}
protected abstract Object background();
protected void foreground(Object result) {}
private class ForegroundHandler extends Handler {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case FOREGROUND:
end(message.obj);
break;
}
}
}
}
pozostały jeszce definicje Layout dla kontrolki:
gallery.xml
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ListView android:id="@+id/galleryList"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</LinearLayout>
dla pozycji na liście:
item.xml
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center">
<ImageView android:id="@+id/image"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="centerInside"/>
</LinearLayout>
kontrolka gotowa wystarczy jej użyć, klasa aktywności:
Gallery.java
public class Gallery extends Activity {
private GalleryView mGallery;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
mGallery = (GalleryView) findViewById(R.id.galleryView);
}
@Override
protected void onResume() {
super.onResume();
//ładujemy obrazki znalezione w /sdcard/images/ metoda [b]findImage[/b] jest uproszczona wiec ładuje tak naprawdę wszystko nie tylko obrazki należy wprowadzić kontrole [b]MIMEType[/b] w filtrze: [b]mFilter[/b]
mGallery.loadImages(findImage("/sdcard/images/"));
}
@Override
protected void onPause() {
super.onPause();
mGallery.clearImages();
}
public File[] findImage(String path) {
return new File(path).listFiles(mFilter);
}
public final FilenameFilter mFilter = new FilenameFilter() {
public boolean accept(File dir, String name) {
return new File(dir.getPath(),name).isFile();
}
};
}
oraz Layout:
main.xml
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<org.me.views.GalleryView android:id="@+id/galleryView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</LinearLayout>
nadgorliwych proszę o wstrzemięźliwość w krytykowaniu użytego słownictwa i analogi miało być możliwie łopatologiczne wyjąsnione