/**
* Copyright 2014, Barend Garvelink
*
* 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.s16.drawing;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
/**
* A square Drawable that renders a single glyph from a Typeface resource. The drawable has a
* content area defined by its bounds and its padding. The glyph is scaled so that its largest
* dimension fills this area. The smaller dimension is then centered.
*/
public class IconFontDrawable extends Drawable {
private static final int DEFAULT_INTRINSIC_SIZE = 48;
/**
* Configurable: alpha channel for the foreground color (default unset). If not set, the alpha
* value from the {@link #color} or {@link #colorStateList} is used. Once set, this overrides
* the alpha information in the assigned color, including with state changes. The unset value
* is {@code -1}.
*/
private int alpha = -1;
/**
* Configurable: foreground color, simple case (default black). Any changes to {@link #alpha}
* are reflected in this variable.
*/
private int color = Color.BLACK;
/**
* Configurable: foreground color, for state-aware rendering (optional, no default). Any changes
* to {@link #alpha} are reflected in this variable. Prevails over {@link #color} if set.
*/
private ColorStateList colorStateList;
/**
* Configurable: glyph to display in the drawable (required).
*/
private final char[] glyph = new char[]{'\0'};
/**
* Configurable: intrinsic size of the icon (optional, default -1 for no intrinsic size).
*/
private int intrinsicSize = -1;
/**
* Configurable: padding around the icon glyph within the bounds (optional, default zero).
*/
private int padding = 0;
/**
* Configurable: the rotation in degrees of the canvas when drawing the glyph. Zero is straight
* up, positive values rotate the glyph clockwise (optional, default zero).
*/
private float rotation;
/**
* Configurable: typeface to select the glyph from.
*/
private Typeface typeface;
/**
* Internal: a rectangle used as a temporary value during layout.
*
* @hide
*/
private Rect drawableArea;
/**
* Internal: the Paint used to draw {@link #glyphPath} onto our canvas.
*
* @hide
*/
private Paint glyphPaint;
/**
* Internal: the font glyph path to draw onto our canvas.
*
* @hide
*/
private Path glyphPath;
/**
* Internal: a float rectangle used as a temporary value during layout.
*
* @hide
*/
private RectF glyphPathBounds;
/**
* Internal: the transformation matrix to use for scaling and centering the glyph.
*
* @hide
*/
private Matrix glyphPathTransform;
/**
* Internal: glyph rendering color, calculated from state, alpha and color values.
*
* @hide
*/
private int renderingColor = Color.BLACK;
public static int getDefaultIntrinsicSize(Context context) {
int screenLayout = (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK);
switch (screenLayout) {
case 1: // mdpi
return DEFAULT_INTRINSIC_SIZE;
case 2: // hdpi
return (int)(DEFAULT_INTRINSIC_SIZE * 1.5f);
case 3: // xhdpi
case 4: // xxdpi
return DEFAULT_INTRINSIC_SIZE * 2;
//case 4: // xxdpi
// return DEFAULT_INTRINSIC_SIZE * 3;
default:
break;
}
if (screenLayout > 4) return DEFAULT_INTRINSIC_SIZE * 3;
return DEFAULT_INTRINSIC_SIZE;
}
private IconFontDrawable(final Typeface typeface) {
this.typeface = typeface;
this.glyphPaint = new Paint();
this.glyphPaint.setAntiAlias(true);
this.glyphPaint.setTypeface(typeface);
this.glyphPathTransform = new Matrix();
this.glyphPath = new Path();
this.drawableArea = new Rect();
this.glyphPathBounds = new RectF();
}
/**
* Fully initializing constructor to support the Builder pattern.
*/
IconFontDrawable(int alpha, int color, ColorStateList colorStateList, char glyph,
int intrinsicSize, int padding, float rotation, Typeface typeface) {
this(typeface);
this.alpha = alpha;
this.color = color;
this.colorStateList = colorStateList;
this.glyph[0] = glyph;
this.intrinsicSize = intrinsicSize;
this.padding = padding;
this.rotation = rotation;
computeRenderingColor();
}
/**
* Construct an icon font drawable without an intrinsic size in a solid color.
*
* @param typeface (nullable) typeface to select the glyph from.
* @param glyph the glyph to use.
* @param color the color in which to render the glyph.
*/
public IconFontDrawable(final Typeface typeface, final char glyph, final int color) {
this(typeface);
this.glyph[0] = glyph;
this.color = color;
computeRenderingColor();
}
/**
* Construct an icon font drawable with an intrinsic size in a solid color.
*
* @param typeface (nullable) typeface to select the glyph from.
* @param glyph the glyph to use.
* @param color the color in which to render the glyph.
* @param intrinsicSize the intrinsic size in pixels.
*/
public IconFontDrawable(final Typeface typeface, final char glyph, final int color, final int intrinsicSize) {
this(typeface);
this.glyph[0] = glyph;
this.color = color;
this.intrinsicSize = intrinsicSize;
computeRenderingColor();
}
/**
* Sets the alpha value, triggering a repaint if the value changed.
*
* @param alpha an alpha value.
*/
@Override
public void setAlpha(int alpha) {
final int newAlpha = (alpha & 0xFF);
if (this.alpha != newAlpha) {
this.alpha = newAlpha;
computeRenderingColor();
}
}
/**
* Unsets the alpha value, thus reverting the transparency to the level encoded in the glyph
* color value. This method triggers a repaint if needed.
*/
public void unsetAlpha() {
setAlpha(-1);
}
/**
* Sets the icon color to a single color, triggering a repaint if the value changed. Note that
* if {@link #colorStateList} is set to a non-null value, it prevails.
*
* @param color a color value. The alpha bits are ignored.
* @see #setAlpha(int)
* @see #setColor(android.content.res.ColorStateList)
*/
public void setColor(int color) {
final int newColor = (color & 0x00FFFFFF);
if (this.color != newColor) {
this.color = newColor;
computeRenderingColor();
}
}
/**
* Sets the icon color to a color state list, triggering a repaint if the value changed.
*
* @param stateColors a color state list. The alpha value is ignored.
* @see #setAlpha(int)
* @see #setColor(int)
*/
public void setColor(ColorStateList stateColors) {
this.colorStateList = stateColors;
computeRenderingColor();
}
/**
* Sets the displayed glyph, triggering a layout and repaint if the value changed.
*
* @param glyph the glyph.
*/
public void setGlyph(char glyph) {
if (glyph != this.glyph[0]) {
this.glyph[0] = glyph;
computeGlyphPath();
}
}
/**
* Sets the intrinsic size of the drawable, triggering a layout and repaint if the value
* changed. The drawable is constrained to square.
*
* @param intrinsicSize the intrinsic size in pixels.
*/
public void setIntrinsicSize(int intrinsicSize) {
if (this.intrinsicSize != intrinsicSize) {
this.intrinsicSize = intrinsicSize;
computeGlyphPath();
}
}
/**
* Sets the padding of the drawable area, triggering a layout and repaint if the value changed.
*
* @param padding the padding value in pixels.
*/
public void setPadding(int padding) {
if (this.padding != padding) {
this.padding = padding;
computeGlyphPath();
}
}
/**
* Sets the rotation of the drawable, triggering a repaint if the value changed.
*
* @param rotation the rotation in degrees. Zero is straight up, positive values rotate the
* glyph clockwise.
*/
public void setRotation(float rotation) {
if (this.rotation != rotation) {
this.rotation = rotation;
invalidateSelf();
}
}
/**
* Sets the typeface asset from which the glyph is taken, triggering a layout and repaint if
* the value changed.
*
* @param typeface the typeface asset.
*/
public void setTypeface(Typeface typeface) {
if (this.typeface != typeface) {
this.typeface = typeface;
this.glyphPaint.setTypeface(typeface);
computeGlyphPath();
}
}
@Override
public void setColorFilter(ColorFilter cf) {
glyphPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public boolean isStateful() {
return colorStateList != null && colorStateList.isStateful();
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
computeGlyphPath();
}
@Override
public int getIntrinsicWidth() {
return intrinsicSize;
}
@Override
public int getIntrinsicHeight() {
return intrinsicSize;
}
@Override
protected boolean onStateChange(int[] state) {
if (colorStateList != null) {
computeRenderingColor();
return true;
}
return false;
}
@Override
public void draw(Canvas canvas) {
canvas.save();
canvas.rotate(rotation, drawableArea.exactCenterX(), drawableArea.exactCenterY());
canvas.drawPath(glyphPath, glyphPaint);
canvas.restore();
}
private void computeGlyphPath() {
drawableArea.set(getBounds());
drawableArea.inset(padding, padding);
glyphPaint.getTextPath(glyph, 0, 1, 0, 0, glyphPath);
// Add an extra path point to fix the icon remaining blank on a Galaxy Note 2 running 4.1.2.
glyphPath.computeBounds(glyphPathBounds, false);
final float centerX = glyphPathBounds.centerX();
final float centerY = glyphPathBounds.centerY();
glyphPath.moveTo(centerX, centerY);
glyphPath.lineTo(centerX + 0.001f, centerY + 0.001f);
final float areaWidthF = (float) drawableArea.width();
final float areaHeightF = (float) drawableArea.height();
final float scaleX = areaWidthF / glyphPathBounds.width();
final float scaleY = areaHeightF / glyphPathBounds.height();
final float scaleFactor = Math.min(scaleX, scaleY);
glyphPathTransform.setScale(scaleFactor, scaleFactor);
glyphPath.transform(glyphPathTransform);
// TODO this two pass calculation irks me.
// It has to be possible to push this into a single Matrix transform; what makes it hard is
// that the origin of Text is not top-left, but baseline-left so need to account for that.
glyphPath.computeBounds(glyphPathBounds, false);
final float areaLeftF = (float) drawableArea.left;
final float areaTopF = (float) drawableArea.top;
float transX = areaLeftF - glyphPathBounds.left;
transX += 0.5f * Math.abs(areaWidthF - glyphPathBounds.width());
float transY = areaTopF - glyphPathBounds.top;
transY += 0.5f * Math.abs(areaHeightF - glyphPathBounds.height());
glyphPath.offset(transX, transY);
invalidateSelf();
}
private void computeRenderingColor() {
final int newColor;
if (colorStateList != null) {
newColor = colorStateList.getColorForState(getState(), renderingColor);
} else {
newColor = color;
}
int colorWithAlpha = newColor;
if (alpha >= 0) {
colorWithAlpha = (newColor & 0x00FFFFFF) | (alpha << 24);
}
if (colorWithAlpha != renderingColor) {
renderingColor = colorWithAlpha;
glyphPaint.setColor(renderingColor);
invalidateSelf();
}
}
/**
* Used for testing only.
*
* @hide
*/
int getRenderingColor() {
return renderingColor;
}
/**
* Obtain a builder.
*
* @param context a context from which to resolve resources.
* @return a builder.
*/
public static Builder builder(Context context) {
return new Builder(context.getResources());
}
/**
* Obtain a builder.
*
* @param resources from which to resolve resources.
* @return a builder.
*/
public static Builder builder(Resources resources) {
return new Builder(resources);
}
/**
* Fluent API builder for font icons.
* <p>
* Instances of this class can be reused to construct multiple font icons. All properties are
* kept between builds.
* </p>
*/
public static class Builder {
private final Resources resources;
private int alpha = -1;
private int color;
private ColorStateList colorStateList;
private char glyph;
private int intrinsicSize = -1;
private int padding;
private float rotation;
private Typeface typeface;
Builder(Resources res) {
this.resources = res;
}
/**
* Transparency value, [0..255].
*
* @param alpha an alpha value.
*/
public Builder setAlphaValue(int alpha) {
this.alpha = alpha;
return this;
}
/**
* Resets the transparency value defined by {@link #setAlphaValue(int)} or
* {@link #setOpacity(float)}.
*/
public Builder unsetAlphaValue() {
this.alpha = -1;
return this;
}
/**
* Color value, rgb, [0..255] for each channel. If a color StateList is set, it is cleared.
*
* @param color a color value. The alpha bits are ignored.
*/
public Builder setColorValue(int color) {
this.color = color;
this.colorStateList = null;
return this;
}
/**
* Color state list, nullable.
*
* @param colorStateList color statelist.
*/
public Builder setColorStateList(ColorStateList colorStateList) {
this.colorStateList = colorStateList;
return this;
}
/**
* Color from resources. If a color StateList is set, it is cleared.
*
* @param colorResId {@code R.color} resource ID.
*/
public Builder setColorResource(int colorResId) {
this.color = resources.getColor(colorResId);
this.colorStateList = null;
return this;
}
/**
* Color StateList from resources.
*
* @param colorResId {@code R.color} resource ID.
*/
public Builder setColorStateListResource(int colorResId) {
this.colorStateList = resources.getColorStateList(colorResId);
return this;
}
/**
* Font glyph to render.
*
* @param glyph the chosen glyph.
*/
public Builder setGlyph(char glyph) {
this.glyph = glyph;
return this;
}
/**
* Intrinsic size in pixels.
*
* @param pixels size in px.
*/
public Builder setIntrinsicSizeInPixels(int pixels) {
this.intrinsicSize = pixels;
return this;
}
/**
* Intrinsic size in density-independent pixels.
*
* @param dips size in dip.
*/
public Builder setIntrinsicSizeInDip(float dips) {
return setIntrinsicSize(dips, TypedValue.COMPLEX_UNIT_DIP);
}
/**
* Intrinsic size from resources.
*
* @param dimensionResId {@code R.dimen} resource ID.
*/
public Builder setIntrinsicSizeResource(int dimensionResId) {
this.intrinsicSize = resources.getDimensionPixelSize(dimensionResId);
return this;
}
/**
* Intrinsic size in a specified unit.
*
* @param size the size.
* @param unit one of {@code TypedValue.COMPLEX_UNIT_*}.
*/
public Builder setIntrinsicSize(float size, int unit) {
float dimension = TypedValue.applyDimension(unit, size, resources.getDisplayMetrics());
this.intrinsicSize = Math.round(dimension);
return this;
}
/**
* Un-sets the intrinsic size (value will be -1).
*/
public Builder setNoIntrinsicSize() {
this.intrinsicSize = -1;
return this;
}
/**
* Transparency value, [0.0f..1.0f].
*
* @param opacity an opacity percentage.
*/
public Builder setOpacity(float opacity) {
this.alpha = Math.round(opacity * 255);
return this;
}
/**
* Padding in pixels.
*
* @param pixels padding in px.
*/
public Builder setPaddingInPixels(int pixels) {
this.padding = pixels;
return this;
}
/**
* Padding in density-independent pixels.
*
* @param dips padding in dip.
*/
public Builder setPaddingInDip(float dips) {
return setPadding(dips, TypedValue.COMPLEX_UNIT_DIP);
}
/**
* Padding from resources.
*
* @param dimensionResId {@code R.dimen} resource ID.
*/
public Builder setPaddingResource(int dimensionResId) {
this.padding = resources.getDimensionPixelSize(dimensionResId);
return this;
}
/**
* Padding in a specified unit.
*
* @param size the size.
* @param unit one of {@code TypedValue.COMPLEX_UNIT_*}.
*/
public Builder setPadding(float size, int unit) {
float dimension = TypedValue.applyDimension(unit, size, resources.getDisplayMetrics());
this.padding = Math.round(dimension);
return this;
}
/**
* Rotation in degrees, where zero is straight up and positive values go clockwise.
*
* @param rotation the rotation in degrees.
*/
public Builder setRotation(float rotation) {
this.rotation = rotation;
return this;
}
/**
* The typeface asset to select the glyph from. No caching is done.
*
* @param typeface the typeface.
*/
public Builder setTypeface(Typeface typeface) {
this.typeface = typeface;
return this;
}
/**
* Build an {@code IconFontDrawable} from the current builder state.
*
* @return the requested drawable.
*/
public IconFontDrawable build() {
return new IconFontDrawable(alpha, color, colorStateList, glyph, intrinsicSize, padding, rotation, typeface);
}
}
}