Why does Canvas::drawArc method draw incorrect angles? - android

I'm trying to draw a 180° arc within an android view.
In the onDraw method I'm using Canvas::drawArc like this:
canvas.drawArc(arcRect, 180.0f, 180.0f, false, arcBackgroundPaint);
The Paint that is being used has a strokeCap of type BUTT. As you can see in the image below, the ends of the arc do not look quite right. The ends are angled up slightly from the inner diameter to the outer diameter.
Does anyone know why this is happening? Is there a way to fix it other than changing the values passed to drawArc so that it actually draws more than 180 degrees? That seems like a nasty hack that I'd rather avoid.
UPDATE:
As requested, I added code to draw a straight line at the bottom of the arc. I draw the line before I draw the arc so the line is behind. I also made the arc black to get better contrast with the line. Here is the outcome:
Yuck.
Here's one with anti-aliasing turned off for the line and arc...

drawArc incorrect angles problem will occur if hardware acceleration is on.
Reproduced with real phone (Nexus 5, Android 6.0.1).
How to test:
Add android:hardwareAccelerated="false" to application tag in AndroidManifest.xml.
NB:
First supported API level of drawArc() is not found in the document, but scaling function of it is from API17.
Hardware Acceleration > Canvas Scaling
https://developer.android.com/guide/topics/graphics/hardware-accel.html
First supported API level
Simple Shapes: 17
Note: 'Simple' shapes are drawRect(), drawCircle(), drawOval(), drawRoundRect(), and drawArc() (with useCenter=false) ...
Updated:
As PICyourBrain wrote in another answer, drawArc() is a part of reasons this problem occurs.
With drawPath(), this problem does not occur.
For information:
Not a same shape but drawArc() with useCenter=true also seems safe to use, as hardware acceleration is not used for it. (Ends of the arc and center of it are connected with line, and the line seems straight.)
Canvas Scaling (same link above)
First supported API level
Complex Shapes: x
Other Q&As related to drawArc()
Canvas.drawArc() artefacts
What's this weird drawArc() / arcTo() bug (graphics glitch) in Android?
[Old]
I'll leave these for your information.
Here are some test results I've tried.
Updated 1-1:
With emulator Nexus5 API10, with arcRect = new RectF(100, 100, 1500, 1400);.
With emulator Nexus5 API16, this does not happen.
(The difference is existence of (host side) hardware acceleration.)
I thought this seems to be an anti-alias related problem, but this happens no matter arcBackgroundPaint.setAntiAlias(true) or setDither(true) are set or not.
NB: This is caused by typo, sorry. Aspect ratio of arcRect should be 1:1 for this test.
With arcRect = new RectF(100, 100, 1400, 1400); and arcPenWidth = 200f;
With arcRect = new RectF(100, 100, 1500, 1300);
1-2: for comparison
With emulator NexusOne (480x800, HDPI) API10, with arcRect = new RectF(100, 100, 500, 500);
Updated 2: Line extended.
I first thought drawing out of view may cause this, but this is also a emulator bug. (Not a drawArc() specific behavior.)
With emulator API10 in landscape(horizontal) orientation, this occurs.
Calculation of line position seems broken.
Please see the right end of the straight line.
final float lineStartX = arcRect.left - 250f;
final float lineEndX = arcRect.right + 250f;
emulator Nexus5 API10
horizontal (API10)
vertical (API10)
2-2: Just for a information
View odd behavior sample of view position (or drawing position) out of range.
Updated 3: Emulator bug
Please see the bottom of the image.
Blue line is a background image of desktop.
emulator Nexus5 API10
Update 4: The result seems to depend on style.
With title bar
Without title bar
Update 5: The result seems to depend on line width.
With arcPenWidth = 430f (API10, horizontal)
Slight notch on the right side is seen.
With 440f
With 450f
Here's my (first) test code.
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
final class TestView extends View
{
RectF arcRect;
Paint arcBackgroundPaint;
Paint linePaint;
public TestView(final Context context, final AttributeSet attrs)
{
super(context, attrs);
//arcRect = new RectF(100, 100, 500, 500);
arcRect = new RectF(100, 100, 1500, 1500); // fixed (old: 1500, 1400)
arcBackgroundPaint = new Paint();
arcBackgroundPaint.setColor(0xFFFFFFFF);
arcBackgroundPaint.setStyle(Paint.Style.STROKE);
arcBackgroundPaint.setStrokeCap(Paint.Cap.BUTT);
arcBackgroundPaint.setStrokeWidth(200f);
linePaint = new Paint();
linePaint.setColor(0xFF00FF00);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeCap(Paint.Cap.BUTT);
linePaint.setStrokeWidth(2f);
}
#Override
protected void onDraw(final Canvas canvas)
{
super.onDraw(canvas);
canvas.drawArc(arcRect, 180.0f, 180.0f, false, arcBackgroundPaint);
final float lineStartX = arcRect.left - 50f;
final float lineEndX = arcRect.right + 50f;
final float lineY = arcRect.centerY();
canvas.drawLine(lineStartX, lineY, lineEndX, lineY, linePaint);
}
}

