Reducing memory usage of wallpaper picker

> Loading preview bitmap only once, instead of loading it twice at BitmapRegionTileSource and BitmapSource
> Maintaing a weak-set of reusable bitmaps and reusing them for decoding bitmaps
> Loading images on a HandlerThread (instead of AsyncTask) and removing any non-started task before submitting a new task
> Loading inbuild images (from resources) on HandlerThread instead of UIThread
> Freeing up unbound GL textures before binding a new texture.

Bug: 18382606
Change-Id: Ic4ca630dd113ded65d2853eb0d291c9e5823637e
(cherry picked from commit 283c2261bd)
This commit is contained in:
Sunny Goyal
2015-02-12 15:34:41 -08:00
committed by Adam Cohen
parent 31178b8237
commit 65b929d6f2
5 changed files with 245 additions and 186 deletions
@@ -696,6 +696,7 @@ public class GLES20Canvas implements GLCanvas {
}
private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) {
deleteRecycledResources();
GLES20.glUseProgram(program);
checkError();
enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA);
@@ -25,25 +25,36 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
import com.android.gallery3d.common.BitmapCropTask;
import com.android.gallery3d.common.BitmapUtils;
import com.android.gallery3d.common.Utils;
import com.android.photos.BitmapRegionTileSource;
import com.android.photos.BitmapRegionTileSource.BitmapSource;
import com.android.photos.BitmapRegionTileSource.BitmapSource.InBitmapProvider;
import com.android.photos.views.TiledImageRenderer.TileSource;
public class WallpaperCropActivity extends Activity {
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import java.util.WeakHashMap;
public class WallpaperCropActivity extends Activity implements Handler.Callback {
private static final String LOGTAG = "Launcher3.CropActivity";
protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width";
@@ -59,13 +70,29 @@ public class WallpaperCropActivity extends Activity {
public static final int MAX_BMAP_IN_INTENT = 750000;
public static final float WALLPAPER_SCREENS_SPAN = 2f;
private static final int MSG_LOAD_IMAGE = 1;
protected CropView mCropView;
protected View mProgressView;
protected Uri mUri;
protected View mSetWallpaperButton;
private HandlerThread mLoaderThread;
private Handler mLoaderHandler;
private LoadRequest mCurrentLoadRequest;
private byte[] mTempStorageForDecoding = new byte[16 * 1024];
// A weak-set of reusable bitmaps
private Set<Bitmap> mReusableBitmaps =
Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLoaderThread = new HandlerThread("wallpaper_loader");
mLoaderThread.start();
mLoaderHandler = new Handler(mLoaderThread.getLooper(), this);
init();
if (!enableRotation()) {
setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT);
@@ -76,6 +103,7 @@ public class WallpaperCropActivity extends Activity {
setContentView(R.layout.wallpaper_cropper);
mCropView = (CropView) findViewById(R.id.cropView);
mProgressView = findViewById(R.id.loading);
Intent cropIntent = getIntent();
final Uri imageUri = cropIntent.getData();
@@ -116,7 +144,7 @@ public class WallpaperCropActivity extends Activity {
}
}
};
setCropViewTileSource(bitmapSource, true, false, onLoad);
setCropViewTileSource(bitmapSource, true, false, null, onLoad);
}
@Override
@@ -124,65 +152,134 @@ public class WallpaperCropActivity extends Activity {
if (mCropView != null) {
mCropView.destroy();
}
if (mLoaderThread != null) {
mLoaderThread.quit();
}
super.onDestroy();
}
public void setCropViewTileSource(
final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled,
final boolean moveToLeft, final Runnable postExecute) {
final Context context = WallpaperCropActivity.this;
final View progressView = findViewById(R.id.loading);
final AsyncTask<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void...args) {
if (!isCancelled()) {
try {
bitmapSource.loadInBackground();
} catch (SecurityException securityException) {
if (isDestroyed()) {
// Temporarily granted permissions are revoked when the activity
// finishes, potentially resulting in a SecurityException here.
// Even though {@link #isDestroyed} might also return true in different
// situations where the configuration changes, we are fine with
// catching these cases here as well.
cancel(false);
} else {
// otherwise it had a different cause and we throw it further
throw securityException;
/**
* This is called on {@link #mLoaderThread}
*/
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MSG_LOAD_IMAGE) {
final LoadRequest req = (LoadRequest) msg.obj;
try {
req.src.loadInBackground(new InBitmapProvider() {
@Override
public Bitmap forPixelCount(int count) {
synchronized (mReusableBitmaps) {
Iterator<Bitmap> itr = mReusableBitmaps.iterator();
while (itr.hasNext()) {
Bitmap b = itr.next();
if (b.getWidth() * b.getHeight() >= count) {
itr.remove();
return b;
}
}
}
return null;
}
}
return null;
}
protected void onPostExecute(Void arg) {
if (!isCancelled()) {
progressView.setVisibility(View.INVISIBLE);
if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
mCropView.setTileSource(
new BitmapRegionTileSource(context, bitmapSource), null);
mCropView.setTouchEnabled(touchEnabled);
if (moveToLeft) {
mCropView.moveToLeft();
}
}
}
if (postExecute != null) {
postExecute.run();
});
} catch (SecurityException securityException) {
if (isDestroyed()) {
// Temporarily granted permissions are revoked when the activity
// finishes, potentially resulting in a SecurityException here.
// Even though {@link #isDestroyed} might also return true in different
// situations where the configuration changes, we are fine with
// catching these cases here as well.
return true;
} else {
// otherwise it had a different cause and we throw it further
throw securityException;
}
}
};
req.result = new BitmapRegionTileSource(this, req.src, mTempStorageForDecoding);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (req == mCurrentLoadRequest) {
onLoadRequestComplete(req,
req.src.getLoadingState() == BitmapSource.State.LOADED);
} else {
addReusableBitmap(req.result);
}
}
});
return true;
}
return false;
}
private void addReusableBitmap(TileSource src) {
synchronized (mReusableBitmaps) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
&& src instanceof BitmapRegionTileSource) {
Bitmap preview = ((BitmapRegionTileSource) src).getBitmap();
if (preview != null && preview.isMutable()) {
mReusableBitmaps.add(preview);
}
}
}
}
protected void onLoadRequestComplete(LoadRequest req, boolean success) {
mCurrentLoadRequest = null;
if (success) {
TileSource oldSrc = mCropView.getTileSource();
mCropView.setTileSource(req.result, null);
mCropView.setTouchEnabled(req.touchEnabled);
if (req.moveToLeft) {
mCropView.moveToLeft();
}
if (req.scaleProvider != null) {
mCropView.setScale(req.scaleProvider.getScale(req.result));
}
// Free last image
if (oldSrc != null) {
// Call yield instead of recycle, as we only want to free GL resource.
// We can still reuse the bitmap for decoding any other image.
oldSrc.getPreview().yield();
}
addReusableBitmap(oldSrc);
}
if (req.postExecute != null) {
req.postExecute.run();
}
}
public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled,
boolean moveToLeft, CropViewScaleProvider scaleProvider, Runnable postExecute) {
final LoadRequest req = new LoadRequest();
req.moveToLeft = moveToLeft;
req.src = bitmapSource;
req.touchEnabled = touchEnabled;
req.postExecute = postExecute;
req.scaleProvider = scaleProvider;
mCurrentLoadRequest = req;
// Remove any pending requests
mLoaderHandler.removeMessages(MSG_LOAD_IMAGE);
Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget();
// We don't want to show the spinner every time we load an image, because that would be
// annoying; instead, only start showing the spinner if loading the image has taken
// longer than 1 sec (ie 1000 ms)
progressView.postDelayed(new Runnable() {
mProgressView.postDelayed(new Runnable() {
public void run() {
if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) {
progressView.setVisibility(View.VISIBLE);
if (mCurrentLoadRequest == req) {
mProgressView.setVisibility(View.VISIBLE);
}
}
}, 1000);
loadBitmapTask.execute();
}
public boolean enableRotation() {
return getResources().getBoolean(R.bool.allow_rotation);
}
@@ -372,4 +469,18 @@ public class WallpaperCropActivity extends Activity {
wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
}
}
static class LoadRequest {
BitmapSource src;
boolean touchEnabled;
boolean moveToLeft;
Runnable postExecute;
CropViewScaleProvider scaleProvider;
TileSource result;
}
interface CropViewScaleProvider {
float getScale(TileSource src);
}
}
@@ -74,6 +74,7 @@ import com.android.gallery3d.common.BitmapUtils;
import com.android.gallery3d.common.Utils;
import com.android.photos.BitmapRegionTileSource;
import com.android.photos.BitmapRegionTileSource.BitmapSource;
import com.android.photos.views.TiledImageRenderer.TileSource;
import java.io.File;
import java.io.FileOutputStream;
@@ -168,7 +169,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity {
}
mBitmapSource = new BitmapRegionTileSource.UriBitmapSource(
a, mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE);
a.setCropViewTileSource(mBitmapSource, true, false, onLoad);
a.setCropViewTileSource(mBitmapSource, true, false, null, onLoad);
}
@Override
public void onSave(final WallpaperPickerActivity a) {
@@ -205,7 +206,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity {
public void onClick(WallpaperPickerActivity a) {
BitmapRegionTileSource.UriBitmapSource bitmapSource =
new BitmapRegionTileSource.UriBitmapSource(a, Uri.fromFile(mFile), 1024);
a.setCropViewTileSource(bitmapSource, false, true, null);
a.setCropViewTileSource(bitmapSource, false, true, null, null);
}
@Override
public void onSave(WallpaperPickerActivity a) {
@@ -231,22 +232,22 @@ public class WallpaperPickerActivity extends WallpaperCropActivity {
mThumb = thumb;
}
@Override
public void onClick(WallpaperPickerActivity a) {
public void onClick(final WallpaperPickerActivity a) {
BitmapRegionTileSource.ResourceBitmapSource bitmapSource =
new BitmapRegionTileSource.ResourceBitmapSource(
mResources, mResId, BitmapRegionTileSource.MAX_PREVIEW_SIZE);
bitmapSource.loadInBackground();
BitmapRegionTileSource source = new BitmapRegionTileSource(a, bitmapSource);
CropView v = a.getCropView();
v.setTileSource(source, null);
Point wallpaperSize = BitmapUtils.getDefaultWallpaperSize(
a.getResources(), a.getWindowManager());
RectF crop = Utils.getMaxCropRect(
source.getImageWidth(), source.getImageHeight(),
wallpaperSize.x, wallpaperSize.y, false);
v.setScale(wallpaperSize.x / crop.width());
v.setTouchEnabled(false);
a.setSystemWallpaperVisiblity(false);
a.setCropViewTileSource(bitmapSource, false, false, new CropViewScaleProvider() {
@Override
public float getScale(TileSource src) {
Point wallpaperSize = BitmapUtils.getDefaultWallpaperSize(
a.getResources(), a.getWindowManager());
RectF crop = Utils.getMaxCropRect(
src.getImageWidth(), src.getImageHeight(),
wallpaperSize.x, wallpaperSize.y, false);
return wallpaperSize.x / crop.width();
}
}, null);
}
@Override
public void onSave(WallpaperPickerActivity a) {
@@ -271,21 +272,26 @@ public class WallpaperPickerActivity extends WallpaperCropActivity {
@Override
public void onClick(WallpaperPickerActivity a) {
CropView c = a.getCropView();
Drawable defaultWallpaper = WallpaperManager.getInstance(a).getBuiltInDrawable(
c.getWidth(), c.getHeight(), false, 0.5f, 0.5f);
if (defaultWallpaper == null) {
Log.w(TAG, "Null default wallpaper encountered.");
c.setTileSource(null, null);
return;
}
c.setTileSource(
new DrawableTileSource(a, defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE), null);
c.setScale(1f);
c.setTouchEnabled(false);
a.setSystemWallpaperVisiblity(false);
LoadRequest req = new LoadRequest();
req.moveToLeft = false;
req.touchEnabled = false;
req.scaleProvider = new CropViewScaleProvider() {
@Override
public float getScale(TileSource src) {
return 1f;
}
};
req.result = new DrawableTileSource(a, defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE);
a.onLoadRequestComplete(req, true);
}
@Override
public void onSave(WallpaperPickerActivity a) {
@@ -348,24 +354,11 @@ public class WallpaperPickerActivity extends WallpaperCropActivity {
}
@Override
public void setCropViewTileSource(BitmapSource bitmapSource,
boolean touchEnabled,
boolean moveToLeft,
final Runnable postExecute) {
// we also want to show our own wallpaper instead of the one in the background
Runnable showPostExecuteRunnable = new Runnable() {
@Override
public void run() {
if(postExecute != null) {
postExecute.run();
}
setSystemWallpaperVisiblity(false);
}
};
super.setCropViewTileSource(bitmapSource,
touchEnabled,
moveToLeft,
showPostExecuteRunnable);
protected void onLoadRequestComplete(LoadRequest req, boolean success) {
super.onLoadRequestComplete(req, success);
if (success) {
setSystemWallpaperVisiblity(false);
}
}
// called by onCreate; this is subclassed to overwrite WallpaperCropActivity
@@ -375,6 +368,9 @@ public class WallpaperPickerActivity extends WallpaperCropActivity {
mCropView = (CropView) findViewById(R.id.cropView);
mCropView.setVisibility(View.INVISIBLE);
mProgressView = findViewById(R.id.loading);
mWallpaperStrip = findViewById(R.id.wallpaper_strip);
mCropView.setTouchCallback(new CropView.TouchCallback() {
ViewPropertyAnimator mAnim;
@@ -20,7 +20,6 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
@@ -28,7 +27,6 @@ import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import com.android.gallery3d.common.BitmapUtils;
@@ -148,8 +146,6 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
private static final String TAG = "BitmapRegionTileSource";
private static final boolean REUSE_BITMAP =
Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
private static final int GL_SIZE_LIMIT = 2048;
// This must be no larger than half the size of the GL_SIZE_LIMIT
// due to decodePreview being allowed to be up to 2x the size of the target
@@ -158,14 +154,14 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
public static abstract class BitmapSource {
private SimpleBitmapRegionDecoder mDecoder;
private Bitmap mPreview;
private int mPreviewSize;
private final int mPreviewSize;
private int mRotation;
public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
private State mState = State.NOT_LOADED;
public BitmapSource(int previewSize) {
mPreviewSize = previewSize;
mPreviewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
}
public boolean loadInBackground() {
public boolean loadInBackground(InBitmapProvider bitmapProvider) {
ExifInterface ei = new ExifInterface();
if (readExif(ei)) {
Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
@@ -181,15 +177,33 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
int width = mDecoder.getWidth();
int height = mDecoder.getHeight();
if (mPreviewSize != 0) {
int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE);
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
opts.inPreferQualityOverSpeed = true;
float scale = (float) previewSize / Math.max(width, height);
float scale = (float) mPreviewSize / Math.max(width, height);
opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
opts.inJustDecodeBounds = false;
mPreview = loadPreviewBitmap(opts);
opts.inMutable = true;
if (bitmapProvider != null) {
int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize);
Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles);
if (reusableBitmap != null) {
// Try loading with reusable bitmap
opts.inBitmap = reusableBitmap;
try {
mPreview = loadPreviewBitmap(opts);
} catch (IllegalArgumentException e) {
Log.d(TAG, "Unable to reusage bitmap", e);
opts.inBitmap = null;
mPreview = null;
}
}
}
if (mPreview == null) {
mPreview = loadPreviewBitmap(opts);
}
}
mState = State.LOADED;
return true;
@@ -208,10 +222,6 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
return mPreview;
}
public int getPreviewSize() {
return mPreviewSize;
}
public int getRotation() {
return mRotation;
}
@@ -219,6 +229,10 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
public abstract boolean readExif(ExifInterface ei);
public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
public interface InBitmapProvider {
Bitmap forPixelCount(int count);
}
}
public static class FilePathBitmapSource extends BitmapSource {
@@ -306,13 +320,13 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
Utils.closeSilently(is);
return true;
} catch (FileNotFoundException e) {
Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
return false;
} catch (IOException e) {
Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
return false;
} catch (NullPointerException e) {
Log.e("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e);
Log.d("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e);
return false;
} finally {
Utils.closeSilently(is);
@@ -372,11 +386,9 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
// For use only by getTile
private Rect mWantRegion = new Rect();
private Rect mOverlapRegion = new Rect();
private BitmapFactory.Options mOptions;
private Canvas mCanvas;
public BitmapRegionTileSource(Context context, BitmapSource source) {
public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) {
mTileSize = TiledImageRenderer.suggestedTileSize(context);
mRotation = source.getRotation();
mDecoder = source.getBitmapRegionDecoder();
@@ -386,27 +398,26 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
mOptions = new BitmapFactory.Options();
mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
mOptions.inPreferQualityOverSpeed = true;
mOptions.inTempStorage = new byte[16 * 1024];
int previewSize = source.getPreviewSize();
if (previewSize != 0) {
previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
// Although this is the same size as the Bitmap that is likely already
// loaded, the lifecycle is different and interactions are on a different
// thread. Thus to simplify, this source will decode its own bitmap.
Bitmap preview = decodePreview(source, previewSize);
if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
mOptions.inTempStorage = tempStorage;
Bitmap preview = source.getPreviewBitmap();
if (preview != null &&
preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
mPreview = new BitmapTexture(preview);
} else {
Log.w(TAG, String.format(
"Failed to create preview of apropriate size! "
+ " in: %dx%d, out: %dx%d",
mWidth, mHeight,
preview.getWidth(), preview.getHeight()));
}
} else {
Log.w(TAG, String.format(
"Failed to create preview of apropriate size! "
+ " in: %dx%d, out: %dx%d",
mWidth, mHeight,
preview.getWidth(), preview.getHeight()));
}
}
}
public Bitmap getBitmap() {
return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null;
}
@Override
public int getTileSize() {
return mTileSize;
@@ -435,10 +446,6 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
@Override
public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
int tileSize = getTileSize();
if (!REUSE_BITMAP) {
return getTileWithoutReusingBitmap(level, x, y, tileSize);
}
int t = tileSize << level;
mWantRegion.set(x, y, x + t, y + t);
@@ -462,64 +469,4 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
}
return bitmap;
}
private Bitmap getTileWithoutReusingBitmap(
int level, int x, int y, int tileSize) {
int t = tileSize << level;
mWantRegion.set(x, y, x + t, y + t);
mOverlapRegion.set(0, 0, mWidth, mHeight);
mOptions.inSampleSize = (1 << level);
Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
if (bitmap == null) {
Log.w(TAG, "fail in decoding region");
}
if (mWantRegion.equals(mOverlapRegion)) {
return bitmap;
}
Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
if (mCanvas == null) {
mCanvas = new Canvas();
}
mCanvas.setBitmap(result);
mCanvas.drawBitmap(bitmap,
(mOverlapRegion.left - mWantRegion.left) >> level,
(mOverlapRegion.top - mWantRegion.top) >> level, null);
mCanvas.setBitmap(null);
return result;
}
/**
* Note that the returned bitmap may have a long edge that's longer
* than the targetSize, but it will always be less than 2x the targetSize
*/
private Bitmap decodePreview(BitmapSource source, int targetSize) {
Bitmap result = source.getPreviewBitmap();
if (result == null) {
return null;
}
// We need to resize down if the decoder does not support inSampleSize
// or didn't support the specified inSampleSize (some decoders only do powers of 2)
float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
if (scale <= 0.5) {
result = BitmapUtils.resizeBitmapByScale(result, scale, true);
}
return ensureGLCompatibleBitmap(result);
}
private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
if (bitmap == null || bitmap.getConfig() != null) {
return bitmap;
}
Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
bitmap.recycle();
return newBitmap;
}
}
@@ -125,6 +125,10 @@ public class TiledImageView extends FrameLayout {
invalidate();
}
public TileSource getTileSource() {
return mRenderer.source;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {