Android Open Source - lib_base Bitmap Surface Renderer






From Project

Back to project page lib_base.

License

The source code is released under:

Apache License

If you think the Android project lib_base listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright 2013 MicaByte Systems/*  ww w  .  j a va  2s . c o  m*/
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.micabyte.android.graphics;

import java.io.IOException;
import java.io.InputStream;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.v7.appcompat.BuildConfig;
import android.util.Log;

import com.micabyte.android.graphics.SurfaceRenderer.ViewPort;

/**
 * GameSurfaceRendererBitmap is a renderer that handles the rendering of a background bitmap to the
 * screen (e.g., a game map). It is able to do this even if the bitmap is too large to fit into
 * memory. The game should subclass the renderer and extend the drawing methods to add other game
 * elements.
 *
 * @author micabyte
 */
@SuppressWarnings("WeakerAccess")
public class BitmapSurfaceRenderer extends SurfaceRenderer {
    private static final String TAG = BitmapSurfaceRenderer.class.getName();
    // Default Settings
    public static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.RGB_565;
    private static final int DEFAULT_SAMPLE_SIZE = 2;
    private static final int DEFAULT_MEM_USAGE = 20;
    private static final float DEFAULT_THRESHOLD = 0.75f;
    // BitmapRegionDecoder - this is the class that does the magic
    private BitmapRegionDecoder decoder_;
    // The cached portion of the background image
    private final CacheBitmap cachedBitmap_ = new CacheBitmap();
    // The low resolution version of the background image
    private Bitmap lowResBitmap_;
    /**
     * Options for loading the bitmaps
     */
    private final BitmapFactory.Options options_ = new BitmapFactory.Options();
    /**
     * What is the down sample size for the sample image? 1=1/2, 2=1/4 3=1/8, etc
     */
    private final int sampleSize_;
    /**
     * What percent of total memory should we use for the cache? The bigger the cache, the longer it
     * takes to read -- 1.2 secs for 25%, 600ms for 10%, 500ms for 5%. User experience seems to be
     * best for smaller values.
     */
    private int memUsage_;
    /**
     * Threshold for using low resolution image
     */
    private final float lowResThreshold_;
    /**
     * Calculated rect
     */
    private final Rect calculatedCacheWindowRect = new Rect();

    private BitmapSurfaceRenderer(Context c) {
        super(c);
        options_.inPreferredConfig = DEFAULT_CONFIG;
        sampleSize_ = DEFAULT_SAMPLE_SIZE;
        memUsage_ = DEFAULT_MEM_USAGE;
        lowResThreshold_ = DEFAULT_THRESHOLD;
    }

    protected BitmapSurfaceRenderer(Context c, Bitmap.Config config, int sampleSize, int memUsage, float threshold) {
        super(c);
        options_.inPreferredConfig = config;
        sampleSize_ = sampleSize;
        memUsage_ = memUsage;
        lowResThreshold_ = threshold;
    }

    /**
     * Set the Background bitmap
     *
     * @param inputStream InputStream to the raw data of the bitmap
     * @throws IOException
     */
    public void setBitmap(InputStream inputStream) throws IOException {
        final BitmapFactory.Options opt = new BitmapFactory.Options();
        decoder_ = BitmapRegionDecoder.newInstance(inputStream, false);
        inputStream.reset();
        // Grab the bounds of the background bitmap
        opt.inPreferredConfig = DEFAULT_CONFIG;
        opt.inJustDecodeBounds = true;
        if (BuildConfig.DEBUG) Log.d(TAG, "Decode inputStream for Background Bitmap");
        BitmapFactory.decodeStream(inputStream, null, opt);
        inputStream.reset();
        backgroundSize_.set(opt.outWidth, opt.outHeight);
        if (BuildConfig.DEBUG)
            Log.i(TAG, "Background Image: w=" + opt.outWidth + " h=" + opt.outHeight);
        // Create the low resolution background
        opt.inJustDecodeBounds = false;
        opt.inSampleSize = (1 << sampleSize_);
        lowResBitmap_ = BitmapFactory.decodeStream(inputStream, null, opt);
        if (BuildConfig.DEBUG)
            Log.i(TAG, "Low Res Image: w=" + lowResBitmap_.getWidth() + " h=" + lowResBitmap_.getHeight());
        // Initialize cache
        if (cachedBitmap_.getState() == CacheState.NOT_INITIALIZED) {
            synchronized (cachedBitmap_) {
                cachedBitmap_.setState(CacheState.IS_INITIALIZED);
            }
        }
    }

    @Override
    protected void drawBase() {
        cachedBitmap_.draw(viewPort_);
    }

    @Override
    protected void drawLayer() {
        // NOOP - override function and add game specific code
    }

    @Override
    protected void drawFinal() {
        // NOOP - override function and add game specific code
    }

    /**
     * Starts the renderer
     */
    @Override
    public void start() {
        cachedBitmap_.start();
    }

    /**
     * Stops the renderer
     */
    @Override
    public void stop() {
        cachedBitmap_.stop();
    }

    /**
     * Suspend the renderer
     */
    @Override
    public void suspend(boolean suspend) {
        cachedBitmap_.suspend(suspend);
    }

    /**
     * Invalidate the cache.
     */
    public void invalidate() {
        cachedBitmap_.invalidate();
    }


    /**
     * Loads the relevant slice of the background bitmap that needs to be kept in memory.
     * <p/>
     * The loading can take a long time depending on the size.
     *
     * @param rect The portion of the background bitmap to be cached
     * @return The bitmap representing the requested area of the background
     */
    Bitmap loadCachedBitmap(Rect rect) {
        return decoder_.decodeRegion(rect, options_);
    }

    /**
     * This function tries to recover from an OutOfMemoryError in the CacheThread.
     *
     * @param error The OutOfMemoryError exception data
     */
    void cacheBitmapOutOfMemoryError(OutOfMemoryError error) {
        if (memUsage_ > 0) memUsage_ -= 1;
        Log.e(TAG, "OutOfMemory caught; reducing cache size to " + memUsage_ + " percent.");
        error.printStackTrace();
    }

    /**
     * This method fills the passed-in bitmap with sample data. This function must return data fast;
     * this is our fall back solution in all the cases where the user is moving too fast for us to
     * load the actual bitmap data from memory. The quality of the user experience rests on the
     * speed of this function.
     */
    void drawLowResolutionBackground(Bitmap bitmap, Rect rect) {
        final int left = (rect.left >> sampleSize_);
        final int top = (rect.top >> sampleSize_);
        final int right = (rect.right >> sampleSize_);
        final int bottom = (rect.bottom >> sampleSize_);
        final Rect srcRect = new Rect(left, top, right, bottom);
        final Rect dstRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        // Draw to Canvas
        final Canvas canvas = new Canvas(bitmap);
        canvas.drawBitmap(lowResBitmap_, srcRect, dstRect, null);
    }

    /**
     * Determine the dimensions of the CacheBitmap based on the current ViewPort.
     * <p/>
     * Minimum size is equal to the viewport; otherwise it is dimensioned relative to the available
     * memory. {@link CacheBitmap} is locked while the calculation is done, so this has to be fast.
     *
     * @param rect The dimensions of the current viewport
     * @return The dimensions of the cache
     */
    Rect calculateCacheDimensions(Rect rect) {
        final int BYTES_PER_PIXEL = 4;
        final long bytesToUse = Runtime.getRuntime().maxMemory() * memUsage_ / 100;
        final Point sz = getBackgroundSize();
        final int vw = rect.width();
        final int vh = rect.height();
        if (BuildConfig.DEBUG) Log.d(TAG, "old cache.originRect = " + rect.toShortString());
        // Calculate the margins within the memory budget
        int tw = 0;
        int th = 0;
        int mw = tw;
        int mh = th;
        while ((vw + tw) * (vh + th) * BYTES_PER_PIXEL < bytesToUse) {
            mw = tw++;
            mh = th++;
        }
        // Trim margins to image size
        if (vw + mw > sz.x) mw = Math.max(0, sz.x - vw);
        if (vh + mh > sz.y) mh = Math.max(0, sz.y - vh);
        // Figure out the left & right based on the margin.
        // LATER: THe logic here assumes that the viewport is <= our size.
        // If that's not the case, then this logic breaks.
        int left = rect.left - (mw >> 1);
        int right = rect.right + (mw >> 1);
        if (left < 0) {
            right = right - left; // Adds the overage on the left side back to the right
            left = 0;
        }
        if (right > sz.x) {
            left = left - (right - sz.x); // Adds overage on right side back to left
            right = sz.x;
        }
        // Figure out the top & bottom based on the margin. We assume our viewport
        // is <= our size. If that's not the case, then this logic breaks.
        int top = rect.top - (mh >> 1);
        int bottom = rect.bottom + (mh >> 1);
        if (top < 0) {
            bottom = bottom - top; // Adds the overage on the top back to the bottom
            top = 0;
        }
        if (bottom > sz.y) {
            top = top - (bottom - sz.y); // Adds overage on bottom back to top
            bottom = sz.y;
        }
        // Set the origin based on our new calculated values.
        calculatedCacheWindowRect.set(left, top, right, bottom);
        if (BuildConfig.DEBUG)
            if (BuildConfig.DEBUG) Log.d(TAG, "new cache.originRect = " + calculatedCacheWindowRect.toShortString() + " size=" + sz.toString());
        return calculatedCacheWindowRect;
    }


