I am trying to copy this dynamic image:
The goal here is to alter the percentage of the circle with a certain color and the rest of a circle the other color depending on circumstances via java code in real-time. (IE, setting 50/50 would be half purple and half blue)
The tricky part is that the circle itself has solid colors but both outside and inside are transparent as there are items behind it that need to be seen; which is where I am getting stuck.
I would love some help figuring out how to try and make this work using either native Android properties or using a library if someone can recommend one.
What I have tried so far:
1) Making 2 circles with transparent outer and inner rings:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Outer layer of circle -->
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thicknessRatio="2"
android:useLevel="false">
<solid android:color="#android:color/transparent" />
<stroke
android:width="2dp"
android:color="#A9A9A9" />
</shape>
</item>
<!-- Inner layer of Circle -->
<item
android:left="12dp"
android:right="12dp"
android:top="12dp"
android:bottom="12dp">
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thicknessRatio="2"
android:useLevel="false">
<solid android:color="#android:color/transparent" />
<stroke
android:width="2dp"
android:color="#A9A9A9" />
</shape>
</item>
</layer-list>
And then included them in a layout with a split linear layout on each side like this:
But I am unsure how to make sections on the outside and inside of the circles invisible AND transparent while making the circle portion visible and NOT transparent.
2) mimicking this code to try and adapt to my own: https://github.com/DmitryMalkovich/circular-with-floating-action-button/blob/master/progress-fab/src/main/java/com/dmitrymalkovich/android/ProgressFloatingActionButton.java
3) Working with progress bars to try and set a percentage and then work the "not set" percentage part to the other color.
All three have not gotten me very far :(
Has anyone successfully done something like this and if so can they tell me the best way to go about recreating it? Thank you!
I ended up making a custom class that was derived from this answer: Android: looking for a drawArc() method with inner & outer radius
Code is below:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Silmarilos on 2017-05-22.
*/
public class MultiColorCircle extends View {
private RectF rect, outerRect, innerRect;
private Paint perimeterPaint;
private List<CustomStrokeObject> strokeObjects;
private int widthOfCircleStroke, widthOfBoarderStroke,
colorOfBoarderStroke, onePercentPixels;
public MultiColorCircle(Context context) {
super(context);
init();
}
public MultiColorCircle(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public MultiColorCircle(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MultiColorCircle(Context context, #Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
/**
* Setter for the width of the circle stroke. Affects all arcs drawn. This is the width of
* the various arcs that make up the actual circle, this is NOT the boarder, that is different
* #param widthOfCircleStroke
*/
public void setWidthOfCircleStroke(int widthOfCircleStroke){
this.widthOfCircleStroke = widthOfCircleStroke;
}
/**
* Setter for the width of the boarder stroke. This is the width of the boarder strokes used
* to make the inner and outer boarder of the rings that surround the main body circle.
* They will default to black and 1 pixel in width. To hide them, pass null as the color
* #param widthOfBoarderStroke
*/
public void setWidthOfBoarderStroke(int widthOfBoarderStroke){
this.widthOfBoarderStroke = widthOfBoarderStroke;
this.perimeterPaint.setStrokeWidth(this.widthOfBoarderStroke);
}
/**
* Set the color of the boarder stroke. Send in null if you want it to be hidden
* #param colorOfBoarderStroke
*/
public void setColorOfBoarderStroke(Integer colorOfBoarderStroke){
if(colorOfBoarderStroke == null){
//Set to transparent
this.colorOfBoarderStroke = Color.parseColor("#00000000");
} else {
this.colorOfBoarderStroke = colorOfBoarderStroke;
}
this.perimeterPaint.setColor(this.colorOfBoarderStroke);
}
private void init(){
this.strokeObjects = new ArrayList<>();
this.onePercentPixels = 0;
this.widthOfCircleStroke = 1; //Default
this.widthOfBoarderStroke = 1; //Default
this.colorOfBoarderStroke = Color.parseColor("#000000"); //Default, black
this.rect = new RectF();
this.outerRect = new RectF();
this.innerRect = new RectF();
this.perimeterPaint = new Paint();
this.perimeterPaint.setStrokeWidth(widthOfBoarderStroke);
this.perimeterPaint.setColor(colorOfBoarderStroke);
this.perimeterPaint.setAntiAlias(true);
this.perimeterPaint.setStyle(Paint.Style.STROKE);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = this.getWidth();
int left = 0;
int top = 0;
int right = (left + width);
int bottom = (top + width);
onePercentPixels = (int)(this.getWidth() * 0.01);
left = left + onePercentPixels + widthOfCircleStroke;
top = top + onePercentPixels + widthOfCircleStroke;
right = right - onePercentPixels - widthOfCircleStroke;
bottom = bottom - onePercentPixels - widthOfCircleStroke;
drawCircle(canvas, left, top, right, bottom);
}
private void drawCircle(Canvas canvas, int left, int top, int right, int bottom){
//Base rect for sides of circle parameters
rect.set(left, top, right, bottom);
if(this.strokeObjects.size() <= 0){
return;
}
for(CustomStrokeObject strokeObject : this.strokeObjects){
if(strokeObject == null){
continue;
}
Paint paint = strokeObject.paint;
paint.setStrokeWidth(this.widthOfCircleStroke);
canvas.drawArc(rect, strokeObject.percentToStartAt,
strokeObject.percentOfCircle, false, paint);
}
drawPerimeterCircle(canvas, left, top, right, bottom);
}
/**
* Draws the outer and inner boarder arcs of black to create a boarder
*/
private void drawPerimeterCircle(Canvas canvas, int left, int top, int right, int bottom){
//Base inner and outer rectanges for circles to be drawn
outerRect.set(
(left - (widthOfCircleStroke / 2)),
(top - (widthOfCircleStroke / 2)),
(right + (widthOfCircleStroke / 2)),
(bottom + (widthOfCircleStroke / 2))
);
innerRect.set(
(left + (widthOfCircleStroke / 2)),
(top + (widthOfCircleStroke / 2)),
(right - (widthOfCircleStroke / 2)),
(bottom - (widthOfCircleStroke / 2))
);
canvas.drawArc(outerRect, 0, 360, false, perimeterPaint);
canvas.drawArc(innerRect, 0, 360, false, perimeterPaint);
}
/**
* Setter method for setting the various strokes on the circle
* #param strokeObjects {#link CustomStrokeObject}
*/
public void setCircleStrokes(List<CustomStrokeObject> strokeObjects){
if(strokeObjects == null){
return;
}
if(strokeObjects.size() == 0){
return;
}
this.strokeObjects = new ArrayList<>();
this.strokeObjects = strokeObjects;
invalidate();
}
/**
* Class used in drawing arcs of circle
*/
public static class CustomStrokeObject {
float percentOfCircle;
float percentToStartAt;
Integer colorOfLine;
Paint paint;
/**
* Constructor. This will also do the calculations to convert the percentages into the
* circle numbers so that passing in 50 will be converted into 180 for mapping on to a
* circle. Also, I am adding in a very tiny amount of overlap (a couple pixels) so that
* there will not be a gap between the arcs because the whitespace gap of a couple pixels
* does not look very good. To remove this, just remove the -.1 and .1 to startAt and circle
* #param percentOfCircle Percent of the circle to fill.
* NOTE! THIS IS BASED OFF OF 100%!
* This is not based off of a full 360 circle so if you want something
* to fill half the circle, pass 50, not 180.
* #param percentToStartAt Percent to start at (for filling multiple colors).
* NOTE! THIS IS BASED OFF OF 100%!
* This is not based off of a full 360 circle so if you want something
* to fill half the circle, pass 50, not 180.
* #param colorOfLine Int color of the line to use
*/
public CustomStrokeObject(float percentOfCircle, float percentToStartAt, Integer colorOfLine){
this.percentOfCircle = percentOfCircle;
this.percentToStartAt = percentToStartAt;
this.colorOfLine = colorOfLine;
if(this.percentOfCircle < 0 || this.percentOfCircle > 100){
this.percentOfCircle = 100; //Default to 100%
}
this.percentOfCircle = (float)((360 * (percentOfCircle + 0.1)) / 100);
if(this.percentToStartAt < 0 || this.percentToStartAt > 100){
this.percentToStartAt = 0;
}
//-90 so it will start at top, Ex: http://www.cumulations.com/images/blog/screen1.png
this.percentToStartAt = (float)((360 * (percentToStartAt - 0.1)) / 100) - 90;
if(this.colorOfLine == null){
this.colorOfLine = Color.parseColor("#000000"); //Default to black
}
paint = new Paint();
paint.setColor(colorOfLine);
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
}
/**
* Overloaded setter, in case you want to set a custom paint object here
* #param paint Paint object to overwrite one set by constructor
*/
public void setPaint(Paint paint){
this.paint = paint;
}
}
}
To use it, define it in the xml:
<com.yourpackage.goeshere.MultiColorCircle
android:id="#+id/my_circle"
android:padding="8dp"
android:layout_margin="8dp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Then in your Java:
MultiColorCircle my_circle = (MultiColorCircle) this.findViewById(R.id.my_circle);
my_circle.setWidthOfCircleStroke(60);
my_circle.setWidthOfBoarderStroke(2);
my_circle.setColorOfBoarderStroke(ContextCompat.getColor(this, R.color.purple));
MultiColorCircle.CustomStrokeObject s1 = new MultiColorCircle.CustomStrokeObject(
50, 0, ContextCompat.getColor(this, R.color.blue)
);
MultiColorCircle.CustomStrokeObject s2 = new MultiColorCircle.CustomStrokeObject(
30, 50, ContextCompat.getColor(this, R.color.red)
);
MultiColorCircle.CustomStrokeObject s3 = new MultiColorCircle.CustomStrokeObject(
20, 80, ContextCompat.getColor(this, R.color.green)
);
List<MultiColorCircle.CustomStrokeObject> myList = new ArrayList<>();
myList.add(s1);
myList.add(s2);
myList.add(s3);
my_circle.setCircleStrokes(myList);
Adjust values accordingly.
Sil
I've got a custom view with the following code:
private final Drawable outerGauge;
private final Drawable innerGauge;
private float rotateX;
private float rotateY;
private int rotation = 0;
{
outerGauge = getContext().getDrawable(R.drawable.gauge_outer);
innerGauge = getContext().getDrawable(R.drawable.gauge_inner);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
outerGauge.draw(canvas);
canvas.rotate(rotation, rotateX, rotateY);
innerGauge.draw(canvas);
canvas.rotate(-rotation, rotateX, rotateY);
}
Most of the time this produces perfectly clear images. However, sometimes the result looks like this:
This only seems to happen on one of my two test devices. The device is a Motorola moto G, with the Android 6 upgrade. The other test device, which always seems to produce perfectly clear images, is an Oneplus X, Android 5. It's also not consistent, it happens sometimes, and then doesn't again the next moment. From what I've been able to test, it does not even depend on the amount of rotation applied. I've never seen it happen on straight angles though, (0, 90, 180 degrees,) and it does seem to be worse at angles closer to 45 or 135 degrees.
The image in question is an imported SVG, placed directly in the res/drawable folder. Therefore it can't be the resolution. (Also, gauge_outer is placed in exactly the same folder and made exactly the same way, though this one does not become blurry.)
Any ideas on how to solve this?
Edit:
Okay, never mind what I said about the complete inconsistency. It appears to be fully consistent, and be worst when the rotation comes closer and closer to 90 degrees. Also, as soon as the rotation is exactly 90 degrees, the indicator completely disappears.
Edit:
Behold: two emulators, one running Android 5 and one running Android 6:
The full source code is as follows:
package nl.dvandenberg.gauge;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
public class GaugeView extends View {
private static final int ORIGINAL_ROTATE_Y = 510;
private static final int ORIGINAL_IMAGE_HEIGHT = 613;
private static final int ORIGINAL_IMAGE_WIDTH = 1046;
private final Drawable outerGauge;
private final Drawable innerGauge;
private float rotateX;
private float rotateY;
private int rotation = 0;
{
outerGauge = getContext().getDrawable(R.drawable.gauge_outer);
innerGauge = getContext().getDrawable(R.drawable.gauge_inner);
}
public GaugeView(Context context) {
super(context);
setProgress(48);
}
public GaugeView(Context context, AttributeSet attrs) {
super(context, attrs);
setProgress(48);
}
public GaugeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setProgress(48);
}
public void setProgress(double percentage) {
this.rotation = (int) (180 * Math.min(100, Math.max(0, percentage)) / 100);
invalidate();
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
double width = MeasureSpec.getSize(widthMeasureSpec);
double idealHeight = ORIGINAL_IMAGE_HEIGHT * width / ORIGINAL_IMAGE_WIDTH;
double height = Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec));
width = width * height / idealHeight;
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) height, MeasureSpec.getMode(heightMeasureSpec));
rotateX = (float) (width / 2f);
rotateY = (float) (height / ORIGINAL_IMAGE_HEIGHT * ORIGINAL_ROTATE_Y);
outerGauge.setBounds(0, 0, (int) width, (int) height);
innerGauge.setBounds(0, 0, (int) width, (int) height);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
outerGauge.draw(canvas);
canvas.rotate(rotation, rotateX, rotateY);
innerGauge.draw(canvas);
}
}
with drawable/gauge_inner.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1046dp"
android:height="613dp"
android:viewportWidth="1046"
android:viewportHeight="613">
<path
android:fillColor="#aa3939"
android:pathData="M142.541,516.071 C145.053,517.623,156.088,519.334,183.255,522.586
C203.832,525.024,251.438,530.676,289.03,535.184
C326.708,539.641,359.782,543.523,362.537,543.896
C365.292,544.268,388.127,547.018,413.445,550.067 L459.289,555.468
L462.946,560.401 C468.075,567.485,479.691,577.405,489.255,582.968
C499.701,589.062,520.069,594.737,531.817,594.883
C571.623,595.225,607.57,570.083,620.01,533.226
C624.956,518.592,626.123,507.412,624.269,492.201
C622.686,479.259,620.262,472.461,612.212,458.518
C602.012,440.852,592.681,431.69,575.424,422.602
C537.988,402.763,489.163,413.401,462.78,447.108 L458.957,452.086
L449.523,453.146 C444.316,453.727,420.115,456.614,395.829,459.552
C371.456,462.538,346.451,465.429,340.177,466.165
C333.904,466.9,293.067,471.772,249.427,476.991
C205.788,482.211,164.951,487.082,158.678,487.817
C144.122,489.408,139.036,491.998,136.796,498.719
C134.433,505.626,136.72,512.388,142.541,516.07 Z" />
</vector>
and drawable/gauge_outer.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1046dp"
android:height="613dp"
android:viewportWidth="1046"
android:viewportHeight="613">
<path
android:fillColor="#aa3939"
android:pathData="M488.981,0.56719 C465.882,2.06727,430.783,6.96753,412.984,11.0677
C392.285,15.768,387.285,17.0681,375.285,20.6683
C231.691,63.4706,113.696,164.376,49.898,299.183
C16.6993,369.187,0,444.491,0,523.495
C0,540.296,0.0999961,541.696,1.99992,543.596
C3.99984,545.596,5.29979,545.596,59.4977,545.596
C113.696,545.596,114.996,545.596,116.995,543.596
C118.895,541.696,118.995,540.296,118.995,522.595
C118.995,504.894,118.895,503.494,116.995,501.594
C115.095,499.694,113.695,499.594,85.2962,499.594 L55.6974,499.594
L56.2974,489.793 C60.0973,433.69,76.3966,372.387,101.396,320.384
C103.996,314.984,106.496,310.383,106.896,310.183
C107.396,309.883,110.796,311.483,114.596,313.683
C118.396,315.983,124.396,319.483,127.995,321.583
C131.595,323.583,139.195,328.083,144.994,331.484
C155.694,337.684,159.993,338.884,163.193,336.284
C164.893,334.984,171.293,324.483,177.992,312.083
C183.292,302.282,183.092,299.882,176.492,295.782
C173.992,294.282,162.593,287.582,151.093,281.081 L130.294,269.08 L135.294,261.58
C166.593,214.877,210.691,170.375,258.589,137.273
C268.189,130.673,269.889,129.873,270.489,131.273
C272.389,136.273,298.388,179.776,299.988,180.576
C300.988,181.176,302.788,181.576,303.888,181.576
C306.288,181.576,334.787,165.275,336.787,162.775
C339.187,159.575,337.987,155.575,330.887,143.274
C326.987,136.574,322.987,129.773,322.087,128.273
C321.187,126.673,318.087,121.273,315.287,116.372
C312.387,111.372,309.987,107.072,309.987,106.671
C309.987,105.371,342.586,90.7702,360.385,84.0698
C388.684,73.5692,427.382,63.5687,455.981,59.6685
C468.68,57.8684,490.98,55.5683,495.579,55.5683 L499.979,55.5683 L499.979,85.0699
C499.979,113.271,500.079,114.671,501.979,116.572
C503.879,118.472,505.279,118.572,522.978,118.572
C540.677,118.572,542.077,118.472,543.977,116.572
C545.877,114.672,545.977,113.272,545.977,84.8703 L545.977,55.2687
L555.977,55.9687 C581.776,57.5688,617.875,63.7691,644.874,71.0695
C670.273,77.9699,702.072,89.7705,722.771,99.871
C729.071,102.971,734.671,105.671,735.271,105.871
C735.871,106.071,730.171,117.072,722.172,131.072
C713.772,145.773,707.973,156.973,707.973,158.573
C707.973,162.273,709.373,163.573,718.973,169.274
C741.272,182.375,743.072,183.075,746.772,179.775
C748.472,178.375,765.571,149.773,773.871,134.373 L776.471,129.773
L787.471,137.373 C834.969,170.075,877.067,212.377,910.266,260.98
C912.866,264.78,914.866,268.28,914.766,268.78
C914.566,269.28,903.866,275.78,890.967,283.181
C878.068,290.581,866.668,297.582,865.768,298.782
C862.268,302.782,863.268,305.182,878.268,330.084
C884.168,339.785,886.468,339.885,900.967,331.484
C906.767,328.084,914.366,323.584,917.966,321.583
C921.566,319.483,927.566,315.983,931.365,313.683
C935.265,311.383,938.565,309.583,938.865,309.583
C939.565,309.583,946.665,324.184,952.164,337.084
C972.463,383.986,986.363,440.49,989.663,489.792 L990.263,499.592
L960.664,499.592 C932.265,499.592,930.865,499.692,928.965,501.592
C927.065,503.492,926.965,504.892,926.965,522.593
C926.965,540.294,927.065,541.694,928.965,543.594
C930.965,545.594,932.265,545.594,986.463,545.594
C1041.86,545.594,1041.96,545.594,1044.06,543.494
C1046.26,541.294,1046.26,540.994,1045.66,513.192
C1044.76,470.69,1040.36,436.088,1031.36,398.586
C1027.46,382.685,1026.86,380.485,1020.26,360.084
C1009.06,325.382,990.461,284.58,971.762,253.578
C923.864,174.276,855.866,108.873,775.07,64.3706
C712.572,29.8688,645.075,8.96764,574.477,2.06727
C555.278,0.16716,507.68,-0.63288,488.981,0.56719 Z" />
</vector>
Though not an answer, I have managed to find a workaround. This workaround relies on drawing the image onto a canvas, which is linked to a bitmap, which is then drawn onto the final, rotated canvas in the onDraw method.
It seems like this problem really only arises with nodpi-drawables, in other words, imported svg's. It is however, very consistent. Whether the shape is a multi-path vector or a simple square does not matter, the problem will always take exactly the same shape, with images disappearing entirely when the canvas is rotated 90°.
The full code I used to bypass this problem is as follows:
package nl.dvandenberg.energymonitor.customViews;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import nl.dvandenberg.energymonitor.R;
public class GaugeView extends View {
private static final int ORIGINAL_ROTATE_Y = 510;
private static final int ORIGINAL_IMAGE_HEIGHT = 613;
private static final int ORIGINAL_IMAGE_WIDTH = 1046;
private final Drawable outerGauge, innerGauge;
private float rotateX;
private float rotateY;
private int rotation = 0;
private Bitmap innerGaugeBitmap;
private final Canvas innerGaugeCanvas;
{
outerGauge = getContext().getDrawable(R.drawable.gauge_outer);
innerGauge = getContext().getDrawable(R.drawable.gauge_inner);
innerGaugeCanvas = new Canvas();
}
public GaugeView(Context context) {
super(context);
}
public GaugeView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public GaugeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setProgress(double percentage) {
this.rotation = (int) (180 * Math.min(100, Math.max(0, percentage)) / 100);
invalidate();
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
double width = MeasureSpec.getSize(widthMeasureSpec);
double idealHeight = ORIGINAL_IMAGE_HEIGHT * width / ORIGINAL_IMAGE_WIDTH;
double height = Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec));
width = width * height / idealHeight;
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) height, MeasureSpec.getMode(heightMeasureSpec));
rotateX = (float) (width / 2f);
rotateY = (float) (height / ORIGINAL_IMAGE_HEIGHT * ORIGINAL_ROTATE_Y);
outerGauge.setBounds(0, 0, (int) width, (int) height);
innerGauge.setBounds(0, 0, (int) width, (int) height);
if (innerGaugeBitmap != null){
innerGaugeBitmap.recycle();
}
innerGaugeBitmap = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888); // Gives LINT-warning draw-allocation, but no other way to upscale bitmaps exists.
innerGaugeCanvas.setBitmap(innerGaugeBitmap);
innerGaugeBitmap.eraseColor(Color.TRANSPARENT);
innerGauge.draw(innerGaugeCanvas);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
protected void onDraw(Canvas canvas) {
outerGauge.draw(canvas);
canvas.rotate(rotation, rotateX, rotateY);
canvas.drawBitmap(innerGaugeBitmap,0,0,null);
}
}
with the important part occuring in the onMeasure method:
if (innerGaugeBitmap != null){
innerGaugeBitmap.recycle();
}
innerGaugeBitmap = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888); // Gives LINT-warning draw-allocation, but no other way to upscale bitmaps exists.
innerGaugeCanvas.setBitmap(innerGaugeBitmap);
innerGaugeBitmap.eraseColor(Color.TRANSPARENT);
innerGauge.draw(innerGaugeCanvas);
I have filed a bugreport at https://code.google.com/p/android/issues/detail?id=208453
This is what I wish to achieve:
Clicky
The container color, the progress color, the progress background color and the rounded edge radius as well as the thickness should all be editable and modifiable.
How could this be achieved with a light weight custom UI element?
After days of research, I was able to achieve what was expected with clear crisp UI and with all the above requirements and flexibility. The exact above UI can be achieved and follow parameters can be achieved as well:
1. Progress Color
2. Progress background color
3. Container color (Color of container to be set by you, you can set color of rounded edges to match the container color)
4. Height and width of the progress bar to suit your needs.
Here's the code and steps to implement it:
I. Put this code in the attrs.xml file under the values folder
<declare-styleable name="SlantingProgressBar">
<attr name="slantingProgress" format="integer"/>
<attr name="borderRadius" format="integer"/>
<attr name="borderColor" format="integer"/>
<attr name="slantingProgressColor" format="string"/>
<attr name="progressBackgroundColor" format="string"/>
<attr name="slantingProgressFullColor" format="string"/>
</declare-styleable>
II. Create a java class like this:
public class SlantingProgressbar extends View {
private float height = 0;
private float width = 0;
private int borderRadius = 20;
private float progress = 0;
private int rawProgress = 0;
private static final String OPACITY_30_PERCENT = "#66";
private int roundedBorderColor;
private String backgroundColor = "";
private String progressColor = "";
private String progressFullColor = "#fc3d39";
public SlantingProgressbar(Context context) {
super(context);
}
public SlantingProgressbar(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.SlantingProgressBar,
0, 0);
try {
setProgress(array.getInt(R.styleable.SlantingProgressBar_slantingProgress, 0));
setBackgroundColor(array.getString(R.styleable.SlantingProgressBar_progressBackgroundColor)); //Default color set in the method
setBorderRadius(array.getInt(R.styleable.SlantingProgressBar_borderRadius, 20));
setRoundedBorderColor(array.getInt(R.styleable.SlantingProgressBar_borderColor, 0));
setProgressColor(array.getString(R.styleable.SlantingProgressBar_slantingProgressColor));
} finally {
array.recycle();
}
}
public void setBorderRadius(int borderRadius) {
this.borderRadius = borderRadius;
}
public int getProgress() {
return rawProgress;
}
public void setProgress(int progress) {
if(progress >=0)
{
this.rawProgress = progress;
this.invalidate();
}
else
Log.e("ChlorophyllProgressBar", "Invalid 'progress' value detected, value should be between 0 and 100");
}
public void setRoundedBorderColor(int roundedBorderColor) {
if ( roundedBorderColor == 0) {
this.roundedBorderColor = getResources().getColor(R.color.white);
Log.e("CUSTOM_TAG", "Color set to White: " + this.roundedBorderColor);
return;
}
this.roundedBorderColor = roundedBorderColor;
Log.e("CUSTOM_TAG", "Color set to custom: " + this.roundedBorderColor);
}
private int getRoundedBorderColor()
{
return roundedBorderColor;
}
public void setSlantingProgressFullColor(String color)
{
if (TextUtils.isEmpty(progressFullColor)) {
this.progressFullColor = "#fc3d39";
return;
}
}
public void setBackgroundColor(String backgroundColor) {
if (TextUtils.isEmpty(backgroundColor)) {
this.backgroundColor = "#bfe8d4";
return;
}
this.backgroundColor = backgroundColor;
}
public void setProgressColor(String progressColor) {
if (TextUtils.isEmpty(progressColor)) {
this.progressColor = "#2bb673"; //Green
return;
}
this.progressColor = progressColor;
}
public float getViewHeight() {
return height;
}
public void setViewHeight(float height) {
this.height = height;
}
public float getViewWidth() {
return width;
}
public void setViewWidth(float width) {
this.width = width;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
height = getHeight();
width = getWidth();
progress = getProcessedProgress();
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.parseColor(backgroundColor));
canvas.drawPaint(paint);
paint.setColor(getProcessedProgressColor());
paint.setAntiAlias(true);
Log.d("CUSTOM_TAG", "Height: " + height);
canvas.drawRect(0, 0, progress, height, paint);
Path triangle = new Path();
triangle.setFillType(Path.FillType.EVEN_ODD);
triangle.moveTo(progress, 0);
triangle.lineTo(progress + height, 0);
triangle.lineTo(progress, height);
triangle.close();
canvas.drawPath(triangle, paint);
drawBorders(canvas, getRoundedBorderColor());
}
private void drawBorders(Canvas canvas, int color) {
float height = getHeight();
float trueWidth = getWidth();
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
//paint.setColor(getResources().getColor(R.color.white));
paint.setAntiAlias(true);
Path border = new Path();
border.moveTo(0, 0);
border.lineTo(0, height / 2);
border.quadTo(height / borderRadius, height / borderRadius, height / 2, 0);
border.lineTo(0, 0);
canvas.drawPath(border, paint);
border.reset();
border.moveTo(0, height);
border.lineTo(height / 2, height);
border.quadTo(height / borderRadius, (height - height / borderRadius), 0, height / 2);
border.lineTo(0, height);
canvas.drawPath(border, paint);
border.reset();
border.moveTo(trueWidth, 0);
border.lineTo(trueWidth - (height / 2), 0);
border.quadTo((trueWidth - height / borderRadius), height / borderRadius, trueWidth, height / 2);
border.lineTo(trueWidth, 0);
canvas.drawPath(border, paint);
border.reset();
border.moveTo(trueWidth, height);
border.lineTo(trueWidth - (height / 2), height);
border.quadTo((trueWidth - height / borderRadius), (height - height / borderRadius), trueWidth, height / 2);
border.lineTo(trueWidth, height);
canvas.drawPath(border, paint);
//Adding 1 pixel color
Paint paint1 = new Paint();
paint1.setStyle(Paint.Style.FILL);
int fadedColor = (color & 0x00FFFFFF) | 0x66000000;
Log.d("CUSTOM_TAG", "Faded Color Code: " + fadedColor);
paint1.setColor(fadedColor);
canvas.drawRect(0, 0, 1, height, paint1);
canvas.drawRect(trueWidth-1, 0, trueWidth, height, paint1);
}
private float getProcessedProgress()
{
return (rawProgress == 99) ? ((getWidth() * 98) / 100) : ((getWidth() * rawProgress) / 100);
}
private int getProcessedProgressColor()
{
if(rawProgress > 100)
{
return Color.parseColor(progressFullColor);
}
else
{
return Color.parseColor(progressColor);
}
}
}
III. To use the layout in your xml file:
<com.whatever.package.SlantingProgressbar
android:id="#+id/progressbar_detail"
android:layout_width="match_parent"
android:layout_height="#dimen/dimension1"
android:layout_alignParentLeft="true"
slanting_progress:borderColor="#color/darkgray"
android:layout_below="#id/alphacon_detail"
android:layout_marginBottom="#dimen/budget_list_item_paddingBottom"
android:progress="50" />
I'm sharing this code after a little while, so I might have missed out a thing or two, I'm pretty sure you can get that worked out, please feel free to correct me.
Explanation:
We're using the 'draw' methods in java to implement this feature. The advantage is that, drawing a UI element gives us a sharp and clear UI no matter how big or small you make it.
There might be some hardcoded values, so be sure to edit those before implementing.
Good luck and don't forget to up-vote if this post helps you. Thanks! :)
I'll post an answer here to show some improvements on your code.
You should avoid creating new objects during draw.
draw is called several times again and again to redraw your custom element and all those calls to new Paint() are creating new objects, that needs new memory allocation, and it drives the garbage collector crazy and makes your View much more resource intensive and probably will cause lag on scrolling elements such as RecyclerView.
Alternatively you should have them declared as private Paint border and then private Paint triangle, etc, etc. And then you should initialise the values of All the paints in a separate method and only if the parameters changed. An example code:
private boolean initPaint = false;
private void initPaintsIfNecessary(){
if(!initPaint) return;
initPaint = false;
triangle = new Paint();
triangle.set.... etc
border = new Paint();
border.set.... etc
}
then on all the methods setRoundedBorderColor, setProgressColor, etc. You call initPaint = true; and on the beginning of draw you call initPaintsIfNecessary();. This will avoid all the extra garbage collector work and will allow the UI of your app to run much smoother.
That also includes all the Paint inside drawBorders method.
use format="color" instead ofstring`.
Calling Color.parse(String) is a very slow call and it is very error prone. Alternatively you should the correct color element, like following:
<attr name="slantingProgressColor" format="color"/>
that not just is the correct way, but gives you a color preview on the editor, can be indexed on app style parameters and avoid this inefficient call to parse
then of course you should adjust or method appriately. For example:
setProgressColor(array.getColor(R.styleable.SlantingProgressBar_slantingProgressColor));
getColor will return an integer that can be directly used in paint.setColor(int);
I hope those tips can help you (and others in the community) to create better more efficient View elements. Happy coding!
I know this this old question to answer but this answer may helpful..
You can use drawArc method to achieve this..
RectF oval = new RectF();
oval.set(left, top ,right, bottom);
canvas.drawArc(oval, 270, 360, false, paint);