Created
April 6, 2012 19:49
-
-
Save shashi/2322461 to your computer and use it in GitHub Desktop.
Canvas with camera background
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2009 Google Inc. All Rights Reserved. | |
package com.google.appinventor.components.runtime; | |
import com.google.appinventor.components.annotations.DesignerComponent; | |
import com.google.appinventor.components.annotations.DesignerProperty; | |
import com.google.appinventor.components.annotations.PropertyCategory; | |
import com.google.appinventor.components.annotations.SimpleEvent; | |
import com.google.appinventor.components.annotations.SimpleFunction; | |
import com.google.appinventor.components.annotations.SimpleObject; | |
import com.google.appinventor.components.annotations.SimpleProperty; | |
import com.google.appinventor.components.annotations.UsesPermissions; | |
import com.google.appinventor.components.common.ComponentCategory; | |
import com.google.appinventor.components.common.ComponentConstants; | |
import com.google.appinventor.components.common.YaVersion; | |
import com.google.appinventor.components.runtime.util.BoundingBox; | |
import com.google.appinventor.components.runtime.util.ErrorMessages; | |
import com.google.appinventor.components.runtime.util.FileUtil; | |
import com.google.appinventor.components.runtime.util.MediaUtil; | |
import com.google.appinventor.components.runtime.util.PaintUtil; | |
import android.app.Activity; | |
import android.content.Context; | |
import android.graphics.Bitmap; | |
import android.graphics.Color; | |
import android.graphics.Paint; | |
import android.graphics.PorterDuff; | |
import android.graphics.Rect; | |
import android.graphics.RectF; | |
import android.graphics.drawable.BitmapDrawable; | |
import android.text.TextUtils; | |
import android.util.Log; | |
import android.view.MotionEvent; | |
import android.view.View; | |
import java.io.File; | |
import java.io.FileNotFoundException; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.util.ArrayList; | |
import java.util.LinkedList; | |
import java.util.List; | |
/** | |
* <p>A two-dimensional touch-sensitive rectangular panel on which drawing can | |
* be done and sprites can be moved.</p> | |
* | |
* <p>Conceptually, a sprite consists of the following layers, from back | |
* to front (with items in front being drawn on top): | |
* <ul> | |
* <li> background color | |
* <li> background image | |
* <li> the "drawing layer", populated through calls to | |
* {@link #DrawPoint(int,int)}, {@link #DrawCircle(int,int,float)}, | |
* {@link #DrawText(String,int,int)}, and | |
* {@link #DrawTextAtAngle(String,int,int,float)}, and | |
* {@link #SetBackgroundPixelColor(int,int,int)} | |
* <li> the sprite layer, where sprites with higher Z values are drawn | |
* in front of (after) sprites with lower Z values. | |
* </ul> | |
* To the user, the first three layers are all the background, in terms | |
* of the behavior of {@link #SetBackgroundPixelColor(int,int,int)} and | |
* {@link #GetBackgroundPixelColor(int,int)}. For historical reasons, | |
* changing the background color or image clears the drawing layer | |
* (@link #clearDrawingLayer()}. | |
* | |
*/ | |
@DesignerComponent(version = YaVersion.CANVAS_COMPONENT_VERSION, | |
description = "<p>A two-dimensional touch-sensitive rectangular panel on " + | |
"which drawing can be done and sprites can be moved.</p> " + | |
"<p>The <code>BackgroundColor</code>, <code>PaintColor</code>, " + | |
"<code>BackgroundImage</code>, <code>Width</code>, and " + | |
"<code>Height</code> of the Canvas can be set in either the Designer or " + | |
"in the Blocks Editor. The <code>Width</code> and <code>Height</code> " + | |
"are measured in pixels and must be positive.</p>" + | |
"<p>Any location on the Canvas can be specified as a pair of " + | |
"(X, Y) values, where <ul> " + | |
"<li>X is the number of pixels away from the left edge of the Canvas</li>" + | |
"<li>Y is the number of pixels away from the top edge of the Canvas</li>" + | |
"</ul>.</p> " + | |
"<p>There are events to tell when and where a Canvas has been touched or " + | |
"a <code>Sprite</code> (<code>ImageSprite</code> or <code>Ball</code>) " + | |
"has been dragged. There are also methods for drawing points, lines, " + | |
"and circles.</p>", | |
category = ComponentCategory.BASIC) | |
@SimpleObject | |
@UsesPermissions(permissionNames = "android.permission.INTERNET," + | |
"android.permission.WRITE_EXTERNAL_STORAGE") | |
public final class Canvas extends AndroidViewComponent implements ComponentContainer { | |
private static final String LOG_TAG = "Canvas"; | |
private final Activity context; | |
private final CanvasView view; | |
// Android can't correctly give the width and height of a canvas until | |
// something has been drawn on it. | |
private boolean drawn; | |
// Variables behind properties | |
private int paintColor; | |
private final Paint paint; | |
private int backgroundColor; | |
private String backgroundImagePath = ""; | |
private int backgroundCameraId = -1; | |
private int textAlignment; | |
// Default values | |
private static final float DEFAULT_LINE_WIDTH = 2; | |
private static final int DEFAULT_PAINT_COLOR = Component.COLOR_BLACK; | |
private static final int DEFAULT_BACKGROUND_COLOR = Component.COLOR_WHITE; | |
// Keep track of enclosed sprites. This list should always be | |
// sorted by increasing sprite.Z(). | |
private final List<Sprite> sprites; | |
// Handle touches and drags | |
private final MotionEventParser motionEventParser; | |
/** | |
* Parser for Android {@link android.view.MotionEvent} sequences, which calls | |
* the appropriate event handlers. Specifically: | |
* <ul> | |
* <li> If a {@link android.view.MotionEvent#ACTION_DOWN} is followed by one | |
* or more {@link android.view.MotionEvent#ACTION_MOVE} events, a sequence of | |
* {@link Sprite#Dragged(float, float, float, float, float, float)} | |
* calls are generated for sprites that were touched, and the final | |
* {@link android.view.MotionEvent#ACTION_UP} is ignored. | |
* | |
* <li> If a {@link android.view.MotionEvent#ACTION_DOWN} is followed by an | |
* {@link android.view.MotionEvent#ACTION_UP} event either immediately or | |
* after {@link android.view.MotionEvent#ACTION_MOVE} events that take it no | |
* further than {@link #TAP_THRESHOLD} pixels horizontally or vertically from | |
* the start point, it is interpreted as a touch, and a single call to | |
* {@link Sprite#Touched(float, float)} for each touched sprite is | |
* generated. | |
* </ul> | |
* | |
* After the {@code Dragged()} or {@code Touched()} methods are called for | |
* any applicable sprites, a call is made to | |
* {@link Canvas#Dragged(float, float, float, float, float, float, boolean)} | |
* or {@link Canvas#Touched(float, float, boolean)}, respectively. The | |
* additional final argument indicates whether it was preceded by one or | |
* more calls to a sprite, i.e., whether the locations on the canvas had a | |
* sprite on them ({@code true}) or were empty of sprites {@code false}). | |
* | |
* | |
*/ | |
class MotionEventParser { | |
/** | |
* The number of pixels right, left, up, or down, a sequence of drags must | |
* move from the starting point to be considered a drag (instead of a | |
* touch). | |
*/ | |
public static final int TAP_THRESHOLD = 30; | |
/** | |
* The width of a finger. This is used in determining whether a sprite is | |
* touched. Specifically, this is used to determine the horizontal extent | |
* of a bounding box that is tested for collision with each sprite. The | |
* vertical extent is determined by {@link #FINGER_HEIGHT}. | |
*/ | |
public static final int FINGER_WIDTH = 24; | |
/** | |
* The width of a finger. This is used in determining whether a sprite is | |
* touched. Specifically, this is used to determine the vertical extent | |
* of a bounding box that is tested for collision with each sprite. The | |
* horizontal extent is determined by {@link #FINGER_WIDTH}. | |
*/ | |
public static final int FINGER_HEIGHT = 24; | |
private static final int HALF_FINGER_WIDTH = FINGER_WIDTH / 2; | |
private static final int HALF_FINGER_HEIGHT = FINGER_HEIGHT / 2; | |
/** | |
* The set of sprites encountered in a touch or drag sequence. Checks are | |
* only made for sprites at the endpoints of each drag. | |
*/ | |
private final List<Sprite> draggedSprites = new ArrayList<Sprite>(); | |
// startX and startY hold the coordinates of where a touch/drag started | |
private static final int UNSET = -1; | |
private float startX = UNSET; | |
private float startY = UNSET; | |
// lastX and lastY hold the coordinates of the previous step of a drag | |
private float lastX = UNSET; | |
private float lastY = UNSET; | |
// Is this sequence of events a drag? I.e., has the touch point moved away | |
// from the start point? | |
private boolean isDrag = false; | |
private boolean drag = false; | |
void parse(MotionEvent event) { | |
int width = Width(); | |
int height = Height(); | |
// Coordinates less than 0 can be returned if a move begins within a | |
// view and ends outside of it. Because negative coordinates would | |
// probably confuse the user (as they did me) and would not be useful, | |
// we replace any negative values with zero. | |
float x = Math.max(0, (int) event.getX()); | |
float y = Math.max(0, (int) event.getY()); | |
// Also make sure that by adding or subtracting a half finger that | |
// we don't go out of bounds. | |
BoundingBox rect = new BoundingBox( | |
Math.max(0, (int) x - HALF_FINGER_HEIGHT), | |
Math.max(0, (int) y - HALF_FINGER_WIDTH), | |
Math.min(width - 1, (int) x + HALF_FINGER_WIDTH), | |
Math.min(height - 1, (int) y + HALF_FINGER_HEIGHT)); | |
switch (event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
draggedSprites.clear(); | |
startX = x; | |
startY = y; | |
lastX = x; | |
lastY = y; | |
drag = false; | |
isDrag = false; | |
for (Sprite sprite : sprites) { | |
if (sprite.Enabled() && sprite.Visible() && sprite.intersectsWith(rect)) { | |
draggedSprites.add(sprite); | |
} | |
} | |
break; | |
case MotionEvent.ACTION_MOVE: | |
// Ensure that this was preceded by an ACTION_DOWN | |
if (startX == UNSET || startY == UNSET || lastX == UNSET || lastY == UNSET) { | |
Log.w(LOG_TAG, "In Canvas.MotionEventParser.parse(), " + | |
"an ACTION_MOVE was passed without a preceding ACTION_DOWN: " + event); | |
} | |
// If the new point is near the start point, it may just be a tap | |
if (!isDrag && | |
(Math.abs(x - startX) < TAP_THRESHOLD && Math.abs(y - startY) < TAP_THRESHOLD)) { | |
break; | |
} | |
// Otherwise, it's a drag. | |
isDrag = true; | |
drag = true; | |
// Update draggedSprites by adding any that are currently being | |
// touched. | |
for (Sprite sprite : sprites) { | |
if (!draggedSprites.contains(sprite) | |
&& sprite.Enabled() && sprite.Visible() | |
&& sprite.intersectsWith(rect)) { | |
draggedSprites.add(sprite); | |
} | |
} | |
// Raise a Dragged event for any affected sprites | |
boolean handled = false; | |
for (Sprite sprite : draggedSprites) { | |
if (sprite.Enabled() && sprite.Visible()) { | |
sprite.Dragged(startX, startY, lastX, lastY, x, y); | |
handled = true; | |
} | |
} | |
// Last argument indicates whether a sprite handled the drag | |
Dragged(startX, startY, lastX, lastY, x, y, handled); | |
lastX = x; | |
lastY = y; | |
break; | |
case MotionEvent.ACTION_UP: | |
// If we never strayed far from the start point, it's a tap. (If we | |
// did stray far, we've already handled the movements in the ACTION_MOVE | |
// case.) | |
if (!drag) { | |
// It's a tap | |
handled = false; | |
for (Sprite sprite : draggedSprites) { | |
if (sprite.Enabled() && sprite.Visible()) { | |
sprite.Touched(startX, startY); | |
handled = true; | |
} | |
} | |
// Last argument indicates that one or more sprites handled the tap | |
Touched(startX, startY, handled); | |
} | |
// Prepare for next drag | |
drag = false; | |
startX = UNSET; | |
startY = UNSET; | |
lastX = UNSET; | |
lastY = UNSET; | |
break; | |
} | |
} | |
} | |
/** | |
* Panel for drawing and manipulating sprites. | |
* | |
*/ | |
private final class CanvasView extends View { | |
// Variables to implement View | |
private android.graphics.Canvas canvas; | |
private Bitmap bitmap; // Bitmap backing Canvas | |
// Support for background images | |
private BitmapDrawable backgroundDrawable; | |
// Support for camera in background. | |
private SurfaceView backgroundSurfaceHolder; | |
// Support for camera in background. | |
private SurfaceView backgroundSurface; | |
// Support for GetBackgroundPixelColor() and GetPixelColor(). | |
// scaledBackgroundBitmap is a scaled version of backgroundDrawable that | |
// is created only if getBackgroundPixelColor() is called. It is set back | |
// to null whenever the canvas size or backgroundDrawable changes. | |
private Bitmap scaledBackgroundBitmap; | |
// completeCache is created if the user calls getPixelColor(). It is set | |
// back to null whenever the view is redrawn. If available, it is used | |
// when the Canvas is saved to a file. | |
private Bitmap completeCache; | |
public CanvasView(Context context) { | |
super(context); | |
bitmap = Bitmap.createBitmap(ComponentConstants.CANVAS_PREFERRED_WIDTH, | |
ComponentConstants.CANVAS_PREFERRED_HEIGHT, | |
Bitmap.Config.ARGB_8888); | |
canvas = new android.graphics.Canvas(bitmap); | |
} | |
/* | |
* Create a bitmap showing the background (image or color) and drawing | |
* (points, lines, circles, text) layer of the view but not any sprites. | |
*/ | |
private Bitmap buildCache() { | |
// First, try building drawing cache. | |
setDrawingCacheEnabled(true); | |
destroyDrawingCache(); // clear any earlier versions we have requested | |
Bitmap cache = getDrawingCache(); // may return null if size is too large | |
// If drawing cache can't be built, build a cache manually. | |
if (cache == null) { | |
int width = getWidth(); | |
int height = getHeight(); | |
cache = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); | |
android.graphics.Canvas c = new android.graphics.Canvas(cache); | |
layout(0, 0, width, height); | |
draw(c); | |
} | |
return cache; | |
} | |
@Override | |
public void onDraw(android.graphics.Canvas canvas0) { | |
completeCache = null; | |
// This will draw the background image and color, if present. | |
super.onDraw(canvas0); | |
// Redraw anything that had been directly drawn on the old Canvas, | |
// such as lines and circles but not Sprites. | |
canvas0.drawBitmap(bitmap, 0, 0, null); | |
// sprites is sorted by Z level, so sprites with low Z values will be | |
// drawn first, potentially being hidden by Sprites with higher Z values. | |
for (Sprite sprite : sprites) { | |
sprite.onDraw(canvas0); | |
} | |
drawn = true; | |
} | |
@Override | |
protected void onSizeChanged(int w, int h, int oldW, int oldH) { | |
int oldBitmapWidth = bitmap.getWidth(); | |
int oldBitmapHeight = bitmap.getHeight(); | |
if (w != oldBitmapWidth || h != oldBitmapHeight) { | |
Bitmap oldBitmap = bitmap; | |
// Create a new bitmap by scaling the old bitmap that contained the | |
// drawing layer (points, lines, text, etc.). | |
// The documentation for Bitmap.createScaledBitmap doesn't specify whether it creates a | |
// mutable or immutable bitmap. Looking at the source code shows that it calls | |
// Bitmap.createBitmap(Bitmap, int, int, int, int, Matrix, boolean), which is documented as | |
// returning an immutable bitmap. However, it actually returns a mutable bitmap. | |
// It's possible that the behavior could change in the future if they "fix" that bug. | |
// Try Bitmap.createScaledBitmap, but if it gives us an immutable bitmap, we'll have to | |
// create a mutable bitmap and scale the old bitmap using Canvas.drawBitmap. | |
Bitmap scaledBitmap = Bitmap.createScaledBitmap(oldBitmap, w, h, false); | |
if (scaledBitmap.isMutable()) { | |
// scaledBitmap is mutable; we can use it in a canvas. | |
bitmap = scaledBitmap; | |
// NOTE(lizlooney) - I tried just doing canvas.setBitmap(bitmap), but after that the | |
// canvas.drawCircle() method did not work correctly. So, we need to create a whole new | |
// canvas. | |
canvas = new android.graphics.Canvas(bitmap); | |
} else { | |
// scaledBitmap is immutable; we can't use it in a canvas. | |
bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); | |
// NOTE(lizlooney) - I tried just doing canvas.setBitmap(bitmap), but after that the | |
// canvas.drawCircle() method did not work correctly. So, we need to create a whole new | |
// canvas. | |
canvas = new android.graphics.Canvas(bitmap); | |
// Draw the old bitmap into the new canvas, scaling as necessary. | |
Rect src = new Rect(0, 0, oldBitmapWidth, oldBitmapHeight); | |
RectF dst = new RectF(0, 0, w, h); | |
canvas.drawBitmap(oldBitmap, src, dst, null); | |
} | |
// The following has nothing to do with the scaling in this method. | |
// It has to do with scaling the background image for GetColor(). | |
// Specifically, it says we need to regenerate the bitmap representing | |
// the background color/image if a call to GetColor() is made. | |
scaledBackgroundBitmap = null; | |
} | |
} | |
@Override | |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
int preferredWidth; | |
int preferredHeight; | |
if (backgroundDrawable != null) { | |
// Drawable.getIntrinsicWidth/Height gives weird values, but Bitmap.getWidth/Height works. | |
Bitmap bitmap = backgroundDrawable.getBitmap(); | |
preferredWidth = bitmap.getWidth(); | |
preferredHeight = bitmap.getHeight(); | |
} else { | |
preferredWidth = ComponentConstants.CANVAS_PREFERRED_WIDTH; | |
preferredHeight = ComponentConstants.CANVAS_PREFERRED_HEIGHT; | |
} | |
setMeasuredDimension(getSize(widthMeasureSpec, preferredWidth), | |
getSize(heightMeasureSpec, preferredHeight)); | |
} | |
private int getSize(int measureSpec, int preferredSize) { | |
int result; | |
int specMode = MeasureSpec.getMode(measureSpec); | |
int specSize = MeasureSpec.getSize(measureSpec); | |
if (specMode == MeasureSpec.EXACTLY) { | |
// We were told how big to be | |
result = specSize; | |
} else { | |
// Use the preferred size. | |
result = preferredSize; | |
if (specMode == MeasureSpec.AT_MOST) { | |
// Respect AT_MOST value if that was what is called for by measureSpec | |
result = Math.min(result, specSize); | |
} | |
} | |
return result; | |
} | |
@Override | |
public boolean onTouchEvent(MotionEvent event) { | |
// The following call results in the Form not grabbing our events and | |
// handling dragging on its own, which it wants to do to handle scrolling. | |
// Its effect only lasts long as the current set of motion events | |
// generated during this touch and drag sequence. Consequently, it needs | |
// to be called here, so that it happens for each touch-drag sequence. | |
container.$form().dontGrabTouchEventsForComponent(); | |
motionEventParser.parse(event); | |
return true; | |
} | |
// Methods supporting properties | |
// This mutates backgroundImagePath in the outer class | |
// and backgroundDrawable in this class. | |
// | |
// This erases the drawing layer (lines, text, etc.), whether or not | |
// a valid image is loaded, to be compatible with earlier versions | |
// of App Inventor. | |
void setBackgroundImage(String path) { | |
backgroundImagePath = (path == null) ? "" : path; | |
backgroundDrawable = null; | |
scaledBackgroundBitmap = null; | |
if (!TextUtils.isEmpty(backgroundImagePath)) { | |
if (path.startsWith("camera://")) { | |
if (Camera.getNumberOfCameras() > 0) { | |
backgroundSurfaceHolder = getCameraSurfaceHolder(); | |
backgroundCameraId = Integer.parseInt(path.substring(7)); | |
try { | |
backgroundCamera = Camera.open(backgroundCameraId); | |
backgroundCamera.setPreviewDisplay(backgroundSurfaceHolder); | |
} catch { | |
throw new RuntimeException("Couldn't open the camera"); | |
} | |
} else { | |
throw new UnsupportedOperationException("There was no camera found on this device."); | |
} | |
} else { | |
try { | |
backgroundDrawable = MediaUtil.getBitmapDrawable(container.$form(), backgroundImagePath); | |
} catch (IOException ioe) { | |
Log.e(LOG_TAG, "Unable to load " + backgroundImagePath); | |
} | |
} | |
} | |
setBackgroundDrawable(backgroundDrawable); | |
// If the path was null or the empty string, or if IOException was | |
// raised, backgroundDrawable will be null. The only difference | |
// from the case of a successful image load is that we must draw | |
// in the background color, if present. | |
if (backgroundDrawable == null) { | |
super.setBackgroundColor(backgroundColor); | |
} | |
clearDrawingLayer(); // will call invalidate() | |
} | |
private void clearDrawingLayer() { | |
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); | |
invalidate(); | |
} | |
// This mutates backgroundColor in the outer class. | |
// This erases the drawing layer (lines, text, etc.) to be compatible | |
// with earlier versions of App Inventor. | |
@Override | |
public void setBackgroundColor(int color) { | |
backgroundColor = color; | |
// Only draw the background color if no image. | |
if (backgroundDrawable == null) { | |
super.setBackgroundColor(color); | |
} | |
clearDrawingLayer(); | |
} | |
// These methods support SimpleFunctions. | |
private void drawTextAtAngle(String text, int x, int y, float angle) { | |
canvas.save(); | |
canvas.rotate(-angle, x, y); | |
canvas.drawText(text, x, y, paint); | |
canvas.restore(); | |
invalidate(); | |
} | |
// This intentionally ignores sprites. | |
private int getBackgroundPixelColor(int x, int y) { | |
// If the request is out of bounds, return COLOR_NONE. | |
if (x < 0 || x >= bitmap.getWidth() || | |
y < 0 || y >= bitmap.getHeight()) { | |
return Component.COLOR_NONE; | |
} | |
try { | |
// First check if anything has been drawn on the bitmap | |
// (such as by DrawPoint, DrawCircle, etc.). | |
int color = bitmap.getPixel(x, y); | |
if (color != Color.TRANSPARENT) { | |
return color; | |
} | |
// If nothing has been drawn on the bitmap at that location, | |
// check if there is a background image. | |
if (backgroundDrawable != null) { | |
if (scaledBackgroundBitmap == null) { | |
scaledBackgroundBitmap = Bitmap.createScaledBitmap( | |
backgroundDrawable.getBitmap(), | |
bitmap.getWidth(), bitmap.getHeight(), | |
false); // false argument indicates not to filter | |
} | |
color = scaledBackgroundBitmap.getPixel(x, y); | |
return color; | |
} | |
// If there is no background image, use the background color. | |
if (Color.alpha(backgroundColor) != 0) { | |
return backgroundColor; | |
} | |
return Component.COLOR_NONE; | |
} catch (IllegalArgumentException e) { | |
// This should never occur, since we have checked bounds. | |
Log.e(LOG_TAG, | |
String.format("Returning COLOR_NONE (exception) from getBackgroundPixelColor.")); | |
return Component.COLOR_NONE; | |
} | |
} | |
private int getPixelColor(int x, int y) { | |
// If the request is out of bounds, return COLOR_NONE. | |
if (x < 0 || x >= bitmap.getWidth() || | |
y < 0 || y >= bitmap.getHeight()) { | |
return Component.COLOR_NONE; | |
} | |
// If the cache isn't available, try to avoid rebuilding it. | |
if (completeCache == null) { | |
// If there are no visible sprites, just call getBackgroundPixelColor(). | |
boolean anySpritesVisible = false; | |
for (Sprite sprite : sprites) { | |
if (sprite.Visible()) { | |
anySpritesVisible = true; | |
break; | |
} | |
} | |
if (!anySpritesVisible) { | |
return getBackgroundPixelColor(x, y); | |
} | |
// TODO(user): If needed for efficiency, check whether there are any | |
// sprites overlapping (x, y). If not, we can just call getBackgroundPixelColor(). | |
// If so, maybe we can just draw those sprites instead of building a full | |
// cache of the view. | |
completeCache = buildCache(); | |
} | |
// Check the complete cache. | |
try { | |
return completeCache.getPixel(x, y); | |
} catch (IllegalArgumentException e) { | |
// This should never occur, since we have checked bounds. | |
Log.e(LOG_TAG, | |
String.format("Returning COLOR_NONE (exception) from getPixelColor.")); | |
return Component.COLOR_NONE; | |
} | |
} | |
} | |
public Canvas(ComponentContainer container) { | |
super(container); | |
context = container.$context(); | |
// Create view and add it to its designated container. | |
view = new CanvasView(context); | |
container.$add(this); | |
paint = new Paint(); | |
// Set default properties. | |
paint.setStrokeWidth(DEFAULT_LINE_WIDTH); | |
PaintColor(DEFAULT_PAINT_COLOR); | |
BackgroundColor(DEFAULT_BACKGROUND_COLOR); | |
TextAlignment(Component.ALIGNMENT_NORMAL); | |
FontSize(Component.FONT_DEFAULT_SIZE); | |
sprites = new LinkedList<Sprite>(); | |
motionEventParser = new MotionEventParser(); | |
} | |
@Override | |
public View getView() { | |
return view; | |
} | |
// Methods related to getting the dimensions of this Canvas | |
/** | |
* Returns whether the layout associated with this view has been computed. | |
* If so, {@link #Width()} and {@link #Height()} will be properly initialized. | |
* | |
* @return {@code true} if it is safe to call {@link #Width()} and {@link | |
* #Height()}, {@code false} otherwise | |
*/ | |
public boolean ready() { | |
return drawn; | |
} | |
// Implementation of container methods | |
/** | |
* Adds a sprite to this Canvas by placing it in {@link #sprites}, | |
* which it ensures remains sorted. | |
* | |
* @param sprite the sprite to add | |
*/ | |
void addSprite(Sprite sprite) { | |
// Add before first element with greater Z value. | |
// This ensures not only that items are in increasing Z value | |
// but that sprites whose Z values are always equal are | |
// ordered by creation time. While we don't wish to guarantee | |
// this behavior going forward, it does provide consistency | |
// with how things worked before Z layering was added. | |
for (int i = 0; i < sprites.size(); i++) { | |
if (sprites.get(i).Z() > sprite.Z()) { | |
sprites.add(i, sprite); | |
return; | |
} | |
} | |
// Add to end if it has the highest Z value. | |
sprites.add(sprite); | |
} | |
/** | |
* Removes a sprite from this Canvas. | |
* | |
* @param sprite the sprite to remove | |
*/ | |
void removeSprite(Sprite sprite) { | |
sprites.remove(sprite); | |
} | |
/** | |
* Updates the sorted set of Sprites and the screen when a Sprite's Z | |
* property is changed. | |
* | |
* @param Sprite the Sprite whose Z property has changed | |
*/ | |
void changeSpriteLayer(Sprite sprite) { | |
removeSprite(sprite); | |
addSprite(sprite); | |
view.invalidate(); | |
} | |
@Override | |
public Activity $context() { | |
return context; | |
} | |
@Override | |
public Form $form() { | |
return container.$form(); | |
} | |
@Override | |
public void $add(AndroidViewComponent component) { | |
throw new UnsupportedOperationException("Canvas.$add() called"); | |
} | |
@Override | |
public void setChildWidth(AndroidViewComponent component, int width) { | |
throw new UnsupportedOperationException("Canvas.setChildWidth() called"); | |
} | |
@Override | |
public void setChildHeight(AndroidViewComponent component, int height) { | |
throw new UnsupportedOperationException("Canvas.setChildHeight() called"); | |
} | |
// Methods executed when a child sprite has changed its location or appearance | |
/** | |
* Indicates that a sprite has changed, triggering invalidation of the view | |
* and a check for collisions. | |
* | |
* @param sprite the sprite whose location, size, or appearance has changed | |
*/ | |
void registerChange(Sprite sprite) { | |
view.invalidate(); | |
findSpriteCollisions(sprite); | |
} | |
// Methods for detecting collisions | |
/** | |
* Checks if the given sprite now overlaps with or abuts any other sprite | |
* or has ceased to do so. If there is a sprite that is newly in collision | |
* with it, {@link Sprite#CollidedWith(Sprite)} is called for each sprite | |
* with the other sprite as an argument. If two sprites that had been in | |
* collision are no longer colliding, | |
* {@link Sprite#NoLongerCollidingWith(Sprite)} is called for each sprite | |
* with the other as an argument. Collisions are only recognized between | |
* sprites that are both | |
* {@link com.google.appinventor.components.runtime.Sprite#Visible()} | |
* and | |
* {@link com.google.appinventor.components.runtime.Sprite#Enabled()}. | |
* | |
* @param movedSprite the sprite that has just changed position | |
*/ | |
protected void findSpriteCollisions(Sprite movedSprite) { | |
for (Sprite sprite : sprites) { | |
if (sprite != movedSprite) { | |
// Check whether we already raised an event for their collision. | |
if (movedSprite.CollidingWith(sprite)) { | |
// If they no longer conflict, note that. | |
if (!movedSprite.Visible() || !movedSprite.Enabled() || | |
!sprite.Visible() || !sprite.Enabled() || | |
!Sprite.colliding(sprite, movedSprite)) { | |
movedSprite.NoLongerCollidingWith(sprite); | |
sprite.NoLongerCollidingWith(movedSprite); | |
} else { | |
// If they still conflict, do nothing. | |
} | |
} else { | |
// Check if they now conflict. | |
if (movedSprite.Visible() && movedSprite.Enabled() && | |
sprite.Visible() && sprite.Enabled() && | |
Sprite.colliding(sprite, movedSprite)) { | |
// If so, raise two CollidedWith events. | |
movedSprite.CollidedWith(sprite); | |
sprite.CollidedWith(movedSprite); | |
} else { | |
// If they still don't conflict, do nothing. | |
} | |
} | |
} | |
} | |
} | |
// Properties | |
/** | |
* Returns the button's background color as an alpha-red-green-blue | |
* integer, i.e., {@code 0xAARRGGBB}. An alpha of {@code 00} | |
* indicates fully transparent and {@code FF} means opaque. | |
* | |
* @return background color in the format 0xAARRGGBB, which includes | |
* alpha, red, green, and blue components | |
*/ | |
@SimpleProperty( | |
description = "The color of the canvas background.", | |
category = PropertyCategory.APPEARANCE) | |
public int BackgroundColor() { | |
return backgroundColor; | |
} | |
/** | |
* Specifies the Canvas's background color as an alpha-red-green-blue | |
* integer, i.e., {@code 0xAARRGGBB}. An alpha of {@code 00} | |
* indicates fully transparent and {@code FF} means opaque. | |
* The background color only shows if there is no background image. | |
* | |
* @param argb background color in the format 0xAARRGGBB, which | |
* includes alpha, red, green, and blue components | |
*/ | |
@DesignerProperty(editorType = DesignerProperty.PROPERTY_TYPE_COLOR, | |
defaultValue = Component.DEFAULT_VALUE_COLOR_WHITE) | |
@SimpleProperty | |
public void BackgroundColor(int argb) { | |
view.setBackgroundColor(argb); | |
} | |
/** | |
* Returns the path of the canvas background image. | |
* | |
* @return the path of the canvas background image | |
*/ | |
@SimpleProperty( | |
description = "The name of a file containing the background image for the canvas", | |
category = PropertyCategory.APPEARANCE) | |
public String BackgroundImage() { | |
return backgroundImagePath; | |
} | |
/** | |
* Specifies the path of the canvas background image. | |
* | |
* <p/>See {@link MediaUtil#determineMediaSource} for information about what | |
* a path can be. | |
* | |
* @param path the path of the canvas background image | |
*/ | |
@DesignerProperty(editorType = DesignerProperty.PROPERTY_TYPE_ASSET, | |
defaultValue = "") | |
@SimpleProperty | |
public void BackgroundImage(String path) { | |
view.setBackgroundImage(path); | |
} | |
/** | |
* Returns background source camera | |
* | |
* @return the path of the canvas background image | |
*/ | |
@SimpleProperty( | |
description = "The ID of the camera being used as the source for the canvas background. -1 implies no camera background.", | |
category = PropertyCategory.APPEARANCE) | |
public int CameraBackground() { | |
return backgroundCameraId; | |
} | |
/** | |
* Specifies the camera to use as input for background | |
* | |
* @param id the id of the camera to use as input | |
*/ | |
@DesignerProperty(editorType = DesignerProperty.PROPERTY_TYPE_ASSET, | |
defaultValue = -1) | |
@SimpleProperty | |
public void CameraBackground(int id) { | |
} | |
/** | |
* Returns the currently specified paint color as an alpha-red-green-blue | |
* integer, i.e., {@code 0xAARRGGBB}. An alpha of {@code 00} | |
* indicates fully transparent and {@code FF} means opaque. | |
* | |
/* @return paint color in the format 0xAARRGGBB, which includes alpha, | |
* red, green, and blue components | |
*/ | |
@SimpleProperty( | |
description = "The color in which lines are drawn", | |
category = PropertyCategory.APPEARANCE) | |
public int PaintColor() { | |
return paintColor; | |
} | |
/** | |
* Specifies the paint color as an alpha-red-green-blue integer, | |
* i.e., {@code 0xAARRGGBB}. An alpha of {@code 00} indicates fully | |
* transparent and {@code FF} means opaque. | |
* | |
* @param argb paint color in the format 0xAARRGGBB, which includes | |
* alpha, red, green, and blue components | |
*/ | |
@DesignerProperty(editorType = DesignerProperty.PROPERTY_TYPE_COLOR, | |
defaultValue = Component.DEFAULT_VALUE_COLOR_BLACK) | |
@SimpleProperty | |
public void PaintColor(int argb) { | |
paintColor = argb; | |
changePaint(paint, argb); | |
} | |
private void changePaint(Paint paint, int argb) { | |
if (argb == Component.COLOR_DEFAULT) { | |
// The default paint color is black. | |
PaintUtil.changePaint(paint, Component.COLOR_BLACK); | |
} else if (argb == Component.COLOR_NONE) { | |
PaintUtil.changePaintTransparent(paint); | |
} else { | |
PaintUtil.changePaint(paint, argb); | |
} | |
} | |
@SimpleProperty( | |
description = "The font size of text drawn on the canvas.", | |
category = PropertyCategory.APPEARANCE) | |
public float FontSize() { | |
return paint.getTextSize(); | |
} | |
@DesignerProperty(editorType = DesignerProperty.PROPERTY_TYPE_NON_NEGATIVE_FLOAT, | |
defaultValue = Component.FONT_DEFAULT_SIZE + "") | |
@SimpleProperty | |
public void FontSize(float size) { | |
paint.setTextSize(size); | |
} | |
/** | |
* Returns the currently specified stroke width | |
* @return width | |
*/ | |
@SimpleProperty( | |
description = "The width of lines drawn on the canvas.", | |
category = PropertyCategory.APPEARANCE) | |
public float LineWidth() { | |
return paint.getStrokeWidth(); | |
} | |
/** | |
* Specifies the stroke width | |
* | |
* @param width | |
*/ | |
@DesignerProperty(editorType = DesignerProperty.PROPERTY_TYPE_NON_NEGATIVE_FLOAT, | |
defaultValue = DEFAULT_LINE_WIDTH + "") | |
@SimpleProperty | |
public void LineWidth(float width) { | |
paint.setStrokeWidth(width); | |
} | |
/** | |
* Returns the alignment of the canvas's text: center, normal | |
* (starting at the specified point in drawText()), or opposite | |
* (ending at the specified point in drawText()). | |
* | |
* @return one of {@link Component#ALIGNMENT_NORMAL}, | |
* {@link Component#ALIGNMENT_CENTER} or | |
* {@link Component#ALIGNMENT_OPPOSITE} | |
*/ | |
@SimpleProperty( | |
category = PropertyCategory.APPEARANCE, | |
userVisible = false) | |
public int TextAlignment() { | |
return textAlignment; | |
} | |
/** | |
* Specifies the alignment of the canvas's text: center, normal | |
* (starting at the specified point in DrawText() or DrawAngle()), | |
* or opposite (ending at the specified point in DrawText() or | |
* DrawAngle()). | |
* | |
* @param alignment one of {@link Component#ALIGNMENT_NORMAL}, | |
* {@link Component#ALIGNMENT_CENTER} or | |
* {@link Component#ALIGNMENT_OPPOSITE} | |
*/ | |
@DesignerProperty(editorType = DesignerProperty.PROPERTY_TYPE_TEXTALIGNMENT, | |
defaultValue = Component.ALIGNMENT_CENTER + "") | |
@SimpleProperty(userVisible = false) | |
public void TextAlignment(int alignment) { | |
this.textAlignment = alignment; | |
switch (alignment) { | |
case Component.ALIGNMENT_NORMAL: | |
paint.setTextAlign(Paint.Align.LEFT); | |
break; | |
case Component.ALIGNMENT_CENTER: | |
paint.setTextAlign(Paint.Align.CENTER); | |
break; | |
case Component.ALIGNMENT_OPPOSITE: | |
paint.setTextAlign(Paint.Align.RIGHT); | |
break; | |
} | |
} | |
// Methods supporting event handling | |
/** | |
* When the user touches a canvas, providing the (x, y) position of | |
* the touch relative to the upper left corner of the canvas. The | |
* value "touchedSprite" is true if a sprite was also in this position. | |
* | |
* @param x x-coordinate of the point that was touched | |
* @param y y-coordinate of the point that was touched | |
* @param touchedSprite {@code true} if a sprite was touched, {@code false} | |
* otherwise | |
*/ | |
@SimpleEvent | |
public void Touched(float x, float y, boolean touchedSprite) { | |
EventDispatcher.dispatchEvent(this, "Touched", x, y, touchedSprite); | |
} | |
/** | |
* When the user does a drag from one point (prevX, prevY) to | |
* another (x, y). The pair (startX, startY) indicates where the | |
* user first touched the screen, and "draggedSprite" indicates whether a | |
* sprite is being dragged. | |
* | |
* @param startX the starting x-coordinate | |
* @param startY the starting y-coordinate | |
* @param prevX the previous x-coordinate (possibly equal to startX) | |
* @param prevY the previous y-coordinate (possibly equal to startY) | |
* @param currentX the current x-coordinate | |
* @param currentY the current y-coordinate | |
* @param draggedSprite {@code true} if | |
* {@link Sprite#Dragged(float, float, float, float, float, float)} | |
* was called for one or more sprites for this segment, {@code false} | |
* otherwise | |
*/ | |
@SimpleEvent | |
public void Dragged(float startX, float startY, float prevX, float prevY, | |
float currentX, float currentY, boolean draggedSprite) { | |
EventDispatcher.dispatchEvent(this, "Dragged", startX, startY, | |
prevX, prevY, currentX, currentY, draggedSprite); | |
} | |
// Functions | |
/** | |
* Clears the canvas, without removing the background image, if one | |
* was provided. | |
*/ | |
@SimpleFunction(description = "Clears anything drawn on this Canvas but " + | |
"not any background color or image.") | |
public void Clear() { | |
view.clearDrawingLayer(); | |
} | |
/** | |
* Draws a point at the given coordinates on the canvas. | |
* | |
* @param x x coordinate | |
* @param y y coordinate | |
*/ | |
@SimpleFunction | |
public void DrawPoint(int x, int y) { | |
view.canvas.drawPoint(x, y, paint); | |
view.invalidate(); | |
} | |
/** | |
* Draws a circle (filled in) at the given coordinates on the canvas, with the | |
* given radius. | |
* | |
* @param x x coordinate | |
* @param y y coordinate | |
* @param r radius | |
*/ | |
@SimpleFunction | |
public void DrawCircle(int x, int y, float r) { | |
view.canvas.drawCircle(x, y, r, paint); | |
view.invalidate(); | |
} | |
/** | |
* Draws a line between the given coordinates on the canvas. | |
* | |
* @param x1 x coordinate of first point | |
* @param y1 y coordinate of first point | |
* @param x2 x coordinate of second point | |
* @param y2 y coordinate of second point | |
*/ | |
@SimpleFunction | |
public void DrawLine(int x1, int y1, int x2, int y2) { | |
view.canvas.drawLine(x1, y1, x2, y2, paint); | |
view.invalidate(); | |
} | |
/** | |
* Draws the specified text relative to the specified coordinates | |
* using the values of the {@link #FontSize(float)} and | |
* {@link #TextAlignment(int)} properties. | |
* | |
* @param text the text to draw | |
* @param x the x-coordinate of the origin | |
* @param y the y-coordinate of the origin | |
*/ | |
@SimpleFunction(description = "Draws the specified text relative to the specified coordinates " | |
+ "using the values of the FontSize and TextAlignment properties.") | |
public void DrawText(String text, int x, int y) { | |
view.canvas.drawText(text, x, y, paint); | |
view.invalidate(); | |
} | |
/** | |
* Draws the specified text starting at the specified coordinates | |
* at the specified angle using the values of the {@link #FontSize(float)} and | |
* {@link #TextAlignment(int)} properties. | |
* | |
* @param text the text to draw | |
* @param x the x-coordinate of the origin | |
* @param y the y-coordinate of the origin | |
* @param angle the angle (in degrees) at which to draw the text | |
*/ | |
@SimpleFunction(description = "Draws the specified text starting at the specified coordinates " | |
+ "at the specified angle using the values of the FontSize and TextAlignment properties.") | |
public void DrawTextAtAngle(String text, int x, int y, float angle) { | |
view.drawTextAtAngle(text, x, y, angle); | |
} | |
/** | |
* <p>Gets the color of the given pixel, ignoring sprites.</p> | |
* | |
* @param x the x-coordinate | |
* @param y the y-coordinate | |
* @return the color at that location as an alpha-red-blue-green integer, | |
* or {@link Component#COLOR_NONE} if that point is not on this Canvas | |
*/ | |
@SimpleFunction(description = "Gets the color of the specified point. " | |
+ "This includes the background and any drawn points, lines, or " | |
+ "circles but not sprites.") | |
public int GetBackgroundPixelColor(int x, int y) { | |
return view.getBackgroundPixelColor(x, y); | |
} | |
/** | |
* <p>Sets the color of the given pixel. This has no effect if the | |
* coordinates are out of bounds.</p> | |
* | |
* @param x the x-coordinate | |
* @param y the y-coordinate | |
* @param color the color as an alpha-red-blue-green integer | |
*/ | |
@SimpleFunction(description = "Sets the color of the specified point. " | |
+ "This differs from DrawPoint by having an argument for color.") | |
public void SetBackgroundPixelColor(int x, int y, int color) { | |
Paint pixelPaint = new Paint(); | |
PaintUtil.changePaint(pixelPaint, color); | |
view.canvas.drawPoint(x, y, pixelPaint); | |
view.invalidate(); | |
} | |
/** | |
* <p>Gets the color of the given pixel, including sprites.</p> | |
* | |
* @param x the x-coordinate | |
* @param y the y-coordinate | |
* @return the color at that location as an alpha-red-blue-green integer, | |
* or {@link Component#COLOR_NONE} if that point is not on this Canvas | |
*/ | |
@SimpleFunction(description = "Gets the color of the specified point.") | |
public int GetPixelColor(int x, int y) { | |
return view.getPixelColor(x, y); | |
} | |
/** | |
* Saves a picture of this Canvas to the device's external storage. | |
* If an error occurs, the Screen's ErrorOccurred event will be called. | |
* | |
* @return the full path name of the saved file, or the empty string if the | |
* save failed | |
*/ | |
@SimpleFunction | |
public String Save() { | |
try { | |
File file = FileUtil.getPictureFile("png"); | |
return saveFile(file, Bitmap.CompressFormat.PNG, "Save"); | |
} catch (IOException e) { | |
container.$form().dispatchErrorOccurredEvent(this, "Save", | |
ErrorMessages.ERROR_MEDIA_FILE_ERROR, e.getMessage()); | |
} catch (FileUtil.FileException e) { | |
container.$form().dispatchErrorOccurredEvent(this, "Save", | |
e.getErrorMessageNumber()); | |
} | |
return ""; | |
} | |
/** | |
* Saves a picture of this Canvas to the device's external storage in the file | |
* named fileName. fileName must end with one of ".jpg", ".jpeg", or ".png" | |
* (which determines the file type: JPEG, or PNG). | |
* | |
* @return the full path name of the saved file, or the empty string if the | |
* save failed | |
*/ | |
@SimpleFunction | |
public String SaveAs(String fileName) { | |
// Figure out desired file format | |
Bitmap.CompressFormat format; | |
if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) { | |
format = Bitmap.CompressFormat.JPEG; | |
} else if (fileName.endsWith(".png")) { | |
format = Bitmap.CompressFormat.PNG; | |
} else if (!fileName.contains(".")) { // make PNG the default to match Save behavior | |
fileName = fileName + ".png"; | |
format = Bitmap.CompressFormat.PNG; | |
} else { | |
container.$form().dispatchErrorOccurredEvent(this, "SaveAs", | |
ErrorMessages.ERROR_MEDIA_IMAGE_FILE_FORMAT); | |
return ""; | |
} | |
try { | |
File file = FileUtil.getExternalFile(fileName); | |
return saveFile(file, format, "SaveAs"); | |
} catch (IOException e) { | |
container.$form().dispatchErrorOccurredEvent(this, "SaveAs", | |
ErrorMessages.ERROR_MEDIA_FILE_ERROR, e.getMessage()); | |
} catch (FileUtil.FileException e) { | |
container.$form().dispatchErrorOccurredEvent(this, "SaveAs", | |
e.getErrorMessageNumber()); | |
} | |
return ""; | |
} | |
// Helper method for Save and SaveAs | |
private String saveFile(File file, Bitmap.CompressFormat format, String method) { | |
try { | |
boolean success = false; | |
FileOutputStream fos = new FileOutputStream(file); | |
// Don't cache, in order to save memory. It seems unlikely to be used again soon. | |
Bitmap bitmap = (view.completeCache == null ? view.buildCache() : view.completeCache); | |
try { | |
success = bitmap.compress(format, | |
100, // quality: ignored for png | |
fos); | |
} finally { | |
fos.close(); | |
} | |
if (success) { | |
return file.getAbsolutePath(); | |
} else { | |
container.$form().dispatchErrorOccurredEvent(this, method, | |
ErrorMessages.ERROR_CANVAS_BITMAP_ERROR); | |
} | |
} catch (FileNotFoundException e) { | |
container.$form().dispatchErrorOccurredEvent(this, method, | |
ErrorMessages.ERROR_MEDIA_CANNOT_OPEN, file.getAbsolutePath()); | |
} catch (IOException e) { | |
container.$form().dispatchErrorOccurredEvent(this, method, | |
ErrorMessages.ERROR_MEDIA_FILE_ERROR, e.getMessage()); | |
} | |
return ""; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment