TextView as a Progress Bar in Kotlin - android

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.

Related

I am calling simple draw rectangle in like 30 views at a time and having slow performance when calling postInvalidate every 50 milis

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.

Android: Canvas Animate Path

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)?

Why is Android Paint strokeWidth default value suddenly different?

Background
I have an app in the Google Play store built in Kotlin.
It currently displays a grid that the user draws her password on.
Here's a snapshot of the grid as it was previously drawn with the previous default paint.strokeWidth.
The grey lines between the (red) posts are drawn with the following method:
private fun DrawGridLines() {
val paint = Paint()
for (y in 0..numOfCells) {
xCanvas!!.drawLine(
(0 + leftOffset).toFloat(), (y * cellSize + topOffset).toFloat(),
(numOfCells * cellSize + leftOffset).toFloat(),
(y * cellSize + topOffset).toFloat(), paint
)
}
for (x in 0..numOfCells) {
xCanvas!!.drawLine(
(x * cellSize + leftOffset).toFloat(), (0 + topOffset).toFloat(),
(x * cellSize + leftOffset).toFloat(), (numOfCells * cellSize + topOffset).toFloat(),
paint
)
}
}
The Problem
While working on updates to the app I ran it on the emulator and saw the following:
As you can see the gridlines are drawn properly. Very odd since it seems to be drawing partial grid lines. NOTE: I ran this on numerous API versions and they all draw the grid lines this way now.
paint.strokeWidth = 0.0
I added some code to examine the value of paint.strokeWidth but that is additionally odd. It shows that the value of strokeWidth is always 0.0.
You can see that in my logcat output:
The Fix
Yes, I can simply fix this by explicitly setting the value myself.
I added the following line of code to the routine above:
paint.strokeWidth = 5F;
Now it looks like the following:
However, I'd like to know why this has suddenly occurred??
I'd also like to know how it seems to draw "some" of the lines since the value of the strokeWidth is actually 0.0???
The first thing I see in your code is that nowhere the Paint gets configured or its strokeWidth assigned a value. You need to set specific values and not use defaults, as defaults don't take into consideration display densities neither may have a valid usable value at all.
In the next sniped of your code you instantiate a new Paint instance and use it straight away without setting any properties to it:
private fun DrawGridLines() {
val paint = Paint()
for (y in 0..numOfCells) {
xCanvas!!.drawLine(....
"Here using already the new paint??? where did you configure it?"
Secondly, notice that Paint.strokeWidth units are in pixels, therefore you need to take into account the device display density and adjust to it.
for example:
val DEFAULT_SIZE_PX = 5.0f
val scaledWidth = DEFAULT_SIZE_PX * context
.resources
.displayMetrics
.density
paint.strokeWidth = scaledWidth
Or, which is the same as:
val DEFAULT_SIZE_PX = 5.0f
val displayMetrics = context.resources.displayMetrics
val scaledWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_SIZE_PX, displayMetrics)
paint.strokeWidth = scaledWidth
The official docs on setStrokeWidth provides a very interesting statement:
"Hairlines always draw a single pixel..."
I suppose the way that is handled is now probably handled differently and has this type of effect on output now. Or it is related to the density issue.
Either way, it is odd that it has changed. And interesting/odd that it states that you can set it to 0 for hairline output.

Paint.setStrokeJoin doesn't work with canvas.drawLines

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.

Problematic clip already set on Canvas in onDraw

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.

Categories

Resources