/*******************************************************************************
* Copyright 2012-present Pixate, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
/**
* Copyright (c) 2012-2013 Pixate, Inc. All rights reserved.
*/
package com.pixate.freestyle.cg.shapes;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Picture;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import com.pixate.freestyle.PixateFreestyle;
import com.pixate.freestyle.cg.math.PXOffsets;
import com.pixate.freestyle.cg.paints.PXPaint;
import com.pixate.freestyle.cg.parsing.PXTransformParser;
import com.pixate.freestyle.cg.shadow.PXShadowPaint;
import com.pixate.freestyle.cg.strokes.PXStrokeRenderer;
import com.pixate.freestyle.util.ObjectPool;
import com.pixate.freestyle.util.ObjectUtil;
/**
* A common base class for all shapes in ShapeKit. This class can be used to
* cache the geometry of the shape it describes.
*/
public abstract class PXShape implements PXRenderable, PXPaintable {
protected PXRenderable parent;
protected PXShapeDocument owningDocument;
protected Path path;
protected PXStrokeRenderer stroke;
protected PXPaint fillColor;
protected float opacity;
protected boolean visible;
protected Matrix transform;
protected PXShape clippingPath;
protected PXShadowPaint shadow;
protected PXOffsets padding;
private Picture picture;
/**
* Constructs a PXShape
*/
public PXShape() {
fillColor = null;
opacity = 1.0F;
visible = true;
transform = new Matrix();
}
/*
* (non-Javadoc)
* @see com.pixate.freestyle.cg.shapes.PXPaintable#getStroke()
*/
public PXStrokeRenderer getStroke() {
return stroke;
}
/**
* Indicate that this shape needs to be redrawn. This method is used to
* indicate that this shape needs to be redrawn. This will effectively call
* postInvalidate on the view that ultimately owns this shape; however, note
* that shape geometry is cached. This will force the shape to be redrawn,
* but it will not force the shape's geometry to be recalculated. If you
* need to clear the cache and refresh, call clearPath.
*/
public void setNeedsDisplay() {
PXShapeDocument scene = getOwningDocument();
if (scene != null && scene.getParentView() != null) {
scene.getParentView().postInvalidate();
}
}
/*
* (non-Javadoc)
* @see
* com.pixate.freestyle.cg.shapes.PXPaintable#setStroke(com.pixate.freestyle
* .pxengine .shape.PXStrokeRenderer)
*/
public void setStroke(PXStrokeRenderer stroke) {
if (!ObjectUtil.areEqual(this.stroke, stroke)) {
this.stroke = stroke;
setNeedsDisplay();
}
}
/**
* Returns the opacity.
*
* @return Opacity
*/
public float getOpacity() {
return opacity;
}
/**
* Set an opacity.
*
* @param opacity An opacity value between 0.0 to 1.0.
*/
public void setOpacity(float opacity) {
// clamp input to valid range
opacity = Math.min(Math.max(0.0F, opacity), 1.0F);
if (this.opacity != opacity) {
this.opacity = opacity;
setNeedsDisplay();
}
}
public boolean getVisible() {
return visible;
}
public void setVisible(boolean visible) {
if (this.visible != visible) {
this.visible = visible;
setNeedsDisplay();
}
}
public PXPaint getFillColor() {
return fillColor;
}
public void setFillColor(PXPaint color) {
if (!ObjectUtil.areEqual(this.fillColor, color)) {
fillColor = color;
setNeedsDisplay();
}
}
public PXShape getClippingPath() {
return clippingPath;
}
public void setClippingPath(PXShape clippingPath) {
if (!ObjectUtil.areEqual(this.clippingPath, clippingPath)) {
this.clippingPath = clippingPath;
setNeedsDisplay();
}
}
public PXOffsets getPadding() {
return padding;
}
public void setPadding(PXOffsets padding) {
if (!ObjectUtil.areEqual(this.padding, padding)) {
this.padding = padding;
setNeedsDisplay();
}
}
public PXShadowPaint getShadow() {
return shadow;
}
public void setShadow(PXShadowPaint shadow) {
if (!ObjectUtil.areEqual(this.shadow, shadow)) {
this.shadow = shadow;
setNeedsDisplay();
}
}
public PXRenderable getParent() {
return parent;
}
public void setParent(PXRenderable parent) {
if (this.parent != null && this.parent.equals(parent)) {
return;
}
owningDocument = null;
this.parent = parent;
}
/**
* Returns the {@link PXShapeDocument} that owns this instance. <br>
* This {@link PXShapeDocument} can be thought of as being analogous to the
* W3C DOM Node#getDocument method.
*/
public PXShapeDocument getOwningDocument() {
if (owningDocument != null) {
return owningDocument;
}
PXRenderable result = this;
PXRenderable parent = result.getParent();
while (parent != null) {
result = parent;
parent = parent.getParent();
}
if (result instanceof PXShapeDocument) {
owningDocument = (PXShapeDocument) result;
return owningDocument;
}
return null;
}
/**
* Sets a {@link Path}.
*
* @param path
*/
public void setPath(Path path) {
if (this.path != null) {
clearPath();
}
this.path = path;
}
/**
* Returns the path data associated with this shape instance. Note that when
* this method is called , the {@link #newPath()} method may be called to
* populate the path cache for the instance. This call may return
* <code>null</code>, which indicates that the instance was unable to create
* a path.
*/
public Path getPath() {
if (path == null) {
path = newPath();
}
return path;
}
/*
* (non-Javadoc)
* @see com.pixate.freestyle.pxengine.cg.PXRenderable#getTransform()
*/
public Matrix getTransform() {
return transform;
}
public void setTransform(Matrix transform) {
if (transform == null) {
// set it to the identity transform to save us the trouble of null
// checking all over.
transform = PXTransformParser.IDENTITY_MATRIX;
}
if (!ObjectUtil.areEqual(transform, this.transform)) {
this.transform = transform;
setNeedsDisplay();
}
}
/**
* Clear the path cache. This method is used to clear the path cache
* maintained by this shape. This can be useful in low memory situations,
* but this is more likely to be used when some attribute of the shape has
* changed which affects its geometry. At which point, the cache is
* invalidated and will be rebuilt the next time the path property is
* accessed.
*/
public void clearPath() {
if (path != null) {
ObjectPool.pathPool.checkIn(path);
path = null;
}
picture = null;
setNeedsDisplay();
}
public Drawable renderToImage(RectF bounds, boolean opaque) {
Drawable result = null;
if (bounds != null && bounds.width() > 0 && bounds.height() > 0) {
// Start new image context
Bitmap bitmap = Bitmap.createBitmap((int) Math.ceil(bounds.width()),
(int) Math.ceil(bounds.height()), Bitmap.Config.ARGB_8888);
// TODO - API 12 and above...
bitmap.setHasAlpha(!opaque);
bitmap.setDensity(PixateFreestyle.getAppContext().getResources().getDisplayMetrics().densityDpi);
// create a Canvas context
Canvas canvas = new Canvas(bitmap);
// translate to bound's origin
canvas.translate(-bounds.left, -bounds.top);
// render this shape
render(canvas);
// return image as drawable
result = new BitmapDrawable(PixateFreestyle.getAppContext().getResources(), bitmap);
}
return result;
}
/*
* (non-Javadoc)
* @see
* com.pixate.freestyle.cg.shapes.PXRenderable#render(android.graphics.Canvas
* )
*/
public void render(Canvas context) {
render(context, true);
}
/*
* (non-Javadoc)
* @see
* com.pixate.freestyle.cg.shapes.PXRenderable#render(android.graphics.Canvas
* , boolean)
*/
public void render(Canvas context, boolean cache) {
// TODO Auto-generated method stub
// Don't draw if we're not visible
if (!visible) {
return;
}
// Using a Picture drawing with Hardware Acceleration on will not work,
// so in case it's on, we use a direct rendering on the given context.
// TODO - isHardwareAccelerated is API 11 and above
if (cache && picture == null && !context.isHardwareAccelerated()) {
// Create a Picture and record all drawings into it.
// We will use this picture later to render the shape.
picture = new Picture();
Rect clipBounds = context.getClipBounds();
innerRender(picture.beginRecording(clipBounds.width(), clipBounds.height()));
picture.endRecording();
}
int saveCount = context.save();
if (picture != null) {
context.drawPicture(picture);
} else {
// direct draw (Cache is false, or Hardware Acceleration is on)
innerRender(context);
}
context.restoreToCount(saveCount);
}
/**
* A {@link PXShape} is asynchronous in case its inner {@link PXPaint} is.
*
* @see com.pixate.freestyle.cg.shapes.PXRenderable#isAsynchronous()
*/
@Override
public boolean isAsynchronous() {
return fillColor != null && fillColor.isAsynchronous();
}
/**
* Inner render the canvas.
*
* @param canvas
*/
private void innerRender(Canvas canvas) {
if (!transform.isIdentity()) {
// apply transform
canvas.concat(transform);
}
// apply clipping path, if we have one
if (clippingPath != null) {
canvas.clipPath(clippingPath.getPath());
}
// setup transparency layer
int alphaSaveCount = -1;
if (opacity < 1.0F) {
int alpha = (int) (opacity * 255);
alphaSaveCount = canvas.saveLayerAlpha(new RectF(canvas.getClipBounds()), alpha,
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
}
// render content
if (getPath() != null) {
// Apply shadow outset
if (shadow != null) {
shadow.applyOutsetToPath(path, canvas);
}
// Set fill
if (fillColor != null) {
Paint fillPaint = ObjectPool.paintPool.checkOut();
fillPaint.setStyle(Style.FILL);
fillColor.applyFillToPath(path, fillPaint, canvas);
// Check the paint back into the pool
ObjectPool.paintPool.checkIn(fillPaint);
}
// Apply shadow insets
if (shadow != null) {
shadow.applyInsetToPath(path, canvas);
}
// Set stroke
if (stroke != null) {
// Passing a null paint. This will eventually be rendered as new
// Paint with Paint.ANTI_ALIAS_FLAG.
stroke.applyStrokeToPath(path, null, canvas);
}
}
// Draw the children
renderChildren(canvas);
if (opacity < 1.0F) {
canvas.restoreToCount(alphaSaveCount);
}
// Release the path after the rendering. Usually, this method will
// render into a Picture, so no new Path will be created, unless a
// complete redraw is performed after a Picture disposal.
ObjectPool.pathPool.checkIn(path);
path = null;
}
/**
* Build's the {@link Path} that contains the geometry of this instance's
* shape. Subclasses may override.
*
* @return A {@link Path}
*/
protected Path newPath() {
return null;
}
/**
* Render any children associated with this shape. This method is used to
* render any child content associated with this shape. In most cases, this
* will only be used by container classes, such as PXShapeGroup. Subclasses
* may override.
*
* @param context The context into which children of this shape should be
* rendered.
*/
protected void renderChildren(Canvas context) {
// No Op
}
}