    /**
     * The current state of the cached bitmap
     */
    private enum CacheState {
        READY, NOT_INITIALIZED, IS_INITIALIZED, BEGIN_UPDATE, IS_UPDATING, DISABLED
    }

    /**
     * The cached bitmap object. This object is continually kept up to date by CacheThread. If the
     * object is locked, the background is updated using the low resolution background image instead
     */
    private class CacheBitmap {
        /**
         * The current position and dimensions of the cache within the background image
         */
        final Rect cacheWindow_ = new Rect(0, 0, 0, 0);
        /**
         * The current state of the cache
         */
        private CacheState state_ = CacheState.NOT_INITIALIZED;
        /**
         * The currently cached bitmap
         */
        Bitmap bitmap_ = null;
        /**
         * The cache bitmap loading thread
         */
        private CacheThread cacheThread_;

        public CacheBitmap() {
        }

        CacheState getState() {
            return state_;
        }

        void setState(CacheState newState) {
            state_ = newState;
        }

        void start() {
            if (cacheThread_ != null) {
                cacheThread_.setRunning(false);
                cacheThread_.interrupt();
                cacheThread_ = null;
            }
            cacheThread_ = new CacheThread(this);
            cacheThread_.setName("cacheThread");
            cacheThread_.start();
        }

        void stop() {
            cacheThread_.setRunning(false);
            cacheThread_.interrupt();
            boolean retry = true;
            while (retry) {
                try {
                    cacheThread_.join();
                    retry = false;
                } catch (InterruptedException e) {
                    // Wait until thread is dead
                }
            }
            cacheThread_ = null;
        }

        void invalidate() {
            synchronized (this) {
                setState(CacheState.IS_INITIALIZED);
                cacheThread_.interrupt();
            }
        }

        public void suspend(boolean suspend) {
            // Suspends or resume the cache thread.
            if (suspend) {
                synchronized (this) {
                    setState(CacheState.DISABLED);
                }
            } else {
                if (getState() == CacheState.DISABLED) {
                    synchronized (this) {
                        setState(CacheState.IS_INITIALIZED);
                    }
                }
            }
        }

        /**
         * Draw the CacheBitmap on the viewport
         */
        void draw(ViewPort p) {
            Bitmap bitmap = null;
            synchronized (this) {
                switch (getState()) {
                    case NOT_INITIALIZED:
                        // Error
                        Log.e(TAG, "Attempting to update an uninitialized CacheBitmap");
                        return;
                    case IS_INITIALIZED:
                        // Start data caching
                        setState(CacheState.BEGIN_UPDATE);
                        cacheThread_.interrupt();
                        break;
                    case BEGIN_UPDATE:
                    case IS_UPDATING:
                        // Currently updating; low resolution version used
                        break;
                    case DISABLED:
                        // Use of high resolution version disabled
                        break;
                    case READY:
                        if (bitmap_ == null) {
                            // No data loaded
                            setState(CacheState.BEGIN_UPDATE);
                            cacheThread_.interrupt();
                        } else if (!cacheWindow_.contains(p.window)) {
                            // No cached data available
                            setState(CacheState.BEGIN_UPDATE);
                            cacheThread_.interrupt();
                        } else {
                            bitmap = bitmap_;
                        }
                        break;
                }
            }
            // Use the low resolution version if the cache is empty or scale factor is < threshold
            if (bitmap == null) //|| (BitmapSurfaceRenderer.this.scaleFactor_ < BitmapSurfaceRenderer.this.lowResThreshold_))
                drawLowResolution();
            else
                drawHighResolution(bitmap);
        }

        /**
         * Used to hold the source Rect for bitmap drawing
         */
        private final Rect srcRect_ = new Rect(0, 0, 0, 0);
        /**
         * Used to hold the dest Rect for bitmap drawing
         */
        private final Rect dstRect_ = new Rect(0, 0, 0, 0);
        private final Point dstSize_ = new Point();

        /**
         * Use the high resolution cached bitmap for drawing
         */
        void drawHighResolution(Bitmap bitmap) {
            final Rect wSize = viewPort_.window;
            if (bitmap != null) {
                synchronized (viewPort_) {
                    final int left = wSize.left - cacheWindow_.left;
                    final int top = wSize.top - cacheWindow_.top;
                    final int right = left + wSize.width();
                    final int bottom = top + wSize.height();
                    viewPort_.getPhysicalSize(dstSize_);
                    srcRect_.set(left, top, right, bottom);
                    dstRect_.set(0, 0, dstSize_.x, dstSize_.y);
                    final Canvas canvas = new Canvas(viewPort_.bitmap_);
                    canvas.drawBitmap(bitmap, srcRect_, dstRect_, null);
                }
            }
        }

