From 5d51d00bea3a696f9c0b7734225749b69fbc78ca Mon Sep 17 00:00:00 2001 From: Mike Dunn Date: Sun, 7 Aug 2016 17:59:32 -0500 Subject: [PATCH 1/7] using a single canvas --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- .../java/com/qozix/tileview/TileView.java | 18 +- .../qozix/tileview/detail/DetailLevel.java | 27 +- .../tileview/detail/DetailLevelManager.java | 15 + .../java/com/qozix/tileview/tiles/Tile.java | 158 ++++++-- .../qozix/tileview/tiles/TileCanvasView.java | 127 ------- .../tileview/tiles/TileCanvasViewGroup.java | 341 ++++++++++++------ .../tileview/tiles/TileRenderHandler.java | 2 +- .../tiles/TileRenderPoolExecutor.java | 38 +- .../tileview/tiles/TileRenderRunnable.java | 80 ++-- 11 files changed, 452 insertions(+), 360 deletions(-) mode change 100644 => 100755 tileview/src/main/java/com/qozix/tileview/tiles/Tile.java delete mode 100644 tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasView.java mode change 100644 => 100755 tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java mode change 100644 => 100755 tileview/src/main/java/com/qozix/tileview/tiles/TileRenderHandler.java mode change 100644 => 100755 tileview/src/main/java/com/qozix/tileview/tiles/TileRenderPoolExecutor.java mode change 100644 => 100755 tileview/src/main/java/com/qozix/tileview/tiles/TileRenderRunnable.java diff --git a/build.gradle b/build.gradle index be515a81..aff4f415 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.0' + classpath 'com.android.tools.build:gradle:2.1.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dc59ab80..9972ac36 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Jul 24 20:21:42 CDT 2016 +#Sun Aug 07 15:38:33 CDT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip diff --git a/tileview/src/main/java/com/qozix/tileview/TileView.java b/tileview/src/main/java/com/qozix/tileview/TileView.java index 71142586..566760c5 100644 --- a/tileview/src/main/java/com/qozix/tileview/TileView.java +++ b/tileview/src/main/java/com/qozix/tileview/TileView.java @@ -267,6 +267,13 @@ public void suppressRender() { mTileCanvasViewGroup.suppressRender(); } + /** + * Notify the TileView that it should resume tiles rendering. + */ + public void resumeRender() { + mTileCanvasViewGroup.resumeRender(); + } + /** * Sets a custom class to perform the getBitmap operation when tile bitmaps are requested for * tile images only. @@ -295,6 +302,7 @@ public void setTransitionsEnabled( boolean enabled ) { * * The default value is true. * + * @deprecated This value is no longer considered - bitmaps are always recycled when they're no longer used. * @param shouldRecycleBitmaps True if bitmaps should call Bitmap.recycle when they are removed from view. */ public void setShouldRecycleBitmaps( boolean shouldRecycleBitmaps ) { @@ -798,7 +806,7 @@ public void onScaleChanged( float scale, float previous ) { @Override public void onPanBegin( int x, int y, Origination origin ) { - suppressRender(); + } @Override @@ -813,8 +821,8 @@ public void onPanEnd( int x, int y, Origination origin ) { @Override public void onZoomBegin( float scale, Origination origin ) { - if( !mShouldUpdateDetailLevelWhileZooming ) { - mDetailLevelManager.lockDetailLevel(); + if ( origin == null ) { + mTileCanvasViewGroup.suppressRender(); } mDetailLevelManager.setScale( scale ); } @@ -826,7 +834,9 @@ public void onZoomUpdate( float scale, Origination origin ) { @Override public void onZoomEnd( float scale, Origination origin ) { - mDetailLevelManager.unlockDetailLevel(); + if ( origin == null ) { + mTileCanvasViewGroup.resumeRender(); + } mDetailLevelManager.setScale( scale ); requestRender(); } diff --git a/tileview/src/main/java/com/qozix/tileview/detail/DetailLevel.java b/tileview/src/main/java/com/qozix/tileview/detail/DetailLevel.java index edbdbe6e..8af26b66 100644 --- a/tileview/src/main/java/com/qozix/tileview/detail/DetailLevel.java +++ b/tileview/src/main/java/com/qozix/tileview/detail/DetailLevel.java @@ -19,6 +19,8 @@ public class DetailLevel implements Comparable { private StateSnapshot mLastStateSnapshot; + private Set mTilesVisibleInViewport = new HashSet<>(); + public DetailLevel( DetailLevelManager detailLevelManager, float scale, Object data, int tileWidth, int tileHeight ) { mDetailLevelManager = detailLevelManager; mScale = scale; @@ -27,6 +29,10 @@ public DetailLevel( DetailLevelManager detailLevelManager, float scale, Object d mTileHeight = tileHeight; } + public DetailLevelManager getDetailLevelManager() { + return mDetailLevelManager; + } + /** * Returns true if there has been a change, false otherwise. * @@ -62,20 +68,27 @@ public Set getVisibleTilesFromLastViewportComputation() { if( mLastStateSnapshot == null ) { throw new StateNotComputedException(); } - Set intersections = new HashSet<>(); + return mTilesVisibleInViewport; + } + + public boolean hasComputedState() { + return mLastStateSnapshot != null; + } + + public void computeVisibleTilesFromViewport() { + mTilesVisibleInViewport.clear(); for( int rowCurrent = mLastStateSnapshot.rowStart; rowCurrent < mLastStateSnapshot.rowEnd; rowCurrent++ ) { for( int columnCurrent = mLastStateSnapshot.columnStart; columnCurrent < mLastStateSnapshot.columnEnd; columnCurrent++ ) { Tile tile = new Tile( columnCurrent, rowCurrent, mTileWidth, mTileHeight, mData, this ); - intersections.add( tile ); + mTilesVisibleInViewport.add( tile ); } } - return intersections; } /** * Ensures that computeCurrentState will return true, indicating a change has occurred. */ - public void invalidate(){ + public void invalidate() { mLastStateSnapshot = null; } @@ -123,10 +136,10 @@ public int hashCode() { } public static class StateNotComputedException extends IllegalStateException { - public StateNotComputedException(){ - super("Grid has not been computed; " + + public StateNotComputedException() { + super( "Grid has not been computed; " + "you must call computeCurrentState at some point prior to calling " + - "getVisibleTilesFromLastViewportComputation."); + "getVisibleTilesFromLastViewportComputation." ); } } diff --git a/tileview/src/main/java/com/qozix/tileview/detail/DetailLevelManager.java b/tileview/src/main/java/com/qozix/tileview/detail/DetailLevelManager.java index 68fb8bbd..f7b9164c 100644 --- a/tileview/src/main/java/com/qozix/tileview/detail/DetailLevelManager.java +++ b/tileview/src/main/java/com/qozix/tileview/detail/DetailLevelManager.java @@ -26,6 +26,7 @@ public class DetailLevelManager { private Rect mViewport = new Rect(); private Rect mComputedViewport = new Rect(); + private Rect mComputedScaledViewport = new Rect(); private DetailLevel mCurrentDetailLevel; @@ -101,6 +102,16 @@ public Rect getComputedViewport() { return mComputedViewport; } + public Rect getComputedScaledViewport(float scale){ + mComputedScaledViewport.set( + (int) (mComputedViewport.left * scale), + (int) (mComputedViewport.top * scale), + (int) (mComputedViewport.right * scale), + (int) (mComputedViewport.bottom * scale) + ); + return mComputedScaledViewport; + } + /** * While the detail level is locked (after this method is invoked, and before unlockDetailLevel is invoked), * the DetailLevel will not change, and the current DetailLevel will be scaled beyond the normal @@ -118,6 +129,10 @@ public void unlockDetailLevel() { mDetailLevelLocked = false; } + public boolean getIsLocked() { + return mDetailLevelLocked; + } + public void resetDetailLevels() { mDetailLevelLinkedList.clear(); update(); diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java b/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java old mode 100644 new mode 100755 index 85c69663..4597e803 --- a/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java @@ -4,27 +4,50 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Rect; import android.view.animation.AnimationUtils; import com.qozix.tileview.detail.DetailLevel; +import com.qozix.tileview.geom.FloatMathHelper; import com.qozix.tileview.graphics.BitmapProvider; +import java.lang.ref.WeakReference; + public class Tile { + public enum State { + UNASSIGNED, + PENDING_DECODE, + DECODED + } + private static final int DEFAULT_TRANSITION_DURATION = 200; + private State mState = State.UNASSIGNED; + private int mWidth; private int mHeight; private int mLeft; private int mTop; + private int mRight; + private int mBottom; + + private float mProgress; private int mRow; private int mColumn; + private float mDetailLevelScale; + private Object mData; private Bitmap mBitmap; - public double renderTimestamp; + private Rect mIntrinsicRect = new Rect(); + private Rect mBaseRect = new Rect(); + private Rect mRelativeRect = new Rect(); + private Rect mScaledRect = new Rect(); + + public Long mRenderTimeStamp; private boolean mTransitionsEnabled; @@ -32,10 +55,10 @@ public class Tile { private Paint mPaint; - private TileCanvasView mParentTileCanvasView; - private DetailLevel mDetailLevel; + private WeakReference mTileRenderRunnableWeakReference; + public Tile( int column, int row, int width, int height, Object data, DetailLevel detailLevel ) { mRow = row; mColumn = column; @@ -43,8 +66,20 @@ public Tile( int column, int row, int width, int height, Object data, DetailLeve mHeight = height; mLeft = column * width; mTop = row * height; + mRight = mLeft + mWidth; + mBottom = mTop + mHeight; mData = data; mDetailLevel = detailLevel; + mDetailLevelScale = mDetailLevel.getScale(); + mIntrinsicRect.set( 0, 0, mWidth, mHeight ); + mBaseRect.set( mLeft, mTop, mRight, mBottom ); // TODO: need this? + mRelativeRect.set( + FloatMathHelper.unscale( mLeft, mDetailLevelScale ), + FloatMathHelper.unscale( mTop, mDetailLevelScale ), + FloatMathHelper.unscale( mRight, mDetailLevelScale ), + FloatMathHelper.unscale( mBottom, mDetailLevelScale ) + ); + mScaledRect.set( mRelativeRect ); } public int getWidth() { @@ -83,49 +118,88 @@ public boolean hasBitmap() { return mBitmap != null; } + public Rect getBaseRect() { + return mBaseRect; + } + + public Rect getRelativeRect() { + return mRelativeRect; + } + + public Rect getScaledRect( float scale ) { + mScaledRect.set( + (int) (mRelativeRect.left * scale), + (int) (mRelativeRect.top * scale), + (int) (mRelativeRect.right * scale), + (int) (mRelativeRect.bottom * scale) + ); + return mScaledRect; + } + public void setTransitionDuration( int transitionDuration ) { mTransitionDuration = transitionDuration; } - public void stampTime() { - renderTimestamp = AnimationUtils.currentAnimationTimeMillis(); + public State getState() { + return mState; } - public void setTransitionsEnabled( boolean enabled ) { - mTransitionsEnabled = enabled; + public void setState( State state ) { + mState = state; } - public DetailLevel getDetailLevel() { - return mDetailLevel; + public void execute( TileRenderPoolExecutor tileRenderPoolExecutor ) { + if(mState != State.UNASSIGNED){ + return; + } + mState = State.PENDING_DECODE; + TileRenderRunnable runnable = new TileRenderRunnable(); + mTileRenderRunnableWeakReference = new WeakReference<>( runnable ); + runnable.setTile( this ); + runnable.setTileRenderPoolExecutor( tileRenderPoolExecutor ); + tileRenderPoolExecutor.execute( runnable ); } - public float getRendered() { + public void computeProgress(){ if( !mTransitionsEnabled ) { - return 1; + return; + } + if( mRenderTimeStamp == null ) { + mRenderTimeStamp = AnimationUtils.currentAnimationTimeMillis(); + mProgress = 0; + return; } - double now = AnimationUtils.currentAnimationTimeMillis(); - double ellapsed = now - renderTimestamp; - float progress = (float) Math.min( 1, ellapsed / mTransitionDuration ); - if( progress == 1 ) { + double elapsed = AnimationUtils.currentAnimationTimeMillis() - mRenderTimeStamp; + mProgress = (float) Math.min( 1, elapsed / mTransitionDuration ); + if( mProgress == 1f ) { + mRenderTimeStamp = null; mTransitionsEnabled = false; } - return progress; + } + + public void setTransitionsEnabled( boolean enabled ) { + mTransitionsEnabled = enabled; + if( enabled ) { + mProgress = 0f; + } + } + + public DetailLevel getDetailLevel() { + return mDetailLevel; } public boolean getIsDirty() { - return mTransitionsEnabled && getRendered() < 1f; + return mTransitionsEnabled && mProgress < 1f; } public Paint getPaint() { if( !mTransitionsEnabled ) { - return null; + return mPaint = null; } if( mPaint == null ) { mPaint = new Paint(); } - float rendered = getRendered(); - int opacity = (int) (rendered * 255); - mPaint.setAlpha( opacity ); + mPaint.setAlpha( (int) (255 * mProgress) ); return mPaint; } @@ -134,35 +208,41 @@ void generateBitmap( Context context, BitmapProvider bitmapProvider ) { return; } mBitmap = bitmapProvider.getBitmap( this, context ); + mState = State.DECODED; } - void setParentTileCanvasView( TileCanvasView tileCanvasView ) { - mParentTileCanvasView = tileCanvasView; - } - - void destroy( boolean shouldRecycle ) { - destroy( shouldRecycle, true ); + /** + * Deprecated + * @param b + */ + void destroy( boolean b ) { + reset(); } - void destroy( boolean shouldRecycle, boolean shouldRemove ) { - if( shouldRecycle && mBitmap != null && !mBitmap.isRecycled() ) { + void reset() { + if( mState == State.PENDING_DECODE ) { + if ( mTileRenderRunnableWeakReference != null ) { + TileRenderRunnable runnable = mTileRenderRunnableWeakReference.get(); + if( runnable != null ) { + runnable.cancel( true ); + } + } + } + mState = State.UNASSIGNED; + mRenderTimeStamp = null; + if( mBitmap != null && !mBitmap.isRecycled() ) { mBitmap.recycle(); } mBitmap = null; - if( shouldRemove && mParentTileCanvasView != null ) { - mParentTileCanvasView.removeTile( this ); - } } /** * @param canvas The canvas the tile's bitmap should be drawn into - * @return True if the tile is dirty (drawing output has changed and needs parent validation) */ - boolean draw( Canvas canvas ) { + public void draw( Canvas canvas ) { if( mBitmap != null ) { - canvas.drawBitmap( mBitmap, mLeft, mTop, getPaint() ); + canvas.drawBitmap( mBitmap, mIntrinsicRect, mRelativeRect, getPaint() ); } - return getIsDirty(); } @Override @@ -176,7 +256,7 @@ public int hashCode() { @Override public boolean equals( Object o ) { - if( this == o ){ + if( this == o ) { return true; } if( o instanceof Tile ) { @@ -188,4 +268,8 @@ public boolean equals( Object o ) { return false; } + public String toShortString(){ + return mColumn + ":" + mRow; + } + } diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasView.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasView.java deleted file mode 100644 index f6d2fb58..00000000 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasView.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.qozix.tileview.tiles; - -import android.content.Context; -import android.graphics.Canvas; -import android.view.View; - -import java.util.HashSet; -import java.util.Set; - -public class TileCanvasView extends View { - - private float mScale = 1; - - private Set mTiles = new HashSet<>(); - - private TileCanvasDrawListener mTileCanvasDrawListener; - - private boolean mHasHadPendingUpdatesSinceLastCompleteDraw; - - public TileCanvasView( Context context ) { - super( context ); - } - - public Set getTiles(){ - return mTiles; - } - - public void setScale( float factor ) { - mScale = factor; - invalidate(); - } - - public float getScale() { - return mScale; - } - - public void setTileCanvasDrawListener( TileCanvasDrawListener tileCanvasDrawListener ) { - mTileCanvasDrawListener = tileCanvasDrawListener; - } - - public void addTile( Tile tile ) { - if( !mTiles.contains( tile ) ) { - mTiles.add( tile ); - tile.setParentTileCanvasView( this ); - invalidate(); - } - } - - public void removeTile( Tile tile ) { - if( mTiles.contains( tile ) ) { - mTiles.remove( tile ); - tile.setParentTileCanvasView( null ); - invalidate(); - } - } - - public void clearTiles( boolean shouldRecycle ) { - for( Tile tile : mTiles ) { - tile.destroy( shouldRecycle, false ); - } - mTiles.clear(); - invalidate(); - } - - /** - * Draw tile bitmaps into the surface canvas displayed by this View. - * @param canvas The Canvas instance to draw tile bitmaps into. - * @return True if there are incomplete tile transitions pending, false otherwise. - */ - private boolean drawTiles( Canvas canvas ) { - boolean pending = false; - for( Tile tile : mTiles ) { - pending = tile.draw( canvas ) || pending; - } - return pending; - } - - /** - * During a draw operation, if any tiles are transitioning in, the operation is considered pending, - * and another redraw is requested immediately (via invalidate). - * - * NOTE: the invalidate invocation in this method should not be necessary, since the - * TileCanvasViewGroup that contains this View should also be listening for onDrawPending, - * at which time it will call invalidate on itself. - * - * @param pending True if tile transitions states are not complete, and an immediate redraw is required. - */ - private void handleDrawState( boolean pending ) { - if( pending ) { - invalidate(); - mHasHadPendingUpdatesSinceLastCompleteDraw = true; - if( mTileCanvasDrawListener != null ) { - mTileCanvasDrawListener.onDrawPending( this ); - } - } else { - if( mHasHadPendingUpdatesSinceLastCompleteDraw ) { - mHasHadPendingUpdatesSinceLastCompleteDraw = false; - if( mTileCanvasDrawListener != null ) { - mTileCanvasDrawListener.onDrawComplete( this ); - } - } - } - } - - @Override - public void onDraw( Canvas canvas ) { - super.onDraw( canvas ); - canvas.save(); - canvas.scale( mScale, mScale ); - boolean pending = drawTiles( canvas ); - canvas.restore(); - handleDrawState( pending ); - } - - /** - * Interface definition for callbacks to be invoked when drawing is complete. - * A "pending" draw is one that indicates tile transitions states are still pending and - * and immediate redraw should be requested. - * A "complete" draw callback will only be invoked if transitions are enabled, and will occur - * the first time a "complete" draw is complete after a "pending" draw. - */ - public interface TileCanvasDrawListener { - void onDrawComplete( TileCanvasView tileCanvasView ); - void onDrawPending( TileCanvasView tileCanvasView ); - } - -} diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java old mode 100644 new mode 100755 index 4adf68f5..9c576b21 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java @@ -1,21 +1,25 @@ package com.qozix.tileview.tiles; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Region; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.util.Log; +import android.view.View; import com.qozix.tileview.detail.DetailLevel; import com.qozix.tileview.graphics.BitmapProvider; import com.qozix.tileview.graphics.BitmapProviderAssets; -import com.qozix.tileview.widgets.ScalingLayout; import java.lang.ref.WeakReference; -import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.Set; -public class TileCanvasViewGroup extends ScalingLayout implements TileCanvasView.TileCanvasDrawListener { +public class TileCanvasViewGroup extends View { private static final int RENDER_FLAG = 1; @@ -24,13 +28,13 @@ public class TileCanvasViewGroup extends ScalingLayout implements TileCanvasView private static final int DEFAULT_TRANSITION_DURATION = 200; + private float mScale = 1; + private BitmapProvider mBitmapProvider; - private HashMap mTileCanvasViewHashMap = new HashMap<>(); private DetailLevel mDetailLevelToRender; - private DetailLevel mLastRequestedDetailLevel; private DetailLevel mLastRenderedDetailLevel; - private TileCanvasView mCurrentTileCanvasView; + private boolean mRenderIsCancelled = false; private boolean mRenderIsSuppressed = false; @@ -43,14 +47,19 @@ public class TileCanvasViewGroup extends ScalingLayout implements TileCanvasView private TileRenderThrottleHandler mTileRenderThrottleHandler; private TileRenderListener mTileRenderListener; + private TileRenderThrowableListener mTileRenderThrowableListener; private int mRenderBuffer = DEFAULT_RENDER_BUFFER; private TileRenderPoolExecutor mTileRenderPoolExecutor; private Set mTilesInCurrentViewport = new HashSet<>(); - private Set mTilesNotInCurrentViewport = new HashSet<>(); - private Set mTilesAlreadyRendered = new HashSet<>(); + private Set mPreviouslyDrawnTiles = new HashSet<>(); + private Set mDecodedTilesInCurrentViewport = new HashSet<>(); + + private Region mDirtyRegion = new Region(); + + private boolean mHasInvalidatedOnCleanOnce; public TileCanvasViewGroup( Context context ) { super( context ); @@ -59,6 +68,19 @@ public TileCanvasViewGroup( Context context ) { mTileRenderPoolExecutor = new TileRenderPoolExecutor(); } + public void setScale( float factor ) { + mScale = factor; + invalidate(); + } + + public float getScale() { + return mScale; + } + + public float getInvertedScale() { + return 1f / mScale; + } + public boolean getTransitionsEnabled() { return mTransitionsEnabled; } @@ -75,7 +97,7 @@ public void setTransitionDuration( int duration ) { mTransitionDuration = duration; } - public BitmapProvider getBitmapProvider(){ + public BitmapProvider getBitmapProvider() { if( mBitmapProvider == null ) { mBitmapProvider = new BitmapProviderAssets(); } @@ -98,14 +120,25 @@ public void setRenderBuffer( int renderBuffer ) { mRenderBuffer = renderBuffer; } + /** + * @return True if tile bitmaps should be recycled. + * @deprecated This value is no longer considered - bitmaps are always recycled when they're no longer used. + */ public boolean getShouldRecycleBitmaps() { return mShouldRecycleBitmaps; } + /** + * @param shouldRecycleBitmaps True if tile bitmaps should be recycled. + * @deprecated This value is no longer considered - bitmaps are always recycled when they're no longer used. + */ public void setShouldRecycleBitmaps( boolean shouldRecycleBitmaps ) { mShouldRecycleBitmaps = shouldRecycleBitmaps; } + public void setTileRenderThrowableListener( TileRenderThrowableListener tileRenderThrowableListener ) { + mTileRenderThrowableListener = tileRenderThrowableListener; + } /** * The layout dimensions supplied to this ViewGroup will be exactly as large as the scaled @@ -116,7 +149,6 @@ public void setShouldRecycleBitmaps( boolean shouldRecycleBitmaps ) { public void requestRender() { mRenderIsCancelled = false; - mRenderIsSuppressed = false; if( mDetailLevelToRender == null ) { return; } @@ -131,7 +163,7 @@ public void requestRender() { */ public void cancelRender() { mRenderIsCancelled = true; - if( mTileRenderPoolExecutor != null ){ + if( mTileRenderPoolExecutor != null ) { mTileRenderPoolExecutor.cancel(); } } @@ -143,103 +175,207 @@ public void suppressRender() { mRenderIsSuppressed = true; } - public void updateTileSet( DetailLevel detailLevel ) { - mDetailLevelToRender = detailLevel; - if( mDetailLevelToRender == null ) { - return; - } - if( mDetailLevelToRender.equals( mLastRequestedDetailLevel ) ) { - return; - } - mLastRequestedDetailLevel = mDetailLevelToRender; - mCurrentTileCanvasView = getCurrentTileCanvasView(); - mCurrentTileCanvasView.bringToFront(); - cancelRender(); - requestRender(); + /** + * Enables new render tasks to start. + */ + public void resumeRender() { + mRenderIsSuppressed = false; } + /** + * Returns true if the TileView has threads currently decoding tile Bitmaps. + * + * @return True if the TileView has threads currently decoding tile Bitmaps. + */ public boolean getIsRendering() { return mIsRendering; } + /** + * Clears existing tiles and cancels any existing render tasks. + */ public void clear() { suppressRender(); cancelRender(); mTilesInCurrentViewport.clear(); - mCurrentTileCanvasView.clearTiles( mShouldRecycleBitmaps ); + invalidate(); } - /** - * Effectively adds any new tiles, without replacing existing tiles, and removes those not in passed set. - * @param recentlyComputedVisibleTileSet Tile Set that should be visible, based on DetailLevel inspection of viewport size and position. - */ - public void reconcile( Set recentlyComputedVisibleTileSet ){ + void renderTiles() { + if( !mRenderIsCancelled && !mRenderIsSuppressed && mDetailLevelToRender != null ) { + beginRenderTask(); + } + } + + private Rect getComputedViewport() { + if( mDetailLevelToRender == null ) { + return null; + } + return mDetailLevelToRender.getDetailLevelManager().getComputedScaledViewport( getInvertedScale() ); + } + + private boolean establishOpaqueRegion() { + boolean shouldInvalidate = false; + mDirtyRegion.set( getComputedViewport() ); for( Tile tile : mTilesInCurrentViewport ) { - if( !recentlyComputedVisibleTileSet.contains( tile ) ) { - mTilesNotInCurrentViewport.add( tile ); + if( tile.getState() == Tile.State.DECODED ) { + tile.computeProgress(); + mDecodedTilesInCurrentViewport.add( tile ); + if( tile.getIsDirty() ) { + shouldInvalidate = true; + } else { + mDirtyRegion.op( tile.getRelativeRect(), Region.Op.DIFFERENCE ); + } } } - mTilesInCurrentViewport.addAll( recentlyComputedVisibleTileSet ); - mTilesInCurrentViewport.removeAll( mTilesNotInCurrentViewport ); - mTilesNotInCurrentViewport.clear(); + return shouldInvalidate; + } + + private boolean drawPreviousTiles( Canvas canvas ) { + boolean shouldInvalidate = false; + Iterator tilesFromLastDetailLevelIterator = mPreviouslyDrawnTiles.iterator(); + while( tilesFromLastDetailLevelIterator.hasNext() ) { + Tile tile = tilesFromLastDetailLevelIterator.next(); + Rect rect = tile.getRelativeRect(); + if( mDirtyRegion.quickReject( rect ) ) { + tilesFromLastDetailLevelIterator.remove(); + } else { + tile.computeProgress(); + tile.draw( canvas ); + shouldInvalidate |= tile.getIsDirty(); + } + } + return shouldInvalidate; } - private float getCurrentDetailLevelScale() { - if( mDetailLevelToRender != null ) { - return mDetailLevelToRender.getScale(); + private boolean drawAndClearCurrentDecodedTiles( Canvas canvas ) { + boolean shouldInvalidate = false; + for( Tile tile : mDecodedTilesInCurrentViewport ) { + // these tiles should already have progress computed by the time they get here + tile.draw( canvas ); + shouldInvalidate |= tile.getIsDirty(); } - return 1; + mDecodedTilesInCurrentViewport.clear(); + return shouldInvalidate; } - private TileCanvasView getCurrentTileCanvasView() { - float levelScale = getCurrentDetailLevelScale(); - if( mTileCanvasViewHashMap.containsKey( levelScale ) ) { - return mTileCanvasViewHashMap.get( levelScale ); + private void handleInvalidation( boolean shouldInvalidate ) { + if( shouldInvalidate ) { + // there's more work to do, partially opaque tiles were drawn + mHasInvalidatedOnCleanOnce = false; + invalidate(); + } else { + // if all tiles were fully opaque, we need another pass to clear our tiles from last level + if( !mHasInvalidatedOnCleanOnce ) { + mHasInvalidatedOnCleanOnce = true; + invalidate(); + } } - TileCanvasView tileGroup = new TileCanvasView( getContext() ); - tileGroup.setTileCanvasDrawListener( this ); - tileGroup.setScale( 1 / levelScale ); - mTileCanvasViewHashMap.put( levelScale, tileGroup ); - addView( tileGroup ); - return tileGroup; } - void renderTiles() { - if( !mRenderIsCancelled && !mRenderIsSuppressed && mDetailLevelToRender != null ) { - beginRenderTask(); + private void drawTilesWithoutConsideringPreviouslyDrawnLevel( Canvas canvas ) { + boolean shouldInvalidate = false; + for( Tile tile : mTilesInCurrentViewport ) { + if( tile.getState() == Tile.State.DECODED ) { + tile.computeProgress(); + tile.draw( canvas ); + shouldInvalidate |= tile.getIsDirty(); + } + } + handleInvalidation( shouldInvalidate ); + } + + private void drawTilesConsideringPreviouslyDrawnLevel( Canvas canvas ) { + // compute states, populate opaque region + boolean shouldInvalidate = establishOpaqueRegion(); + // draw any previous tiles that are in viewport and not under full opaque current tiles + shouldInvalidate |= drawPreviousTiles( canvas ); + // draw the current tile set + shouldInvalidate |= drawAndClearCurrentDecodedTiles( canvas ); + // depending on transition states and previous tile draw ops, add'l invalidation might be needed + handleInvalidation( shouldInvalidate ); + } + + /** + * Draw tile bitmaps into the surface canvas displayed by this View. + * + * @param canvas The Canvas instance to draw tile bitmaps into. + */ + private void drawTiles( Canvas canvas ) { + if( mPreviouslyDrawnTiles.size() > 0 ) { + drawTilesConsideringPreviouslyDrawnLevel( canvas ); + } else { + drawTilesWithoutConsideringPreviouslyDrawnLevel( canvas ); } + Log.d( getClass().getSimpleName(), "prevous tile count: " + mPreviouslyDrawnTiles.size() ); + } + + public void updateTileSet( DetailLevel detailLevel ) { + if( detailLevel == null ) { + return; + } + if( detailLevel.equals( mDetailLevelToRender ) ) { + return; + } + cancelRender(); + markTilesAsPrevious(); + mDetailLevelToRender = detailLevel; + requestRender(); + } + + private void markTilesAsPrevious() { + for( Tile tile : mTilesInCurrentViewport ) { + if( tile.getState() == Tile.State.DECODED ) { + mPreviouslyDrawnTiles.add( tile ); + } + } + mTilesInCurrentViewport.clear(); } private void beginRenderTask() { + // if visible columns and rows are same as previously computed, fast-fail boolean changed = mDetailLevelToRender.computeCurrentState(); if( !changed && mDetailLevelToRender.equals( mLastRenderedDetailLevel ) ) { return; } - Set visibleTiles = mDetailLevelToRender.getVisibleTilesFromLastViewportComputation(); - reconcile( visibleTiles ); - if( mTileRenderPoolExecutor != null ){ - mTileRenderPoolExecutor.queue( this, getRenderSet() ); + // determine tiles are mathematically within the current viewport; force re-computation + mDetailLevelToRender.computeVisibleTilesFromViewport(); + // get rid of anything outside, use previously computed intersections + cleanup(); + // are there any new tiles the Executor isn't already aware of? + boolean wereTilesAdded = mTilesInCurrentViewport.addAll( mDetailLevelToRender.getVisibleTilesFromLastViewportComputation() ); + // if so, start up a new batch + if( wereTilesAdded ) { + mTileRenderPoolExecutor.queue( this, mTilesInCurrentViewport ); } } - private void clearOutOfViewportTiles(){ - Set condemned = new HashSet<>( mTilesAlreadyRendered ); - condemned.removeAll( mTilesInCurrentViewport ); - mTilesAlreadyRendered.removeAll( condemned ); - for( Tile tile : condemned ) { - tile.destroy( mShouldRecycleBitmaps ); + /** + * This should seldom be necessary, as it's built into beginRenderTask + */ + public void cleanup() { + if( mDetailLevelToRender == null || !mDetailLevelToRender.hasComputedState() ) { + return; + } + // these tiles are mathematically within the current viewport, and should be already computed + Set recentlyComputedVisibleTileSet = mDetailLevelToRender.getVisibleTilesFromLastViewportComputation(); + // use an iterator to avoid concurrent modification + Iterator tilesInCurrentViewportIterator = mTilesInCurrentViewport.iterator(); + while( tilesInCurrentViewportIterator.hasNext() ) { + Tile tile = tilesInCurrentViewportIterator.next(); + // this tile was visible previously, but is no longer, destroy and de-list it + if( !recentlyComputedVisibleTileSet.contains( tile ) ) { + tile.reset(); + tilesInCurrentViewportIterator.remove(); + } } - mCurrentTileCanvasView.invalidate(); } - private void cleanup() { - clearOutOfViewportTiles(); - for( TileCanvasView tileGroup : mTileCanvasViewHashMap.values() ) { - if( mCurrentTileCanvasView != tileGroup ) { - tileGroup.clearTiles( mShouldRecycleBitmaps ); - } + // this tile has been decoded by the time it gets passed here + void addTileToCanvas( final Tile tile ) { + if( mTilesInCurrentViewport.contains( tile ) ) { + invalidate(); } - invalidate(); } void onRenderTaskPreExecute() { @@ -261,30 +397,9 @@ void onRenderTaskPostExecute() { mTileRenderThrottleHandler.post( mRenderPostExecuteRunnable ); } - Set getRenderSet() { - Set renderSet = new HashSet<>( mTilesInCurrentViewport ); - renderSet.removeAll( mTilesAlreadyRendered ); - return renderSet; - } - - void generateTileBitmap( Tile tile ) { - tile.generateBitmap( getContext(), getBitmapProvider() ); - } - - void addTileToCurrentTileCanvasView( final Tile tile ) { - if( !mTilesInCurrentViewport.contains( tile ) ) { - return; - } - tile.setTransitionsEnabled( mTransitionsEnabled ); - tile.setTransitionDuration( mTransitionDuration ); - tile.stampTime(); - mTilesAlreadyRendered.add( tile ); - mCurrentTileCanvasView.addTile( tile ); - } - void handleTileRenderException( Throwable throwable ) { - if( throwable instanceof OutOfMemoryError ){ - cleanup(); + if( mTileRenderThrowableListener != null ) { + mTileRenderThrowableListener.onRenderThrow( throwable ); } } @@ -292,30 +407,23 @@ boolean getRenderIsCancelled() { return mRenderIsCancelled; } - @Override - public void onDrawComplete( TileCanvasView tileCanvasView ) { - if( mTransitionsEnabled && tileCanvasView == mCurrentTileCanvasView ) { - cleanup(); - } - } - - @Override - public void onDrawPending( TileCanvasView tileCanvasView ) { - invalidate(); - } - - public void destroy(){ + public void destroy() { mTileRenderPoolExecutor.shutdownNow(); clear(); - for( TileCanvasView tileGroup : mTileCanvasViewHashMap.values() ) { - tileGroup.clearTiles( mShouldRecycleBitmaps ); - } - mTileCanvasViewHashMap.clear(); if( !mTileRenderThrottleHandler.hasMessages( RENDER_FLAG ) ) { mTileRenderThrottleHandler.removeMessages( RENDER_FLAG ); } } + @Override + public void onDraw( Canvas canvas ) { + super.onDraw( canvas ); + canvas.save(); + canvas.scale( mScale, mScale ); + drawTiles( canvas ); + canvas.restore(); + } + private static class TileRenderThrottleHandler extends Handler { private final WeakReference mTileCanvasViewGroupWeakReference; @@ -343,19 +451,22 @@ public interface TileRenderListener { void onRenderComplete(); } + // ideally this would be part of TileRenderListener, but that's a breaking change + public interface TileRenderThrowableListener { + void onRenderThrow( Throwable throwable ); + } + // This runnable is required to run on UI thread - private Runnable mRenderPostExecuteRunnable = new Runnable() { + private Runnable mRenderPostExecuteRunnable = new Runnable() { @Override public void run() { - if ( !mTransitionsEnabled ) { - cleanup(); - } + cleanup(); if( mTileRenderListener != null ) { mTileRenderListener.onRenderComplete(); } mLastRenderedDetailLevel = mDetailLevelToRender; - invalidate(); requestRender(); } }; + } diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderHandler.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderHandler.java old mode 100644 new mode 100755 index eb170ede..c71b5623 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderHandler.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderHandler.java @@ -70,7 +70,7 @@ public void handleMessage( Message message ) { tileCanvasViewGroup.handleTileRenderException( tileRenderRunnable.getThrowable() ); break; case RENDER_COMPLETE: - tileCanvasViewGroup.addTileToCurrentTileCanvasView( tile ); + tileCanvasViewGroup.addTileToCanvas( tile ); break; } } diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderPoolExecutor.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderPoolExecutor.java old mode 100644 new mode 100755 index 6ac5d5a1..84a3f6a3 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderPoolExecutor.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderPoolExecutor.java @@ -1,8 +1,6 @@ package com.qozix.tileview.tiles; -import android.content.Context; - -import com.qozix.tileview.graphics.BitmapProvider; +import android.os.Handler; import java.lang.ref.WeakReference; import java.util.Set; @@ -36,8 +34,6 @@ public TileRenderPoolExecutor() { public void queue( TileCanvasViewGroup tileCanvasViewGroup, Set renderSet ) { mTileCanvasViewGroupWeakReference = new WeakReference<>( tileCanvasViewGroup ); mHandler.setTileCanvasViewGroup( tileCanvasViewGroup ); - final Context context = tileCanvasViewGroup.getContext(); - final BitmapProvider bitmapProvider = tileCanvasViewGroup.getBitmapProvider(); tileCanvasViewGroup.onRenderTaskPreExecute(); for( Runnable runnable : getQueue() ) { if( runnable instanceof TileRenderRunnable ) { @@ -46,14 +42,8 @@ public void queue( TileCanvasViewGroup tileCanvasViewGroup, Set renderSet continue; } Tile tile = tileRenderRunnable.getTile(); - if( tile == null ) { - continue; - } - if( renderSet.contains( tile ) ) { - renderSet.remove( tile ); - } else { - tileRenderRunnable.cancel( true ); - remove( tileRenderRunnable ); + if( tile != null && !renderSet.contains( tile ) ) { + tile.reset(); } } } @@ -61,13 +51,19 @@ public void queue( TileCanvasViewGroup tileCanvasViewGroup, Set renderSet if( isShutdownOrTerminating() ) { return; } - TileRenderRunnable runnable = new TileRenderRunnable(); - runnable.setTile( tile ); - runnable.setContext( context ); - runnable.setBitmapProvider( bitmapProvider ); - runnable.setHandler( mHandler ); - execute( runnable ); + tile.execute( this ); + } + } + + public Handler getHandler(){ + return mHandler; + } + + public TileCanvasViewGroup getTileCanvasViewGroup(){ + if( mTileCanvasViewGroupWeakReference == null ) { + return null; } + return mTileCanvasViewGroupWeakReference.get(); } private void broadcastCancel() { @@ -84,6 +80,10 @@ public void cancel() { if( runnable instanceof TileRenderRunnable ) { TileRenderRunnable tileRenderRunnable = (TileRenderRunnable) runnable; tileRenderRunnable.cancel( true ); + Tile tile = tileRenderRunnable.getTile(); + if( tile != null ) { + tile.reset(); // TODO: + } } } getQueue().clear(); diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderRunnable.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderRunnable.java old mode 100644 new mode 100755 index 30a19d3d..92a629e3 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderRunnable.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderRunnable.java @@ -1,12 +1,9 @@ package com.qozix.tileview.tiles; -import android.content.Context; import android.os.Handler; import android.os.Message; import android.os.Process; -import com.qozix.tileview.graphics.BitmapProvider; - import java.lang.ref.WeakReference; /** @@ -15,9 +12,7 @@ class TileRenderRunnable implements Runnable { private WeakReference mTileWeakReference; - private WeakReference mHandlerWeakReference; - private WeakReference mContextWeakReference; - private WeakReference mBitmapProviderWeakReference; + private WeakReference mTileRenderPoolExecutorWeakReference; private boolean mCancelled = false; private boolean mComplete = false; @@ -32,6 +27,12 @@ public boolean cancel( boolean mayInterrupt ) { } boolean cancelled = mCancelled; mCancelled = true; + if( mTileRenderPoolExecutorWeakReference != null ) { + TileRenderPoolExecutor tileRenderPoolExecutor = mTileRenderPoolExecutorWeakReference.get(); + if(tileRenderPoolExecutor != null){ + tileRenderPoolExecutor.remove( this ); + } + } return !cancelled; } @@ -43,37 +44,8 @@ public boolean isDone() { return mComplete; } - public void setHandler( Handler handler ) { - mHandlerWeakReference = new WeakReference<>( handler ); - } - - public Handler getHandler() { - if( mHandlerWeakReference == null ) { - return null; - } - return mHandlerWeakReference.get(); - } - - public void setContext( Context context ) { - mContextWeakReference = new WeakReference<>( context ); - } - - public void setBitmapProvider( BitmapProvider bitmapProvider ) { - mBitmapProviderWeakReference = new WeakReference<>( bitmapProvider ); - } - - public Context getContext() { - if( mContextWeakReference == null ) { - return null; - } - return mContextWeakReference.get(); - } - - public BitmapProvider getBitmapProvider() { - if( mBitmapProviderWeakReference == null ) { - return null; - } - return mBitmapProviderWeakReference.get(); + public void setTileRenderPoolExecutor(TileRenderPoolExecutor tileRenderPoolExecutor ) { + mTileRenderPoolExecutorWeakReference = new WeakReference<>(tileRenderPoolExecutor); } public void setTile( Tile tile ) { @@ -103,22 +75,22 @@ public TileRenderHandler.Status renderTile() { if( tile == null ) { return TileRenderHandler.Status.INCOMPLETE; } - Context context = getContext(); - if( context == null ) { + TileRenderPoolExecutor tileRenderPoolExecutor = mTileRenderPoolExecutorWeakReference.get(); + if( tileRenderPoolExecutor == null ) { return TileRenderHandler.Status.INCOMPLETE; } - BitmapProvider bitmapProvider = getBitmapProvider(); - if( bitmapProvider == null ) { + TileCanvasViewGroup tileCanvasViewGroup = tileRenderPoolExecutor.getTileCanvasViewGroup(); + if(tileCanvasViewGroup == null ) { return TileRenderHandler.Status.INCOMPLETE; } try { - tile.generateBitmap( context, bitmapProvider ); + tile.generateBitmap( tileCanvasViewGroup.getContext(), tileCanvasViewGroup.getBitmapProvider() ); } catch( Throwable throwable ) { mThrowable = throwable; return TileRenderHandler.Status.ERROR; } if( mCancelled || tile.getBitmap() == null || mThread.isInterrupted() ) { - tile.destroy( true ); + tile.reset(); return TileRenderHandler.Status.INCOMPLETE; } return TileRenderHandler.Status.COMPLETE; @@ -134,10 +106,24 @@ public void run() { if( status == TileRenderHandler.Status.COMPLETE ) { mComplete = true; } - Handler handler = getHandler(); - if( handler != null ) { - Message message = handler.obtainMessage( status.getMessageCode(), this ); - message.sendToTarget(); + TileRenderPoolExecutor tileRenderPoolExecutor = mTileRenderPoolExecutorWeakReference.get(); + if( tileRenderPoolExecutor != null ) { + TileCanvasViewGroup tileCanvasViewGroup = tileRenderPoolExecutor.getTileCanvasViewGroup(); + if( tileCanvasViewGroup != null ) { + Tile tile = getTile(); + if( tile != null ) { + Handler handler = tileRenderPoolExecutor.getHandler(); + if( handler != null ) { + // need to stamp time now, since it'll be drawn before the handler posts + tile.setTransitionsEnabled( tileCanvasViewGroup.getTransitionsEnabled() ); + tile.setTransitionDuration( tileCanvasViewGroup.getTransitionDuration() ); + Message message = handler.obtainMessage( status.getMessageCode(), this ); + message.sendToTarget(); + } + } + } + } } + } From d107c768d3245261ba8e9db545214abf5f3f02b5 Mon Sep 17 00:00:00 2001 From: Mike Dunn Date: Sun, 7 Aug 2016 18:17:16 -0500 Subject: [PATCH 2/7] add deprecated methods to prevent breaking changes --- .../java/com/qozix/tileview/tiles/Tile.java | 15 +++++++++++++ .../tileview/tiles/TileCanvasViewGroup.java | 22 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java b/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java index 4597e803..d33ec50c 100755 --- a/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java @@ -126,6 +126,21 @@ public Rect getRelativeRect() { return mRelativeRect; } + /** + * @deprecated + * @return + */ + public float getRendered() { + return mProgress; + } + + /** + * @deprecated + */ + public void stampTime() { + // no op + } + public Rect getScaledRect( float scale ) { mScaledRect.set( (int) (mRelativeRect.left * scale), diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java index 9c576b21..1e828dbb 100755 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java @@ -8,7 +8,7 @@ import android.os.Looper; import android.os.Message; import android.util.Log; -import android.view.View; +import android.view.ViewGroup; import com.qozix.tileview.detail.DetailLevel; import com.qozix.tileview.graphics.BitmapProvider; @@ -19,7 +19,11 @@ import java.util.Iterator; import java.util.Set; -public class TileCanvasViewGroup extends View { +/** + * This class extends ViewGroup for legacy reasons, and may be changed to extend View at + * some future point; consider all ViewGroup methods deprecated. + */ +public class TileCanvasViewGroup extends ViewGroup { private static final int RENDER_FLAG = 1; @@ -68,6 +72,11 @@ public TileCanvasViewGroup( Context context ) { mTileRenderPoolExecutor = new TileRenderPoolExecutor(); } + @Override + protected void onLayout( boolean changed, int l, int t, int r, int b ) { + + } + public void setScale( float factor ) { mScale = factor; invalidate(); @@ -201,6 +210,15 @@ public void clear() { invalidate(); } + /** + * This function is now a no-op + * @deprecated + * @param recentlyComputedVisibleTileSet + */ + public void reconcile( Set recentlyComputedVisibleTileSet ) { + // noop + } + void renderTiles() { if( !mRenderIsCancelled && !mRenderIsSuppressed && mDetailLevelToRender != null ) { beginRenderTask(); From 2fe07d86ddca24ab1cc5f0dcacd62655c3649316 Mon Sep 17 00:00:00 2001 From: Mike Dunn Date: Sun, 7 Aug 2016 18:30:03 -0500 Subject: [PATCH 3/7] bumping version number, updating readme --- README.md | 6 ++++++ tileview/build.gradle | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9eac05e4..48bda92b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![Release Badge](https://img.shields.io/github/release/moagrius/TileView.svg) +_**08/07/16** 2.2 is released, and provides some much-needed improvements in how tiles are rendered - please consider upgrading, but be aware there are some minor potential breaking changes (that should not affect 99% of users). + _**03/18/16** if you're using a version earlier than 2.1, there were significant performance gains realized with 2.1 so we'd advise you to start using the most recent version (2.1 or later) immediately. The improvements made also make fast-render viable, especially if not fetching tile data from across a network, so we'd also encourage you to try `TileView.setShouldRenderWhilePanning(true);` if you'd like more responsive tile rendering._ #Version 2.0 @@ -24,6 +26,7 @@ Major goals were: (Only major and minor changes are tracked here, consult git history for patches) **2.1** Rewrite of threading strategy, thanks to @peterLaurence and @bnsantos. Tile render performance is substantially improved. +**2.2** Rewrite of tile rendering strategy, again with the help of @peterLaurence. Peak memory consumption should be reduceds, and Tile render performance should be improved. #TileView The TileView widget is a subclass of ViewGroup that provides a mechanism to asynchronously display tile-based images, with additional functionality for 2D dragging, flinging, pinch or double-tap to zoom, adding overlaying Views (markers), built-in Hot Spot support, dynamic path drawing, multiple levels of detail, and support for any relative positioning or coordinate system. @@ -298,3 +301,6 @@ tileView.addView( downSample, 0 ); ###Contributing See [here](https://github.com/moagrius/TileView/wiki/Contributing). + +###Contributors +Several members of the github community have contributed and made `TileView` better, but over the last year or so, @peterLaurence has been as involved as myself and been integral in the last few major updates. Thanks Peter. diff --git a/tileview/build.gradle b/tileview/build.gradle index 531e4fe1..072b312f 100644 --- a/tileview/build.gradle +++ b/tileview/build.gradle @@ -7,7 +7,7 @@ android { minSdkVersion 11 targetSdkVersion 22 versionCode 32 - versionName "2.1.8" + versionName "2.2" } buildTypes { release { From 6e1b7c073f519b853345bbc11af7ee6e970cf449 Mon Sep 17 00:00:00 2001 From: Mike Dunn Date: Tue, 9 Aug 2016 17:56:21 -0500 Subject: [PATCH 4/7] updating demo, wip --- .../demo/BuildingPlansTileViewActivity.java | 6 + .../demo/FictionalMapTileViewActivity.java | 3 + .../demo/LargeImageTileViewActivity.java | 6 +- .../demo/RealMapInternetTileViewActivity.java | 146 +++++++++++++++++- .../demo/RealMapTileViewActivity.java | 31 +--- .../demo/provider/BitmapProviderPicasso.java | 6 +- .../tileview/tiles/TileCanvasViewGroup.java | 9 +- 7 files changed, 163 insertions(+), 44 deletions(-) diff --git a/demo/src/main/java/tileview/demo/BuildingPlansTileViewActivity.java b/demo/src/main/java/tileview/demo/BuildingPlansTileViewActivity.java index 339f1ce3..bacb67ca 100644 --- a/demo/src/main/java/tileview/demo/BuildingPlansTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/BuildingPlansTileViewActivity.java @@ -21,6 +21,12 @@ public void onCreate( Bundle savedInstanceState ) { // size of original image at 100% mScale tileView.setSize( 2736, 2880 ); + // small map, let's let it resize to 200% + tileView.setScaleLimits( 0, 2 ); + + // we're running from assets, should be fairly fast decodes, go ahead and render asap + tileView.setShouldRenderWhilePanning( true ); + // detail levels tileView.addDetailLevel( 1.000f, "tiles/plans/1000/%d_%d.jpg"); tileView.addDetailLevel( 0.500f, "tiles/plans/500/%d_%d.jpg"); diff --git a/demo/src/main/java/tileview/demo/FictionalMapTileViewActivity.java b/demo/src/main/java/tileview/demo/FictionalMapTileViewActivity.java index 89f2f8fc..d20a00bc 100644 --- a/demo/src/main/java/tileview/demo/FictionalMapTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/FictionalMapTileViewActivity.java @@ -17,6 +17,9 @@ public void onCreate( Bundle savedInstanceState ) { // size of original image at 100% mScale tileView.setSize( 4015, 4057 ); + + // we're running from assets, should be fairly fast decodes, go ahead and render asap + tileView.setShouldRenderWhilePanning( true ); // detail levels tileView.addDetailLevel( 1.000f, "tiles/fantasy/1000/%d_%d.jpg"); diff --git a/demo/src/main/java/tileview/demo/LargeImageTileViewActivity.java b/demo/src/main/java/tileview/demo/LargeImageTileViewActivity.java index a6ef7c40..d189b48a 100644 --- a/demo/src/main/java/tileview/demo/LargeImageTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/LargeImageTileViewActivity.java @@ -13,9 +13,9 @@ public void onCreate( Bundle savedInstanceState ) { // multiple references TileView tileView = getTileView(); - - // by disabling transitions, we won't see a flicker of background color when moving between tile sets - tileView.setTransitionsEnabled( false ); + + // let the image explode + tileView.setScaleLimits( 0, 2 ); // size of original image at 100% mScale tileView.setSize( 2835, 4289 ); diff --git a/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java b/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java index 4f132374..0466e642 100644 --- a/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java @@ -1,12 +1,27 @@ package tileview.demo; +import android.graphics.CornerPathEffect; +import android.graphics.Paint; import android.os.Bundle; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; +import android.widget.ImageView; import com.qozix.tileview.TileView; +import com.qozix.tileview.markers.MarkerLayout; + +import java.util.ArrayList; import tileview.demo.provider.BitmapProviderPicasso; public class RealMapInternetTileViewActivity extends TileViewActivity { + + public static final double NORTH_WEST_LATITUDE = 39.9639998777094; + public static final double NORTH_WEST_LONGITUDE = -75.17261900652977; + public static final double SOUTH_EAST_LATITUDE = 39.93699709962642; + public static final double SOUTH_EAST_LONGITUDE = -75.12462846235614; + @Override public void onCreate( Bundle savedInstanceState ) { @@ -21,9 +36,77 @@ public void onCreate( Bundle savedInstanceState ) { // by disabling transitions, we won't see a flicker of background color when moving between tile sets tileView.setTransitionsEnabled( false ); + // this is default behavior so the method invocation is redundant, but for network work fast render might be less appropriate + //tileView.setShouldRenderWhilePanning( false ); + // size and geolocation tileView.setSize( 8967, 6726 ); + // markers should align to the coordinate along the horizontal center and vertical bottom + tileView.setMarkerAnchorPoints( -0.5f, -1.0f ); + + // render asap + tileView.setShouldRenderWhilePanning( true ); + + // provide the corner coordinates for relative positioning + tileView.defineBounds( + NORTH_WEST_LONGITUDE, + NORTH_WEST_LATITUDE, + SOUTH_EAST_LONGITUDE, + SOUTH_EAST_LATITUDE + ); + + // get metrics for programmatic DP + DisplayMetrics metrics = getResources().getDisplayMetrics(); + + // get the default paint and style it. the same effect could be achieved by passing a custom Paint instnace + Paint paint = tileView.getDefaultPathPaint(); + + // add markers for all the points + for( double[] point : points ) { + // any view will do... + ImageView marker = new ImageView( this ); + // save the coordinate for centering and callout positioning + marker.setTag( point ); + // give it a standard marker icon - this indicator points down and is centered, so we'll use appropriate anchors + marker.setImageResource( Math.random() < 0.75 ? R.drawable.map_marker_normal : R.drawable.map_marker_featured ); + // on tap show further information about the area indicated + // this could be done using a OnClickListener, which is a little more "snappy", since + // MarkerTapListener uses GestureDetector.onSingleTapConfirmed, which has a delay of 300ms to + // confirm it's not the start of a double-tap. But this would consume the touch event and + // interrupt dragging + tileView.getMarkerLayout().setMarkerTapListener( markerTapListener ); + // add it to the view tree + tileView.addMarker( marker, point[0], point[1], null, null ); + } + + // let's start off framed to the center of all points + double x = 0; + double y = 0; + for( double[] point : points ) { + x = x + point[0]; + y = y + point[1]; + } + int size = points.size(); + x = x / size; + y = y / size; + frameTo( x, y ); + + // dress up the path effects and draw it between some points + paint.setShadowLayer( + TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 4, metrics ), + TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, metrics ), + TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, metrics ), + 0x66000000 + ); + paint.setStrokeWidth( TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, metrics ) ); + paint.setPathEffect( + new CornerPathEffect( + TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, metrics ) + ) + ); + tileView.drawPath( points.subList( 1, 5 ), null ); + // we won't use a downsample here, so color it similarly to tiles tileView.setBackgroundColor( 0xFFe7e7e7 ); @@ -32,11 +115,66 @@ public void onCreate( Bundle savedInstanceState ) { tileView.addDetailLevel( 0.5000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-250000-%d_%d.jpg" ); tileView.addDetailLevel( 1.0000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-500000-%d_%d.jpg" ); - // let's use 0-1 positioning... - tileView.defineBounds( 0, 0, 1, 1 ); + } + + private MarkerLayout.MarkerTapListener markerTapListener = new MarkerLayout.MarkerTapListener() { - // frame to center - frameTo( 0.5, 0.5 ); + @Override + public void onMarkerTap( View view, int x, int y ) { + // get reference to the TileView + TileView tileView = getTileView(); + // we saved the coordinate in the marker's tag + double[] position = (double[]) view.getTag(); + // lets center the screen to that coordinate + tileView.slideToAndCenter( position[0], position[1] ); + // create a simple callout + SampleCallout callout = new SampleCallout( view.getContext() ); + // add it to the view tree at the same position and offset as the marker that invoked it + tileView.addCallout( callout, position[0], position[1], -0.5f, -1.0f ); + // a little sugar + callout.transitionIn(); + // stub out some text + callout.setTitle( "MAP CALLOUT" ); + callout.setSubtitle( "Info window at coordinate:\n" + position[1] + ", " + position[0] ); + } + }; + // a list of points to demonstrate markers and paths + private ArrayList points = new ArrayList<>(); + + { + points.add( new double[] {-75.1489070, 39.9484760} ); + points.add( new double[] {-75.1494000, 39.9487722} ); + points.add( new double[] {-75.1468350, 39.9474180} ); + points.add( new double[] {-75.1472000, 39.9482000} ); + points.add( new double[] {-75.1437980, 39.9508290} ); + points.add( new double[] {-75.1479650, 39.9523130} ); + points.add( new double[] {-75.1445500, 39.9472960} ); + points.add( new double[] {-75.1506100, 39.9490630} ); + points.add( new double[] {-75.1521278, 39.9508083} ); + points.add( new double[] {-75.1477600, 39.9475320} ); + points.add( new double[] {-75.1503800, 39.9489900} ); + points.add( new double[] {-75.1464200, 39.9482000} ); + points.add( new double[] {-75.1464850, 39.9498500} ); + points.add( new double[] {-75.1487030, 39.9524300} ); + points.add( new double[] {-75.1500167, 39.9488750} ); + points.add( new double[] {-75.1458360, 39.9479700} ); + points.add( new double[] {-75.1498222, 39.9515389} ); + points.add( new double[] {-75.1501990, 39.9498900} ); + points.add( new double[] {-75.1460060, 39.9474210} ); + points.add( new double[] {-75.1490230, 39.9533960} ); + points.add( new double[] {-75.1471980, 39.9485350} ); + points.add( new double[] {-75.1493500, 39.9490200} ); + points.add( new double[] {-75.1500910, 39.9503850} ); + points.add( new double[] {-75.1483930, 39.9485040} ); + points.add( new double[] {-75.1517260, 39.9473720} ); + points.add( new double[] {-75.1525630, 39.9471360} ); + points.add( new double[] {-75.1438400, 39.9473390} ); + points.add( new double[] {-75.1468240, 39.9495400} ); + points.add( new double[] {-75.1466410, 39.9499900} ); + points.add( new double[] {-75.1465050, 39.9501110} ); + points.add( new double[] {-75.1473460, 39.9436200} ); + points.add( new double[] {-75.1501570, 39.9480430} ); } + } diff --git a/demo/src/main/java/tileview/demo/RealMapTileViewActivity.java b/demo/src/main/java/tileview/demo/RealMapTileViewActivity.java index 2112179b..35580b90 100644 --- a/demo/src/main/java/tileview/demo/RealMapTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/RealMapTileViewActivity.java @@ -1,10 +1,6 @@ package tileview.demo; -import android.graphics.CornerPathEffect; -import android.graphics.Paint; import android.os.Bundle; -import android.util.DisplayMetrics; -import android.util.TypedValue; import android.view.View; import android.widget.ImageView; @@ -50,27 +46,6 @@ public void onCreate( Bundle savedInstanceState ) { SOUTH_EAST_LATITUDE ); - // get the default paint and style it. the same effect could be achieved by passing a custom Paint instnace - Paint paint = tileView.getDefaultPathPaint(); - - // get metrics for programmatic DP - DisplayMetrics metrics = getResources().getDisplayMetrics(); - - // dress up the path effects and draw it between some points - paint.setShadowLayer( - TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 4, metrics ), - TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, metrics ), - TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, metrics ), - 0x66000000 - ); - paint.setStrokeWidth( TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, metrics ) ); - paint.setPathEffect( - new CornerPathEffect( - TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, metrics ) - ) - ); - tileView.drawPath( points.subList( 1, 5 ), null ); - // add markers for all the points for( double[] point : points ) { // any view will do... @@ -107,12 +82,12 @@ public void onCreate( Bundle savedInstanceState ) { // start small and allow zoom tileView.setScale( 0.5f ); + // with padding, we might be fast enough to create the illusion of a seamless image + tileView.setViewportPadding( 256 ); + // we're running from assets, should be fairly fast decodes, go ahead and render asap tileView.setShouldRenderWhilePanning( true ); - // for quickly drawn tiles _without_ a downsample, transitions aren't particularly useful - tileView.setTransitionsEnabled( false ); - } private MarkerLayout.MarkerTapListener markerTapListener = new MarkerLayout.MarkerTapListener() { diff --git a/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java b/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java index a3db360a..b4677bbe 100644 --- a/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java +++ b/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java @@ -8,8 +8,6 @@ import com.squareup.picasso.MemoryPolicy; import com.squareup.picasso.Picasso; -import java.io.IOException; - /** * @author Mike Dunn, 2/19/16. */ @@ -21,10 +19,10 @@ public Bitmap getBitmap( Tile tile, Context context ) { String formattedFileName = String.format( unformattedFileName, tile.getColumn(), tile.getRow() ); try { return Picasso.with( context ).load( formattedFileName ).memoryPolicy( MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE ).get(); - } catch( IOException e ) { + } catch( Throwable t ) { // probably couldn't find the file } } return null; } -} +} \ No newline at end of file diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java index 1e828dbb..2f63835b 100755 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java @@ -7,7 +7,6 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.util.Log; import android.view.ViewGroup; import com.qozix.tileview.detail.DetailLevel; @@ -212,8 +211,9 @@ public void clear() { /** * This function is now a no-op - * @deprecated + * * @param recentlyComputedVisibleTileSet + * @deprecated */ public void reconcile( Set recentlyComputedVisibleTileSet ) { // noop @@ -232,7 +232,7 @@ private Rect getComputedViewport() { return mDetailLevelToRender.getDetailLevelManager().getComputedScaledViewport( getInvertedScale() ); } - private boolean establishOpaqueRegion() { + private boolean establishDirtyRegion() { boolean shouldInvalidate = false; mDirtyRegion.set( getComputedViewport() ); for( Tile tile : mTilesInCurrentViewport ) { @@ -305,7 +305,7 @@ private void drawTilesWithoutConsideringPreviouslyDrawnLevel( Canvas canvas ) { private void drawTilesConsideringPreviouslyDrawnLevel( Canvas canvas ) { // compute states, populate opaque region - boolean shouldInvalidate = establishOpaqueRegion(); + boolean shouldInvalidate = establishDirtyRegion(); // draw any previous tiles that are in viewport and not under full opaque current tiles shouldInvalidate |= drawPreviousTiles( canvas ); // draw the current tile set @@ -325,7 +325,6 @@ private void drawTiles( Canvas canvas ) { } else { drawTilesWithoutConsideringPreviouslyDrawnLevel( canvas ); } - Log.d( getClass().getSimpleName(), "prevous tile count: " + mPreviouslyDrawnTiles.size() ); } public void updateTileSet( DetailLevel detailLevel ) { From cd1f734abea01a05ea9b506b309d8dc821bee3a7 Mon Sep 17 00:00:00 2001 From: Mike Dunn Date: Tue, 9 Aug 2016 20:14:25 -0500 Subject: [PATCH 5/7] updating demo, readme --- README.md | 42 ++++++++++--------- .../demo/RealMapInternetTileViewActivity.java | 25 ++++++----- .../demo/provider/BitmapProviderPicasso.java | 1 + 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 48bda92b..0728d5d7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ ![Release Badge](https://img.shields.io/github/release/moagrius/TileView.svg) -_**08/07/16** 2.2 is released, and provides some much-needed improvements in how tiles are rendered - please consider upgrading, but be aware there are some minor potential breaking changes (that should not affect 99% of users). +#TileView +The TileView widget is a subclass of ViewGroup that provides a mechanism to asynchronously display tile-based images, with additional functionality for 2D dragging, flinging, pinch or double-tap to zoom, adding overlaying Views (markers), built-in Hot Spot support, dynamic path drawing, multiple levels of detail, and support for any relative positioning or coordinate system. + +![Demo](https://cloud.githubusercontent.com/assets/701344/17538476/6933099e-5e6b-11e6-9e18-45e924c19c91.gif) +_Properly configured, TileView can render tiles quickly enough to create the illusion of a seamless image_ +[](https://cloud.githubusercontent.com/assets/701344/10954033/d20843bc-8310-11e5-83ad-4e062b9b1be0.gif) + +_**08/07/16** 2.2 is released, and provides some much-needed improvements in how tiles are rendered - please consider upgrading, but be aware there are some minor potential breaking changes (that should not affect 99% of users)._ -_**03/18/16** if you're using a version earlier than 2.1, there were significant performance gains realized with 2.1 so we'd advise you to start using the most recent version (2.1 or later) immediately. The improvements made also make fast-render viable, especially if not fetching tile data from across a network, so we'd also encourage you to try `TileView.setShouldRenderWhilePanning(true);` if you'd like more responsive tile rendering._ +_**03/18/16** if you're using a version earlier than 2.1, there were significant performance gains realized with 2.1 so we'd advise you to start using the most recent version (2.1 or later) immediately. The improvements made also make fast-render viable, so we'd also encourage you to try `TileView.setShouldRenderWhilePanning(true);` if you'd like more responsive tile rendering._ -#Version 2.0 +##Version 2.0 **Version 2.0 released 10.25.15** @@ -22,16 +29,11 @@ Major goals were: 6. General refactoring. There are too many simplifications and optimization to mention, but each class and each method has been revisited. 7. Hooks hooks hooks! While pan and zoom events are broadcast using a familiar listener mechanism, and should be sufficient for most use-cases, public hooks exist for a large number of operations that can be overriden by subclasses for custom functionality. -#Change Log +##Change Log (Only major and minor changes are tracked here, consult git history for patches) +**2.2** Rewrite of tile rendering strategy, again with the help of @peterLaurence. Peak memory consumption should be reduced, and Tile render performance should be improved. **2.1** Rewrite of threading strategy, thanks to @peterLaurence and @bnsantos. Tile render performance is substantially improved. -**2.2** Rewrite of tile rendering strategy, again with the help of @peterLaurence. Peak memory consumption should be reduceds, and Tile render performance should be improved. - -#TileView -The TileView widget is a subclass of ViewGroup that provides a mechanism to asynchronously display tile-based images, with additional functionality for 2D dragging, flinging, pinch or double-tap to zoom, adding overlaying Views (markers), built-in Hot Spot support, dynamic path drawing, multiple levels of detail, and support for any relative positioning or coordinate system. - -![Demo](https://cloud.githubusercontent.com/assets/701344/10954033/d20843bc-8310-11e5-83ad-4e062b9b1be0.gif) ###Documentation Javadocs are [here](http://moagrius.github.io/TileView/index.html?com/qozix/tileview/TileView.html). Wiki is [here](https://github.com/moagrius/TileView/wiki). @@ -39,7 +41,7 @@ Javadocs are [here](http://moagrius.github.io/TileView/index.html?com/qozix/tile ###Installation Gradle: ``` -compile 'com.qozix:tileview:2.1.8' +compile 'com.qozix:tileview:2.2.0' ``` The library is hosted on jcenter, and is not currently available from maven. @@ -59,7 +61,7 @@ A demo application, built in Android Studio, is available in the `demo` folder o at the 2nd column from left and 3rd row from top. 1. Create a new application with a single activity ('Main'). 1. Save the image tiles to your `assets` directory. -1. Add `compile 'com.qozix:tileview:2.1.8'` to your gradle dependencies. +1. Add `compile 'com.qozix:tileview:2.2.0'` to your gradle dependencies. 1. In the Main Activity, use this for `onCreate`: ``` @Override @@ -79,15 +81,15 @@ That's it. You should have a tiled image that only renders the pieces of the im ![detail-levels](https://cloud.githubusercontent.com/assets/701344/10954031/d2059c3e-8310-11e5-821d-26dd8691d4d3.gif) -A TileView instance can have any number of detail levels, which is a single image made up of many tiles. These tiles are positioned appropriately to show the portion of the image that the device's viewport is displayed - other tiles are recyled (and their memory freed) as they move out of the visible area. Detail levels often showing the same content at different magnifications, but may show different details as well - for example, a detail level showing a larger area will probably label features differently than a detail level showing a smaller area (imagine a TileView representing the United States may show the Rocky Mountains at a very low detail level, while a higher detail level may show individual streets or addresses. +A TileView instance can have any number of detail levels, which is a single image made up of many tiles; each DetailLevel exists in the same space, but are useful to show different levels of details (thus the class name), and to further break down large images into smaller tiles sets. These tiles are positioned appropriately to show the portion of the image that the device's viewport is displayed - other tiles are recycled (and their memory freed) as they move out of the visible area. Detail levels often show the same content at different magnifications, but may show different details as well - for example, a detail level showing a larger area will probably label features differently than a detail level showing a smaller area (imagine a TileView representing the United States may show the Rocky Mountains at a very low detail level, while a higher detail level may show individual streets or addresses. -Each detail level is passed a float value, indicating the scale value that it represents (e.g., a detail level passed 0.5f scale would be displayed when the TileView was zoomed out by 50%). Additionally, each detail level is passed an aribtrary data object that is attached to each tile and can provide instructions on how to generate the tile's bitmap. +Each detail level is passed a float value, indicating the scale value that it represents (e.g., a detail level passed 0.5f scale would be displayed when the TileView was zoomed out by 50%). Additionally, each detail level is passed an arbitrary data object that is attached to each tile and can provide instructions on how to generate the tile's bitmap. That data object is often a String, formatted to provide the path to the bitmap image for that Tile, but can be any kind of Object whatsoever - during the decode process, each tile has access to the data object for the detail level. ####Tiles -A Tile is a class instance that represents a Bitmap which is a portion of the total image. Each Tile provides position information, and methods to manage the Bitmap, and is also passed to the TileView's `BitmapProvider` implementation, which is how individual bitmaps are generated. +A Tile is a class instance that represents a Bitmap - a portion of the total image. Each Tile provides position information, and methods to manage the Bitmap's state and behavior. Each Tile instanced is also passed to the TileView's `BitmapProvider` implementation, which is how individual bitmaps are generated. Tile instances uses an `equals` method that compares only row, column and detail level, and are often passed in `Set` collections, so that Tile instances already in process are simply excluded by the unique nature of the Set if the program or user tries to add a single Tile more than once. -Each TileView uses a `BitmapProvider` implementation to generate tile bitmaps. The interface defines a single method: `public Bitmap getBitmap( Tile tile, Context context );`. This method is called each time a bitmap is required, and has access to the Tile instance for that position and detail level, and a Context object to access system resources. The `BitmapProvider` implementation can generate the bitmap in any way it chooses - assets, resources, http requests, dynamically drawn, SVG, decoded regions, etc. The default implementation, `BitmapProviderAssets`, parses a String (the data object passed to the DetailLevel) and returns a bitmap found by file name in the app's assets directory. +Each TileView instance must reference a `BitmapProvider` implementation to generate tile bitmaps. The interface defines a single method: `public Bitmap getBitmap( Tile tile, Context context );`. This method is called each time a bitmap is required, and has access to the Tile instance for that position and detail level, and a Context object to access system resources. The `BitmapProvider` implementation can generate the bitmap in any way it chooses - assets, resources, http requests, dynamically drawn, SVG, decoded regions, etc. The default implementation, `BitmapProviderAssets`, parses a String (the data object passed to the DetailLevel) and returns a bitmap found by file name in the app's assets directory. ####Markers & Callouts @@ -148,14 +150,14 @@ tileView.addPath( drawablePath ); ####Scaling The `setScale(1)` method sets the initial scale of the TileView. -`setScaleLimits(0, 1)` sets the minimum and maximum scale which controls how far a TileView can be zoomed in or out. `0` means completely zoomed out, `1` means zoomed in to the most detailled level (with the pixels of the tiles matching the screen dpi). For example by using `setScaleLimits(0, 3)` you allow users to zoom in even further then the most detailled level (stretching the image). +`setScaleLimits(0, 1)` sets the minimum and maximum scale which controls how far a TileView can be zoomed in or out. `0` means completely zoomed out, `1` means zoomed in to the most detailed level (with the pixels of the tiles matching the screen dpi). For example by using `setScaleLimits(0, 3)` you allow users to zoom in even further then the most detailed level (stretching the image). `setMinimumScaleMode(ZoomPanLayout.MinimumScaleMode.FILL)` controls how far a image can be zoomed out based on the dimensions of the image: - `FILL`: Limit the minimum scale to no less than what would be required to fill the container - `FIT`: Limit the minimum scale to no less than what would be required to fit inside the container - `NONE`: Limit to the minimum scale level set by `setScaleLimits` -_When using `FILL` or `FIT`, the minimum scale level of `setScaleLimits` is ignored_ +_When using `FILL` or `FIT`, the minimum scale level of `setScaleLimits` is ignored._ ####Hooks and Listeners @@ -176,7 +178,7 @@ tileView.addZoomPanListener( new ZoomPanListener(){ }); ``` -Additionally, TileView reports most significant operations to hooks. TileView implements `ZoomPanLayout.ZoomPanListener`, `TileCanvasViewGroup.TileRenderListener`, and `DetailLevelManager.DetailLevelChangeListener`, and it's super class implements `GestureDetector.OnGestureListener`, `GestureDetector.OnDoubleTapListener`,`ScaleGestureDetector.OnScaleGestureListener`, and `TouchUpGestureDetector.OnTouchUpListener`. As such, the following hooks are available to be overriden by subclasses of TileView: +Additionally, TileView reports most significant operations to hooks. TileView implements `ZoomPanLayout.ZoomPanListener`, `TileCanvasViewGroup.TileRenderListener`, and `DetailLevelManager.DetailLevelChangeListener`, and it's super class implements `GestureDetector.OnGestureListener`, `GestureDetector.OnDoubleTapListener`,`ScaleGestureDetector.OnScaleGestureListener`, and `TouchUpGestureDetector.OnTouchUpListener`. As such, the following hooks are available to be overridden by subclasses of TileView: ``` protected void onScrollChanged( int l, int t, int oldl, int oldt ); @@ -217,7 +219,7 @@ See the [wiki entry here](https://github.com/moagrius/TileView/wiki/Creating-Til ####...use relative coordinates (like latitude and longitude)? The TileView method `defineBounds( double left, double top, double right, double bottom )` establishes a coordinate system for further positioning method calls (e.g., `scrollTo`, `addMarker`, etc). After relative coordinates are established by invoking the `defineBounds` method, any subsequent method invocations that affect position *and* accept `double` parameters will compute the value as relative of the provided bounds, rather than absolute pixels. That's to say that: - 1. A TileView instance is intialized with `setSize( 5000, 5000 );` + 1. A TileView instance is initialized with `setSize( 5000, 5000 );` 1. That TileView instance calls `defineBounds( 0, 100, 0, 100 );` 1. That TileView instance calls `scrollTo( 25d, 50d );` 1. That TileView will immediately scroll to the pixel at 1250, 2500. diff --git a/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java b/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java index 0466e642..090b9bd9 100644 --- a/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java @@ -30,15 +30,14 @@ public void onCreate( Bundle savedInstanceState ) { // multiple references TileView tileView = getTileView(); + tileView.addDetailLevel( 0.0125f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-62500-%d_%d.jpg" ); + tileView.addDetailLevel( 0.2500f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-125000-%d_%d.jpg" ); + tileView.addDetailLevel( 0.5000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-250000-%d_%d.jpg" ); + tileView.addDetailLevel( 1.0000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-500000-%d_%d.jpg" ); + // simple http provider tileView.setBitmapProvider( new BitmapProviderPicasso() ); - // by disabling transitions, we won't see a flicker of background color when moving between tile sets - tileView.setTransitionsEnabled( false ); - - // this is default behavior so the method invocation is redundant, but for network work fast render might be less appropriate - //tileView.setShouldRenderWhilePanning( false ); - // size and geolocation tileView.setSize( 8967, 6726 ); @@ -110,10 +109,16 @@ public void onCreate( Bundle savedInstanceState ) { // we won't use a downsample here, so color it similarly to tiles tileView.setBackgroundColor( 0xFFe7e7e7 ); - tileView.addDetailLevel( 0.0125f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-62500-%d_%d.jpg" ); - tileView.addDetailLevel( 0.2500f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-125000-%d_%d.jpg" ); - tileView.addDetailLevel( 0.5000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-250000-%d_%d.jpg" ); - tileView.addDetailLevel( 1.0000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-500000-%d_%d.jpg" ); + // this will result in a lot more network work, but should be a better UX + // set to false to minimize work on the wire + tileView.setShouldRenderWhilePanning( true ); + + // since there's going to be significant time between tiles loaded over http, + // using a low res version of the image stretched to fill all available space can + // create the illusion of progressive rendering + ImageView downSample = new ImageView( this ); + downSample.setImageResource( R.drawable.downsample ); + tileView.addView( downSample, 0 ); } diff --git a/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java b/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java index b4677bbe..0826d9b1 100644 --- a/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java +++ b/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java @@ -18,6 +18,7 @@ public Bitmap getBitmap( Tile tile, Context context ) { String unformattedFileName = (String) tile.getData(); String formattedFileName = String.format( unformattedFileName, tile.getColumn(), tile.getRow() ); try { + // TODO: this is throwing... return Picasso.with( context ).load( formattedFileName ).memoryPolicy( MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE ).get(); } catch( Throwable t ) { // probably couldn't find the file From 23d48f3f543b63d597fce878af946d1ae87cd638 Mon Sep 17 00:00:00 2001 From: Mike Dunn Date: Tue, 9 Aug 2016 20:17:49 -0500 Subject: [PATCH 6/7] updating todos, removing unused files --- .../main/assets/samples/boston-overview.jpg | Bin 166895 -> 0 bytes .../main/assets/samples/boston-pedestrian.jpg | Bin 144796 -> 0 bytes demo/src/main/assets/samples/middle-earth.jpg | Bin 89251 -> 0 bytes demo/src/main/assets/samples/mona-lisa.jpg | Bin 59270 -> 0 bytes demo/src/main/assets/samples/plans.JPG | Bin 166969 -> 0 bytes .../demo/provider/BitmapProviderPicasso.java | 3 +-- .../tileview/paths/CompositePathView.java | 2 +- .../java/com/qozix/tileview/tiles/Tile.java | 2 +- .../tileview/tiles/TileRenderPoolExecutor.java | 2 +- 9 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 demo/src/main/assets/samples/boston-overview.jpg delete mode 100644 demo/src/main/assets/samples/boston-pedestrian.jpg delete mode 100644 demo/src/main/assets/samples/middle-earth.jpg delete mode 100644 demo/src/main/assets/samples/mona-lisa.jpg delete mode 100644 demo/src/main/assets/samples/plans.JPG diff --git a/demo/src/main/assets/samples/boston-overview.jpg b/demo/src/main/assets/samples/boston-overview.jpg deleted file mode 100644 index 9054ed744fc073b76962dcf14eaad694fffee247..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 166895 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