/* Copyright (C) 2014,2015 Björn Stelter * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> */ package de.hu_berlin.informatik.spws2014.mapever.largeimageview; import android.graphics.Bitmap; import android.graphics.BitmapFactory.Options; import android.graphics.BitmapRegionDecoder; import android.graphics.Rect; import android.os.AsyncTask; import android.support.v4.util.LruCache; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; // Der LruCache-bezogene Code wurde in Anlehnung an folgendes Tutorial erstellt: // http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html public class CachedImage extends LruCache<String, Bitmap> { interface CacheMissResolvedCallback { public void onCacheMissResolved(); } // ////// CONSTANTS // Tilegröße (Breite und Höhe, sollte Zweierpotenz sein) public static final int TILESIZE = 512; // ////// BITMAP, TILE AND CACHE STUFF // BitmapRegionDecoder (liest Bildausschnitte aus InputStreams) private BitmapRegionDecoder regionDecoder = null; // Liste für Keys der Tiles, die aktuell von TileWorkerTasks generiert werden private ArrayList<String> workingTileTasks = new ArrayList<String>(); // Callback, wenn nach einem Cache-Miss das gesuchte Tile erzeugt und gecachet wurde. private CacheMissResolvedCallback cacheMissResolvedCallback; // //////////////////////////////////////////////////////////////////////// // //////////// CONSTRUCTORS AND INITIALIZATION // //////////////////////////////////////////////////////////////////////// /** * Initialisiert und erzeugt einen Tile-Cache als LRU-Cache. * * @param inputStream Stream zur Bilddatei (nur JPEG und PNG) * @param cacheCallback Callback, wenn ein Tile nach einem Cache-Miss generiert und im Cache gespeichert wurde. * @throws IOException Wird geworfen, wenn BitmapRegionDecoder nicht instanziiert werden kann (falls das Bild * weder JPEG noch PNG ist, oder bei einem anderen IO-Fehler) */ public CachedImage(InputStream inputStream, CachedImage.CacheMissResolvedCallback cacheCallback) throws IOException { // Tilecache erzeugen durch Aufruf des LruCache<String, Bitmap>-Konstruktors super(calculateCacheSize()); // Callback setzen cacheMissResolvedCallback = cacheCallback; // BitmapRegionDecoder instanziieren. Wirft bei nicht unterstütztem Format (andere als JPEG und PNG) // eine IOException. regionDecoder = BitmapRegionDecoder.newInstance(inputStream, true); if (regionDecoder == null) { throw new IOException("BitmapRegionDecoder could not create instance for unknown reasons"); } } // ////// LRUCACHE METHODS OVERRIDES /** * Größe eines Cache-Eintrags (Bitmap) in Kilobyte. Die Größe des Caches insgesamt wird also an der Menge der * Bitmapdaten statt an der Anzahl der Einträge gemessen. */ @Override protected int sizeOf(String key, Bitmap bitmap) { // (getByteCount() (API 12) == getRowBytes() * getHeight()) return (bitmap.getRowBytes() * bitmap.getHeight()) / 1024; } /** * Berechnet die optimale Cachegröße. */ private static int calculateCacheSize() { // Get max available VM memory, exceeding this amount will throw an OutOfMemory exception. // Stored in kilobytes as LruCache takes an int in its constructor. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); Log.d("CachedImage/calculateCacheSize", "Memory max: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB, total: " + Runtime.getRuntime().totalMemory() / 1024 / 1024 + " MB, free: " + Runtime.getRuntime().freeMemory() / 1024 / 1024 + " MB"); // Use 1/8th of the available memory for this memory cache. // TODO Gute Wahl? Nein lieber abhängig von max-total(+free) machen, oder? (Und dann vielleicht ruhig // .... größer.) (Außerdem: Gerätabhängig? Hm :/) (Problem, wenn Cache nicht ausreicht und noch benötigte // .... Tiles sofort rausgeworfen werden... Hmmmmm.) // (mal 1/4 nehmen und gucken, wies damit so läuft) final int cacheSize = maxMemory / 4; Log.d("CachedImage/calculateCacheSize", "Max memory: " + maxMemory / 1024 + " MB, thus creating a cache of size " + cacheSize / 1024 + " MB"); return cacheSize; } // //////////////////////////////////////////////////////////////////////// // //////////// IMAGE PROPERTIES // //////////////////////////////////////////////////////////////////////// /** * Gibt die Breite des Bildes zurück. (Tatsächliche Bildgröße, auch wenn nur kleinere Teile geladen sind.) */ public int getWidth() { return regionDecoder.getWidth(); } /** * Gibt die Höhe des Bildes zurück. (Tatsächliche Bildgröße, auch wenn nur kleinere Teile geladen sind.) */ public int getHeight() { return regionDecoder.getHeight(); } // //////////////////////////////////////////////////////////////////////// // //////////// VERWALTUNG DES BILDAUSSCHNITT-CACHES // //////////////////////////////////////////////////////////////////////// /** * Generiert aus x, y, scale den Cachekey (x_y_sampleSize). * * @param x Linke Eckkoordinate. * @param y Obere Eckkoordinate. * @param sampleSize Samplesize (n ist 1/n mal so groß wie das Original) * @return String x+"_"+y+"_"+sampleSize */ private static String getCacheKey(int x, int y, int sampleSize) { return x + "_" + y + "_" + sampleSize; } /** * Liefert Ausschnitt ab x, y mit Samplesize scale zurück, falls im Cache vorhanden, ansonsten null. * * @param x Linke Eckkoordinate. * @param y Obere Eckkoordinate. * @param samplingSize Samplesize (n ist 1/n mal so groß wie das Original) * @return Tile-Bitmap oder null */ private Bitmap getCachedTileBitmap(int x, int y, int samplingSize) { return get(getCacheKey(x, y, samplingSize)); } /** * Speichert gegebenen Tile im Cache. * * @param x Linke Eckkoordinate. * @param y Obere Eckkoordinate. * @param sampleSize Samplesize (n ist 1/n mal so groß wie das Original) * @param tile Tile-Bitmap */ private void putTileInCache(int x, int y, int sampleSize, Bitmap tile) { if (tile == null) { Log.e("CachedImage/putTileInCache", "tile == null, won't put into cache!"); return; } // Key erzeugen String key = getCacheKey(x, y, sampleSize); Log.d("CachedImage/putTileInCache", "Putting tile " + key + " into cache."); // Tile im Cache speichern put(key, tile); } /** * Generiert Bildausschnitt ab Koordinaten (left, top) mit sampleSize gibt ihn als Bitmap zurück. * Sollte nicht direkt aufgerufen werden, sondern asynchron über einen TileWorkerTask. * * @param left Linke Eckkoordinate. * @param top Obere Eckkoordinate. * @param sampleSize Samplesize (n ist 1/n mal so groß wie das Original) * @return Bitmap des Tiles (maximal TILESIZE*TILESIZE Pixel groß) */ private Bitmap generateTileBitmap(int left, int top, int sampleSize) { // Key erzeugen String key = getCacheKey(left, top, sampleSize); // Kein neues Tile generieren, falls es bereits vorhanden ist. if (get(key) != null) { return null; } Log.d("CachedImage/generateTileBitmap", "Generating tile " + key + " ..."); Log.d("CachedImage/generateTileBitmap", "Memory max: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB, total: " + Runtime.getRuntime().totalMemory() / 1024 / 1024 + " MB, free: " + Runtime.getRuntime().freeMemory() / 1024 / 1024 + " MB"); // TODO OutOfMemory-Exceptions auffangen, Cachegröße verkleinern? Oder so? // Wenn Tile komplett außerhalb des Bildbereichs liegt, gibt es kein Tile. // (< 0 statt < -TILESIZE reicht aus, da left,top % TILESIZE = 0 angenommen wird.) if (left < 0 || left >= getWidth() || top < 0 || top >= getHeight()) { return null; } // Berechne Maße/Eckpunkte des Tiles (gesampelte Tiles sollen dennoch TILESIZE groß sein, aber der gewünschte // Bildausschnitt wird dadurch natürlich größer, daher *sampleSize) // min(), um Tile am Rand abschneiden, wenn Bildrest nicht groß genug. int right = Math.min(getWidth(), left + sampleSize * TILESIZE); int bottom = Math.min(getHeight(), top + sampleSize * TILESIZE); // SampleSize festlegen, um großes Bild bei geringer Zoomstufe runterzuskalieren Options opts = new Options(); opts.inSampleSize = sampleSize; // Tile generieren und zurückgeben return regionDecoder.decodeRegion(new Rect(left, top, right, bottom), opts); } /** * Asynchroner Task, der ein Tile generiert und es anschließend im Cache speichert. */ class TileWorkerTask extends AsyncTask<Void, Void, Bitmap> { private int x, y, sampleSize; public TileWorkerTask(int x, int y, int sampleSize) { this.x = x; this.y = y; this.sampleSize = sampleSize; } @Override protected Bitmap doInBackground(Void... params) { // Tile generieren return generateTileBitmap(x, y, sampleSize); } @Override protected void onPostExecute(Bitmap result) { if (result == null) { Log.e("TileWorkerTask/onPostExecute", "Generated tile, but it's == null! What?"); return; } // Tile in Cache speichern falls ungleich null putTileInCache(x, y, sampleSize, result); // bei Fertigstellung wird der Eintrag in workingTileTasks entfernt workingTileTasks.remove(getCacheKey(x, y, sampleSize)); // Callback aufrufen, das in der LargeImageView dann this.invalidated. if (cacheMissResolvedCallback != null) { cacheMissResolvedCallback.onCacheMissResolved(); } } } /** * Gibt den Ausschnitt des Bildes zurück, der bei x,y beginnt und TILESIZE breit und hoch ist, bzw. am Rand kleiner. * Tiles werden mit LRU gecachet (nach x, y, scale). * * @param x Linke Eckkoordinate. * @param y Obere Eckkoordinate. * @param sampleSize Samplesize (n ist 1/n mal so groß wie das Original) * @return */ public Bitmap getTileBitmap(int x, int y, int sampleSize) { // Tile aus Cache laden, falls vorhanden, sonst null. Bitmap tile = getCachedTileBitmap(x, y, sampleSize); // Tile in asynchronen Task generieren, falls es nicht im Cache gefunden wurde. if (tile == null) { // Key erzeugen String key = getCacheKey(x, y, sampleSize); // Prüfe zunächst, ob dieser Tile bereits einen laufenden TileWorkerTask hat if (workingTileTasks.contains(key)) { Log.d("CachedImage/getTileBitmap", "Tile " + key + " is already being generated..."); // Ja, also kein Bild zurückgeben return null; } else if (!workingTileTasks.isEmpty()) { // Wir generieren immer nur ein Tile zur selben Zeit (siehe #234) Log.d("CachedImage/getTileBitmap", "Tile " + key + " not found in cache, but we're already generating a tile... Wait..."); return null; } else { Log.d("CachedImage/getTileBitmap", "Tile " + key + " not found in cache -> generating (async)..."); // Starte Task TileWorkerTask task = new TileWorkerTask(x, y, sampleSize); task.execute(); // Wir merken uns, dass dieses Tile jetzt generiert wird, damit bei einem nächsten Aufruf vor der // Fertigstellung des Tiles nicht noch ein gleicher Task erzeugt wird. workingTileTasks.add(key); // null zurückgeben um zu signalisieren, dass NOCH kein Bild vorhanden ist. // TileWorkerTask veranlasst nach dem Laden ein invalidate(); return null; } } return tile; } }