alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Android example source code file (LolcatView.java)

This example Android source code file (LolcatView.java) is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Android by Example" TM.

Java - Android tags/keywords

action_down, android, bitmap, bottom, canvas, caption, computing, drawing, graphics, lolcatview, matrix, override, paint, rect, rectf, string, top, ui, utilities, utils

The LolcatView.java Android example source code

/*
 * Copyright (C) 2008 Google Inc.
 *
 * 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.android.lolcat;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ImageView;


/**
 * Lolcat-specific subclass of ImageView, which manages the various
 * scaled-down Bitmaps and knows how to render and manipulate the
 * image captions.
 */
public class LolcatView extends ImageView {
    private static final String TAG = "LolcatView";

    // Standard lolcat size is 500x375.  (But to preserve the original
    // image's aspect ratio, we rescale so that the larger dimension ends
    // up being 500 pixels.)
    private static final float SCALED_IMAGE_MAX_DIMENSION = 500f;

    // Other standard lolcat image parameters
    private static final int FONT_SIZE = 44;

    private Bitmap mScaledBitmap;  // The photo picked by the user, scaled-down
    private Bitmap mWorkingBitmap;  // The Bitmap we render the caption text into

    // Current state of the captions.
    // TODO: This array currently has a hardcoded length of 2 (for "top"
    // and "bottom" captions), but eventually should support as many
    // captions as the user wants to add.
    private final Caption[] mCaptions = new Caption[] { new Caption(), new Caption() };

    // State used while dragging a caption around
    private boolean mDragging;
    private int mDragCaptionIndex;  // index of the caption (in mCaptions[]) that's being dragged
    private int mTouchDownX, mTouchDownY;
    private final Rect mInitialDragBox = new Rect();
    private final Rect mCurrentDragBox = new Rect();
    private final RectF mCurrentDragBoxF = new RectF();  // used in onDraw()
    private final RectF mTransformedDragBoxF = new RectF();  // used in onDraw()
    private final Rect mTmpRect = new Rect();

    public LolcatView(Context context) {
        super(context);
    }

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

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

    public Bitmap getWorkingBitmap() {
        return mWorkingBitmap;
    }

    public String getTopCaption() {
        return mCaptions[0].caption;
    }

    public String getBottomCaption() {
        return mCaptions[1].caption;
    }

    /**
     * @return true if the user has set caption(s) for this LolcatView.
     */
    public boolean hasValidCaption() {
        return !TextUtils.isEmpty(mCaptions[0].caption)
                || !TextUtils.isEmpty(mCaptions[1].caption);
    }

    public void clear() {
        mScaledBitmap = null;
        mWorkingBitmap = null;
        setImageDrawable(null);

        // TODO: Anything else we need to do here to release resources
        // associated with this object, like maybe the Bitmap that got
        // created by the previous setImageURI() call?
    }

