Skip to content

Instantly share code, notes, and snippets.

@fullkomnun
Last active July 21, 2017 16:37
Show Gist options
  • Save fullkomnun/5b2addfe5b4cdc1f91caf49632d6fc61 to your computer and use it in GitHub Desktop.
Save fullkomnun/5b2addfe5b4cdc1f91caf49632d6fc61 to your computer and use it in GitHub Desktop.
ColorBarDrawable
package ornoyman.com.colorbardrawable;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import static com.fernandocejas.arrow.checks.Preconditions.checkArgument;
/**
* A drawable that draws a rounded-corners bar with multiple colored sections.
* Either each color is assigned a weight or all given the same weight.
*/
public abstract class ColorBarDrawable extends Drawable {
private final int[] sectionColors;
private final float[] sectionWeights;
protected final Paint backgroundPaint = new Paint();
protected int width;
protected int height;
protected float radius;
protected int left;
protected int top;
protected int right;
protected int bottom;
protected ColorBarDrawable(@NonNull int[] sectionColors, @Nullable float[] sectionWeights) {
checkArgument(sectionColors.length != 0, "at least one color must be defined");
checkArgument(sectionWeights == null || sectionColors.length == sectionWeights.length,
"if defined all colors should be assigned weights");
checkArgument(sectionColors.length == 1 && sectionWeights != null,
"weights cannot be defined for single color");
this.sectionColors = fixColors(sectionColors);
this.sectionWeights = fixWeights(sectionWeights);
}
@NonNull private int[] fixColors(@NonNull int[] sectionColors) {
return sectionColors.length > 1 ? sectionColors
: new int[] { sectionColors[0], sectionColors[0] };
}
@Nullable private float[] fixWeights(@Nullable float[] sectionWeights) {
if (sectionWeights == null) {
return null;
}
float weightSum = 1f;
for (int i = 0; i < sectionWeights.length; i++) {
weightSum -= sectionWeights[i];
}
if (weightSum > 0) {
sectionWeights[sectionWeights.length - 1] += weightSum;
}
return sectionWeights;
}
@Override public void draw(@NonNull Canvas canvas) {
final Rect bounds = getBounds();
left = bounds.left;
top = bounds.top;
right = bounds.right;
bottom = bounds.bottom;
width = bounds.right - left;
height = bounds.bottom - top;
radius = getCornersRadius();
drawLastSection(canvas);
float drawPosition = drawFirstSection(canvas);
for (int i = 1; i < sectionsCount() - 1; i++) {
drawPosition += drawSection(canvas, drawPosition, i);
}
}
protected int sectionsCount() {
return sectionColors.length;
}
protected abstract float getCornersRadius();
private void drawLastSection(Canvas canvas) {
drawSection(canvas, 0, sectionsCount() - 1);
}
private float drawFirstSection(Canvas canvas) {
return drawSection(canvas, getInitialDrawPosition(), 0);
}
protected abstract int getInitialDrawPosition();
protected abstract float drawSection(@NonNull Canvas canvas, float drawPosition, int idx);
protected int getSectionColor(int idx) {
return sectionColors[idx];
}
protected float getSectionWeight(int idx) {
return sectionWeights != null ? sectionWeights[idx] : 1f / sectionsCount();
}
@Override public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
}
@Override public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override public int getOpacity() {
return PixelFormat.UNKNOWN;
}
public static final class HorizontalColorBarDrawable extends ColorBarDrawable {
public HorizontalColorBarDrawable(@NonNull int[] sectionColors,
@Nullable float[] sectionWeights) {
super(sectionColors, sectionWeights);
}
@Override protected float getCornersRadius() {
return height / 2f;
}
@Override protected int getInitialDrawPosition() {
return left;
}
@Override protected float drawSection(@NonNull Canvas canvas, float drawPosition, int idx) {
backgroundPaint.setColor(getSectionColor(idx));
final float sectionWidth = getSectionWidth(idx);
if (idx == 0) {
drawRoundedLeftRect(canvas, drawPosition, top, drawPosition + sectionWidth, height, radius);
} else if (idx == sectionsCount() - 1) {
drawRoundedRightRect(canvas, width - sectionWidth, top, width, height, radius);
} else {
canvas.drawRect(drawPosition, top, drawPosition + sectionWidth, height, backgroundPaint);
}
return sectionWidth;
}
private float getSectionWidth(int idx) {
return getSectionWeight(idx) * width;
}
private void drawRoundedLeftRect(@NonNull Canvas canvas, float left, float top, float right,
float bottom, float radius) {
canvas.drawCircle(radius, radius, radius, backgroundPaint);
canvas.drawRect(left + radius, top, right, bottom, backgroundPaint);
}
private void drawRoundedRightRect(@NonNull Canvas canvas, float left, float top, float right,
float bottom, float radius) {
canvas.drawCircle(right - radius, radius, radius, backgroundPaint);
canvas.drawRect(left, top, right - radius, bottom, backgroundPaint);
}
}
public static final class VerticalColorBarDrawable extends ColorBarDrawable {
public VerticalColorBarDrawable(@NonNull int[] sectionColors,
@Nullable float[] sectionWeights) {
super(sectionColors, sectionWeights);
}
@Override protected float getCornersRadius() {
return width / 2f;
}
@Override protected int getInitialDrawPosition() {
return top;
}
@Override protected float drawSection(@NonNull Canvas canvas, float drawPosition, int idx) {
backgroundPaint.setColor(getSectionColor(idx));
final float sectionHeight = getSectionHeight(idx);
if (idx == 0) {
drawRoundedTopRect(canvas, left, drawPosition, right, drawPosition + sectionHeight, radius);
} else if (idx == sectionsCount() - 1) {
drawRoundedBottomRect(canvas, left, height - sectionHeight, width, height, radius);
} else {
canvas.drawRect(left, drawPosition, right, drawPosition + sectionHeight, backgroundPaint);
}
return sectionHeight;
}
private float getSectionHeight(int idx) {
return getSectionWeight(idx) * height;
}
private void drawRoundedTopRect(@NonNull Canvas canvas, float left, float top, float right,
float bottom, float radius) {
canvas.drawCircle(radius, radius, radius, backgroundPaint);
canvas.drawRect(left, top + radius, right, bottom, backgroundPaint);
}
private void drawRoundedBottomRect(@NonNull Canvas canvas, float left, float top, float right,
float bottom, float radius) {
canvas.drawCircle(radius, bottom - radius, radius, backgroundPaint);
canvas.drawRect(left, top, right, bottom - radius, backgroundPaint);
}
}
}
package ornoyman.com.colorbardrawable.example;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import ornoyman.com.colorbardrawable.ColorBarDrawable;
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final View bar = findViewById(R.id.bar);
bar.setBackground(new ColorBarDrawable.HorizontalColorBarDrawable(new int[] { Color.RED, Color.CYAN, Color.GREEN}, new float[] { 0.05f, 0.8f, 0.15f }));
}
}
@fullkomnun
Copy link
Author

  1. As much as I hate working with Java's arrays directly and prefer collections whenever possible(as "Effective Java" recommends),
    when writing custom views(or drawables) usually performance is also a consideration, so if I am not mistaken usage of collections such as
    ArrayList does incur boxing/unboxing for primitives while arrays doesn't. Besides, combining the colors and weights into one class is cleaner but incurs the overhead of another object to create and hold for each. I would also like to the allow defining no weights for uniform distribution, so the api should support this.
    Using a factory method or builder could also help to make things cleaner but requires some more code..

Adding setter methods that invalidate for redraw(and possibly apply smooth animations) is also an option if required.

  1. design mode is a great idea. Will try to support it.

@fullkomnun
Copy link
Author

Design mode through "isInEditMode" is relevant in the context of custom views but not so much for custom drawables

@mirono
Copy link

mirono commented Apr 25, 2017

In this case maybe the default colors/weights should be set to green/red 60:40 so it will show in design, and the setter will override it on runtime set( [] colors, [] weights ) . or maybe use tools:backgroundDrawable if exists (I don't remember) to set to a predefined custom drawable that will show the 60:40 etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment