package com.marshalchen.common.uimodule.tileView.tileview.tiles; import android.content.Context; import android.view.View; import android.view.animation.AlphaAnimation; import android.widget.ImageView; import com.marshalchen.common.uimodule.tileView.layouts.FixedLayout; import com.marshalchen.common.uimodule.tileView.layouts.ScalingLayout; import com.marshalchen.common.uimodule.tileView.os.AsyncTask; import com.marshalchen.common.uimodule.tileView.tileview.detail.DetailLevel; import com.marshalchen.common.uimodule.tileView.tileview.detail.DetailLevelEventListener; import com.marshalchen.common.uimodule.tileView.tileview.detail.DetailManager; import com.marshalchen.common.uimodule.tileView.tileview.graphics.BitmapDecoder; import com.marshalchen.common.uimodule.tileView.tileview.graphics.BitmapDecoderAssets; import java.util.HashMap; import java.util.LinkedList; public class TileManager extends ScalingLayout implements DetailLevelEventListener { private static final int RENDER_FLAG = 1; private static final int RENDER_BUFFER = 250; private static final int TRANSITION_DURATION = 200; private LinkedList<Tile> scheduledToRender = new LinkedList<Tile>(); private LinkedList<Tile> alreadyRendered = new LinkedList<Tile>(); private BitmapDecoder decoder = new BitmapDecoderAssets(); private HashMap<Double, ScalingLayout> tileGroups = new HashMap<Double, ScalingLayout>(); private TileCache cache; private DetailLevel detailLevelToRender; private DetailLevel lastRenderedDetailLevel; private TileRenderTask lastRunRenderTask; private ScalingLayout currentTileGroup; private DetailManager detailManager; private boolean renderIsCancelled = false; private boolean renderIsSuppressed = false; private boolean isRendering = false; private boolean transitionsEnabled = true; private int transitionDuration = TRANSITION_DURATION; private TileRenderHandler handler; private TileRenderListener renderListener; private TileTransitionListener transitionListener; public TileManager( Context context, DetailManager zm ) { super( context ); detailManager = zm; detailManager.addDetailLevelEventListener( this ); handler = new TileRenderHandler( this ); transitionListener = new TileTransitionListener( this ); } public void setTransitionsEnabled( boolean enabled ) { transitionsEnabled = enabled; } public void setTransitionDuration( int duration ) { transitionDuration = duration; } public void setDecoder( BitmapDecoder d ){ decoder = d; } public void setCacheEnabled( boolean shouldCache ) { if ( shouldCache ){ if ( cache == null ){ cache = new TileCache( getContext() ); } } else { if ( cache != null ) { cache.destroy(); } cache = null; } } public void setTileRenderListener( TileRenderListener listener ){ renderListener = listener; } public void requestRender() { // if we're requesting it, we must really want one renderIsCancelled = false; renderIsSuppressed = false; // if there's no data about the current detail level, don't bother if ( detailLevelToRender == null ) { return; } // throttle requests if ( handler.hasMessages( RENDER_FLAG ) ) { handler.removeMessages( RENDER_FLAG ); } // give it enough buffer that (generally) successive calls will be captured handler.sendEmptyMessageDelayed( RENDER_FLAG, RENDER_BUFFER ); } public void cancelRender() { // hard cancel - further render tasks won't start, and we'll attempt to interrupt the currently executing task renderIsCancelled = true; // if the currently executing task isn't null... if ( lastRunRenderTask != null ) { // ... and it's in a cancellable state if ( lastRunRenderTask.getStatus() != AsyncTask.Status.FINISHED ) { // ... then squash it lastRunRenderTask.cancel( true ); } } // give it to gc lastRunRenderTask = null; } public void suppressRender() { // this will prevent new tasks from starting, but won't actually cancel the currently executing task renderIsSuppressed = true; } public void updateTileSet() { // grab reference to this detail level, so we can get it's tile set for comparison to viewport detailLevelToRender = detailManager.getCurrentDetailLevel(); // fast-fail if it's null if(detailLevelToRender == null){ return; } // fast-fail if there's no change (same tile set) if( detailLevelToRender.equals( lastRenderedDetailLevel ) ) { return; } // we made it this far, cache the new level to test for changes on next invocation lastRenderedDetailLevel = detailLevelToRender; // fetch appropriate child currentTileGroup = getCurrentTileGroup(); // show it currentTileGroup.setVisibility( View.VISIBLE ); // bring it to top of stack currentTileGroup.bringToFront(); } public boolean getIsRendering() { return isRendering; } public void clear() { // suppress and cancel renders suppressRender(); cancelRender(); // destroy all tiles for ( Tile m : scheduledToRender ) { m.destroy(); } scheduledToRender.clear(); for ( Tile m : alreadyRendered ) { m.destroy(); } alreadyRendered.clear(); // the above should clear everything, but let's be redundant for ( ScalingLayout tileGroup : tileGroups.values() ) { int totalChildren = tileGroup.getChildCount(); for ( int i = 0; i < totalChildren; i++ ) { View child = tileGroup.getChildAt( i ); if ( child instanceof ImageView ) { ImageView imageView = (ImageView) child; imageView.setImageBitmap( null ); } } tileGroup.removeAllViews(); } // clear the cache if ( cache != null ) { cache.clear(); } } private ScalingLayout getCurrentTileGroup() { // get the registered scale for the active detail level double levelScale = detailManager.getCurrentDetailLevelScale(); // if a tile group has already been created and registered... if ( tileGroups.containsKey( levelScale ) ) { // ... we're done. return cached level. return tileGroups.get( levelScale ); } // otherwise create one ScalingLayout tileGroup = new ScalingLayout( getContext() ); // scale it to the inverse of the levels scale (so 0.25 levels are shown at 400%) tileGroup.setScale( 1 / levelScale ); // register it scale (key) for re-use tileGroups.put( levelScale, tileGroup ); // add it to the view tree // MATCH_PARENT should work here but doesn't, roll back if reverting to FrameLayout addView( tileGroup, new LayoutParams( detailManager.getWidth(), detailManager.getHeight() ) ); // send it off return tileGroup; } // access omitted deliberately - need package level access for the TileRenderHandler void renderTiles() { // has it been canceled since it was requested? if ( renderIsCancelled ) { return; } // can we keep rending existing tasks, but not start new ones? if ( renderIsSuppressed ) { return; } // fast-fail if there's no available data if ( detailLevelToRender == null ) { return; } // decode and render the bitmaps asynchronously beginRenderTask(); } private void beginRenderTask() { // find all matching tiles LinkedList<Tile> intersections = detailLevelToRender.getIntersections(); // if it's the same list, don't bother if ( scheduledToRender.equals( intersections ) ) { return; } // if we made it here, then replace the old list with the new list scheduledToRender = intersections; // cancel task if it's already running if ( lastRunRenderTask != null ) { if ( lastRunRenderTask.getStatus() != AsyncTask.Status.FINISHED ) { lastRunRenderTask.cancel( true ); } } // start a new one lastRunRenderTask = new TileRenderTask( this ); lastRunRenderTask.execute(); } private FixedLayout.LayoutParams getLayoutFromTile( Tile m ) { int w = m.getWidth(); int h = m.getHeight(); int x = m.getLeft(); int y = m.getTop(); return new FixedLayout.LayoutParams( w, h, x, y ); } private void cleanup() { // start with all rendered tiles... LinkedList<Tile> condemned = new LinkedList<Tile>( alreadyRendered ); // now remove all those that were just qualified condemned.removeAll( scheduledToRender ); // for whatever's left, destroy and remove from list for ( Tile m : condemned ) { m.destroy(); alreadyRendered.remove( m ); } // hide all other groups for ( ScalingLayout tileGroup : tileGroups.values() ) { if ( currentTileGroup == tileGroup ) { continue; } tileGroup.setVisibility( View.GONE ); } } /* * render tasks (invoked in asynctask's thread) */ void onRenderTaskPreExecute(){ // set a flag that we're working isRendering = true; // notify anybody interested if ( renderListener != null ) { renderListener.onRenderStart(); } } void onRenderTaskCancelled() { if ( renderListener != null ) { renderListener.onRenderCancelled(); } isRendering = false; } void onRenderTaskPostExecute() { // set flag that we're done isRendering = false; // everything's been rendered, so get rid of the old tiles cleanup(); // recurse - request another round of render - if the same intersections are discovered, recursion will end anyways requestRender(); // notify anybody interested if ( renderListener != null ) { renderListener.onRenderComplete(); } } LinkedList<Tile> getRenderList(){ return new LinkedList<Tile>( scheduledToRender ); } // package level access so it can be invoked by the render task void decodeIndividualTile( Tile m ) { m.decode( getContext(), cache, decoder ); } // package level access so it can be invoked by the render task void renderIndividualTile( Tile tile ) { // if it's already rendered, quit now if ( alreadyRendered.contains( tile ) ) { return; } // create the image view if needed, with default settings tile.render( getContext() ); // add it to the list of those rendered alreadyRendered.add( tile ); // get reference to the actual image view ImageView imageView = tile.getImageView(); // get layout params from the tile's predefined dimensions LayoutParams layoutParams = getLayoutFromTile( tile ); // add it to the appropriate set (which is already scaled) currentTileGroup.addView( imageView, layoutParams ); // shouldn't be necessary, but is postInvalidate(); // do we want to animate in tiles? if( transitionsEnabled){ // do we have an appropriate duration? if( transitionDuration > 0 ) { // create the animation (will be cleared by tile.destroy). do this here for the postInvalidate listener AlphaAnimation fadeIn = new AlphaAnimation( 0f, 1f ); // set duration fadeIn.setDuration( transitionDuration ); // this listener posts invalidate on complete, again should not be necessary but is fadeIn.setAnimationListener( transitionListener ); // start it up imageView.startAnimation( fadeIn ); } } } boolean getRenderIsCancelled() { return renderIsCancelled; } // TODO: instead of implements, use a member? @Override public void onDetailLevelChanged() { updateTileSet(); } @Override public void onDetailScaleChanged( double scale ) { setScale( scale ); } }