For anyone else who comes across this I did find a workaround. Instead of using Canvas::drawArc, create a Path, add an arc to it and then draw the path on the canvas. Like this:
Path arcPath = new Path();
Rect arcRect = ... // Some rect that will contain the arc
path.addArc(arcRect, 180, 180);
canvas.drawPath(arcPath, backgroundArcPaint);
Then the rendering problem highlighted no longer occurs.
Since my question was not "how do I fix the problem" but rather "why is this a problem" I am not marking this as the correct answer.

Related

Trying to draw on canvas using canvas.clipPath

I am trying to draw circle on darken background, trying to achive smth like this -
This actually worked on my Samsung s4 and Samsung tab 3, but won't work on s2 and some emulator (all sorrounding viewgroup is darken, and inside oval too, seems like it does not see my circleSelectionPath). Help me please to find the way to make it work on every device
final Paint paint = new Paint();
paint.setColor(Color.parseColor("#77000000"));
Path circleSelectionPath = new Path();
mRectF.set(l, t, r, b);
circleSelectionPath.addOval(mRectF, Path.Direction.CW);
canvas.clipPath(circleSelectionPath, Region.Op.XOR);
canvas.drawRect(bitmapRect.left, bitmapRect.top, bitmapRect.right, bitmapRect.bottom, paint );
canvas.restore();
bitmapRect contains my viewgroup dimens (for example : 0,0, 500,500)
Got it. Android has a bug with canvas.clipRect, they have optimized it, but on some android apis it simple don't work after optimization ) I found an issue.
So fix - disable hrdware acceleration for this view
(Build.VERSION.SDK_INT <= 19 && mCropShape == CropImageView.CropShape.OVAL) {
//TURN off hardware acceleration
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

Rendering path with Canvas.drawPath() in ICS with hardware acceleration

On ICS device, I tried the following code to draw two rectangles.
Path p1 = new Path();
p1.moveTo(0, 0);
p1.lineTo(0, 100);
p1.lineTo(100, 100);
p1.lineTo(100, 0);
p1.close;
Path p2 = new Path();
Matrix scaling = new Matrix();
scaling.preScale(2, 2);
p1.transform(scaling, p2);
canvas.drawPath(p1);
canvas.drawPath(p2);
Running the above code on ICS device with hardware acceleration enabled (as it is by default), p1 is drawn where as p2 is not.
In general, what happened to me is, as long as a Path is not hand-wired (i.e. by calling lineTo(), quadTo(), etc.), but obtained by copying or transforming (i.e. by calling the copy constructor, transform(matrix, dest), translate(x, y, dest), etc.), it is not drawn.
I found a "widely known" issue that is similar but not exactly the same as my problem: https://groups.google.com/forum/#!msg/android-developers/eTxV4KPy1G4/tAe2zUPCjMcJ
Therefore, can anyone tell me what is the issue I am running into? In my case, I have to resort to path transformation otherwise code complexity will be greatly increase. Thanks!
try setting android:layerType="software" in xml on the view to see if that fixes it. some methods aren't available with hardware acceleration on all apis.
the list is here:
http://developer.android.com/guide/topics/graphics/hardware-accel.html
note that if changing the layer type fixes it, you should create a separate layout for the newer APIs for optimal performance

compute bounds of a path crash

I have closed Path-s, which consist of many Bezier segments. These Bezier segments have integer coordinates up to 5000,5000. I need to compute if a point is inside one of these closed Paths. I use this code:
// p is a Path, bounds is a RectF
p.computeBounds(bounds, true);
Region region = new android.graphics.Region();
region.setPath(path, new android.graphics.Region((int)bounds.left, (int)bounds.top, (int)bounds.right, (int)bounds.bottom));
I do that once per Path and then do
region.contains(x, y);
Problem is, that the computeBounds crashes the app for my big Paths. There is no force close, it just receives SIGSEGV and returns to home screen, with no message. I tried to downscale the coordinates to smaller number (divided by 1000), but it did not help, program still crashes.
Is there any other way to compute if a point is inside a complex Path, which will not crash?
EDIT
Is there a way to compute this with RenderScript? I cannot find any RenderScript examples with paths/Bezier curves...
EDIT 2
This happens in Nexus 7 with 4.1.1 and also 4.1.2 and also in ICS x86 tablet emulator
Normally Java code results in exception rather than segmentation fault, meaning there is something wrong with Java Virtual Machine, unless you have own JNI code in your project and that is causing segmentation fault.
Instead of computing path's bounds, which seems to be too expensive operation for your complex path you can use huge enough clip rectangle to bound all possible paths as clip region so you can avoid calling heavy and unnecessary Path.computeBounds.
import android.graphics.Region;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Rect;
private static final String id = "Graphics";
...
Path path = new Path();
/* Initialize path here... */
/* Huge rectangle to bound all possible paths */
Region clip = new Region(0, 0, 10000, 10000);
/* Define the region */
Region region = new Region();
if (region.setPath(path, clip)) {
Log.d(id, "This region is fine");
} else {
Log.e(id, "This region is empty");
}

Color banding and artifacts with gradients despite using RGBA_8888 everywhere

I'm aware that colour banding is an old chestnut of a problem that has been discussed many times before with various solutions provided (which essentially boil down to use 32-bit throughout, or use dithering). In fact not so long ago I asked and subsequently answered my own SO question regarding this. Back then, I thought that the solution I put in the answer to that question (which is to apply setFormat(PixelFormat.RGBA_8888) to the Window and also to the Holder in the case of a SurfaceView) had solved the problem in my application for good. At least the solution made the gradient look very nice on the devices I was developing on back then (most probably Android 2.2).
I'm now developing with a HTC One X (Android 4.0) and an Asus Nexus 7 (Android 4.1). What I tried to do was apply a grey gradient to the entire area of a SurfaceView. Even though I supposedly ensured that the containing Window and the Holder are configured for 32-bit colour, I get horrible banding artifacts. In fact, on the Nexus 7, I even see the artifacts move about. This occurs not only on the SurfaceView which is of course continuously drawing, but also in a normal View I added alongside to draw exactly the same gradient for test purposes, which would have drawn once. The way that these artifacts are there and also appear to move around of course looks absolutely awful, and it's actually like viewing an analogue TV with a poor signal. Both the View and SurfaceView exhibit exactly the same artifacts, which move around together.
My intention is to use 32-bit throughout, and not use dithering. I am under the impression that the Window was 32-bit by default long before Android 4.0. By applying RGBA_8888 in the SurfaceView I would have expected everything to have been 32-bit throughout, thus avoiding any artifacts.
I do note that there are some other questions on SO where people have observed that the RGBA_8888 no longer seems to be effective on the 4.0 / 4.1 platforms.
This is a screenshot from my Nexus 7, with a normal View at the top and a SurfaceView below, both applying the same gradient to the Canvas. Of course, it does not show the artifacts as well as they do when looking at the display, and so it is probably fairly pointless showing this screen grab. I want to emphasise though that the banding really does look terrible on the screen of the Nexus. Edit: In fact, the screenshot really doesn't show the artifacts at all. The artifacts I'm seeing on the Nexus 7 aren't uniform banding; it looks random in nature.
The test Activity used to create the above:
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Shader;
import android.os.Bundle;
import android.os.Handler;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import android.view.SurfaceHolder.Callback;
import android.widget.LinearLayout;
public class GradientTest extends Activity {
#Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
getWindow().setFormat(PixelFormat.RGBA_8888);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
lp.copyFrom(getWindow().getAttributes());
lp.format = PixelFormat.RGBA_8888;
getWindow().setAttributes(lp);
LinearLayout ll = new LinearLayout(this);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(500,500);
params.setMargins(20, 0, 0, 0);
ll.addView(new GradientView(this), params);
ll.addView(new GradientSurfaceView(this), params);
ll.setOrientation(LinearLayout.VERTICAL);
setContentView(ll);
}
public class GradientView extends View {
public GradientView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(false);
paint.setFilterBitmap(false);
paint.setDither(false);
Shader shader = new LinearGradient(
0,
0,
0,
500,
//new int[]{0xffafafaf, 0xff414141},
new int[]{0xff333333, 0xff555555},
null,
Shader.TileMode.CLAMP
);
paint.setShader(shader);
canvas.drawRect(0,0,500,500, paint);
}
}
public class GradientSurfaceView extends SurfaceView implements Callback {
public GradientSurfaceView(Context context) {
super(context);
getHolder().setFormat(PixelFormat.RGBA_8888); // Ensure no banding on gradients
SurfaceHolder holder = getHolder();
holder.addCallback(this);
}
Paint paint;
private GraphThread thread;
#Override
public void surfaceCreated(SurfaceHolder holder) {
holder.setFormat(PixelFormat.RGBA_8888); // Ensure no banding on gradients
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(false);
paint.setFilterBitmap(false);
paint.setDither(false);
Shader shader = new LinearGradient(
0,
0,
0,
500,
//new int[]{0xffafafaf, 0xff414141},
new int[]{0xff333333, 0xff555555},
null,
Shader.TileMode.CLAMP
);
paint.setShader(shader);
thread = new GraphThread(holder, new Handler() );
thread.setName("GradientSurfaceView_thread");
thread.start();
}
class GraphThread extends Thread {
/** Handle to the surface manager object we interact with */
private SurfaceHolder mSurfaceHolder;
public GraphThread(SurfaceHolder holder, Handler handler) {
mSurfaceHolder = holder;
holder.setFormat(PixelFormat.RGBA_8888); // Ensure no banding on gradients
}
#Override
public void run() {
Canvas c = null;
while (true) {
try {
c = mSurfaceHolder.lockCanvas();
synchronized (mSurfaceHolder) {
if (c != null){
c.drawRect(0,0,500,500, paint);
}
}
}
finally {
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
}
I have installed an application called Display Tester from Google Play. This application can be used to create test gradients on the screen. Although its gradients do not look perfect, they seem a bit better than what I have been able to achieve, which makes me wonder if there is a further measure I can do to prevent banding.
The other thing I note is that the Display Tester application reports that my Nexus' screen is 32-bit.
For information, I am explicitly enabling hardware acceleration. My SDK levels are:
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15"></uses-sdk>
Another thing I notice is that the default gradient background for the Activity, which I understand to be a feature of Holo, is also very banded. This also doesn't show at all in the screenshot. And, I also just noticed the banding of the background moves about briefly on my Nexus 7, in sympathy with the banding movement in my two Views. If I create a completely new Android project with the default 'empty' Activity, the Activity shows a nasty banded gradient background on both my Nexus and HTC One X. Is this normal? I understand that this black / purple gradient default background is what an Activity shall have if hardware acceleration is enabled. Well, regardless of whether hardware acceleration is enabled or not, I see the same nasty banded Activity background gradient. This even happens in my empty test project, whose target SDK is 15. To clarify, the way I am enabling or disabling hardware acceleration is explicitly using android:hardwareAccelerated="true" and android:hardwareAccelerated="false".
I'm not sure if my observation about the Holo black / purple Activity gradient background has anything to do with my primary question, but it does seem oddly related. It's also odd that it looks such poor quality (i.e. banded) and looks the same regardless of whether hardware acceleration is turned on. So, a secondary question would be: When you have an Activity with the default Holo gradient background, and for each case of hardware acceleration being explicity enabled and then disabled, should this gradient background (a) be present and (b) look perfectly smooth? I would ask this in a separate question, but again, it seems related.
So, in summary: The basic problem I have is that applying a gradient background to a SurfaceView simply cannot be done, it seems, on my Nexus 7. It's not just banding that's the problem (which I could happily put up with if it were just that); it's actually the fact that the banding is random in nature on each draw. This means that a SurfaceView that constantly redraws ends up having a moving, fuzzy background.
Just to wrap this up with an answer, the conclusion I have reached is that the Nexus 7 just has some hardware / firmware issue which means that it is utterly pants at rendering gradients.
Have you tried setting the pixelformat for the surface view itself ?
final SurfaceHolder holder = getHolder();
holder.setFormat(PixelFormat.RGBA_8888);
If you see no effect of setting the pixel format in ICS and up, it is most likely due to hardware acceleration which will always render to the native pixel format.
Most of the time that should be just ARGB_8888 though. Make sure you also set the window pixel format of your activity, not just the pixel format on the SurfaceView.
You can easily verify if that is the case by turning acceleration off. You mention that you tested that, but you don't mention how you did that. Setting the target sdk level is not the most explicit way to do that.
From the top of my head the software rendering switched to ARGB_8888 by default in HoneyComb (3.0), but again you will need to set it explicitly to correctly support older devices where this is not the default.
Why have you set to false the dithering on your paint. I would suggest to activate dithering
paint.setDither(true);
Android doc clearly says that it will downgrade your rendering:
setting or clearing the DITHER_FLAG bit Dithering affects how colors
that are higher precision than the device are down-sampled. No
dithering is generally faster, but higher precision colors are just
truncated down (e.g. 8888 -> 565). Dithering tries to distribute the
error inherent in this process, to reduce the visual artifacts.
You can also try to add the FLAG_DITHER to the window:
window.setFormat(PixelFormat.RGBA_8888);
window.setFlags(WindowManager.LayoutParams.FLAG_DITHER, WindowManager.LayoutParams.FLAG_DITHER);

Canvas.drawCircle() with large circles under hardware acceleration

In the course of developing an Android application, I'm finding a need to draw
several unfilled concentric circles centered on an arbitrary point, enough that
some of them are only partly visible on the display. However, this does not
appear to work with hardware acceleration. My test rig is a stock Samsung Galaxy
Tab 10.1 running Android 3.2.
The following code comes from a test subclass of View I wrote to isolate the
issue:
private Paint paint = new Paint();
private int count = 0;
private static final int[] COLORS = { 0xffff0000, 0xff00ff00, 0xff0000ff, 0xffff00ff };
public TestCircles(Context context) {
super(context);
paint.setStrokeWidth(1.0f);
paint.setStyle(Paint.Style.STROKE);
}
public TestCircles(Context context, AttributeSet attributes) {
super(context, attributes);
paint.setStrokeWidth(1.0f);
paint.setStyle(Paint.Style.STROKE);
}
public boolean onTouchEvent(MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_DOWN)
invalidate();
return true;
}
protected void onDraw(Canvas canvas) {
// Pick the color to use, cycling through the colors list repeatedly, so that we can
// see the different redraws.
paint.setColor(COLORS[count++]);
count %= COLORS.length;
// Set up the parameters for the circles; they will be centered at the center of the
// canvas and have a maximum radius equal to the distance between a canvas corner
// point and its center.
final float x = canvas.getWidth() / 2f;
final float y = canvas.getHeight() / 2f;
final float maxRadius = (float) Math.sqrt((x * x) + (y * y));
// Paint the rings until the rings are too large to see.
for (float radius = 20; radius < maxRadius;
radius += 20)
canvas.drawCircle(x, y, radius, paint);
}
I am running TestCircles as the only View in an Activity, laying it out to fill
the available width and height (i.e. it is nearly full-screen). I can tap on
the display (triggering redraws) only a few times before the redraws no longer
occur (i.e. the circles' color doesn't change). Actually, the onDraw() code is
still running in response to each tap -- as proven with diagnostic messages --
but nothing changes onscreen.
When onDraw() first starts to fail to redraw, the debug log includes the
following entry, once for every call to onDraw():
E/OpenGLRenderer(21867): OpenGLRenderer is out of memory!
If I turn off hardware acceleration in the manifest, these problems go away --
not surprising since clearly OpenGL is having problems -- and actually it is
a good deal faster than the few times it actually works under hardware
acceleration.
My questions are:
Am I misusing Canvas, or is this a bug, or both? Is Android allocating large
bitmaps under the hood to draw these circles? It doesn't seem like this should be
this challenging to OpenGL, but I'm new to hardware accelerated app development.
What's a good alternative way to draw large unfilled circles that have portions
extending out of the clipping region of the Canvas? Losing hardware acceleration
is not an option.
Thanks in advance...
I've since learned from others that the problem I described here is the result of a bug in Android 3.2. The workaround for now is of course to use a software layer instead of hardware acceleration. Apparently this problem is fixed in Android 4.0 (Ice Cream Sandwich).

Categories

Resources