I'm not looking for anything fancy. I have tried to follow tutorials about pie and bar graphs, but a lot of the tutorials don't follow the guidelines of my app. In my app, in one of my fragment layouts, I would like to simply draw a pie chart of two values.
as simple as -
Draw Circle
(%value) of the circle is red.
(other%value) of circle is blue.
Can this not be done this way?
No need to do the leg work when someone else has made it available.
I highly recommend HoloGraphLibrary. I use it anytime I need to make a pie graph.
https://bitbucket.org/danielnadeau/holographlibrary/wiki/Home
It is very easy to use and it looks great.
If all you want is the chart you describe, then I think adding any library is major overkill. This very simple example should give you some ideas. The setPercentage() method sets the size of the red area; the remainder will be blue. Please note that there is no exception handling implemented, and the percentage should be between 0 and 100, inclusive.
public class MainActivity extends Activity
{
SimplePieChart pie;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
pie = new SimplePieChart(this);
pie.setPercentage(65);
setContentView(pie);
}
public class SimplePieChart extends View
{
private RectF rect = new RectF();
private Paint paint = new Paint();
private int percentage;
public SimplePieChart(Context context)
{
this(context, null);
}
public SimplePieChart(Context context, AttributeSet attrs)
{
super(context, attrs);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
}
public void setPercentage(int percentage)
{
this.percentage = percentage;
invalidate();
}
public int getPercentage()
{
return percentage;
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
if (w > h)
{
rect.set(w / 2 - h / 2, 0, w / 2 + h / 2, h);
}
else
{
rect.set(0, h / 2 - w / 2, w, h / 2 + w / 2);
}
}
#Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
paint.setColor(Color.RED);
canvas.drawArc(rect, 0, 360 * percentage / 100, true, paint);
paint.setColor(Color.BLUE);
canvas.drawArc(rect, 360 * percentage / 100, 360 - 360 * percentage / 100, true, paint);
}
}
}
Related
I used this code to draw text vertically.
RectF rectF2 = new RectF();
matrix.mapRect(rectF2, bounds);
canvas.save();
canvas.rotate(90, rectF2.right, rectF2.top);
canvas.drawText(text, rectF2.left, rectF2.bottom, mTextPaint);
canvas.restore();
This works well, but I want to change the coordinates as well. Because later I tap on the object and do the drag and drop.
Now the problem is, As you see in the following image, the coordinates are drawn as rectangle. So when I tap on that rectangle area can only be able to move around the text on canvas.
So I want to rotate the original coordinates as well when I rotate the canvas. I tried matrix.setRotate But I can't able to achieve what I want.
In onDraw(), you can transform your canvas using a matrix.
And in onTouch(), you can transform-back the screen coordinates using the matrix inverse.
private static class MyView extends View {
private final Paint texPaint = new Paint(){{
setTextSize(60);
setColor(Color.BLACK);
}};
private final Paint rectPaint = new Paint(){{
setStrokeWidth(20);
setColor(Color.RED);
setStyle(Paint.Style.STROKE);
}};
private final Matrix matrix = new Matrix();
private final RectF rect = new RectF(0, 0, 400, 150);
public MyView(Context context) {
super(context);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
matrix.reset();
matrix.postRotate(45);
matrix.postScale(1.5f, 1.5f, 0, 0);
matrix.postTranslate(w/2, h/2);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.concat(matrix);
canvas.drawRect(rect, rectPaint);
//Bottom line of the drawn text is at y, if you want the text to inside the rect
// increase the y by the text height, textHeight is the textSize
canvas.drawText("SomeText", 0, texPaint.getTextSize(), texPaint);
canvas.restore();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
float x = event.getX();
float y = event.getY();
Toast.makeText(getContext(), isInsideRegion(x, y) ? "Inside" : "Outside", Toast.LENGTH_SHORT).show();
}
return true;
}
private boolean isInsideRegion(float x, float y) {
Matrix invertedMatrix = new Matrix();
if (!matrix.invert(invertedMatrix)) {
throw new RuntimeException("Matrix can't be inverted");
}
float[] point = {x, y};
invertedMatrix.mapPoints(point);
return rect.contains(point[0], point[1]);
}
}
Well, if you want drag objects in this view, you probably need to create ViewGroup and place objects as Views. Because all of your object drawn on canvas you can't tap and drag on them.
You can rotate, translate, and resize views and also intercept touch events on them to perform dragging
Your solution works only like Image with text and, if I understand you correctly, you can't do with them what you want
In my app I need to draw circles using bitmap and the drawCircle() method.
Everything was working fine and exactly as it should up until Android 6.0.
It still draws circles on all the previous versions, but draws rectangles when I use the app on 6.0. But if I change it to be filled, it draws a circle both in api 22 and api 23.
Anyone has the same problem or any idea why this happens?
Here is the source code and a screenshot (app running on API 23 on the left, and API 22 on the right). same app on different api's
public final class Circle1View extends View {
private float xCenter, yCenter;
private Bitmap grid = null;
public Circle1View (Context context) {
super(context);
init();
}
private void init() {
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
#Override
protected void onDraw(Canvas canvas) {
int w = getWidth();
int h = getHeight();
xCenter = w / 2;
yCenter = h / 2;
drawBitmaps(w, h);
canvas.translate(xCenter, yCenter);
canvas.scale(xCenter, yCenter);
canvas.drawBitmap(grid, null, new RectF(-1, -1, 1, 1), null);
}
private void drawBitmaps(int w, int h) {
grid = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas();
canvas.translate(xCenter, yCenter);
canvas.scale(xCenter, yCenter);
Paint gridPaint = new Paint();
gridPaint.setStrokeWidth(0.01f);
// Works with FILL
// gridPaint.setStyle(Paint.Style.FILL);
gridPaint.setStyle(Paint.Style.STROKE);
canvas.setBitmap(grid);
canvas.drawCircle(0, 0, 0.5f, gridPaint);
}
}
I think it has something to do with the scaling and translation you do. Imagine the circle that is drawn is so small, it only takes 4 pixels. When enlarging this back to the full size, you are left with 4 straight lines between these pixels.
When I change the stroke width to 0.04f, the issue is gone. I would suggest you simplify your code by drawing on the supplied Canvas directly:
#Override
protected void onDraw(Canvas canvas) {
int w = getWidth();
int h = getHeight();
xCenter = w / 2;
yCenter = h / 2;
Paint gridPaint = new Paint();
gridPaint.setStrokeWidth(1f);
gridPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(xCenter, yCenter, w/4, gridPaint);
}
As for your question about the difference between API levels: Marshmallow introduced changes for drawBitmap(). You can have a look at the respective source code for Lollipop and Marshmallow.
Illustration
I am drawing multiple segments of an arc using the same RectF, however the arcs do not line up properly. I tried all CAP Options, but it never looks symmetrical.
private void initPaint(TypedArray a){
segmentInactivePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
segmentInactivePaint.setColor(a.getColor(R.styleable.SegmentsCustomView_segmentBgColor, segmentBgColor));
segmentInactivePaint.setStrokeWidth(30f);
segmentInactivePaint.setStyle(Paint.Style.STROKE);
segmentsBGPaint = new Paint(segmentInactivePaint);
segmentsBGPaint.setAlpha(64);
segmentActivePaint = new Paint(segmentInactivePaint);
segmentActivePaint.setColor(a.getColor(R.styleable.SegmentsCustomView_segmentActiveColor,segmentActiveColor));
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if(getWidth()<getHeight()) {
rectF.set(getLeft(), getTop(), getWidth() - getLeft(), getWidth() - getLeft());
}else{
rectF.set(getLeft(), getTop(), getHeight() - getTop(), getHeight() - getTop());
}
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
position = 0;
for(int i=0; i < segmentSizes.length; i++){
canvas.drawArc(rectF,position-180,segmentSizes[i]-segmentGap,false,
i != segmentActive ?
i < segmentActive ? segmentInactivePaint : segmentsBGPaint
: segmentActivePaint);
position+=segmentSizes[i];
}
}
I also tried using a static RectF, that is not changed by any resizing events, so that is not the problem.
RectF (0):﹕ [75.0,75.0][759.0,759.0]
RectF (1):﹕ [75.0,75.0][759.0,759.0]
RectF (2):﹕ [75.0,75.0][759.0,759.0]
RectF (3):﹕ [75.0,75.0][759.0,759.0]
RectF (4):﹕ [75.0,75.0][759.0,759.0]
My guess would be, that the Canvas.drawArc method creates a partial path and therefore the interpolation of the arc always differs from the arc of a full circle.
A push in the right direction would be greatly appreciated.
So I managed to fix the problem by using a Path instead of the drawArc() method of the canvas. By rewinding the path whenever I am drawing a new segment.
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
position = 0;
for(int i=0; i < segmentSizes.length; i++){
if(i == segmentActive){
tmpPaint = segmentActivePaint;
}else if(i > segmentActive){
tmpPaint = segmentsBGPaint;
}else{
tmpPaint = segmentInactivePaint;
}
path.rewind();
path.addArc(rectF,position - 180, segmentSizes[i] - segmentGap);
canvas.drawPath(path, tmpPaint);
position+=segmentSizes[i];
}
path.reset();
}
The rewinding seems to manage to draw a nearly perfect circle regardless of the number of sub-arcs.
The drawArc() method seems to work fairly similar to addArc(), however it resets the path on every call.
I read different questions here about this topic, but I still can't find an answer. Feel free to close this question for any reason.
I have a simple Circle class that extends View.
The code for this class is:
public class ProgressCircle extends View {
Paint mCirclePaint;
float extRadius;
float viewWidth, viewHeight;
float centerX, centerY;
public ProgressCircle(Context context, AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
init();
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
float xpad = (float) getPaddingLeft() + getPaddingRight();
float ypad = (float) getPaddingTop() + getPaddingBottom();
float ww = (float)w - xpad; float hh = (float)h - ypad;
extRadius = Math.min(ww, hh) / 2;
viewWidth = this.getWidth();
viewHeight = this.getHeight();
centerX = viewWidth / 2; centerY = viewHeight / 2;
super.onSizeChanged(w, h, oldw, oldh);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(centerX, centerY, extRadius, mCirclePaint);
canvas.drawText("ciao", 0, 0, mCirclePaint);
}
private void init() {
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setColor(0x666666);
mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
I confirmed that every method in this classed is called when the main activity is created (through the use of some Log.d()s). I added a <com.mypackage.Circle> element in my main activity's LinearLayout and after that I added a sample testing button.
What I achieved is that the button is shown while my Circle isn't, but still the button (which in the LinearLayout comes after the Circle) is not the first element of the layout: that makes me think that something actually happens, but nothing gets drawn.
It was just a silly problem: the color in mCirclePaint.setColor(0x666666) was an invalid one. It worked with mCirclePaint.setColor(Color.RED) or with any other color defined in the res folder.
If you need to specify a color value, you'll have to include the transparency byte (otherwise it is not a 32bit integer that you are specifying, but a 24bit). So 0x666666 is invalid, but 0xff666666 is a valid color and will draw.
After reading the documentation(http://developer.android.com/guide/topics/graphics/2d-graphics.html):
The Android framework will only call onDraw() as necessary. Each time
that your application is prepared to be drawn, you must request your
View be invalidated by calling invalidate(). This indicates that you'd
like your View to be drawn and Android will then call your onDraw()
method (though is not guaranteed that the callback will be
instantaneous).
Also another thing worth checking is the dimensions you're drawing insure nothing is invalid like a height of 0 etc..
I notice you're not overriding View.onMeasure().
Because you're not overriding this method onsizeChanged() might be passed size 0. You can check this by putting a breakpoint in the onSizechanged() method or printing to Logcat.
I would like to create a generic ViewGroup which can then be reused in XML layouts to round the corners of anything that is put into it.
For some reason canvas.clipPath() doesn't seem to have an effect. What am I doing wrong?
Here is the Java code:
package rounded;
import static android.graphics.Path.Direction.CCW;
public class RoundedView extends FrameLayout {
private float radius;
private Path path = new Path();
private RectF rect = new RectF();
public RoundedView(Context context, AttributeSet attrs) {
super(context, attrs);
this.radius = attrs.getAttributeFloatValue(null, "corner_radius", 0f);
}
#Override
protected void onDraw(Canvas canvas) {
int savedState = canvas.save();
float w = getWidth();
float h = getHeight();
path.reset();
rect.set(0, 0, w, h);
path.addRoundRect(rect, radius, radius, CCW);
path.close();
boolean debug = canvas.clipPath(path);
super.onDraw(canvas);
canvas.restoreToCount(savedState);
}
}
Usage in XML:
<?xml version="1.0" encoding="utf-8"?>
<rounded.RoundedView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
corner_radius="40.0" >
<RelativeLayout
android:id="#+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent" >
...
</RelativeLayout>
</rounded.RoundedView>
The right way to create a ViewGroup that clip its children is to do it in the dispatchDraw(Canvas) method.
This is an example on how you can clip any children of a ViewGroup with a circle:
private Path path = new Path();
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// compute the path
float halfWidth = w / 2f;
float halfHeight = h / 2f;
float centerX = halfWidth;
float centerY = halfHeight;
path.reset();
path.addCircle(centerX, centerY, Math.min(halfWidth, halfHeight), Path.Direction.CW);
path.close();
}
#Override
protected void dispatchDraw(Canvas canvas) {
int save = canvas.save();
canvas.clipPath(circlePath);
super.dispatchDraw(canvas);
canvas.restoreToCount(save);
}
the dispatchDraw method is the one called to clip children. No need to setWillNotDraw(false) if your layout just clip its children.
This image is obtained with the code above, I just extended Facebook ProfilePictureView (which is a FrameLayout including a square ImageView with the facebook profile picture):
So to achieve a round border you do something like this:
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// compute the path
path.reset();
rect.set(0, 0, w, h);
path.addRoundRect(rect, radius, radius, Path.Direction.CW);
path.close();
}
#Override
protected void dispatchDraw(Canvas canvas) {
int save = canvas.save();
canvas.clipPath(path);
super.dispatchDraw(canvas);
canvas.restoreToCount(save);
}
You can actually create any complex path :)
Remember you can call clipPath multiple times with the "Op" operation you please to intersect multiple clipping in the way you like.
NOTE: I created the Path in the onSizeChanged because doing so in the onDraw is bad for performance.
NOTE2: clipping a Path is done without anti-aliasing :/ so if you want smooth borders you'll need to do it in some other way. I'm not aware of any way of making clipping use anti-aliasing right now.
UPDATE (Outline)
Since Android Lollipop (API 21) elevation and shadows can be applied to views. A new concept called Outline has been introduced. This is a path that tells the framework the shape of the view to be used to compute the shadow and other things (like ripple effects).
The default Outline of the view is a rectangular of the size of the view but can be easily made an oval/circle or a rounded rectangular. To define a custom Outline you have to use the method setOutlineProvider() on the view, if it's a custom View you may want to set it in the constructor with your custom ViewOutlineProvider defined as inner class of your custom View. You can define your own Outline provider using a Path of your choice, as long as it is a convex path (mathematical concept meaning a closed path with no recess and no holes, as an example neither a star shape nor a gear shape are convex).
You can also use the method setClipToOutline(true) to make the Outline also clip (and I think this also works with anti-aliasing, can someone confirm/refute in comments?), but this is only supported for non-Path Outline.
Good luck
You can override the draw(Canvas canvas) method:
public class RoundedLinearLayout extends LinearLayout {
Path mPath;
float mCornerRadius;
public RoundedLinearLayout(Context context) {
super(context);
}
public RoundedLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RoundedLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public void draw(Canvas canvas) {
canvas.save();
canvas.clipPath(mPath);
super.draw(canvas);
canvas.restore();
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
RectF r = new RectF(0, 0, w, h);
mPath = new Path();
mPath.addRoundRect(r, mCornerRadius, mCornerRadius, Direction.CW);
mPath.close();
}
public void setCornerRadius(int radius) {
mCornerRadius = radius;
invalidate();
}
}
onDraw of FrameLayout is not called if Ur Layout's background not set;
You should override dispatchDraw;
ViewGroup (and hence its subclasses) sets a flag indicating that it doesn't do any drawing by default. In source it looks somewhat like this:
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
So your onDraw(...) probably doesn't get hit at all right now. If you want to do any manual drawing, call setWillNotDraw(false).
Don't forget to ask for onDraw to be called with setWillNotDraw(false); and set a value to mRadius then just do something like that :
#Override
protected void onDraw(Canvas canvas) {
mPath.reset();
mRect.set(0, 0, canvas.getWidth(), canvas.getHeight());
mPath.addRoundRect(mRect, mRadius, mRadius, Direction.CCW);
mPath.close();
canvas.clipPath(mPath);
}
U need to override the drawChild() method to clip childViews.
#Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
int flag = canvas.save();
canvas.clipPath(pathToClip);
boolean result=super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(flag);
return result;
}
If u want to clip the background of the ViewGroup too,override draw() instead.like this
#Override
public void draw(Canvas canvas) {
int flag = canvas.save();
canvas.clipPath(pathToClip);
super.draw(canvas);
canvas.restoreToCount(flag);
}
Why not just define a ShapeDrawable as the background of your layout and just change the corner radius of the drawable at runtime.