Skip to content

Instantly share code, notes, and snippets.

@shashi
Created April 6, 2012 19:49
Show Gist options
  • Save shashi/2322461 to your computer and use it in GitHub Desktop.
Save shashi/2322461 to your computer and use it in GitHub Desktop.
Canvas with camera background
// 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