    public void loadFromUri(Uri uri) {
        // For now, directly load the specified Uri.
        setImageURI(uri);

        // TODO: Rather than calling setImageURI() with the URI of
        // the (full-size) photo, it would be better to turn the URI into
        // a scaled-down Bitmap right here, and load *that* into ourself.
        // I'd do that basically the same way that ImageView.setImageURI does it:
        //     [ . . . ]
        //     android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
        //     android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:304)
        //     android.graphics.drawable.Drawable.createFromStream(Drawable.java:635)
        //     android.widget.ImageView.resolveUri(ImageView.java:477)
        //     android.widget.ImageView.setImageURI(ImageView.java:281)
        //     [ . . . ]
        // But for now let's let ImageView do the work: we call setImageURI (above)
        // and immediately pull out a Bitmap (below).

        // Stash away a scaled-down bitmap.
        // TODO: is it safe to assume this will always be a BitmapDrawable?
        BitmapDrawable drawable = (BitmapDrawable) getDrawable();
        Log.i(TAG, "===> current drawable: " + drawable);

        Bitmap fullSizeBitmap = drawable.getBitmap();
        Log.i(TAG, "===> fullSizeBitmap: " + fullSizeBitmap
              + "  dimensions: " + fullSizeBitmap.getWidth()
              + " x " + fullSizeBitmap.getHeight());

        Bitmap.Config config = fullSizeBitmap.getConfig();
        Log.i(TAG, "  - config = " + config);

        // Standard lolcat size is 500x375.  But we don't want to distort
        // the image if it isn't 4x3, so let's just set the larger
        // dimension to 500 pixels and preserve the source aspect ratio.

        float origWidth = fullSizeBitmap.getWidth();
        float origHeight = fullSizeBitmap.getHeight();
        float aspect = origWidth / origHeight;
        Log.i(TAG, "  - aspect = " + aspect + "(" + origWidth + " x " + origHeight + ")");

        float scaleFactor = ((aspect > 1.0) ? origWidth : origHeight) / SCALED_IMAGE_MAX_DIMENSION;
        int scaledWidth = Math.round(origWidth / scaleFactor);
        int scaledHeight = Math.round(origHeight / scaleFactor);

        mScaledBitmap = Bitmap.createScaledBitmap(fullSizeBitmap,
                                                  scaledWidth,
                                                  scaledHeight,
                                                  true /* filter */);
        Log.i(TAG, "  ===> mScaledBitmap: " + mScaledBitmap
              + "  dimensions: " + mScaledBitmap.getWidth()
              + " x " + mScaledBitmap.getHeight());
        Log.i(TAG, "       isMutable = " + mScaledBitmap.isMutable());
    }

    /**
     * Sets the captions for this LolcatView.
     */
    public void setCaptions(String topCaption, String bottomCaption) {
        Log.i(TAG, "setCaptions: '" + topCaption + "', '" + bottomCaption + "'");
        if (topCaption == null) topCaption = "";
        if (bottomCaption == null) bottomCaption = "";

        mCaptions[0].caption = topCaption;
        mCaptions[1].caption = bottomCaption;

        // If the user clears a caption, reset its position (so that it'll
        // come back in the default position if the user re-adds it.)
        if (TextUtils.isEmpty(mCaptions[0].caption)) {
            Log.i(TAG, "- invalidating position of caption 0...");
            mCaptions[0].positionValid = false;
        }
        if (TextUtils.isEmpty(mCaptions[1].caption)) {
            Log.i(TAG, "- invalidating position of caption 1...");
            mCaptions[1].positionValid = false;
        }

        // And *any* time the captions change, blow away the cached
        // caption bounding boxes to make sure we'll recompute them in
        // renderCaptions().
        mCaptions[0].captionBoundingBox = null;
        mCaptions[1].captionBoundingBox = null;

        renderCaptions(mCaptions);
    }

    /**
     * Clears the captions for this LolcatView.
     */
    public void clearCaptions() {
        setCaptions("", "");
    }

