ru.jango.j0widget.imagebrowser.ImageBrowserView.java Source code

Java tutorial

Introduction

Here is the source code for ru.jango.j0widget.imagebrowser.ImageBrowserView.java

Source

/*
 * The MIT License Copyright (c) 2014 Krayushkin Konstantin (jangokvk@gmail.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package ru.jango.j0widget.imagebrowser;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.OverScroller;

public class ImageBrowserView extends View {

    public static final float DEFAULT_MAX_ZOOM = 15f;

    private static final float AUTOZOOM_AMOUNT = 0.25f;

    private Bitmap bitmap;
    private float maxZoom;

    private final Paint bgPaint = new Paint();
    private final Rect contentRect = new Rect();
    private final Rect viewport = new Rect();

    // special vars for optimization: some methods need such objects (Rect and Point), so to not
    // create them every time, they are created here and reused where needed
    private final Rect rectBuffer = new Rect();
    private final Point pointBuffer = new Point();

    private ScaleGestureDetector scaleGestureDetector;
    private GestureDetectorCompat gestureDetector;
    private OverScroller scroller;
    private Zoomer zoomer;

    public ImageBrowserView(Context context) {
        super(context);
        init(context);
    }

    public ImageBrowserView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ImageBrowserView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    private void init(Context ctx) {
        scaleGestureDetector = new ScaleGestureDetector(ctx, new ScaleListener());
        gestureDetector = new GestureDetectorCompat(ctx, new GestureListener());
        scroller = new OverScroller(ctx);
        zoomer = new Zoomer(getResources().getInteger(android.R.integer.config_shortAnimTime));

        maxZoom = DEFAULT_MAX_ZOOM;
    }

    public void setImageBitmap(Bitmap bmp) {
        bitmap = bmp;
        if (bitmap == null)
            return;

        viewport.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
        bgPaint.setColor(bmp.getPixel(0, 0));

        ViewCompat.postInvalidateOnAnimation(this);
    }

    public void setImageDrawable(Drawable drawable) {
        if (drawable == null) {
            bitmap = null;
            return;
        }

        setImageBitmap(drawableToBitmap(drawable));
    }

    public void setImageResource(int res) {
        setImageBitmap(BitmapFactory.decodeResource(getResources(), res));
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public void setMaxZoom(float maxZoom) {
        this.maxZoom = maxZoom;
    }

    public float getMaxZoom() {
        return maxZoom;
    }

    private Bitmap drawableToBitmap(Drawable drawable) {
        if (drawable instanceof BitmapDrawable)
            return ((BitmapDrawable) drawable).getBitmap();

        int width = drawable.getIntrinsicWidth();
        width = width > 0 ? width : 1;
        int height = drawable.getIntrinsicHeight();
        height = height > 0 ? height : 1;

        final Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldW, int oldH) {
        super.onSizeChanged(w, h, oldW, oldH);
        contentRect.set(getPaddingLeft(), getPaddingTop(), w - getPaddingRight(), h - getPaddingBottom());
        checkViewportEmpty();
        checkViewportShapeAndBounds();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bitmap == null)
            return;

        checkViewportEmpty();
        makeViewportDestRect(rectBuffer);

        canvas.drawRect(contentRect, bgPaint);
        canvas.drawBitmap(bitmap, viewport, rectBuffer, null);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (bitmap == null)
            return false;

        return scaleGestureDetector.onTouchEvent(event) | gestureDetector.onTouchEvent(event)
                | super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (bitmap == null)
            return;

        if (scroller.computeScrollOffset()) {
            viewport.offset(scroller.getCurrX() - viewport.left, 0);
            viewport.offset(0, scroller.getCurrY() - viewport.top);

            checkViewportBounds();
            ViewCompat.postInvalidateOnAnimation(this);
        }

        if (zoomer.computeZoom()) {
            viewport.set(zoomer.getCurrentRect());
            checkViewportShapeAndBounds();
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    /**
     * Viewport determines some special part of the bitmap, that should be shown. But a View
     * itself also has paddings and margins. This method creates a rect, that determines, where
     * View's content should actually be drawn.
     *
     * @param ret object, through witch the result would be returned
     */
    private void makeViewportDestRect(final Rect ret) {
        ret.set(0, 0, 0, 0);
        ret.right = (int) Math.floor(viewport.width() / getViewportScaleFactor());
        ret.bottom = (int) Math.floor(viewport.height() / getViewportScaleFactor());

        if (contentRect.width() - ret.right > 1)
            ret.offset((int) Math.ceil((contentRect.width() - ret.right) / 2), 0);
        if (contentRect.height() - ret.bottom > 1)
            ret.offset(0, (int) Math.ceil((contentRect.height() - ret.bottom) / 2));
    }

    /**
     * Checks if viewport rect was initialized. If it wasn't - method initializes it.
     */
    private void checkViewportEmpty() {
        if (bitmap == null)
            return;
        if (!viewport.isEmpty())
            return;

        viewport.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
    }

    private double getViewportScaleFactor() {
        if (viewport.isEmpty() || contentRect.isEmpty())
            return 1d;

        double hScale = ((double) viewport.width()) / ((double) contentRect.width());
        double vScale = ((double) viewport.height()) / ((double) contentRect.height());
        return (viewport.height() > contentRect.height() || viewport.width() > contentRect.width())
                ? Math.max(vScale, hScale)
                : Math.min(vScale, hScale);
    }

    /**
     * Checks viewport bounds to be in acceptable range and position.
     * <ul>
     * <li>viewport must have the same proportions, as bitmap has</li>
     * <li>viewport must not be smaller special value, according to {@link #getMaxZoom()}</li>
     * <li>viewport bounds must not be greater than bitmap bounds</li>
     * <li>viewport position must be inside bitmap bounds (to show some part of the bitmap,
     * not just empty space)</li>
     * </ul>
     */
    private void checkViewportShapeAndBounds() {
        if (bitmap == null)
            return;

        calculateSize(viewport.width() * viewport.height(),
                ((float) contentRect.width()) / ((float) contentRect.height()), pointBuffer);

        viewport.set(viewport.centerX() - pointBuffer.x / 2, viewport.centerY() - pointBuffer.y / 2,
                viewport.centerX() + pointBuffer.x / 2, viewport.centerY() + pointBuffer.y / 2);

        checkViewportBounds();
    }

    /**
     * Checks viewport bounds to be in acceptable range and position.
     * <ul>
     * <li>viewport must not be smaller special value, according to {@link #getMaxZoom()}</li>
     * <li>viewport bounds must not be greater than bitmap bounds</li>
     * <li>viewport position must be inside bitmap bounds (to show some part of the bitmap,
     * not just empty space)</li>
     * </ul>
     */
    private void checkViewportBounds() {
        if (bitmap == null)
            return;

        if (viewport.width() < bitmap.getWidth() / maxZoom || viewport.height() < bitmap.getHeight() / maxZoom) {
            int spread = (int) Math.max(bitmap.getWidth() / maxZoom - viewport.width(),
                    (bitmap.getHeight() / maxZoom - viewport.height()));
            viewport.inset(-spread, -spread);
        }

        if (viewport.width() > bitmap.getWidth()) {
            viewport.left = 0;
            viewport.right = bitmap.getWidth();
        } else if (viewport.left < 0)
            viewport.offset(-viewport.left, 0);
        else if (viewport.right > bitmap.getWidth())
            viewport.offset(bitmap.getWidth() - viewport.right, 0);

        if (viewport.height() > bitmap.getHeight()) {
            viewport.top = 0;
            viewport.bottom = bitmap.getHeight();
        } else if (viewport.top < 0)
            viewport.offset(0, -viewport.top);
        else if (viewport.bottom > bitmap.getHeight())
            viewport.offset(0, bitmap.getHeight() - viewport.bottom);
    }

    /**
     * Calculates rectangle sides based on it's square and proportions. The point is to create a
     * new rect with the same square, but with the appropriate proportions of sides. This method
     * is a part of this process.
     *
     * @param square square of the source rectangle (width * height)
     * @param pro    sides factor of the source rectangle (width / height)
     * @param ret    object, through witch the result would be returned
     */
    private void calculateSize(float square, float pro, final Point ret) {
        float height = (float) Math.sqrt(square / pro);
        float width = height * pro;
        ret.set((int) width, (int) height);
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        private float initSpan;
        private final Rect initViewport = new Rect();

        @Override
        public boolean onScaleBegin(ScaleGestureDetector sgd) {
            initSpan = sgd.getCurrentSpan();
            initViewport.set(viewport);
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector sgd) {
            int dx = (int) ((sgd.getFocusX() - contentRect.exactCenterX()) * getViewportScaleFactor()
                    * (1f - 1 / sgd.getScaleFactor()));
            int dy = (int) ((sgd.getFocusY() - contentRect.exactCenterY()) * getViewportScaleFactor()
                    * (1f - 1 / sgd.getScaleFactor()));

            int centerX = viewport.centerX() + dx;
            int centerY = viewport.centerY() + dy;
            float scale = initSpan / sgd.getCurrentSpan();
            viewport.set(centerX - ((int) ((initViewport.width() * scale) / 2)),
                    centerY - ((int) ((initViewport.height() * scale) / 2)),
                    centerX + ((int) ((initViewport.width() * scale) / 2)),
                    centerY + ((int) ((initViewport.height() * scale) / 2)));

            checkViewportShapeAndBounds();
            ViewCompat.postInvalidateOnAnimation(ImageBrowserView.this);
            return true;
        }
    }

    private class GestureListener extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onDown(MotionEvent e) {
            scroller.forceFinished(true);
            ViewCompat.postInvalidateOnAnimation(ImageBrowserView.this);
            return true;
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            makeViewportDestRect(rectBuffer);

            int viewportX = viewport.left + ((int) ((e.getX() - rectBuffer.left) * getViewportScaleFactor()));
            int viewportY = viewport.top + ((int) ((e.getY() - rectBuffer.top) * getViewportScaleFactor()));
            if (viewportX < 0)
                viewportX = 0;
            if (viewportY < 0)
                viewportY = 0;

            zoomer.forceFinished(true);
            zoomer.startZoom(viewport, new Point(viewportX, viewportY), AUTOZOOM_AMOUNT);

            ViewCompat.postInvalidateOnAnimation(ImageBrowserView.this);
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            viewport.offset((int) (getViewportScaleFactor() * distanceX),
                    (int) (getViewportScaleFactor() * distanceY));
            checkViewportBounds();

            ViewCompat.postInvalidateOnAnimation(ImageBrowserView.this);
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            scroller.forceFinished(true);
            scroller.fling(viewport.left, viewport.top, (int) (-velocityX), (int) (-velocityY), 0,
                    bitmap.getWidth() - viewport.width(), 0, bitmap.getHeight() - viewport.height(), 0, 0);

            ViewCompat.postInvalidateOnAnimation(ImageBrowserView.this);
            return true;
        }
    }
}