public class ImageSurfaceView extends SurfaceView implements SurfaceHolder.Callback, OnGestureListener {
private final static String TAG = ImageSurfaceView.class.getSimpleName();
private InputStreamScene scene;
private final Touch touch;
private GestureDetector gestureDectector;
private ScaleGestureDetector scaleGestureDetector;
private long lastScaleTime = 0;
private long SCALE_MOVE_GUARD = 500; // milliseconds after scale to ignore move events
private DrawThread drawThread;
//region getters and setters
public void getViewport(Point p){
scene.getViewport().getOrigin(p);
}
public void setViewport(Point viewport){
scene.getViewport().setOrigin(viewport.x, viewport.y);
}
public void setViewportCenter() {
Point viewportSize = new Point();
Point sceneSize = scene.getSceneSize();
scene.getViewport().getSize(viewportSize);
int x = (sceneSize.x - viewportSize.x) / 2;
int y = (sceneSize.y - viewportSize.y) / 2;
scene.getViewport().setOrigin(x, y);
}
public void setInputStream(InputStream inputStream) throws IOException {
scene = new InputStreamScene(inputStream);
}
//endregion
//region extends SurfaceView
@Override
public boolean onTouchEvent(MotionEvent me) {
boolean consumed = gestureDectector.onTouchEvent(me);
if (consumed)
return true;
scaleGestureDetector.onTouchEvent(me);
switch (me.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: return touch.down(me);
case MotionEvent.ACTION_MOVE:
if (scaleGestureDetector.isInProgress() || System.currentTimeMillis()-lastScaleTime<SCALE_MOVE_GUARD)
break;
return touch.move(me);
case MotionEvent.ACTION_UP: return touch.up(me);
case MotionEvent.ACTION_CANCEL: return touch.cancel(me);
}
return super.onTouchEvent(me);
}
//endregion
//region SurfaceHolder.Callback constructors
public ImageSurfaceView(Context context) {
super(context);
touch = new Touch(context);
init(context);
}
public ImageSurfaceView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
touch = new Touch(context);
init(context);
}
public ImageSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
touch = new Touch(context);
init(context);
}
private void init(Context context){
gestureDectector = new GestureDetector(context,this);
getHolder().addCallback(this);
scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener());
}
//endregion
//region class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
private PointF screenFocus = new PointF();
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
if (scaleFactor!=0f && scaleFactor!=1.0f){
scaleFactor = 1/scaleFactor;
screenFocus.set(detector.getFocusX(),detector.getFocusY());
scene.getViewport().zoom(
scaleFactor,
screenFocus);
invalidate();
}
lastScaleTime = System.currentTimeMillis();
return true;
}
}
//endregion
//region implements SurfaceHolder.Callback
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
scene.getViewport().setSize(width, height);
Log.d(TAG,String.format("onSizeChanged(w=%d,h=%d)",width,height));
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
drawThread = new DrawThread(holder);
drawThread.setName("drawThread");
drawThread.setRunning(true);
drawThread.start();
scene.start();
touch.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
touch.stop();
scene.stop();
drawThread.setRunning(false);
boolean retry = true;
while (retry) {
try {
drawThread.join();
retry = false;
} catch (InterruptedException e) {
// we will try it again and again...
}
}
}
//endregion
//region implements OnGestureListener
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return touch.fling( e1, e2, velocityX, velocityY);
}
//region the rest are defaults
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
//endregion
//endregion
//region class DrawThread
class DrawThread extends Thread {
private SurfaceHolder surfaceHolder;
private boolean running = false;
public void setRunning(boolean value){ running = value; }
public DrawThread(SurfaceHolder surfaceHolder){
this.surfaceHolder = surfaceHolder;
}
@Override
public void run() {
Canvas c;
while (running) {
try {
// Don't hog the entire CPU
Thread.sleep(5);
} catch (InterruptedException e) {}
c = null;
try {
c = surfaceHolder.lockCanvas();
if (c!=null){
synchronized (surfaceHolder) {
scene.draw(c);// draw it
}
}
} finally {
if (c != null) {
surfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
}
//endregion
//region class Touch
enum TouchState {UNTOUCHED,IN_TOUCH,START_FLING,IN_FLING};
class Touch {
TouchState state = TouchState.UNTOUCHED;
/** Where on the view did we initially touch */
final Point viewDown = new Point(0,0);
/** What was the coordinates of the viewport origin? */
final Point viewportOriginAtDown = new Point(0,0);
final Scroller scroller;
TouchThread touchThread;
Touch(Context context){
scroller = new Scroller(context);
}
void start(){
touchThread = new TouchThread(this);
touchThread.setName("touchThread");
touchThread.start();
}
void stop(){
touchThread.running = false;
touchThread.interrupt();
boolean retry = true;
while (retry) {
try {
touchThread.join();
retry = false;
} catch (InterruptedException e) {
// we will try it again and again...
}
}
touchThread = null;
}
Point fling_viewOrigin = new Point();
Point fling_viewSize = new Point();
Point fling_sceneSize = new Point();
boolean fling( MotionEvent e1, MotionEvent e2, float velocityX, float velocityY){
scene.getViewport().getOrigin(fling_viewOrigin);
scene.getViewport().getSize(fling_viewSize);
scene.getSceneSize(fling_sceneSize);
synchronized(this){
state = TouchState.START_FLING;
scene.setSuspend(true);
scroller.fling(
fling_viewOrigin.x,
fling_viewOrigin.y,
(int)-velocityX,
(int)-velocityY,
0,
fling_sceneSize.x-fling_viewSize.x,
0,
fling_sceneSize.y-fling_viewSize.y);
touchThread.interrupt();
}
// Log.d(TAG,String.format("scroller.fling(%d,%d,%d,%d,%d,%d,%d,%d)",
// fling_viewOrigin.x,
// fling_viewOrigin.y,
// (int)-velocityX,
// (int)-velocityY,
// 0,
// fling_sceneSize.x-fling_viewSize.x,
// 0,
// fling_sceneSize.y-fling_viewSize.y));
return true;
}
boolean down(MotionEvent event){
scene.setSuspend(false); // If we were suspended because of a fling
synchronized(this){
state = TouchState.IN_TOUCH;
viewDown.x = (int) event.getX();
viewDown.y = (int) event.getY();
Point p = new Point();
scene.getViewport().getOrigin(p);
viewportOriginAtDown.set(p.x,p.y);
}
return true;
}
boolean move(MotionEvent event){
if (state==TouchState.IN_TOUCH){
float zoom = scene.getViewport().getZoom();
float deltaX = zoom * ((float)(event.getX()-viewDown.x));
float deltaY = zoom * ((float)(event.getY()-viewDown.y));
float newX = ((float)(viewportOriginAtDown.x - deltaX));
float newY = ((float)(viewportOriginAtDown.y - deltaY));
scene.getViewport().setOrigin((int)newX, (int)newY);
invalidate();
}
return true;
}
boolean up(MotionEvent event){
if (state==TouchState.IN_TOUCH){
state = TouchState.UNTOUCHED;
}
return true;
}
boolean cancel(MotionEvent event){
if (state==TouchState.IN_TOUCH){
state = TouchState.UNTOUCHED;
}
return true;
}
class TouchThread extends Thread {
final Touch touch;
boolean running = false;
void setRunning(boolean value){ running = value; }
TouchThread(Touch touch){ this.touch = touch; }
@Override
public void run() {
running=true;
while(running){
while(touch.state!=TouchState.START_FLING && touch.state!=TouchState.IN_FLING){
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {}
if (!running)
return;
}
synchronized (touch) {
if (touch.state==TouchState.START_FLING){
touch.state = TouchState.IN_FLING;
}
}
if (touch.state==TouchState.IN_FLING){
scroller.computeScrollOffset();
scene.getViewport().setOrigin(scroller.getCurrX(), scroller.getCurrY());
if (scroller.isFinished()){
scene.setSuspend(false);
synchronized (touch) {
touch.state = TouchState.UNTOUCHED;
try{
Thread.sleep(5);
} catch (InterruptedException e) {}
}
}
}
}
}
}
}
//endregion
}
public class InputStreamScene extends Scene {
private static final String TAG=InputStreamScene.class.getSimpleName();
private static final boolean DEBUG = false;
private static final BitmapFactory.Options options = new BitmapFactory.Options();
/** What is the downsample size for the sample image? 1=1/2, 2=1/4 3=1/8, etc */
private static final int DOWN_SAMPLE_SHIFT = 2;
/** How many bytes does one pixel use? */
private final int BYTES_PER_PIXEL = 4;
/** What percent of total memory should we use for the cache? The bigger the cache,
* the longer it takes to read -- 1.2 secs for 25%, 600ms for 10%, 500ms for 5%.
* User experience seems to be best for smaller values.
*/
private int percent = 5; // Above 25 and we get OOMs
private BitmapRegionDecoder decoder;
private Bitmap sampleBitmap;
static {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
public InputStreamScene(InputStream inputStream) throws IOException {
BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
this.decoder = BitmapRegionDecoder.newInstance(inputStream, false);
// Grab the bounds for the scene dimensions
tmpOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, tmpOptions);
setSceneSize(tmpOptions.outWidth, tmpOptions.outHeight);
// Create the sample image
tmpOptions.inJustDecodeBounds = false;
tmpOptions.inSampleSize = (1<< DOWN_SAMPLE_SHIFT);
sampleBitmap = BitmapFactory.decodeStream(inputStream, null, tmpOptions);
initialize();
}
@Override
protected Bitmap fillCache(Rect origin) {
Bitmap bitmap = null;
if (decoder!=null)
bitmap = decoder.decodeRegion( origin, options );
return bitmap;
}
private static Paint red = new Paint();
static{
red.setColor(Color.RED);
red.setStrokeWidth(5L);
}
@Override
protected void drawSampleRectIntoBitmap(Bitmap bitmap, Rect rectOfSample) {
if (bitmap!=null){
Canvas c = new Canvas(bitmap);
int left = (rectOfSample.left>> DOWN_SAMPLE_SHIFT);
int top = (rectOfSample.top>> DOWN_SAMPLE_SHIFT);
int right = left + (rectOfSample.width()>> DOWN_SAMPLE_SHIFT);
int bottom = top + (rectOfSample.height()>> DOWN_SAMPLE_SHIFT);
Rect srcRect = new Rect( left, top, right, bottom );
Rect identity= new Rect(0,0,c.getWidth(),c.getHeight());
c.drawBitmap(
sampleBitmap,
srcRect,
identity,
null
);
// c.drawLine(0L,0L,c.getWidth(),c.getHeight(),red);
}
}
// @Override
// protected Rect calculateCacheWindow(Rect viewportRect) {
// // Simplest implementation
// return viewportRect;
// }
private Rect calculatedCacheWindowRect = new Rect();
@Override
protected Rect calculateCacheWindow(Rect viewportRect) {
long bytesToUse = Runtime.getRuntime().maxMemory() * percent / 100;
Point size = getSceneSize();
int vw = viewportRect.width();
int vh = viewportRect.height();
// Calculate the max size of the margins to fit in our memory budget
int tw=0;
int th=0;
int mw = tw;
int mh = th;
while((vw+tw) * (vh+th) * BYTES_PER_PIXEL < bytesToUse){
mw = tw++;
mh = th++;
}
// Trim the margins if they're too big.
if (vw+mw > size.x) // viewport width + margin width > width of the image
mw = Math.max(0, size.x-vw);
if (vh+mh > size.y) // viewport height + margin height > height of the image
mh = Math.max(0, size.y-vh);
// Figure out the left & right based on the margin. We assume our viewportRect
// is <= our size. If that's not the case, then this logic breaks.
int left = viewportRect.left - (mw>>1);
int right = viewportRect.right + (mw>>1);
if (left<0){
right = right - left; // Add's the overage on the left side back to the right
left = 0;
}
if (right>size.x){
left = left - (right-size.x); // Adds overage on right side back to left
right = size.x;
}
// Figure out the top & bottom based on the margin. We assume our viewportRect
// is <= our size. If that's not the case, then this logic breaks.
int top = viewportRect.top - (mh>>1);
int bottom = viewportRect.bottom + (mh>>1);
if (top<0){
bottom = bottom - top; // Add's the overage on the top back to the bottom
top = 0;
}
if (bottom>size.y){
top = top - (bottom-size.y); // Adds overage on bottom back to top
bottom = size.y;
}
// Set the origin based on our new calculated values.
calculatedCacheWindowRect.set(left, top, right, bottom);
if (DEBUG) Log.d(TAG,"new cache.originRect = "+calculatedCacheWindowRect.toShortString()+" size="+size.toString());
return calculatedCacheWindowRect;
}
@Override
protected void fillCacheOutOfMemoryError(OutOfMemoryError error) {
if (percent>0)
percent -= 1;
Log.e(TAG,String.format("caught oom -- cache now at %d percent.",percent));
}
@Override
protected void drawComplete(Canvas canvas) {
// TODO Auto-generated method stub
}
}
public abstract class Scene {
private final String TAG = "Scene";
private final static int MINIMUM_PIXELS_IN_VIEW = 50;
/** The size of the Scene */
private Point size = new Point();
/** The viewport */
private final Viewport viewport = new Viewport();
/** The cache */
private final Cache cache = new Cache();
//region [gs]etSceneSize
/** Set the size of the scene */
public void setSceneSize(int width, int height){
size.set(width, height);
}
/** Returns a Point representing the size of the scene. Don't modify the returned Point! */
public Point getSceneSize(){
return size;
}
/** Set the passed-in point to the size of the scene */
public void getSceneSize(Point point){
point.set(size.x, size.y);
}
//endregion
//region getViewport()
public Viewport getViewport(){return viewport;}
//endregion
//region initialize/start/stop/suspend/invalidate the cache
/** Initializes the cache */
public void initialize(){
if (cache.getState()==CacheState.UNINITIALIZED){
synchronized(cache){
cache.setState(CacheState.INITIALIZED);
}
}
}
/** Starts the cache thread */
public void start(){
cache.start();
}
/** Stops the cache thread */
public void stop(){
cache.stop();
}
/**
* Suspends or unsuspends the cache thread. This can be
* used to temporarily stop the cache from updating
* during a fling event.
* @param suspend True to suspend the cache. False to unsuspend.
*/
public void setSuspend(boolean suspend){
if (suspend) {
synchronized(cache){
cache.setState(CacheState.SUSPEND);
}
} else {
if (cache.getState()==CacheState.SUSPEND) {
synchronized(cache){
cache.setState(CacheState.INITIALIZED);
}
}
}
}
/** Invalidate the cache. This causes it to refill */
@SuppressWarnings("unused")
public void invalidate(){
cache.invalidate();
}
//endregion
//region void draw(Canvas c)
/**
* Draw the scene to the canvas. This operation fills the canvas with
* the bitmap referenced by the viewport's location within the Scene.
* If the cache already has the data (and is not suspended), then the
* high resolution bitmap from the cache is used. If it's not available,
* then the lower resolution bitmap from the sample is used.
*/
public void draw(Canvas c){
viewport.draw(c);
}
//endregion
//region protected abstract
/**
* This method must return a high resolution Bitmap that the Scene
* will use to fill out the viewport bitmap upon request. This bitmap
* is normally larger than the viewport so that the viewport can be
* scrolled without having to refresh the cache. This method runs
* on a thread other than the UI thread, and it is not under a lock, so
* it is expected that this method can run for a long time (seconds?).
* @param rectOfCache The Rect representing the area of the Scene that
* the Scene wants cached.
* @return the Bitmap representing the requested area of the larger bitmap
*/
protected abstract Bitmap fillCache(Rect rectOfCache);
/**
* The memory allocation you just did in fillCache caused an OutOfMemoryError.
* You can attempt to recover. Experience shows that when we get an
* OutOfMemoryError, we're pretty hosed and are going down. For instance, if
* we're trying to decode a bitmap region with
* {@link android.graphics.BitmapRegionDecoder} and we run out of memory,
* we're going to die somewhere in the C code with a SIGSEGV.
* @param error The OutOfMemoryError exception data
*/
protected abstract void fillCacheOutOfMemoryError( OutOfMemoryError error );
/**
* Calculate the Rect of the cache's window based on the current viewportRect.
* The returned Rect must at least contain the viewportRect, but it can be
* larger if the system believes a bitmap of the returned size will fit into
* memory. This function must be fast as it happens while the cache lock is held.
* @param viewportRect The returned must be able to contain this Rect
* @return The Rect that will be used to fill the cache
*/
protected abstract Rect calculateCacheWindow(Rect viewportRect);
/**
* This method fills the passed-in bitmap with sample data. This function must
* return as fast as possible so it shouldn't have to do any IO at all -- the
* quality of the user experience rests on the speed of this function.
* @param bitmap The Bitmap to fill
* @param rectOfSample Rectangle within the Scene that this bitmap represents.
*/
protected abstract void drawSampleRectIntoBitmap(Bitmap bitmap, Rect rectOfSample);
/**
* The Cache is done drawing the bitmap -- time to add the finishing touches
* @param canvas a canvas on which to draw
*/
protected abstract void drawComplete(Canvas canvas);
//endregion
//region class Viewport
public class Viewport {
/** The bitmap of the current viewport */
Bitmap bitmap = null;
/** A Rect that defines where the Viewport is within the scene */
final Rect window = new Rect(0,0,0,0);
float zoom = 1.0f;
public void setOrigin(int x, int y){
synchronized(this){
int w = window.width();
int h = window.height();
// check bounds
if (x < 0)
x = 0;
if (y < 0)
y = 0;
if (x + w > size.x)
x = size.x - w;
if (y + h > size.y)
y = size.y - h;
window.set(x, y, x+w, y+h);
}
}
public void setSize( int w, int h ){
synchronized (this) {
if (bitmap !=null){
bitmap.recycle();
bitmap = null;
}
bitmap = Bitmap.createBitmap(w, h, Config.RGB_565);
window.set(
window.left,
window.top,
window.left + w,
window.top + h);
}
}
public void getOrigin(Point p){
synchronized (this) {
p.set(window.left, window.top);
}
}
public void getSize(Point p){
synchronized (this) {
p.x = window.width();
p.y = window.height();
}
}
public void getPhysicalSize(Point p){
synchronized (this){
p.x = getPhysicalWidth();
p.y = getPhysicalHeight();
}
}
public int getPhysicalWidth(){
return bitmap.getWidth();
}
public int getPhysicalHeight(){
return bitmap.getHeight();
}
public float getZoom(){
return zoom;
}
public void zoom(float factor, PointF screenFocus){
if (factor!=1.0){
PointF screenSize = new PointF(bitmap.getWidth(),bitmap.getHeight());
PointF sceneSize = new PointF(getSceneSize());
float screenWidthToHeight = screenSize.x / screenSize.y;
float screenHeightToWidth = screenSize.y / screenSize.x;
synchronized (this){
float newZoom = zoom * factor;
RectF w1 = new RectF(window);
RectF w2 = new RectF();
PointF sceneFocus = new PointF(
w1.left + (screenFocus.x/screenSize.x)*w1.width(),
w1.top + (screenFocus.y/screenSize.y)*w1.height()
);
float w2Width = getPhysicalWidth() * newZoom;
if (w2Width > sceneSize.x){
w2Width = sceneSize.x;
newZoom = w2Width / getPhysicalWidth();
}
if (w2Width < MINIMUM_PIXELS_IN_VIEW){
w2Width = MINIMUM_PIXELS_IN_VIEW;
newZoom = w2Width / getPhysicalWidth();
}
float w2Height = w2Width * screenHeightToWidth;
if (w2Height > sceneSize.y){
w2Height = sceneSize.y;
w2Width = w2Height * screenWidthToHeight;
newZoom = w2Width / getPhysicalWidth();
}
if (w2Height < MINIMUM_PIXELS_IN_VIEW){
w2Height = MINIMUM_PIXELS_IN_VIEW;
w2Width = w2Height * screenWidthToHeight;
newZoom = w2Width / getPhysicalWidth();
}
w2.left = sceneFocus.x - ((screenFocus.x/screenSize.x) * w2Width);
w2.top = sceneFocus.y - ((screenFocus.y/screenSize.y) * w2Height);
if (w2.left<0)
w2.left=0;
if (w2.top<0)
w2.top=0;
w2.right = w2.left+w2Width;
w2.bottom= w2.top+w2Height;
if (w2.right>sceneSize.x){
w2.right=sceneSize.x;
w2.left=w2.right-w2Width;
}
if (w2.bottom>sceneSize.y){
w2.bottom=sceneSize.y;
w2.top=w2.bottom-w2Height;
}
window.set((int)w2.left,(int)w2.top,(int)w2.right,(int)w2.bottom);
zoom = newZoom;
// Log.d(TAG,String.format(
// "f=%.2f, z=%.2f, scrf(%.0f,%.0f), scnf(%.0f,%.0f) w1s(%.0f,%.0f) w2s(%.0f,%.0f) w1(%.0f,%.0f,%.0f,%.0f) w2(%.0f,%.0f,%.0f,%.0f)",
// factor,
// zoom,
// screenFocus.x,
// screenFocus.y,
// sceneFocus.x,
// sceneFocus.y,
// w1.width(),w1.height(),
// w2Width, w2Height,
// w1.left,w1.top,w1.right,w1.bottom,
// w2.left,w2.top,w2.right,w2.bottom
// ));
}
}
}
void draw(Canvas c){
cache.update(this);
synchronized (this){
if (c!=null && bitmap !=null){
c.drawBitmap(bitmap, 0F, 0F, null);
drawComplete(c);
}
}
}
}
//endregion
//region class Cache
private enum CacheState {UNINITIALIZED,INITIALIZED,START_UPDATE,IN_UPDATE,READY,SUSPEND}
/**
* Keep track of the cached bitmap
*/
private class Cache {
/** A Rect that defines where the Cache is within the scene */
final Rect window = new Rect(0,0,0,0);
/** The bitmap of the current cache */
Bitmap bitmapRef = null;
CacheState state = CacheState.UNINITIALIZED;
void setState(CacheState newState){
if (Debug.isDebuggerConnected())
Log.i(TAG,String.format("cacheState old=%s new=%s",state.toString(),newState.toString()));
state = newState;
}
CacheState getState(){ return state; }
/** Our load from disk thread */
CacheThread cacheThread;
void start(){
if (cacheThread!=null){
cacheThread.setRunning(false);
cacheThread.interrupt();
cacheThread = null;
}
cacheThread = new CacheThread(this);
cacheThread.setName("cacheThread");
cacheThread.start();
}
void stop(){
cacheThread.running = false;
cacheThread.interrupt();
boolean retry = true;
while (retry) {
try {
cacheThread.join();
retry = false;
} catch (InterruptedException e) {
// we will try it again and again...
}
}
cacheThread = null;
}
void invalidate(){
synchronized(this){
setState(CacheState.INITIALIZED);
cacheThread.interrupt();
}
}
/** Fill the bitmap with the part of the scene referenced by the viewport Rect */
void update(Viewport viewport){
Bitmap bitmap = null; // If this is null at the bottom, then load from the sample
synchronized(this){
switch(getState()){
case UNINITIALIZED:
// nothing can be done -- should never get here
return;
case INITIALIZED:
// time to cache some data
setState(CacheState.START_UPDATE);
cacheThread.interrupt();
break;
case START_UPDATE:
// I already told the thread to start
break;
case IN_UPDATE:
// Already reading some data, just use the sample
break;
case SUSPEND:
// Loading from cache suspended.
break;
case READY:
// I have some data to show
if (bitmapRef==null){
// Start the cache off right
if (Debug.isDebuggerConnected())
Log.d(TAG,"bitmapRef is null");
setState(CacheState.START_UPDATE);
cacheThread.interrupt();
} else if (!window.contains(viewport.window)){
if (Debug.isDebuggerConnected())
Log.d(TAG,"viewport not in cache");
setState(CacheState.START_UPDATE);
cacheThread.interrupt();
} else {
// Happy case -- the cache already contains the Viewport
bitmap = bitmapRef;
}
break;
}
}
if (bitmap==null)
loadSampleIntoViewport();
else
loadBitmapIntoViewport(bitmap);
}
void loadBitmapIntoViewport(Bitmap bitmap){
if (bitmap!=null){
synchronized(viewport){
int left = viewport.window.left - window.left;
int top = viewport.window.top - window.top;
int right = left + viewport.window.width();
int bottom = top + viewport.window.height();
viewport.getPhysicalSize(dstSize);
srcRect.set( left, top, right, bottom );
dstRect.set(0, 0, dstSize.x, dstSize.y);
Canvas c = new Canvas(viewport.bitmap);
c.drawColor(Color.BLACK);
c.drawBitmap(
bitmap,
srcRect,
dstRect,
null);
// try {
// FileOutputStream fos = new FileOutputStream("/sdcard/viewport.png");
// viewport.bitmap.compress(Bitmap.CompressFormat.PNG, 99, fos);
// Thread.sleep(1000);
// } catch (Exception e){
// System.out.print(e.getMessage());
// }
}
}
}
final Rect srcRect = new Rect(0,0,0,0);
final Rect dstRect = new Rect(0,0,0,0);
final Point dstSize = new Point();
void loadSampleIntoViewport(){
if (getState()!=CacheState.UNINITIALIZED){
synchronized(viewport){
drawSampleRectIntoBitmap(
viewport.bitmap,
viewport.window
);
}
}
}
}
//endregion
//region class CacheThread
/**
* <p>The CacheThread's job is to wait until the {@link Cache#state} is
* {@link CacheState#START_UPDATE} and then update the {@link Cache} given
* the current {@link Viewport#window}. It does not want to hold the cache
* lock during the call to {@link Scene#fillCache(Rect)} because the call
* can take a long time. If we hold the lock, the user experience is very
* jumpy.</p>
* <p>The CacheThread and the {@link Cache} work hand in hand, both using the
* cache itself to synchronize on and using the {@link Cache#state}.
* The {@link Cache} is free to update any part of the cache object as long
* as it holds the lock. The CacheThread is careful to make sure that it is
* the {@link Cache#state} is {@link CacheState#IN_UPDATE} as it updates
* the {@link Cache}. It locks and unlocks the cache all along the way, but
* makes sure that the cache is not locked when it calls
* {@link Scene#fillCache(Rect)}.
*/
class CacheThread extends Thread {
final Cache cache;
boolean running = false;
void setRunning(boolean value){ running = value; }
CacheThread(Cache cache){ this.cache = cache; }
@Override
public void run() {
running=true;
Rect viewportRect = new Rect(0,0,0,0);
while(running){
while(running && cache.getState()!=CacheState.START_UPDATE)
try {
// Sleep until we have something to do
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException ignored) {}
if (!running)
return;
long start = System.currentTimeMillis();
boolean cont = false;
synchronized (cache) {
if (cache.getState()==CacheState.START_UPDATE){
cache.setState(CacheState.IN_UPDATE);
cache.bitmapRef = null;
cont = true;
}
}
if (cont){
synchronized(viewport){
viewportRect.set(viewport.window);
}
synchronized (cache) {
if (cache.getState()==CacheState.IN_UPDATE)
//cache.setWindowRect(viewportRect);
cache.window.set(calculateCacheWindow(viewportRect));
else
cont = false;
}
if (cont){
try{
Bitmap bitmap = fillCache(cache.window);
if (bitmap!=null){
synchronized (cache){
if (cache.getState()==CacheState.IN_UPDATE){
cache.bitmapRef = bitmap;
cache.setState(CacheState.READY);
} else {
Log.w(TAG,"fillCache operation aborted");
}
}
}
long done = System.currentTimeMillis();
if (Debug.isDebuggerConnected())
Log.d(TAG,String.format("fillCache in %dms",done-start));
} catch (OutOfMemoryError e){
Log.d(TAG,"CacheThread out of memory");
/*
* Attempt to recover. Experience shows that if we
* do get an OutOfMemoryError, we're pretty hosed and are going down.
*/
synchronized (cache){
fillCacheOutOfMemoryError(e);
if (cache.getState()==CacheState.IN_UPDATE){
cache.setState(CacheState.START_UPDATE);
}
}
}
}
}
}
}
}
//endregion
}