    /**
     * Renders this LolcatView's current image captions into our
     * underlying ImageView.
     *
     * We start with a scaled-down version of the photo originally chosed
     * by the user (mScaledBitmap), make a mutable copy (mWorkingBitmap),
     * render the specified strings into the bitmap, and show the
     * resulting image onscreen.
     */
    public void renderCaptions(Caption[] captions) {
        // TODO: handle an arbitrary array of strings, rather than
        // assuming "top" and "bottom" captions.

        String topString = captions[0].caption;
        boolean topStringValid = !TextUtils.isEmpty(topString);

        String bottomString = captions[1].caption;
        boolean bottomStringValid = !TextUtils.isEmpty(bottomString);

        Log.i(TAG, "renderCaptions: '" + topString + "', '" + bottomString + "'");

        if (mScaledBitmap == null) return;

        // Make a fresh (mutable) copy of the scaled-down photo Bitmap,
        // and render the desired text into it.

        Bitmap.Config config = mScaledBitmap.getConfig();
        Log.i(TAG, "  - mScaledBitmap config = " + config);

        mWorkingBitmap = mScaledBitmap.copy(config, true /* isMutable */);
        Log.i(TAG, "  ===> mWorkingBitmap: " + mWorkingBitmap
              + "  dimensions: " + mWorkingBitmap.getWidth()
              + " x " + mWorkingBitmap.getHeight());
        Log.i(TAG, "       isMutable = " + mWorkingBitmap.isMutable());

        Canvas canvas = new Canvas(mWorkingBitmap);
        Log.i(TAG, "- Canvas: " + canvas
              + "  dimensions: " + canvas.getWidth() + " x " + canvas.getHeight());

        Paint textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(FONT_SIZE);
        textPaint.setColor(0xFFFFFFFF);
        Log.i(TAG, "- Paint: " + textPaint);

        Typeface face = textPaint.getTypeface();
        Log.i(TAG, "- default typeface: " + face);

        // The most standard font for lolcat captions is Impact.  (Arial
        // Black is also common.)  Unfortunately we don't have either of
        // these on the device by default; the closest we can do is
        // DroidSans-Bold:
        face = Typeface.DEFAULT_BOLD;
        Log.i(TAG, "- new face: " + face);
        textPaint.setTypeface(face);

        // Look up the positions of the captions, or if this is our very
        // first time rendering them, initialize the positions to default
        // values.

        final int edgeBorder = 20;
        final int fontHeight = textPaint.getFontMetricsInt(null);
        Log.i(TAG, "- fontHeight: " + fontHeight);

        Log.i(TAG, "- Caption positioning:");
        int topX = 0;
        int topY = 0;
        if (topStringValid) {
            if (mCaptions[0].positionValid) {
                topX = mCaptions[0].xpos;
                topY = mCaptions[0].ypos;
                Log.i(TAG, "  - TOP: already had a valid position: " + topX + ", " + topY);
            } else {
                // Start off with the "top" caption at the upper-left:
                topX = edgeBorder;
                topY = edgeBorder + (fontHeight * 3 / 4);
                mCaptions[0].setPosition(topX, topY);
                Log.i(TAG, "  - TOP: initializing to default position: " + topX + ", " + topY);
            }
        }

        int bottomX = 0;
        int bottomY = 0;
        if (bottomStringValid) {
            if (mCaptions[1].positionValid) {
                bottomX = mCaptions[1].xpos;
                bottomY = mCaptions[1].ypos;
                Log.i(TAG, "  - Bottom: already had a valid position: "
                      + bottomX + ", " + bottomY);
            } else {
                // Start off with the "bottom" caption at the lower-right:
                final int bottomTextWidth = (int) textPaint.measureText(bottomString);
                Log.i(TAG, "- bottomTextWidth (" + bottomString + "): " + bottomTextWidth);
                bottomX = canvas.getWidth() - edgeBorder - bottomTextWidth;
                bottomY = canvas.getHeight() - edgeBorder;
                mCaptions[1].setPosition(bottomX, bottomY);
                Log.i(TAG, "  - BOTTOM: initializing to default position: "
                      + bottomX + ", " + bottomY);
            }
        }

        // Finally, render the text.

        // Standard lolcat captions are drawn in white with a heavy black
        // outline (i.e. white fill, black stroke).  Our Canvas APIs can't
        // do this exactly, though.
        // We *could* get something decent-looking using a regular
        // drop-shadow, like this:
        //   textPaint.setShadowLayer(3.0f, 3, 3, 0xff000000);
        // but instead let's simulate the "outline" style by drawing the
        // text 4 separate times, with the shadow in a different direction
        // each time.
        // (TODO: This is a hack, and still doesn't look as good
        // as a real "white fill, black stroke" style.)

        final float shadowRadius = 2.0f;
        final int shadowOffset = 2;
        final int shadowColor = 0xff000000;

        // TODO: Right now we use offsets of 2,2 / -2,2 / 2,-2 / -2,-2 .
        // But 2,0 / 0,2 / -2,0 / 0,-2 might look better.

        textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, shadowColor);
        if (topStringValid) canvas.drawText(topString, topX, topY, textPaint);
        if (bottomStringValid) canvas.drawText(bottomString, bottomX, bottomY, textPaint);
        //
        textPaint.setShadowLayer(shadowRadius, -shadowOffset, shadowOffset, shadowColor);
        if (topStringValid) canvas.drawText(topString, topX, topY, textPaint);
        if (bottomStringValid) canvas.drawText(bottomString, bottomX, bottomY, textPaint);
        //
        textPaint.setShadowLayer(shadowRadius, shadowOffset, -shadowOffset, shadowColor);
        if (topStringValid) canvas.drawText(topString, topX, topY, textPaint);
        if (bottomStringValid) canvas.drawText(bottomString, bottomX, bottomY, textPaint);
        //
        textPaint.setShadowLayer(shadowRadius, -shadowOffset, -shadowOffset, shadowColor);
        if (topStringValid) canvas.drawText(topString, topX, topY, textPaint);
        if (bottomStringValid) canvas.drawText(bottomString, bottomX, bottomY, textPaint);

        // Stash away bounding boxes for the captions if this
        // is our first time rendering them.
        // Watch out: the x/y position we use for drawing the text is
        // actually the *lower* left corner of the bounding box...

        int textWidth, textHeight;

        if (topStringValid && mCaptions[0].captionBoundingBox == null) {
            Log.i(TAG, "- Computing initial bounding box for top caption...");
            textPaint.getTextBounds(topString, 0, topString.length(), mTmpRect);
            textWidth = mTmpRect.width();
            textHeight = mTmpRect.height();
            Log.i(TAG, "-  text dimensions: " + textWidth + " x " + textHeight);
            mCaptions[0].captionBoundingBox = new Rect(topX, topY - textHeight,
                                                       topX + textWidth, topY);
            Log.i(TAG, "-   RESULTING RECT: " + mCaptions[0].captionBoundingBox);
        }
        if (bottomStringValid && mCaptions[1].captionBoundingBox == null) {
            Log.i(TAG, "- Computing initial bounding box for bottom caption...");
            textPaint.getTextBounds(bottomString, 0, bottomString.length(), mTmpRect);
            textWidth = mTmpRect.width();
            textHeight = mTmpRect.height();
            Log.i(TAG, "-  text dimensions: " + textWidth + " x " + textHeight);
            mCaptions[1].captionBoundingBox = new Rect(bottomX, bottomY - textHeight,
                                                       bottomX + textWidth, bottomY);
            Log.i(TAG, "-   RESULTING RECT: " + mCaptions[1].captionBoundingBox);
        }

        // Finally, display the new Bitmap to the user:
        setImageBitmap(mWorkingBitmap);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Log.i(TAG, "onDraw: " + canvas);
        super.onDraw(canvas);

