Good morning
I have created custom view where I draw a line based on input like this:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val width = dpToPixels(context.resources, 300);
if (_value != null) {
val calculatedPath = createPath(_value !!, width.toFloat());
canvas.drawPath(calculatedPath !!, _paint)
} else {
canvas.drawPath(defaultPath, _paint)
}
}
The both calculatedPath and defaultPath are simply curved line similar to function
f(x) = x^( - 2).
As I am not Android programmer I am looking for an easy way to draw these lines with animation. I have tried something with ValueAnimator with listener like:
animator.addUpdateListener { valueAnimator ->
paint.pathEffect = DashPathEffect(dashes, (valueAnimator.animatedValue as Int).toFloat())
invalidate()
}
However I am not interested in Dashed path effect but rather Solid which I couldn't find.
Is there simply way to draw these lines with animation like
canvas.drawPath(path, paint, animation)?
Related
I want to have smooth look of my progress so I like 50 ms as it looks good but after some time my app stats to freeze ui randomly for seconds. When I change to 100ms everything looks fine just is not as smooth to my eyes. Any way to get better performance or trick so it looks better ?
Drawing code is like this:
init {
backgroundEach(50) { view.postInvalidate() }
register(view.eventOnDraw.listen(::onDraw))
}
private val paint by lazy {
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = view.foregroundTint
isAntiAlias = true
strokeWidth = dpToPixelF(30)
}
}
private fun onDraw(canvas: Canvas) {
val position: Float = player.asStarted()?.inLoopIndex
?.div(player.commands.size.toFloat()) ?: 0f
if (position > 0)
canvas.drawRect(0f, 0f, width * position, height.toFloat(), paint)
}
And backgroundEach uses ScheduledThreadPoolExecutor with one worker and standard scheduleAtFixedRate( { function() }, delay, period, MILLISECONDS
I did some improvements:
Increaser executor threads to 3:
val executor = ScheduledThreadPoolExecutor(3)
and now posting on view.postInvalidateOnAnimation()
I also removed antialiasing as I don't see difference and maybe it eats some performance. Will see later if I have to dig into this more.
I am trying to replicate the TextView as progress bar described in this question using Kotlin, overriding the TextView class and using the canvas clipping trick in Zielony's comment.
The code almost works: I can see the text change color at the specified percentage (fixed, for now) but the background stays the same (yellow, the last background color I set).
Here's my code:
class ProgressTextView(context: Context): TextView(context) {
override fun onDraw(canvas: Canvas) {
val color1 = ContextCompat.getColor(context, R.color.primaryColor)
val color2 = ContextCompat.getColor(context, R.color.secondaryColor)
var percent = 0.3 // Fixed progress % at 30%, for testing
// first half
canvas.save()
setTextColor(Color.RED)
setBackgroundColor(Color.GREEN)
canvas.clipRect(Rect(0, 0, (width * percent).toInt(), height))
super.onDraw(canvas)
canvas.restore()
// second half
canvas.save()
setTextColor(Color.BLACK)
setBackgroundColor(Color.YELLOW)
canvas.clipRect(Rect((width * percent).toInt(), 0, width, height))
super.onDraw(canvas)
canvas.restore()
}
}
What am I missing?
I managed to get it to work painting a rectangle instead of clipping for the first half, but I am still curious about how it should be done "properly", as this solution seems more elegant.
I am trying to draw a line chart using canvas.drawLines(...), but it seems that the lines are not properly connected. As I understand using Paint.setStrokeJoin should use the miter join:
chartLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
chartLinePaint.setStyle(Paint.Style.STROKE);
chartLinePaint.setStrokeJoin(Paint.Join.MITER);
chartLinePaint.setStrokeWidth(6.0f);
How do I fix this problem and make the lines properly joined?
As I told you in the comment, Paint objects are fully applied only when you draw them with Path.
In drawLine documentation there is a paragraph with: 'the Style is ignored in the paint' and the same thing is applied to drawLines method.
To test this, I created a simple custom view:
class CanvasTestView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val textPaint1 = Paint(ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeJoin = Paint.Join.MITER
strokeWidth = 12.0f
color = Color.RED
}
private val textPaint2 = Paint(ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeJoin = Paint.Join.MITER
strokeWidth = 12.0f
color = Color.BLUE
}
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.apply {
val floatArray = floatArrayOf(250f, 550f, 450f, 200f, 450f, 200f, 650f, 700f)
drawLines(floatArray, textPaint2)
val path = Path()
path.moveTo(200f, 500f)
path.lineTo(400f, 200f)
path.lineTo(600f, 700f)
drawPath(path, textPaint1)
}
}
}
And the result is this:
So using drawLines partially apply the styles of Paint obj, like colours, but is not applying strokeJoin like properties. drawPath seems to apply all of them instead.
If you have a performance problem maybe you can try to cache the result somewhere, pre-compute the animation or try with a simpler one.
Remember that if you don't have particular requirements there is this
awesome library: MPAndroidChart which already has some built-in animations
Problem
As you may have noticed in the Android documentation, you can’t apply
a style to a drawLine.
ref
From the Canvas documentation
drawLine:
public void drawLine (float startX, float startY, float stopX, float stopY, Paint paint)
Draw a line segment with the specified start and stop x,y coordinates,
using the specified paint.
Note that since a line is always "framed", the Style is ignored in the
paint.
Degenerate lines (length is 0) will not be drawn.
drawLines:
public void drawLines (float[] pts, int offset, int count, Paint paint)
Draw a series of lines. Each line is taken from 4 consecutive values
in the pts array. Thus to draw 1 line, the array must contain at least
4 values. This is logically the same as drawing the array as
follows:
drawLine(pts[0], pts[1], pts[2], pts[3])
followed by:
drawLine(pts[4], pts[5], pts[6], pts[7])
and so on.
Solution
If you need to apply a style, the solution is to use drawPath instead.
It will apply the style set in the paint object.
For anyone else looking for to use the StrokeJoin or StrokeCap with DrawPath(), you can try SkPathEffect such as CreateCorner():
MS Docs, SkiaSharp, but kind of the same.
I subclassed TextView to provide a custom onDraw. But canvas has a clip region applied that is nonsensical: the x is set to something well outside the view bounds. I think that's thwarting my purposes.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// draw numberLabel
if (numberLabel == 0)
return
val right = this.width - this.resources.getDimension(R.dimen.topNavBadgeEndMargin)
// top needs to add the top margin and estimated text height
val top = this.resources.getDimension(R.dimen.topNavBadgeTopMargin) + this.badgePaint.textSize
canvas.drawText(numberLabel.toString(), right, top, this.badgePaint)
val r = Rect()
canvas.getClipBounds(r)
Log.d("TopNav", "canvas.clipBounds: $r")
}
Logcat printed:
D/TopNav: canvas.clipBounds: Rect(524187, 0 - 524389, 147)
FYI, I have also tried drawing a circle r=50 center=(100,100) and it doesn't show. So what would help is a) why this happens? b) I know there's no way to reset the clip region, but is there any workaround that would help me?
Seems like if you override onDraw in a TextView you need to offset by scrollX (probably should do scrollY as well, though it was zero). scrollX was the rediculously large int and I have no idea why it would be nonzero in a TextView that doesn't need to scroll.
val right = this.scrollX + this.width - this.resources.getDimension(R.dimen.topNavBadgeEndMargin)
If you have several operations then canvas.translate wrapped by save and restore probably helps.
I'm implementing custom path effect for route on top of MapView and I came up with the problem how to make my beginning and ending of the path rounded (like Paint.setStrokeCap(Cap.ROUND) does). See screenshot - black lines - is my route I want to round at the end
Here is how I implemented my custom PathEffect:
public RouteOverlay(Context context)
{
mContext = context;
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(COLOR_DEFAULT);
mPaint.setAntiAlias(true);
mPaint.setStrokeCap(Cap.ROUND); // this one does not work...
mPaint.setStrokeJoin(Join.ROUND);
PathEffect e1 = new PathDashPathEffect(createRouteLineStyle(), 10, 3, PathDashPathEffect.Style.MORPH);
PathEffect e2 = new CornerPathEffect(10);
mPaint.setPathEffect(new ComposePathEffect(e1, e2));
}
private Path createRouteLineStyle()
{
Path p = new Path();
p.moveTo(-5, ROUTE_LINE_WIDTH/2);
p.lineTo(5,ROUTE_LINE_WIDTH/2);
p.lineTo(5,ROUTE_LINE_WIDTH/2-currentThickness);
p.lineTo(-5, ROUTE_LINE_WIDTH/2-currentThickness);
p.close();
p.moveTo(-5, -(ROUTE_LINE_WIDTH/2));
p.lineTo(5,-(ROUTE_LINE_WIDTH/2));
p.lineTo(5, -(ROUTE_LINE_WIDTH/2-currentThickness));
p.lineTo(-5, -(ROUTE_LINE_WIDTH/2-currentThickness));
return p;
}
#Override
public void draw(Canvas canvas, final MapView mapView, boolean shadow)
{
if(shadow) return;
if(mDrawEnabled)
{
synchronized(mPoints)
{
canvas.drawPath(mPath, mPaint);
}
}
}
As you can see on the screenshot, the ending of the line is not rounded (as well as beginning...). setStrokeCap(Cap.ROUND) doesn't help.
So the question is - how to add round cap to my custom path? I was thinking of using addArc() or addCircle() to the end (and beginning) of my path, but this doesn't seem right.
The reason why I need custom path effect - is that I need to draw the route around actual road - so route should be empty inside and have inner and outer stroke lines.
In case somebody knows how to make this kind of path effect in some other way - please let me know, because this solution has big cons I have to deal with..
I don't see any reason why that should not work unless you are running into the problem mentioned here http://code.google.com/p/android/issues/detail?id=24873.
I managed to find a solution for my problem.
So I got rid of my custom path effect and started to use usual stroke (where stroke cap works as expected). So I basically draw my path 2 times: at first I draw black line, after that I draw thiner transparent line to clear the center of previous black line.
The only trick in this approach is that I need to draw my path in a separate bitmap (using temp canvas) and when path bitmap is ready - render it to the main canvas.
#Override
public void draw(Canvas canvas, final MapView mapView, boolean shadow)
{
//Generate new bitmap if old bitmap doesn't equal to the screen size (f.i. when screen orientation changes)
if(pathBitmap == null || pathBitmap.isRecycled() || pathBitmap.getWidth()!=canvas.getWidth() || pathBitmap.getHeight()!=canvas.getHeight())
{
if(pathBitmap != null)
{
pathBitmap.recycle();
}
pathBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Config.ARGB_8888);
tempCanvas.setBitmap(pathBitmap);
}
//Render routes to the temporary bitmap
renderPathBitmap();
//Render temporary bitmap onto main canvas
canvas.drawBitmap(pathBitmap, 0, 0, null);
}
}
private void renderPath(Path path, Canvas canvas)
{
routePaint.setStrokeWidth(ROUTE_LINE_WIDTH);
routePaint.setColor(OUTER_COLOR);
routePaint.setXfermode(null);
canvas.drawPath(path, routePaint); //render outer line
routePaint.setStrokeWidth(ROUTE_LINE_WIDTH/1.7f);
routePaint.setColor(Color.TRANSPARENT);
routePaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR));
canvas.drawPath(path, routePaint); //render inner line
}
So result looks like: