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.
Related
I just switched from using iText to the native android PdfDocument, due to lack of documentation of how to print iText documents.
I am trying to draw a table on the Canvas, by looping through a list and adding a right and left column for each item, represented by using the Rect
The function responsible for drawing the table rows is shown below:
fun drawTableRow(canvas: Canvas, question: Document.H) {
val textBounds = Rect()
var cellHeight = if(question.value.isNullOrEmpty()) {
20f
} else {
paint.getTextBounds(question.value, 0, question.value!!.length, textBounds)
textBounds.height().toFloat()
}
if(cellHeight <= 20f) cellHeight = 20f
paint.textAlign = Paint.Align.LEFT
val top = yPointer
val right = (width.toFloat() - (pageMargin * 2)) / 3
val bottom = height - (yPointer + cellHeight + pageMargin)
val leftCell = RectF(pageMargin, top, right, bottom)
val rightCell = RectF(leftCell.right, top, pageMargin, bottom)
var textY = yPointer + yPadding
canvas.drawRect(rightCell, contentBgPaint)
canvas.drawRect(leftCell, contentBgPaint)
canvas.drawText(question.h.hQuestion.title, 0, question.h.hQuestion.title.length, pageMargin, textY, paint)
canvas.drawText(question.h.hQuestion.subtitle, 0, question.h.hQuestion.subtitle.length, pageMargin, textY + 16f, paint)
canvas.drawText(question.value!!, 0, question.value!!.length, leftCell.right, textY, paint)
yPointer += cellHeight.toInt()
}
And when running the debugger, the Rect coordinates seem to be set correctly.
However, an usual behavior occurs - either only one rectangle is being drawn, or the rectangles are overlapping each other exactly, despite having different coordinates.
Because if I attempt to only draw the right rectangle, it still appears on the left side in the document, with the exact proportions of the left rectangle, as seen in the image below.
It was a stupid mistake. The documentation for RectF clearly states that left <= right and top <= bottom. The debugger clearly shows that the right cell has a left value that is > right.
I replaced pageMargin with width.toFloat() - pageMargin in the right cell, which solved the issue.
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.
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 have a custom view that extends relative layout. In the dispatchDraw method I draw lines and images inside a for loop because i have different start and end points for multiple lines.
I would like to know the easiest way to draw lines with animations from point a to point b in a slow way for example, this animation must be set only for a line that I decide and not for all the others.
I would also like to draw images with animations, I don't want to move the image but I would like to draw the image a little bigger first and then reduce it to the right size, or draw the image from top to bottom. Again this animation must be set only for a image that I decide and not for all the others.
#Override
protected void dispatchDraw(Canvas canvas) {
int width = getWidth();
int height = getHeight();
super.dispatchDraw(canvas);
size = width / (lines);
insideMargin = size / margin;
vMargin = (height - lines * size) / 2f;
canvas.translate(0,vMargin);
for(int x=0; x<lines;x++){
for(int y=0;y<lines;y++){
if(/*a condintion is true*/) {
//draw line with animation, how to do it?
}else{
canvas.drawLine(x * size,
y * size,
(x + 1) * size,
y* size,
paint
);
}
if(/*a condition is true*/) {
//draw bitmap with animation, how to do it?
}else
canvas.drawBitmap(image, src, dest, null);
}
}
}
//i have on touch event that will call invalidate
I think you should use ValueAnimator for your purpose. As I understood, you want to animate values once a touch event occurs. The value returned by the animator must be used for all objects so the animated value should be general as possible. I recommend to use 0 to 1 as float value. You can use this value for the animation. For the bitmap animation, you can define target rectangle start and end size then you can find appropriate size by facilitating the animated fraction value. For the line case, you can use the slope for the animation. I think you already have the line's start and end point so you can use slope and fraction to calculate animated end point.
I need to draw something like this:
I was hoping that this guy posted some code of how he drew his segmented circle to begin with, but alas he didn't.
I also need to know which segment is where after interaction with the wheel - for instance if the wheel is rotated, I need to know where the original segments are after the rotation action.
Two questions:
Do I draw this segmented circle (with varying colours and content placed on the segment) with OpenGL or using Android Canvas?
Using either of the options, how do I register which segment is where?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
EDIT:
Ok, so I've figured out how to draw the segmented circle using Canvas (I'll post the code as an answer). And I'm sure I'll figure out how to rotate the circle soon. But I'm still unsure how I'll recognize a separate segment of the drawn wheel after the rotation action.
Because, what I'm thinking of doing is drawing the segmented circle with these wedges, and the sort of handling the entire Canvas as an ImageView when I want to rotate it as if it's spinning. But when the spinning stops, how do I differentiate between the original segments drawn on the Canvas?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I've read about how to draw a segment on its own (here also), OpenGL, Canvas and even drawing shapes and layering them, but I've yet to see someone explaining how to recognize the separate segments.
Can drawBitmap() or createBitmap() perhaps be used?
If I go with OpenGL, I'll probably be able to rotate the segmented wheel using OpenGL's rotation, right?
I've also read that OpenGL might be too powerful for what I'd like to do, so should I rather consider "the graphic components of a game library built on top of OpenGL"?
This kind of answers my first question above - how to draw the segmented circle using Android Canvas:
Using the code found here, I do this in the onDraw function:
// Starting values
private int startAngle = 0;
private int numberOfSegments = 11;
private int sweepAngle = 360 / numberOfSegments;
#Override
protected void onDraw(Canvas canvas) {
setUpPaint();
setUpDrawingArea();
colours = getColours();
Log.d(TAG, "Draw the segmented circle");
for (int i = 0; i < numberOfSegments; i++) {
// pick a colour that is not the previous colour
paint.setColor(colours.get(pickRandomColour()));
// Draw arc
canvas.drawArc(rectF, startAngle, sweepAngle, true, paint);
// Set variable values
startAngle -= sweepAngle;
}
}
This is how I set up the drawing area based on the device's screen size:
private void setUpDrawingArea() {
Log.d(TAG, "Set up drawing area.");
// First get the screen dimensions
Point size = new Point();
Display display = DrawArcActivity.this.getWindowManager().getDefaultDisplay();
display.getSize(size);
int width = size.x;
int height = size.y;
Log.d(TAG, "Screen size = "+width+" x "+height);
// Set up the padding
int paddingLeft = (int) DrawArcActivity.this.getResources().getDimension(R.dimen.padding_large);
int paddingTop = (int) DrawArcActivity.this.getResources().getDimension(R.dimen.padding_large);
int paddingRight = (int) DrawArcActivity.this.getResources().getDimension(R.dimen.padding_large);
int paddingBottom = (int) DrawArcActivity.this.getResources().getDimension(R.dimen.padding_large);
// Then get the left, top, right and bottom Xs and Ys for the rectangle we're going to draw in
int left = 0 + paddingLeft;
int top = 0 + paddingTop;
int right = width - paddingRight;
int bottom = width - paddingBottom;
Log.d(TAG, "Rectangle placement -> left = "+left+", top = "+top+", right = "+right+", bottom = "+bottom);
rectF = new RectF(left, top, right, bottom);
}
That (and the other functions which are pretty straight forward, so I'm not going to paste the code here) draws this:
The segments are different colours with every run.