Pastebin launched a little side project called VERYVIRAL.com, check it out ;-) Want more features on Pastebin? Sign Up, it's FREE!
Guest

Untitled

By: a guest on Nov 26th, 2012  |  syntax: None  |  size: 26.08 KB  |  views: 85  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  1. /*
  2.  * Copyright (C) 2008 Google Inc.
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *      http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */
  16.  
  17. package cap.shot;
  18.  
  19. import android.content.Context;
  20. import android.graphics.Bitmap;
  21. import android.graphics.Canvas;
  22. import android.graphics.Matrix;
  23. import android.graphics.Paint;
  24. import android.graphics.Rect;
  25. import android.graphics.RectF;
  26. import android.graphics.Typeface;
  27. import android.graphics.drawable.BitmapDrawable;
  28. import android.net.Uri;
  29. import android.text.TextUtils;
  30. import android.util.AttributeSet;
  31. import android.util.Log;
  32. import android.view.MotionEvent;
  33. import android.widget.ImageView;
  34.  
  35.  
  36. /**
  37.  * Lolcat-specific subclass of ImageView, which manages the various
  38.  * scaled-down Bitmaps and knows how to render and manipulate the
  39.  * image captions.
  40.  */
  41. public class LolcatView extends ImageView {
  42.     private static final String TAG = "LolcatView";
  43.  
  44.    
  45.     // Standard lolcat size is 500x375.  (But to preserve the original
  46.     // image's aspect ratio, we rescale so that the larger dimension ends
  47.     // up being 500 pixels.)
  48.     private static final float SCALED_IMAGE_MAX_DIMENSION = 500f;
  49.  
  50.     // Other standard lolcat image parameters
  51.     private static final int FONT_SIZE = 44;
  52.  
  53.     private Bitmap mScaledBitmap;  // The photo picked by the user, scaled-down
  54.     private Bitmap mWorkingBitmap;  // The Bitmap we render the caption text into
  55.  
  56.     // Current state of the captions.
  57.     // TODO: This array currently has a hardcoded length of 2 (for "top"
  58.     // and "bottom" captions), but eventually should support as many
  59.     // captions as the user wants to add.
  60.     private final Caption[] mCaptions = new Caption[] { new Caption(), new Caption() };
  61.  
  62.     // State used while dragging a caption around
  63.     private boolean mDragging;
  64.     private int mDragCaptionIndex;  // index of the caption (in mCaptions[]) that's being dragged
  65.     private int mTouchDownX, mTouchDownY;
  66.     private final Rect mInitialDragBox = new Rect();
  67.     private final Rect mCurrentDragBox = new Rect();
  68.     private final RectF mCurrentDragBoxF = new RectF();  // used in onDraw()
  69.     private final RectF mTransformedDragBoxF = new RectF();  // used in onDraw()
  70.     private final Rect mTmpRect = new Rect();
  71.  
  72.     public LolcatView(Context context) {
  73.         super(context);
  74.     }
  75.  
  76.     public LolcatView(Context context, AttributeSet attrs) {
  77.         super(context, attrs);
  78.     }
  79.  
  80.     public LolcatView(Context context, AttributeSet attrs, int defStyle) {
  81.         super(context, attrs, defStyle);
  82.     }
  83.  
  84.     public Bitmap getWorkingBitmap() {
  85.         return mWorkingBitmap;
  86.     }
  87.  
  88.     public String getTopCaption() {
  89.         return mCaptions[0].caption;
  90.     }
  91.  
  92.     public String getBottomCaption() {
  93.         return mCaptions[1].caption;
  94.     }
  95.  
  96.     /**
  97.      * @return true if the user has set caption(s) for this LolcatView.
  98.      */
  99.     public boolean hasValidCaption() {
  100.         return !TextUtils.isEmpty(mCaptions[0].caption)
  101.                 || !TextUtils.isEmpty(mCaptions[1].caption);
  102.     }
  103.  
  104.     public void clear() {
  105.         mScaledBitmap = null;
  106.         mWorkingBitmap = null;
  107.         setImageDrawable(null);
  108.  
  109.         // TODO: Anything else we need to do here to release resources
  110.         // associated with this object, like maybe the Bitmap that got
  111.         // created by the previous setImageURI() call?
  112.     }
  113.  
  114.     public void loadFromUri(Uri uri) {
  115.         // For now, directly load the specified Uri.
  116.         setImageURI(uri);
  117.  
  118.         // TODO: Rather than calling setImageURI() with the URI of
  119.         // the (full-size) photo, it would be better to turn the URI into
  120.         // a scaled-down Bitmap right here, and load *that* into ourself.
  121.         // I'd do that basically the same way that ImageView.setImageURI does it:
  122.         //     [ . . . ]
  123.         //     android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
  124.         //     android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:304)
  125.         //     android.graphics.drawable.Drawable.createFromStream(Drawable.java:635)
  126.         //     android.widget.ImageView.resolveUri(ImageView.java:477)
  127.         //     android.widget.ImageView.setImageURI(ImageView.java:281)
  128.         //     [ . . . ]
  129.         // But for now let's let ImageView do the work: we call setImageURI (above)
  130.         // and immediately pull out a Bitmap (below).
  131.  
  132.         // Stash away a scaled-down bitmap.
  133.         // TODO: is it safe to assume this will always be a BitmapDrawable?
  134.         BitmapDrawable drawable = (BitmapDrawable) getDrawable();
  135.         Log.i(TAG, "===> current drawable: " + drawable);
  136.  
  137.         Bitmap fullSizeBitmap = drawable.getBitmap();
  138.         Log.i(TAG, "===> fullSizeBitmap: " + fullSizeBitmap
  139.               + "  dimensions: " + fullSizeBitmap.getWidth()
  140.               + " x " + fullSizeBitmap.getHeight());
  141.  
  142.         Bitmap.Config config = fullSizeBitmap.getConfig();
  143.         Log.i(TAG, "  - config = " + config);
  144.  
  145.         // Standard lolcat size is 500x375.  But we don't want to distort
  146.         // the image if it isn't 4x3, so let's just set the larger
  147.         // dimension to 500 pixels and preserve the source aspect ratio.
  148.  
  149.         float origWidth = fullSizeBitmap.getWidth();
  150.         float origHeight = fullSizeBitmap.getHeight();
  151.         float aspect = origWidth / origHeight;
  152.         Log.i(TAG, "  - aspect = " + aspect + "(" + origWidth + " x " + origHeight + ")");
  153.  
  154.         float scaleFactor = ((aspect > 1.0) ? origWidth : origHeight) / SCALED_IMAGE_MAX_DIMENSION;
  155.         int scaledWidth = Math.round(origWidth / scaleFactor);
  156.         int scaledHeight = Math.round(origHeight / scaleFactor);
  157.  
  158.         mScaledBitmap = Bitmap.createScaledBitmap(fullSizeBitmap,
  159.                                                   scaledWidth,
  160.                                                   scaledHeight,
  161.                                                   true /* filter */);
  162.         Log.i(TAG, "  ===> mScaledBitmap: " + mScaledBitmap
  163.               + "  dimensions: " + mScaledBitmap.getWidth()
  164.               + " x " + mScaledBitmap.getHeight());
  165.         Log.i(TAG, "       isMutable = " + mScaledBitmap.isMutable());
  166.     }
  167.  
  168.     /**
  169.      * Sets the captions for this LolcatView.
  170.      */
  171.     public void setCaptions(String topCaption, String bottomCaption) {
  172.         Log.i(TAG, "setCaptions: '" + topCaption + "', '" + bottomCaption + "'");
  173.         if (topCaption == null) topCaption = "";
  174.         if (bottomCaption == null) bottomCaption = "";
  175.  
  176.         mCaptions[0].caption = topCaption;
  177.         mCaptions[1].caption = bottomCaption;
  178.  
  179.         // If the user clears a caption, reset its position (so that it'll
  180.         // come back in the default position if the user re-adds it.)
  181.         if (TextUtils.isEmpty(mCaptions[0].caption)) {
  182.             Log.i(TAG, "- invalidating position of caption 0...");
  183.             mCaptions[0].positionValid = false;
  184.         }
  185.         if (TextUtils.isEmpty(mCaptions[1].caption)) {
  186.             Log.i(TAG, "- invalidating position of caption 1...");
  187.             mCaptions[1].positionValid = false;
  188.         }
  189.  
  190.         // And *any* time the captions change, blow away the cached
  191.         // caption bounding boxes to make sure we'll recompute them in
  192.         // renderCaptions().
  193.         mCaptions[0].captionBoundingBox = null;
  194.         mCaptions[1].captionBoundingBox = null;
  195.  
  196.         renderCaptions(mCaptions);
  197.     }
  198.  
  199.     /**
  200.      * Clears the captions for this LolcatView.
  201.      */
  202.     public void clearCaptions() {
  203.         setCaptions("", "");
  204.     }
  205.  
  206.     /**
  207.      * Renders this LolcatView's current image captions into our
  208.      * underlying ImageView.
  209.      *
  210.      * We start with a scaled-down version of the photo originally chosed
  211.      * by the user (mScaledBitmap), make a mutable copy (mWorkingBitmap),
  212.      * render the specified strings into the bitmap, and show the
  213.      * resulting image onscreen.
  214.      * @return
  215.      */
  216.     public void renderCaptions(Caption[] captions) {
  217.        
  218.         // TODO: handle an arbitrary array of strings, rather than
  219.         // assuming "top" and "bottom" captions.
  220.  
  221.         String topString = captions[0].caption;
  222.         boolean topStringValid = !TextUtils.isEmpty(topString);
  223.  
  224.         String bottomString = captions[1].caption;
  225.         boolean bottomStringValid = !TextUtils.isEmpty(bottomString);
  226.  
  227.         Log.i(TAG, "renderCaptions: '" + topString + "', '" + bottomString + "'");
  228.  
  229.         if (mScaledBitmap == null) return;
  230.  
  231.         // Make a fresh (mutable) copy of the scaled-down photo Bitmap,
  232.         // and render the desired text into it.
  233.  
  234.         Bitmap.Config config = mScaledBitmap.getConfig();
  235.         Log.i(TAG, "  - mScaledBitmap config = " + config);
  236.  
  237.         mWorkingBitmap = mScaledBitmap.copy(config, true /* isMutable */);
  238.         Log.i(TAG, "  ===> mWorkingBitmap: " + mWorkingBitmap
  239.               + "  dimensions: " + mWorkingBitmap.getWidth()
  240.               + " x " + mWorkingBitmap.getHeight());
  241.         Log.i(TAG, "       isMutable = " + mWorkingBitmap.isMutable());
  242.  
  243.         Canvas canvas = new Canvas(mWorkingBitmap);
  244.         Log.i(TAG, "- Canvas: " + canvas
  245.               + "  dimensions: " + canvas.getWidth() + " x " + canvas.getHeight());
  246.  
  247.         Paint textPaint = new Paint();
  248.         textPaint.setAntiAlias(true);
  249.         textPaint.setTextSize(FONT_SIZE);
  250.         textPaint.setColor(0xFFFFFFFF);
  251.         Log.i(TAG, "- Paint: " + textPaint);
  252.  
  253.         Typeface face = textPaint.getTypeface();
  254.         Log.i(TAG, "- default typeface: " + face);
  255.  
  256.         // The most standard font for lolcat captions is Impact.  (Arial
  257.         // Black is also common.)  Unfortunately we don't have either of
  258.         // these on the device by default; the closest we can do is
  259.         // DroidSans-Bold:
  260.      
  261.         Typeface  mFace = Typeface.createFromAsset(getContext().getAssets(),"fonts/impact.ttf");
  262.         Paint mPaint = new Paint ();
  263.         mPaint.setTypeface(mFace);
  264.         Log.i(TAG, "- new face: " + face);
  265.         textPaint.setTypeface(mFace);
  266.  
  267.         // Look up the positions of the captions, or if this is our very
  268.         // first time rendering them, initialize the positions to default
  269.         // values.
  270.  
  271.         final int edgeBorder = 20;
  272.         final int fontHeight = textPaint.getFontMetricsInt(null);
  273.         Log.i(TAG, "- fontHeight: " + fontHeight);
  274.  
  275.         Log.i(TAG, "- Caption positioning:");
  276.         int topX = 0;
  277.         int topY = 0;
  278.         if (topStringValid) {
  279.             if (mCaptions[0].positionValid) {
  280.                 topX = mCaptions[0].xpos;
  281.                 topY = mCaptions[0].ypos;
  282.                 Log.i(TAG, "  - TOP: already had a valid position: " + topX + ", " + topY);
  283.             } else {
  284.                 // Start off with the "top" caption at the upper-left:
  285.                 topX = edgeBorder;
  286.                 topY = edgeBorder + (fontHeight * 3 / 4);
  287.                 mCaptions[0].setPosition(topX, topY);
  288.                 Log.i(TAG, "  - TOP: initializing to default position: " + topX + ", " + topY);
  289.             }
  290.         }
  291.  
  292.         int bottomX = 0;
  293.         int bottomY = 0;
  294.         if (bottomStringValid) {
  295.             if (mCaptions[1].positionValid) {
  296.                 bottomX = mCaptions[1].xpos;
  297.                 bottomY = mCaptions[1].ypos;
  298.                 Log.i(TAG, "  - Bottom: already had a valid position: "
  299.                       + bottomX + ", " + bottomY);
  300.             } else {
  301.                 // Start off with the "bottom" caption at the lower-right:
  302.                 final int bottomTextWidth = (int) textPaint.measureText(bottomString);
  303.                 Log.i(TAG, "- bottomTextWidth (" + bottomString + "): " + bottomTextWidth);
  304.                 bottomX = canvas.getWidth() - edgeBorder - bottomTextWidth;
  305.                 bottomY = canvas.getHeight() - edgeBorder;
  306.                 mCaptions[1].setPosition(bottomX, bottomY);
  307.                 Log.i(TAG, "  - BOTTOM: initializing to default position: "
  308.                       + bottomX + ", " + bottomY);
  309.             }
  310.         }
  311.  
  312.         // Finally, render the text.
  313.  
  314.         // Standard lolcat captions are drawn in white with a heavy black
  315.         // outline (i.e. white fill, black stroke).  Our Canvas APIs can't
  316.         // do this exactly, though.
  317.         // We *could* get something decent-looking using a regular
  318.         // drop-shadow, like this:
  319.         //   textPaint.setShadowLayer(3.0f, 3, 3, 0xff000000);
  320.         // but instead let's simulate the "outline" style by drawing the
  321.         // text 4 separate times, with the shadow in a different direction
  322.         // each time.
  323.         // (TODO: This is a hack, and still doesn't look as good
  324.         // as a real "white fill, black stroke" style.)
  325.  
  326.         final float shadowRadius = 2.0f;
  327.         final int shadowOffset = 2;
  328.         final int shadowColor = 0xff000000;
  329.  
  330.         // TODO: Right now we use offsets of 2,2 / -2,2 / 2,-2 / -2,-2 .
  331.         // But 2,0 / 0,2 / -2,0 / 0,-2 might look better.
  332.  
  333.         textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, shadowColor);
  334.         if (topStringValid) canvas.drawText(topString, topX, topY, textPaint);
  335.         if (bottomStringValid) canvas.drawText(bottomString, bottomX, bottomY, textPaint);
  336.         //
  337.         textPaint.setShadowLayer(shadowRadius, -shadowOffset, shadowOffset, shadowColor);
  338.         if (topStringValid) canvas.drawText(topString, topX, topY, textPaint);
  339.         if (bottomStringValid) canvas.drawText(bottomString, bottomX, bottomY, textPaint);
  340.         //
  341.         textPaint.setShadowLayer(shadowRadius, shadowOffset, -shadowOffset, shadowColor);
  342.         if (topStringValid) canvas.drawText(topString, topX, topY, textPaint);
  343.         if (bottomStringValid) canvas.drawText(bottomString, bottomX, bottomY, textPaint);
  344.         //
  345.         textPaint.setShadowLayer(shadowRadius, -shadowOffset, -shadowOffset, shadowColor);
  346.         if (topStringValid) canvas.drawText(topString, topX, topY, textPaint);
  347.         if (bottomStringValid) canvas.drawText(bottomString, bottomX, bottomY, textPaint);
  348.  
  349.         // Stash away bounding boxes for the captions if this
  350.         // is our first time rendering them.
  351.         // Watch out: the x/y position we use for drawing the text is
  352.         // actually the *lower* left corner of the bounding box...
  353.  
  354.         int textWidth, textHeight;
  355.  
  356.         if (topStringValid && mCaptions[0].captionBoundingBox == null) {
  357.             Log.i(TAG, "- Computing initial bounding box for top caption...");
  358.             textPaint.getTextBounds(topString, 0, topString.length(), mTmpRect);
  359.             textWidth = mTmpRect.width();
  360.             textHeight = mTmpRect.height();
  361.             Log.i(TAG, "-  text dimensions: " + textWidth + " x " + textHeight);
  362.             mCaptions[0].captionBoundingBox = new Rect(topX, topY - textHeight,
  363.                                                        topX + textWidth, topY);
  364.             Log.i(TAG, "-   RESULTING RECT: " + mCaptions[0].captionBoundingBox);
  365.         }
  366.         if (bottomStringValid && mCaptions[1].captionBoundingBox == null) {
  367.             Log.i(TAG, "- Computing initial bounding box for bottom caption...");
  368.             textPaint.getTextBounds(bottomString, 0, bottomString.length(), mTmpRect);
  369.             textWidth = mTmpRect.width();
  370.             textHeight = mTmpRect.height();
  371.             Log.i(TAG, "-  text dimensions: " + textWidth + " x " + textHeight);
  372.             mCaptions[1].captionBoundingBox = new Rect(bottomX, bottomY - textHeight,
  373.                                                        bottomX + textWidth, bottomY);
  374.             Log.i(TAG, "-   RESULTING RECT: " + mCaptions[1].captionBoundingBox);
  375.         }
  376.  
  377.         // Finally, display the new Bitmap to the user:
  378.         setImageBitmap(mWorkingBitmap);
  379.     }
  380.  
  381.         @Override
  382.     protected void onDraw(Canvas canvas) {
  383.         Log.i(TAG, "onDraw: " + canvas);
  384.         super.onDraw(canvas);
  385.  
  386.         if (mDragging) {
  387.             Log.i(TAG, "- dragging!  Drawing box at " + mCurrentDragBox);
  388.  
  389.             // mCurrentDragBox is in the coordinate system of our bitmap;
  390.             // need to convert it into the coordinate system of the
  391.             // overall LolcatView.
  392.             //
  393.             // To transform between coordinate systems we need to apply the
  394.             // transformation described by the ImageView's matrix *and* also
  395.             // account for our left and top padding.
  396.  
  397.             Matrix m = getImageMatrix();
  398.  
  399.             mCurrentDragBoxF.set(mCurrentDragBox);
  400.             m.mapRect(mTransformedDragBoxF, mCurrentDragBoxF);
  401.             mTransformedDragBoxF.offset(getPaddingLeft(), getPaddingTop());
  402.  
  403.             Paint p = new Paint();
  404.             p.setColor(0xFFFFFFFF);
  405.             p.setStyle(Paint.Style.STROKE);
  406.             p.setStrokeWidth(2f);
  407.             Log.i(TAG, "- Paint: " + p);
  408.  
  409.             canvas.drawRect(mTransformedDragBoxF, p);
  410.         }
  411.     }
  412.  
  413.     @Override
  414.     public boolean onTouchEvent(MotionEvent ev) {
  415.         Log.i(TAG, "onTouchEvent: " + ev);
  416.  
  417.         // Watch out: ev.getX() and ev.getY() are in the
  418.         // coordinate system of the entire LolcatView, although
  419.         // all the positions and rects we use here (like
  420.         // mCaptions[].captionBoundingBox) are relative to the bitmap
  421.         // that's drawn inside the LolcatView.
  422.         //
  423.         // To transform between coordinate systems we need to apply the
  424.         // transformation described by the ImageView's matrix *and* also
  425.         // account for our left and top padding.
  426.  
  427.         Matrix m = getImageMatrix();
  428.  
  429.         Matrix invertedMatrix = new Matrix();
  430.         m.invert(invertedMatrix);
  431.  
  432.         float[] pointArray = new float[] { ev.getX() - getPaddingLeft(),
  433.                                            ev.getY() - getPaddingTop() };
  434.         Log.i(TAG, "  - BEFORE: pointArray = " + pointArray[0] + ", " + pointArray[1]);
  435.  
  436.         // Transform the X/Y position of the DOWN event back into bitmap coords
  437.         invertedMatrix.mapPoints(pointArray);
  438.         Log.i(TAG, "  - AFTER:  pointArray = " + pointArray[0] + ", " + pointArray[1]);
  439.  
  440.         int eventX = (int) pointArray[0];
  441.         int eventY = (int) pointArray[1];
  442.  
  443.         int action = ev.getAction();
  444.         switch (action) {
  445.             case MotionEvent.ACTION_DOWN:
  446.                 if (mDragging) {
  447.                     Log.w(TAG, "Got an ACTION_DOWN, but we were already dragging!");
  448.                     mDragging = false;  // and continue as if we weren't already dragging...
  449.                 }
  450.                 if (!hasValidCaption()) {
  451.                     Log.w(TAG, "No caption(s) yet; ignoring this ACTION_DOWN event.");
  452.                     return true;
  453.                 }
  454.  
  455.                 // See if this DOWN event hit one of the caption bounding
  456.                 // boxes.  If so, start dragging!
  457.                 for (int i = 0; i < mCaptions.length; i++) {
  458.                     Rect boundingBox = mCaptions[i].captionBoundingBox;
  459.                     Log.i(TAG, "  - boundingBox #" + i + ": " + boundingBox + "...");
  460.  
  461.                     if (boundingBox != null) {
  462.                         // Expand the bounding box by a fudge factor to make it
  463.                         // easier to hit (since touch accuracy is pretty poor on a
  464.                         // real device, and the captions are fairly small...)
  465.                         mTmpRect.set(boundingBox);
  466.  
  467.                         final int touchPositionSlop = 40;  // pixels
  468.                         mTmpRect.inset(-touchPositionSlop, -touchPositionSlop);
  469.  
  470.                         Log.i(TAG, "  - Checking expanded bounding box #" + i
  471.                               + ": " + mTmpRect + "...");
  472.                         if (mTmpRect.contains(eventX, eventY)) {
  473.                             Log.i(TAG, "    - Hit! " + mCaptions[i]);
  474.                             mDragging = true;
  475.                             mDragCaptionIndex = i;
  476.                             break;
  477.                         }
  478.                     }
  479.                 }
  480.                 if (!mDragging) {
  481.                     Log.i(TAG, "- ACTION_DOWN event didn't hit any captions; ignoring.");
  482.                     return true;
  483.                 }
  484.  
  485.                 mTouchDownX = eventX;
  486.                 mTouchDownY = eventY;
  487.  
  488.                 mInitialDragBox.set(mCaptions[mDragCaptionIndex].captionBoundingBox);
  489.                 mCurrentDragBox.set(mCaptions[mDragCaptionIndex].captionBoundingBox);
  490.  
  491.                 invalidate();
  492.  
  493.                 return true;
  494.  
  495.             case MotionEvent.ACTION_MOVE:
  496.                 if (!mDragging) {
  497.                     return true;
  498.                 }
  499.  
  500.                 int displacementX = eventX - mTouchDownX;
  501.                 int displacementY = eventY - mTouchDownY;
  502.  
  503.                 mCurrentDragBox.set(mInitialDragBox);
  504.                 mCurrentDragBox.offset(displacementX, displacementY);
  505.  
  506.                 invalidate();
  507.  
  508.                 return true;
  509.  
  510.             case MotionEvent.ACTION_UP:
  511.                 if (!mDragging) {
  512.                     return true;
  513.                 }
  514.  
  515.                 mDragging = false;
  516.  
  517.                 // Reposition the selected caption!
  518.                 Log.i(TAG, "- Done dragging!  Repositioning caption #" + mDragCaptionIndex + ": "
  519.                       + mCaptions[mDragCaptionIndex]);
  520.  
  521.                 int offsetX = eventX - mTouchDownX;
  522.                 int offsetY = eventY - mTouchDownY;
  523.                 Log.i(TAG, "  - OFFSET: " + offsetX + ", " + offsetY);
  524.  
  525.                 // Reposition the the caption we just dragged, and blow
  526.                 // away the cached bounding box to make sure it'll get
  527.                 // recomputed in renderCaptions().
  528.                 mCaptions[mDragCaptionIndex].xpos += offsetX;
  529.                 mCaptions[mDragCaptionIndex].ypos += offsetY;
  530.                 mCaptions[mDragCaptionIndex].captionBoundingBox = null;
  531.  
  532.                 Log.i(TAG, "  - Updated caption: " + mCaptions[mDragCaptionIndex]);
  533.  
  534.                 // Finally, refresh the screen.
  535.                 renderCaptions(mCaptions);
  536.                 return true;
  537.  
  538.             // This case isn't expected to happen.
  539.             case MotionEvent.ACTION_CANCEL:
  540.                 if (!mDragging) {
  541.                     return true;
  542.                 }
  543.  
  544.                 mDragging = false;
  545.                 // Refresh the screen.
  546.                 renderCaptions(mCaptions);
  547.                 return true;
  548.  
  549.             default:
  550.                 return super.onTouchEvent(ev);
  551.         }
  552.     }
  553.  
  554.     /**
  555.      * Returns an array containing the xpos/ypos of each Caption in our
  556.      * array of captions.  (This method and setCaptionPositions() are used
  557.      * by LolcatActivity to save and restore the activity state across
  558.      * orientation changes.)
  559.      */
  560.     public int[] getCaptionPositions() {
  561.         // TODO: mCaptions currently has a hardcoded length of 2 (for
  562.         // "top" and "bottom" captions).
  563.         int[] captionPositions = new int[4];
  564.  
  565.         if (mCaptions[0].positionValid) {
  566.             captionPositions[0] = mCaptions[0].xpos;
  567.             captionPositions[1] = mCaptions[0].ypos;
  568.         } else {
  569.             captionPositions[0] = -1;
  570.             captionPositions[1] = -1;
  571.         }
  572.  
  573.         if (mCaptions[1].positionValid) {
  574.             captionPositions[2] = mCaptions[1].xpos;
  575.             captionPositions[3] = mCaptions[1].ypos;
  576.         } else {
  577.             captionPositions[2] = -1;
  578.             captionPositions[3] = -1;
  579.         }
  580.  
  581.         Log.i(TAG, "getCaptionPositions: returning " + captionPositions);
  582.         return captionPositions;
  583.     }
  584.  
  585.     /**
  586.      * Sets the xpos and ypos values of each Caption in our array based on
  587.      * the specified values.  (This method and getCaptionPositions() are
  588.      * used by LolcatActivity to save and restore the activity state
  589.      * across orientation changes.)
  590.      */
  591.     public void setCaptionPositions(int[] captionPositions) {
  592.         // TODO: mCaptions currently has a hardcoded length of 2 (for
  593.         // "top" and "bottom" captions).
  594.  
  595.         Log.i(TAG, "setCaptionPositions(" + captionPositions + ")...");
  596.  
  597.         if (captionPositions[0] < 0) {
  598.             mCaptions[0].positionValid = false;
  599.             Log.i(TAG, "- TOP caption: no valid position");
  600.         } else {
  601.             mCaptions[0].setPosition(captionPositions[0], captionPositions[1]);
  602.             Log.i(TAG, "- TOP caption: got valid position: "
  603.                   + mCaptions[0].xpos + ", " + mCaptions[0].ypos);
  604.         }
  605.  
  606.         if (captionPositions[2] < 0) {
  607.             mCaptions[1].positionValid = false;
  608.             Log.i(TAG, "- BOTTOM caption: no valid position");
  609.         } else {
  610.             mCaptions[1].setPosition(captionPositions[2], captionPositions[3]);
  611.             Log.i(TAG, "- BOTTOM caption: got valid position: "
  612.                   + mCaptions[1].xpos + ", " + mCaptions[1].ypos);
  613.         }
  614.  
  615.         // Finally, refresh the screen.
  616.         renderCaptions(mCaptions);
  617.     }
  618.  
  619.     /**
  620.      * Structure used to hold the entire state of a single caption.
  621.      */
  622.     class Caption {
  623.         public String caption;
  624.         public Rect captionBoundingBox;  // updated by renderCaptions()
  625.         public int xpos, ypos;
  626.         public boolean positionValid;
  627.  
  628.         public void setPosition(int x, int y) {
  629.             positionValid = true;
  630.             xpos = x;
  631.             ypos = y;
  632.             // Also blow away the cached bounding box, to make sure it'll
  633.             // get recomputed in renderCaptions().
  634.             captionBoundingBox = null;
  635.         }
  636.  
  637.         @Override
  638.         public String toString() {
  639.             return "Caption['" + caption + "'; bbox " + captionBoundingBox
  640.                     + "; pos " + xpos + ", " + ypos + "; posValid = " + positionValid + "]";
  641.         }
  642.     }
  643.  
  644. }