/*******************************************************************************
* 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.
******************************************************************************/
package com.pixate.freestyle.styling.stylers;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.util.DisplayMetrics;
import android.util.LruCache;
import android.view.View;
import android.widget.GridView;
import com.pixate.freestyle.PixateFreestyle;
import com.pixate.freestyle.cg.math.PXOffsets;
import com.pixate.freestyle.cg.paints.PXPaint;
import com.pixate.freestyle.cg.paints.PXPaintGroup;
import com.pixate.freestyle.cg.paints.PXSolidPaint;
import com.pixate.freestyle.cg.shadow.PXShadow;
import com.pixate.freestyle.cg.shadow.PXShadowGroup;
import com.pixate.freestyle.cg.shadow.PXShadowPaint;
import com.pixate.freestyle.cg.shapes.PXBoundable;
import com.pixate.freestyle.cg.shapes.PXBoxModel;
import com.pixate.freestyle.cg.shapes.PXRectangle;
import com.pixate.freestyle.cg.shapes.PXShape;
import com.pixate.freestyle.cg.strokes.PXStroke;
import com.pixate.freestyle.styling.PXDeclaration;
import com.pixate.freestyle.styling.adapters.PXStyleAdapter;
import com.pixate.freestyle.styling.fonts.PXFontRegistry;
import com.pixate.freestyle.styling.infos.PXAnimationInfo;
import com.pixate.freestyle.styling.virtualStyleables.PXVirtualStyleable;
import com.pixate.freestyle.util.PXDrawableUtil;
import com.pixate.freestyle.util.Size;
import com.pixate.freestyle.util.StringUtil;
/**
* Styler context.
*/
public class PXStylerContext {
/**
* A class that hold fading edge styles for views that supports it while
* scrolling. A property with a <code>null</code> value in this class
* indicates that no CSS style value was assigned to it. This is important
* later on when applying this edge style on a {@link View}.
*/
public static class FadingEdgeStyle {
public Integer edgeLength;
public Boolean horizontalEnabled;
public Boolean verticalEnabled;
}
/**
* A class that hold overscrolling styles for views that supports it while
* scrolling beyond their data area. A property with a <code>null</code>
* value in this class indicates that no CSS style value was assigned to it.
*/
public static class OverscrollStyle {
public int distance;
public PXPaint header;
public PXPaint footer;
}
/**
* A class that hold icon styles for views that supports it (like TextView
* and its descendants). A property with a <code>null</code> value in this
* class indicates that no CSS style value was assigned to it.
*/
public static class CompoundIcons {
public Drawable top;
public Drawable right;
public Drawable bottom;
public Drawable left;
}
/**
* The position of the icon that this adapter is handling.
*/
public enum IconPosition {
LEFT("icon-left"),
TOP("icon-top"),
RIGHT("icon-right"),
BOTTOM("icon-bottom");
private String elementName;
private IconPosition(String elementName) {
this.elementName = elementName;
}
public String getElementName() {
return elementName;
}
};
/**
* A class that holds style properties for a grid layout (like for GridView)
* such as the vertical and horizontal spacing, column width, etc.
*/
public static class GridStyle {
/**
* See {@link android.widget.GridView#setStretchMode(int)}
*
* @author Bill Dawson
*/
public enum PXColumnStretchMode {
NONE("none", GridView.NO_STRETCH),
SPACING("spacing", GridView.STRETCH_SPACING),
SPACING_UNIFORM("spacing-uniform", GridView.STRETCH_SPACING_UNIFORM),
COLUMN_WIDTH("column-width", GridView.STRETCH_COLUMN_WIDTH);
private final String cssValue;
private final int androidValue;
private PXColumnStretchMode(String cssValue, int androidValue) {
this.cssValue = cssValue;
this.androidValue = androidValue;
}
private static Map<String, PXColumnStretchMode> cssValueToEnumMap;
static {
cssValueToEnumMap = new HashMap<String, PXColumnStretchMode>(4);
for (PXColumnStretchMode mode : PXColumnStretchMode.values()) {
cssValueToEnumMap.put(mode.getCssValue(), mode);
}
}
public String getCssValue() {
return this.cssValue;
}
public int getAndroidValue() {
return this.androidValue;
}
public static PXColumnStretchMode ofCssValue(String cssValue) {
return cssValueToEnumMap.get(cssValue);
}
}
public int columnCount = Integer.MIN_VALUE;
public int columnWidth = Integer.MIN_VALUE;
public int columnGap = Integer.MIN_VALUE;
public int columnStretchMode = Integer.MIN_VALUE;
public int rowGap = Integer.MIN_VALUE;
}
private static final int NO_COLOR_VAL = Integer.MIN_VALUE;
private Object styleable;
private String activeStateName;
private PXShape shape;
// For PXLayoutStyler
private float top;
private float left;
private float width;
private float height;
private RectF bounds;
private PXOffsets padding;
private Matrix transform;
private PXBoxModel boxModel;
private PXPaint fill;
private PXPaint imageFill;
private PXPaint dividerFill;
/*
* Just a note: Depending on how shadows are (or will be) implemented in
* Android, we may not need to separate inner and outer shadows. We kind of
* get outer shadows for free in iOS, but it's a process to get inner
* shadows. I separate them out here to make that division easier to process
* later (Kevin)
*/
private PXShadowGroup innerShadow;
private PXShadowGroup outerShadow;
private PXShadowPaint textShadow;
private float opacity;
private Size imageSize;
private PXOffsets insets;
private String text;
private String fontName;
private String fontStyle;
private String fontWeight;
private float fontSize;
private String fontStretch;
private List<PXAnimationInfo> transitionInfos;
private List<PXAnimationInfo> animationInfos;
private Map<String, Object> properties;
private int styleHash;
// TODO - What's a reasonable size for this? We may also need to overwrite
// the sizeOf() to limit this cache to a memory size.
private static LruCache<Integer, Drawable> IMAGE_CACHE = new LruCache<Integer, Drawable>(10);
// Holds all fading styles, in case any was set.
private FadingEdgeStyle fadingStyle;
// Holds overscroll styling
private OverscrollStyle overscroll;
// Holds the icon styling
private CompoundIcons compoundIcons;
// Holds grid styling
private GridStyle grid;
private boolean isVirtual;
public PXStylerContext() {
shape = new PXRectangle(new RectF());
setTop(setLeft(Float.MAX_VALUE));
setHeight(0.0f);
setWidth(0.0f);
boxModel = new PXBoxModel();
imageSize = new Size(0.0f, 0.0f);
transform = new Matrix();
opacity = 1.0f;
// Standard name given in Android, then each implementation takes it
// from there.
fontName = "sans-serif";
fontStyle = "normal";
fontWeight = "normal";
// fontStretch = "normal";
fontSize = 16.0f;
}
/**
* Constructs a new {@link PXStylerContext}.
*
* @param styleable
* @param stateName
* @param styleHash A style hash that was computed from the
* {@link PXDeclaration}s that will be involved in the rendering.
*/
public PXStylerContext(Object styleable, String stateName, int styleHash) {
this();
this.styleable = styleable;
this.isVirtual = (styleable instanceof PXVirtualStyleable);
this.activeStateName = stateName;
this.styleHash = styleHash;
}
// Statics
/**
* Reset any drawables cache that the {@link PXStylerContext} may hold.
*/
public static void resetCache() {
IMAGE_CACHE.evictAll();
}
// Methods
// TODO equiv? - (void)applyOuterShadowToLayer:(CALayer *)layer
public void applyOuterShadow(View view) {
}
public Object getPropertyValue(String propertyName) {
if (properties == null) {
return null;
}
return properties.get(propertyName);
}
public void setPropertyValue(Object value, String propertyName) {
if (properties == null) {
properties = new HashMap<String, Object>();
}
properties.put(propertyName, value);
}
public boolean usesColorOnly() {
boolean result = false;
// this.color has a color value if we have a fill that is a solid paint
// only.
if (getColor() != NO_COLOR_VAL) {
result = isRectangle() && (innerShadow == null || innerShadow.size() == 0)
&& (boxModel == null || !(boxModel.hasCornerRadius() || boxModel.hasBorder()))
&& imageFill == null;
}
return result;
}
public boolean usesImage() {
return imageFill != null || (fill != null && getColor() == NO_COLOR_VAL)
|| (innerShadow != null && innerShadow.size() > 0) || !isRectangle()
|| (boxModel != null && (boxModel.hasCornerRadius() || boxModel.hasBorder()));
}
// Getters
public PXBoxModel getBoxModel() {
return boxModel;
}
/**
* Returns the active state name for this context. The state name can later
* be mapped into a {@link Drawable} state integer by using the
* {@link PXDrawableUtil} class.
*
* @return The active state name (can be <code>null</code>)
*/
public String getActiveStateName() {
return activeStateName;
}
public PXPaint getCombinedPaints() {
if (fill != null && imageFill != null) {
PXPaintGroup group = new PXPaintGroup(fill, imageFill);
return group;
} else {
return imageFill != null ? imageFill : fill;
}
}
public Drawable getBackgroundImage(RectF bounds) {
this.bounds = bounds;
return getBackgroundImage();
}
public Drawable getBackgroundImage() {
// Check the cache first!
Drawable cachedDrawable = IMAGE_CACHE.get(styleHash);
if (cachedDrawable != null) {
return cachedDrawable;
}
// No luck with the cache... compute the drawable.
// Update bounds
if (Size.isNonZero(imageSize)) {
bounds = new RectF(0.0f, 0.0f, imageSize.width, imageSize.height);
} else if (bounds == null || bounds.isEmpty()) {
PXStyleAdapter styleAdapter = PXStyleAdapter.getStyleAdapter(styleable);
bounds = styleAdapter.getBounds(styleable);
if (bounds == null || bounds.isEmpty()) {
// Set default size to 32, 32 if zero.
bounds = new RectF(0.0f, 0.0f, 32.0f, 32.0f);
}
}
// apply bounds
// NOTE: This updates the bounds of the underlying geometry used to draw
// the background image.
// This does not resize the styleable. (Note taken from Obj-C).
if (shape instanceof PXBoundable) {
PXBoundable boundable = (PXBoundable) shape;
boundable.setBounds(bounds);
}
// apply fill
shape.setFillColor(getCombinedPaints());
// apply stroke and possibly modify geoometry bounds
if (boxModel.hasBorder()) {
// NOTE: We're using top border since we set all borders
// the same right now.
float strokeWidth = boxModel.getBorderTopWidth();
PXPaint strokeColor = boxModel.getBorderTopPaint();
PXStroke stroke = new PXStroke(strokeWidth);
if (strokeColor != null) {
stroke.setColor(strokeColor);
}
this.shape.setStroke(stroke);
// shrink bounds by half of the stroke width
if (shape instanceof PXBoundable) {
PXBoundable boundable = (PXBoundable) shape;
float insetDelta = 0.5f * strokeWidth;
RectF insetBounds = new RectF(bounds);
insetBounds.inset(insetDelta, insetDelta);
boundable.setBounds(insetBounds);
}
}
// set corner radius
if (shape instanceof PXRectangle) {
PXRectangle rect = (PXRectangle) shape;
rect.setRadiusTopLeft(boxModel.getRadiusTopLeft());
rect.setRadiusTopRight(boxModel.getRadiusTopRight());
rect.setRadiusBottomRight(boxModel.getRadiusBottomRight());
rect.setRadiusBottomLeft(boxModel.getRadiusBottomLeft());
}
// apply inner shadows
if (innerShadow != null && innerShadow.size() > 0) {
shape.setShadow(innerShadow);
}
// generate image
boolean isOpaque = this.isOpaque();
Drawable result = shape.renderToImage(bounds, isOpaque);
if (padding != null && padding.hasOffset()) {
// Wrap the result with an InsetDrawable that will hold the padding
// information (TODO - The padding should actually go in between the
// border and the content. This implementation just match what we
// have on the iOS side, which is also pending a fix)
result = new InsetDrawable(result, (int) padding.getLeft(), (int) padding.getTop(),
(int) padding.getRight(), (int) padding.getBottom());
}
// apply insets, if we have any (similar to the padding)
if (insets != null && insets.hasOffset()) {
// TODO Nine-patch. This code doesn't work, unfortunately. We'll
// need to investigate more now to render the bitmap with cap
// insets like in iOS.
/* @formatter:off
Bitmap bitmap;
if (result instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) result).getBitmap();
} else {
bitmap = Bitmap.createBitmap(result.getIntrinsicWidth(),
result.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
result.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
result.draw(canvas);
}
result = NinePatchUtil.createNinePatch(Pixate.getAppContext().getResources(), bitmap,
insets, null);
*/
// @formatter:on
}
// Cache the result and return
IMAGE_CACHE.put(styleHash, result);
return result;
}
public int getStyleHash() {
return styleHash;
}
public boolean isOpaque() {
return opacity == 1.0f && boxModel.isOpaque() && (fill != null && fill.isOpaque())
&& (imageFill != null && imageFill.isOpaque());
}
public Typeface getFont() {
Typeface result = null;
if (!StringUtil.isEmpty(fontName)) {
result = PXFontRegistry.getTypeface(fontName, fontWeight, fontStyle);
}
return result;
}
// Since Typeface won't include size, here's a getter for that.
public float getFontSize() {
return fontSize;
}
// The background color, if specified.
public int getColor() {
if (fill instanceof PXSolidPaint) {
return ((PXSolidPaint) fill).getColor();
} else {
return NO_COLOR_VAL;
}
}
public float getTop() {
return top;
}
public float getLeft() {
return left;
}
public float getWidth() {
return width;
}
public float getHeight() {
return height;
}
public Object getStyleable() {
// in case the styleable is virtual, return the view that this styleable
// is nested in.
if (isVirtual && styleable != null) {
return ((PXVirtualStyleable) styleable).getParent();
}
return styleable;
}
public PXPaint getFill() {
return fill;
}
public PXPaint getDividerFill() {
return dividerFill;
}
public PXOffsets getInsets() {
if (insets == null) {
insets = new PXOffsets();
}
return insets;
}
public PXOffsets getPadding() {
if (padding == null) {
padding = new PXOffsets();
}
return padding;
}
public Matrix getTransform() {
return transform;
}
public float getOpacity() {
return opacity;
}
public PXShape getShape() {
return shape;
}
public String getText() {
return text;
}
public PXShadowPaint getTextShadow() {
return textShadow;
}
public List<PXAnimationInfo> getTransitionInfos() {
return transitionInfos;
}
public List<PXAnimationInfo> getAnimationInfos() {
return animationInfos;
}
public DisplayMetrics getDisplayMetrics() {
return PixateFreestyle.getAppContext().getResources().getDisplayMetrics();
}
public String getFontName() {
return fontName;
}
public String getFontStretch() {
return fontStretch;
}
// SETTERS
public void setShadow(PXShadowPaint shadow) {
if (shadow != null) {
innerShadow = new PXShadowGroup();
outerShadow = new PXShadowGroup();
if (shadow instanceof PXShadow) {
PXShadow _shadow = (PXShadow) shadow;
if (_shadow.isInset()) {
innerShadow.add(_shadow);
} else {
outerShadow.add(_shadow);
}
} else if (shadow instanceof PXShadowGroup) {
PXShadowGroup shadowGroup = (PXShadowGroup) shadow;
for (PXShadowPaint shadowPaint : shadowGroup) {
if (shadowPaint instanceof PXShadow) {
PXShadow _shadow = (PXShadow) shadowPaint;
if (_shadow.isInset()) {
innerShadow.add(_shadow);
} else {
outerShadow.add(_shadow);
}
}
}
}
} else {
innerShadow = outerShadow = null;
}
}
public void setTop(float top) {
this.top = top;
}
public float setLeft(float left) {
this.left = left;
return left;
}
public void setWidth(float width) {
this.width = width;
}
public void setHeight(float height) {
this.height = height;
}
public void setFill(PXPaint paint) {
fill = paint;
}
public void setImageSize(Size size) {
imageSize = size;
}
public void setInsets(PXOffsets insets) {
this.insets = insets;
}
public void setImageFill(PXPaint paint) {
this.imageFill = paint;
}
public void setDividerFill(PXPaint paint) {
this.dividerFill = paint;
}
public void setPadding(PXOffsets padding) {
this.padding = padding;
}
public void setFontName(String fontName) {
this.fontName = fontName;
}
public void setFontSize(float size) {
this.fontSize = size;
}
public void setFontStyle(String style) {
this.fontStyle = style;
}
public void setFontWeight(String weight) {
this.fontWeight = weight;
}
public void setFontStretch(String stretch) {
this.fontStretch = stretch;
}
public void setOpacity(float opacity) {
this.opacity = opacity;
}
public void setShape(PXShape shape) {
this.shape = shape;
}
public void setText(String text) {
this.text = text;
}
public void setTextShadow(PXShadowPaint textShadow) {
this.textShadow = textShadow;
}
public void setTransform(Matrix transform) {
this.transform = transform;
}
public void setTransitionInfos(List<PXAnimationInfo> transitionInfos) {
this.transitionInfos = transitionInfos;
}
public void setAnimationInfos(List<PXAnimationInfo> animationInfos) {
this.animationInfos = animationInfos;
}
// Fading edge attributes
public void setFadingEdgeLength(int fadingEdgeLength) {
if (fadingStyle == null) {
fadingStyle = new FadingEdgeStyle();
}
fadingStyle.edgeLength = fadingEdgeLength;
}
public void setHorizontalFadingEdgeEnabled(boolean enabled) {
if (fadingStyle == null) {
fadingStyle = new FadingEdgeStyle();
}
fadingStyle.horizontalEnabled = enabled;
}
public void setVerticalFadingEdgeEnabled(boolean enabled) {
if (fadingStyle == null) {
fadingStyle = new FadingEdgeStyle();
}
fadingStyle.verticalEnabled = enabled;
}
/**
* Returns the fading edge style. <code>null</code> in case no modifications
* to the original View style were made.
*
* @return A {@link FadingEdgeStyle}. Can be <code>null</code>.
*/
public FadingEdgeStyle getFadingStyle() {
return fadingStyle;
}
// Overscroll styling
public void setOverscrollDistance(float distance) {
if (overscroll == null) {
overscroll = new OverscrollStyle();
}
overscroll.distance = (int) Math.ceil(distance);
}
public void setOverscrollHeader(PXPaint paint) {
if (overscroll == null) {
overscroll = new OverscrollStyle();
}
overscroll.header = paint;
}
public void setOverscrollFooter(PXPaint paint) {
if (overscroll == null) {
overscroll = new OverscrollStyle();
}
overscroll.footer = paint;
}
/**
* Returns the overscroll style. <code>null</code> in case no modifications
* to the original View style were made.
*
* @return A {@link OverscrollStyle}. Can be <code>null</code>.
*/
public OverscrollStyle getOverscrollStyle() {
return overscroll;
}
// Compound Icons
/**
* Sets the {@link Drawable} to one of the four icons that can be defined
* for the View.
*
* @param position The {@link IconPosition}
* @param drawable
*/
public void setCompoundIcon(IconPosition position, Drawable drawable) {
if (compoundIcons == null) {
compoundIcons = new CompoundIcons();
}
switch (position) {
case TOP:
compoundIcons.top = drawable;
break;
case RIGHT:
compoundIcons.right = drawable;
break;
case BOTTOM:
compoundIcons.bottom = drawable;
break;
default:
compoundIcons.left = drawable;
break;
}
}
/**
* Returns the {@link CompoundIcons} instance in case at least one of its
* icons was defined. <code>null</code> in case no modifications to the
* original View style were made. The icon style can be applied for views
* that support it (like TextView and its descendants)
*
* @return A {@link CompoundIcons}. Can be <code>null</code>.
*/
public CompoundIcons getCompoundIcons() {
return compoundIcons;
}
// Grid
/**
* Number of columns to show in grid.
*
* @param columnCount
*/
public void setColumnCount(int numColumns) {
if (grid == null) {
grid = new GridStyle();
}
grid.columnCount = numColumns;
}
/**
* Fixed width for columns.
*
* @param width
*/
public void setColumnWidth(int width) {
if (grid == null) {
grid = new GridStyle();
}
grid.columnWidth = width;
}
/**
* How columns stretch to fill space.
*
* @param mode
*/
public void setColumnStretchMode(int mode) {
if (grid == null) {
grid = new GridStyle();
}
grid.columnStretchMode = mode;
}
/**
* Default horizontal spacing between columns.
*
* @param spacing
*/
public void setColumnGap(int spacing) {
if (grid == null) {
grid = new GridStyle();
}
grid.columnGap = spacing;
}
/**
* Default vertical spacing between rows.
*
* @param spacing
*/
public void setRowGap(int spacing) {
if (grid == null) {
grid = new GridStyle();
}
grid.rowGap = spacing;
}
/**
* How columns should stretch to fill available space, if at all.
*
* @param mode See {@link android.widget.GridView#setStretchMode} for
* values.
*/
public void setStretchMode(int mode) {
if (grid == null) {
grid = new GridStyle();
}
grid.columnStretchMode = mode;
}
/**
* Returns the style properties to set for GridViews and any subclasses
* thereof. <code>null</code> in case no modifications to the original View
* style were made.
*
* @return A {@link GridStyle}. Can be <code>null</code>.
*/
public GridStyle getGridStyle() {
return grid;
}
// PRIVATE
private boolean isRectangle() {
return shape == null || (shape instanceof PXRectangle);
}
}