        void drawLowResolution() {
            if (getState() != CacheState.NOT_INITIALIZED) {
                synchronized (viewPort_) {
                    drawLowResolutionBackground(viewPort_.bitmap_, viewPort_.window);
                }
            }
        }

    }

    /**
     * This thread handles the background loading of the {@link CacheBitmap}.
     * <p/>
     * The CacheThread starts an update when the {@link CacheBitmap#state_} is
     * {@link CacheState#BEGIN_UPDATE} and updates the bitmap given the current window.
     * <p/>
     * The CacheThread needs to be careful how it locks {@link CacheBitmap} in order to ensure the
     * smoothest possible performance (loading can take a while).
     */
    class CacheThread extends Thread {
        private boolean isRunning_ = false;
        // The CacheBitmap
        private final CacheBitmap cache_;

        CacheThread(CacheBitmap cache) {
            setName("CacheThread");
            cache_ = cache;
        }

        @Override
        public void run() {
            isRunning_ = true;
            final Rect viewportRect = new Rect(0, 0, 0, 0);
            while (isRunning_) {
                // Wait until we are ready to go
                while (isRunning_ && cache_.getState() != CacheState.BEGIN_UPDATE) {
                    try {
                        Thread.sleep(Integer.MAX_VALUE);
                    } catch (InterruptedException e) {
                        // NOOP
                    }
                }
                if (!isRunning_) return;
                // Start Loading Timer
                final long startTime = System.currentTimeMillis();
                // Load Data
                boolean continueLoading = false;
                synchronized (cache_) {
                    if (cache_.getState() == CacheState.BEGIN_UPDATE) {
                        cache_.setState(CacheState.IS_UPDATING);
                        cache_.bitmap_ = null;
                        continueLoading = true;
                    }
                }
                if (continueLoading) {
                    synchronized (viewPort_) {
                        viewportRect.set(viewPort_.window);
                    }
                    synchronized (cache_) {
                        if (cache_.getState() == CacheState.IS_UPDATING)
                            cache_.cacheWindow_.set(calculateCacheDimensions(viewportRect));
                        else
                            continueLoading = false;
                    }
                    if (continueLoading) {
                        try {
                            final Bitmap bitmap = loadCachedBitmap(cache_.cacheWindow_);
                            if (bitmap != null) {
                                synchronized (cache_) {
                                    if (cache_.getState() == CacheState.IS_UPDATING) {
                                        cache_.bitmap_ = bitmap;
                                        cache_.setState(CacheState.READY);
                                    } else {
                                        Log.w(TAG, "Loading of background image cache aborted");
                                    }
                                }
                            }
                            // End Loading Timer
                            final long endTime = System.currentTimeMillis();
                            if (BuildConfig.DEBUG)
                                if (BuildConfig.DEBUG) Log.d(TAG, "Loaded background image in " + (endTime - startTime) + "ms");
                        } catch (OutOfMemoryError e) {
                            if (BuildConfig.DEBUG) Log.d(TAG, "CacheThread out of memory");
                            // Out of memory error detected. Lower the memory allocation
                            synchronized (cache_) {
                                cacheBitmapOutOfMemoryError(e);
                                if (cache_.getState() == CacheState.IS_UPDATING) {
                                    cache_.setState(CacheState.BEGIN_UPDATE);
                                }
                            }
                        }
                    }
                }
            }
        }

        @SuppressWarnings("SameParameterValue")
        public void setRunning(boolean b) {
            isRunning_ = b;
        }

    }

}




Java Source Code List

com.micabyte.android.ApplicationTest.java
com.micabyte.android.BaseObject.java
com.micabyte.android.app.BalloonPopup.java
com.micabyte.android.app.BaseActivity.java
com.micabyte.android.app.BaseFragment.java
com.micabyte.android.app.Popup.java
com.micabyte.android.graphics.BitmapSurfaceRenderer.java
com.micabyte.android.graphics.HexMapSurfaceRenderer.java
com.micabyte.android.graphics.ImageHandler.java
com.micabyte.android.graphics.MicaSurfaceView.java
com.micabyte.android.graphics.SurfaceListener.java
com.micabyte.android.graphics.SurfaceRenderer.java
com.micabyte.android.graphics.TileMapSurfaceRenderer.java
com.micabyte.android.map.HexMap.java
com.micabyte.android.map.TileMapZone.java
com.micabyte.android.map.TileMap.java
com.micabyte.android.math.Polygon.java
com.micabyte.android.media.MusicHandler.java
com.micabyte.android.util.GameHelperUtils.java
com.micabyte.android.util.GameHelper.java
com.micabyte.android.util.GameUtils.java
com.micabyte.android.util.RandomHandler.java
com.micabyte.android.util.StringHandler.java