        if (mDragging) {
            Log.i(TAG, "- dragging!  Drawing box at " + mCurrentDragBox);

            // mCurrentDragBox is in the coordinate system of our bitmap;
            // need to convert it into the coordinate system of the
            // overall LolcatView.
            //
            // To transform between coordinate systems we need to apply the
            // transformation described by the ImageView's matrix *and* also
            // account for our left and top padding.

            Matrix m = getImageMatrix();

            mCurrentDragBoxF.set(mCurrentDragBox);
            m.mapRect(mTransformedDragBoxF, mCurrentDragBoxF);
            mTransformedDragBoxF.offset(getPaddingLeft(), getPaddingTop());

            Paint p = new Paint();
            p.setColor(0xFFFFFFFF);
            p.setStyle(Paint.Style.STROKE);
            p.setStrokeWidth(2f);
            Log.i(TAG, "- Paint: " + p);

            canvas.drawRect(mTransformedDragBoxF, p);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.i(TAG, "onTouchEvent: " + ev);

        // Watch out: ev.getX() and ev.getY() are in the
        // coordinate system of the entire LolcatView, although
        // all the positions and rects we use here (like
        // mCaptions[].captionBoundingBox) are relative to the bitmap
        // that's drawn inside the LolcatView.
        //
        // To transform between coordinate systems we need to apply the
        // transformation described by the ImageView's matrix *and* also
        // account for our left and top padding.

        Matrix m = getImageMatrix();

        Matrix invertedMatrix = new Matrix();
        m.invert(invertedMatrix);

        float[] pointArray = new float[] { ev.getX() - getPaddingLeft(),
                                           ev.getY() - getPaddingTop() };
        Log.i(TAG, "  - BEFORE: pointArray = " + pointArray[0] + ", " + pointArray[1]);

        // Transform the X/Y position of the DOWN event back into bitmap coords
        invertedMatrix.mapPoints(pointArray);
        Log.i(TAG, "  - AFTER:  pointArray = " + pointArray[0] + ", " + pointArray[1]);

        int eventX = (int) pointArray[0];
        int eventY = (int) pointArray[1];

        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (mDragging) {
                    Log.w(TAG, "Got an ACTION_DOWN, but we were already dragging!");
                    mDragging = false;  // and continue as if we weren't already dragging...
                }
                if (!hasValidCaption()) {
                    Log.w(TAG, "No caption(s) yet; ignoring this ACTION_DOWN event.");
                    return true;
                }

                // See if this DOWN event hit one of the caption bounding
                // boxes.  If so, start dragging!
                for (int i = 0; i < mCaptions.length; i++) {
                    Rect boundingBox = mCaptions[i].captionBoundingBox;
                    Log.i(TAG, "  - boundingBox #" + i + ": " + boundingBox + "...");

                    if (boundingBox != null) {
                        // Expand the bounding box by a fudge factor to make it
                        // easier to hit (since touch accuracy is pretty poor on a
                        // real device, and the captions are fairly small...)
                        mTmpRect.set(boundingBox);

                        final int touchPositionSlop = 40;  // pixels
                        mTmpRect.inset(-touchPositionSlop, -touchPositionSlop);

                        Log.i(TAG, "  - Checking expanded bounding box #" + i
                              + ": " + mTmpRect + "...");
                        if (mTmpRect.contains(eventX, eventY)) {
                            Log.i(TAG, "    - Hit! " + mCaptions[i]);
                            mDragging = true;
                            mDragCaptionIndex = i;
                            break;
                        }
                    }
                }
                if (!mDragging) {
                    Log.i(TAG, "- ACTION_DOWN event didn't hit any captions; ignoring.");
                    return true;
                }

                mTouchDownX = eventX;
                mTouchDownY = eventY;

                mInitialDragBox.set(mCaptions[mDragCaptionIndex].captionBoundingBox);
                mCurrentDragBox.set(mCaptions[mDragCaptionIndex].captionBoundingBox);

                invalidate();

                return true;

            case MotionEvent.ACTION_MOVE:
                if (!mDragging) {
                    return true;
                }

                int displacementX = eventX - mTouchDownX;
                int displacementY = eventY - mTouchDownY;

                mCurrentDragBox.set(mInitialDragBox);
                mCurrentDragBox.offset(displacementX, displacementY);

                invalidate();

                return true;

            case MotionEvent.ACTION_UP:
                if (!mDragging) {
                    return true;
                }

                mDragging = false;

                // Reposition the selected caption!
                Log.i(TAG, "- Done dragging!  Repositioning caption #" + mDragCaptionIndex + ": "
                      + mCaptions[mDragCaptionIndex]);

                int offsetX = eventX - mTouchDownX;
                int offsetY = eventY - mTouchDownY;
                Log.i(TAG, "  - OFFSET: " + offsetX + ", " + offsetY);

                // Reposition the the caption we just dragged, and blow
                // away the cached bounding box to make sure it'll get
                // recomputed in renderCaptions().
                mCaptions[mDragCaptionIndex].xpos += offsetX;
                mCaptions[mDragCaptionIndex].ypos += offsetY;
                mCaptions[mDragCaptionIndex].captionBoundingBox = null;

                Log.i(TAG, "  - Updated caption: " + mCaptions[mDragCaptionIndex]);

                // Finally, refresh the screen.
                renderCaptions(mCaptions);
                return true;

            // This case isn't expected to happen.
            case MotionEvent.ACTION_CANCEL:
                if (!mDragging) {
                    return true;
                }

                mDragging = false;
                // Refresh the screen.
                renderCaptions(mCaptions);
                return true;

            default:
                return super.onTouchEvent(ev);
        }
    }

    /**
     * Returns an array containing the xpos/ypos of each Caption in our
     * array of captions.  (This method and setCaptionPositions() are used
     * by LolcatActivity to save and restore the activity state across
     * orientation changes.)
     */
    public int[] getCaptionPositions() {
        // TODO: mCaptions currently has a hardcoded length of 2 (for
        // "top" and "bottom" captions).
        int[] captionPositions = new int[4];

        if (mCaptions[0].positionValid) {
            captionPositions[0] = mCaptions[0].xpos;
            captionPositions[1] = mCaptions[0].ypos;
        } else {
            captionPositions[0] = -1;
            captionPositions[1] = -1;
        }

        if (mCaptions[1].positionValid) {
            captionPositions[2] = mCaptions[1].xpos;
            captionPositions[3] = mCaptions[1].ypos;
        } else {
            captionPositions[2] = -1;
            captionPositions[3] = -1;
        }

        Log.i(TAG, "getCaptionPositions: returning " + captionPositions);
        return captionPositions;
    }

    /**
     * Sets the xpos and ypos values of each Caption in our array based on
     * the specified values.  (This method and getCaptionPositions() are
     * used by LolcatActivity to save and restore the activity state
     * across orientation changes.)
     */
    public void setCaptionPositions(int[] captionPositions) {
        // TODO: mCaptions currently has a hardcoded length of 2 (for
        // "top" and "bottom" captions).

        Log.i(TAG, "setCaptionPositions(" + captionPositions + ")...");

        if (captionPositions[0] < 0) {
            mCaptions[0].positionValid = false;
            Log.i(TAG, "- TOP caption: no valid position");
        } else {
            mCaptions[0].setPosition(captionPositions[0], captionPositions[1]);
            Log.i(TAG, "- TOP caption: got valid position: "
                  + mCaptions[0].xpos + ", " + mCaptions[0].ypos);
        }

        if (captionPositions[2] < 0) {
            mCaptions[1].positionValid = false;
            Log.i(TAG, "- BOTTOM caption: no valid position");
        } else {
            mCaptions[1].setPosition(captionPositions[2], captionPositions[3]);
            Log.i(TAG, "- BOTTOM caption: got valid position: "
                  + mCaptions[1].xpos + ", " + mCaptions[1].ypos);
        }

        // Finally, refresh the screen.
        renderCaptions(mCaptions);
    }

    /**
     * Structure used to hold the entire state of a single caption.
     */
    class Caption {
        public String caption;
        public Rect captionBoundingBox;  // updated by renderCaptions()
        public int xpos, ypos;
        public boolean positionValid;

        public void setPosition(int x, int y) {
            positionValid = true;
            xpos = x;
            ypos = y;
            // Also blow away the cached bounding box, to make sure it'll
            // get recomputed in renderCaptions().
            captionBoundingBox = null;
        }

        @Override
        public String toString() {
            return "Caption['" + caption + "'; bbox " + captionBoundingBox
                    + "; pos " + xpos + ", " + ypos + "; posValid = " + positionValid + "]";
        }
    }

}

Other Android examples (source code examples)

Here is a short list of links related to this Android LolcatView.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.