Suppose I have a single TextView like this
As you can see, the text is broken into three lines.
Is there any way that I can get the text areas in Rects? As this text is broken into three lines, I would need three Rects.
It is important to highlight that the left of a Rect is the left of the first character of the line, and the right is the right of the last character of the line.
Onik has the right idea, but the results will all be relative to zero. You will have to do a little more computation if you want to know where in the canvas the text of your TextView lies.
Here is a custom TextView that will outline the text on the screen.
class CustomTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.textViewStyle
) : AppCompatTextView(context, attrs, defStyleAttr) {
private val mPaint = Paint().apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
}
private val mLineOutline = Rect()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.save()
// This is the view's padding which we want to ignore.
canvas.translate(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
for (line in 0 until lineCount) {
// This gets the outline of the text on a line but it is all relative to zero.
paint.getTextBounds(
text.toString(), layout.getLineStart(line), layout.getLineEnd(line), mLineOutline
)
canvas.save()
// We have the outline relative to zero, shift it so it outlines the text.
canvas.translate(layout.getLineLeft(line), layout.getLineBaseline(line).toFloat())
canvas.drawRect(mLineOutline, mPaint)
canvas.restore()
}
canvas.restore()
}
}
This is what is displayed:
You might not need this TextView, but you can grab its computations.
I find this posting very helpful when thinking about Android typography.
I'd do it as follows (in Kotlin):
var lineStart = 0
var lineEnd = 0
var lineText = ""
val paint = textView.paint
val rectList = arrayListOf<Rect>()
for (i in 0 until textView.lineCount) {
lineStart = textView.layout.getLineStart(i)
lineEnd = textView.layout.getLineEnd(i)
lineText = textView.text.substring(lineStart, lineEnd)
val rect = Rect()
paint.getTextBounds(lineText, 0, lineText.length - 1, rect)
rectList.add(rect)
}
Related
I have built a custom view class called progressCircle which is a snapchat-like, circular progress bar - this is inside a constraint layout, over a circular button.
This view has parameter angle which when called from onViewCreated(), will work perfectly if run
progressCircle.angle = 100f
However, I am trying to animate this onClick. If I run this same code, onClick, the progressCircle will not show up?! After trial and error, I found that updating the background colour here made the view visible & it was updated. IE;
button.setOnClickListener {
progressCircle.setBackgroundColor(android.R.color.transparent)
progressCircle.angle = 270f
}
Whats going on here, and how can I fix this so I can animate it properly...
Edit:
class ProgressCircle(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val paint: Paint
private val rect: RectF
private val fillPaint: Paint
private val fillRect: RectF
var angle: Float
var startAngle: Float = 0f
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressCircle)
angle = typedArray.getFloat(R.styleable.ProgressCircle_angle, 0f)
typedArray.recycle()
val offsetAngle = 0f
val color = getColor(context, R.color.outputON)
val strokeWidth = 15f
val circleSize = 276f
paint = Paint().apply {
setAntiAlias(true)
setStyle(Paint.Style.STROKE)
setStrokeWidth(strokeWidth)
setColor(color)
}
rect = RectF(
strokeWidth,
strokeWidth,
(circleSize - strokeWidth),
(circleSize - strokeWidth)
)
fillPaint = Paint().apply {
setAntiAlias(true)
setStyle(Paint.Style.FILL)
setColor(getColor(context, R.color.flat_blue_1))
}
val offsetFill = strokeWidth
fillRect = RectF(
offsetFill,
offsetFill,
(circleSize - offsetFill),
(circleSize - offsetFill)
)
//Initial Angle (optional, it can be zero)
angle = offsetAngle
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (getColor(context, R.color.flat_blue_1) > 0) {
canvas.drawArc(rect, 0f, 360f, false, fillPaint)
}
canvas.drawArc(rect, startAngle, angle, false, paint)
}
}
TIA
By default, a View only redraws itself when something has changed - i.e., when the view is "invalidated" as per the View documentation:
Drawing is handled by walking the tree and recording the drawing commands of any View that needs to update. After this, the drawing commands of the entire tree are issued to screen, clipped to the newly damaged area.
If you want your custom view to redraw itself when the angle property is changed, you need to call invalidate():
var angle: Float
set(value) {
// Do the default behavior of setting the value
field = value
// Then call invalidate() to force a redraw
invalidate()
}
}
I am extending Chip class to perform some drawing over it for my lib , my use case is more complex but for simplicity let's say i am just drawing a diagonal line
my code
class MyChip (context: Context,attributeSet: AttributeSet) : Chip(context,attributeSet){
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//just want to draw a diagonal line
canvas.drawLine(0f,0f,width/1f,height/1f,paint)
}
}
xml
<com.abhinav.chouhan.loaderchipdemo.MyChip
android:layout_width="200dp"
android:layout_height="50dp"
android:textAlignment="center"
android:text="SOME TEXT"/>
when i don't have attribute android:textAlignment="center" everything works fine , but with that attribute we can not draw anything on chip.
I tried everything but couldn't figure out why is it happening.
Please Help
The Chip widget adheres strongly to the Material Design guidelines and is not (IMO) amenable to change. I suggest that you look at other widgets - maybe a MaterialButton to see if something else may suit your needs.
The attribute you reference, textAlignment, is available in TextView which is a class that Chip is based on. Chips expects wrap_content and also expect for text to be aligned to the start. Somewhere in the mix, your over-draw is lost. I doubt that something like that has ever been tested as it does not adhere to the Material guidelines.
However, if you must use a Chip, here is a way to do so with a custom view:
class MyChip #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Chip(context, attrs, defStyleAttr) {
private val mLinePaint = Paint().apply {
color = Color.BLUE
strokeWidth = 10f
}
init {
doOnNextLayout {
// Turn off center alignment if specified. We will handle centering ourselves.
if (isTextAlignmentCenter()) {
textAlignment = View.TEXT_ALIGNMENT_GRAVITY
// Get the length of all the stuff before the text.
val lengthBeforeText =
if (isChipIconVisible) iconStartPadding + chipIconSize + iconEndPadding else 0f
val chipCenter = width / 2
// Chips have only one line, so we can get away with this.
val textWidth = layout.getLineWidth(0)
val newTextStartPadding = chipCenter - (textWidth / 2) - lengthBeforeText
textStartPadding = max(0f, newTextStartPadding)
textEndPadding = 0f
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//just want to draw a diagonal line
canvas.drawLine(0f, 0f, width.toFloat(), height.toFloat(), mLinePaint)
}
private fun isTextAlignmentCenter() = textAlignment == View.TEXT_ALIGNMENT_CENTER
}
A sample layout:
activity_main.xml
<com.example.myapplication.MyChip
android:id="#+id/chip"
android:layout_width="300dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:text="Some Chip"
android:textAlignment="center"
app:chipIcon="#drawable/ic_baseline_cloud_download_24"
app:chipIconVisible="true"
app:closeIcon="#drawable/ic_baseline_clear_24"
app:closeIconVisible="true" />
</LinearLayout>
And the result:
I looked into the issue a little more deeply. For some reason, canvas operations such a drawLine(), drawRect(), etc. do not function. However, I can draw text on the canvas and fill it with a paint or a color.
Update: So, I tracked the problem down to the bringTextIntoView() method of TextView. For a reason that is not clear to me, the view is scrolled quite a few places (large, like 10's of thousands) in the positive direction. It is in this scrolled position that the text is written. To draw on the Chip, we must also scroll to this position. This scroll explains why I could fill the canvas with a color but could not draw on it so it would be visible.
The following will capture the "bad" scroll position and scroll to that position before drawing on the Chip. The screen capture looks the same as the above image.
MyChip.kt
class MyChip #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Chip(context, attrs, defStyleAttr)/*, View.OnScrollChangeListener*/ {
private val mLinePaint = Paint().apply {
color = Color.BLUE
strokeWidth = 10f
}
private var mBadScroll = 0f
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//just want to draw a diagonal line
canvas.withTranslation(x = mBadScroll) {
canvas.drawLine(0f, 0f, this#MyChip.width.toFloat(), height.toFloat(), mLinePaint)
}
}
private fun isTextAlignmentCenter() = textAlignment == View.TEXT_ALIGNMENT_CENTER
override fun scrollTo(x: Int, y: Int) {
super.scrollTo(x, y)
if (isTextAlignmentCenter()) {
mBadScroll = scrollX.toFloat()
}
}
}
Update: Horizontal scrolling is still the issue. In onMeasure() method of TextView, the wanted width is set to VERY_WIDE if horizontal scrolling is enabled for the view. (And it is for Chip.) VERY_WIDE is 1024*1024 or one megabyte. This is the source of the large scroll that we see later on. This problem with Chip can be replicated directly with TextView if we call setHorizontallyScrolling(true) in the TextView.
Chips have only one line and, probably, shouldn't scroll. Calling setHorizontallyScrolling(true) doesn't make the Chip or TextView scrollable anyway. The above code now just becomes:
MyChip.kt
class MyChip #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Chip(context, attrs, defStyleAttr) {
private val mLinePaint = Paint().apply {
color = Color.BLUE
strokeWidth = 10f
}
init {
setHorizontallyScrolling(false)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//just want to draw a diagonal line
canvas.drawLine(0f, 0f, this#MyChip.width.toFloat(), height.toFloat(), mLinePaint)
}
}
Horizontal scrolling is set to "true" in the constructor for Chip.
I'm trying to create a custom ImageView or Drawable in Kotlin which enables dynamic file extensions can be drawn on a base image at runtime. The end result will look like this. Tried creating custom AppCompatImageView class and overriding onDraw() with no luck. Being a novice in this area, can you suggest me a good starting point to achieve this?
EDIT
The file extension is a text that needs to be drawn on the base image with a background as shown in the attachment.
You could create a LayerDrawable at runtime resulting in the superposition of two drawables (one for the background and one for the extension) and position the extension drawable at the bottom right.
It would look like this
val layerDrawable = LayerDrawable(
arrayOf(
AppCompatResources.getDrawable(context, R.drawable.ic_base_sound_file),
AppCompatResources.getDrawable(context, R.drawable.ic_aiff_extension)
)
).apply {
setLayerInset(1, 20, 40, 0, 10)
}
imageView.setImageDrawable(layerDrawable)
The method setLayerInset(index, left, top, right, bottom) will add insets to the drawable at position 'index' (here 1 -> the extension drawable).
You can also use a remote image if needed for the base image.
I prefer to use a custom view than a custom drawable. because of its flexibility in measuring and customizing height and width.
So I've created the FileView:
import android.content.Context
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.TextPaint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
class FileView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
init {
setImageResource(R.drawable.ic_file)
}
var icon: Drawable? = null
set(value) {
field = value
postInvalidate()
}
var ext: CharSequence? = null
set(value) {
field = value
postInvalidate()
}
private val iconRect = Rect()
private val extRect = Rect()
private val extPaint by lazy {
TextPaint().apply {
style = Paint.Style.FILL
color = Color.WHITE
isAntiAlias = true
textAlign = Paint.Align.CENTER
textSize = 12f * Resources.getSystem().displayMetrics.density + 0.5f
}
}
private val extBackgroundPaint by lazy {
TextPaint().apply {
style = Paint.Style.FILL
color = Color.BLACK
isAntiAlias = true
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = width / 2
val centerY = height / 2
icon?.let { icon ->
iconRect.set(
centerX - icon.intrinsicWidth / 2,
centerY - icon.intrinsicHeight / 2,
centerX + icon.intrinsicWidth / 2,
centerY + icon.intrinsicHeight / 2
)
icon.bounds = iconRect
icon.draw(canvas)
}
ext?.let { ext ->
val truncatedExt =
if (ext.length > 6) ext.subSequence(0, 6).toString().plus('…')
else ext
// extRect is used for measured ext height
extPaint.getTextBounds("X", 0, 1, extRect)
val extHeight = extRect.height() // keep ext height
val extWidth = extPaint.measureText(truncatedExt, 0, truncatedExt.length).toInt() // keep ext width
val extPadding = 4.toPx
val extMargin = 4.toPx
val extRight = width - extMargin
val extBottom = height - extMargin
// extRect is reused for ext background bound
extRect.set(
extRight - extWidth - extPadding * 2,
extBottom - extHeight - extPadding * 2,
extRight,
extBottom
)
canvas.drawRect(extRect, extBackgroundPaint)
canvas.drawText(
truncatedExt,
0,
truncatedExt.length,
extRect.exactCenterX(),
extRect.bottom - ((extRect.height() - extHeight) / 2f),
extPaint
)
}
}
private val Int.toPx get() = (this * Resources.getSystem().displayMetrics.density).toInt()
}
and use it:
with(binding.fileView) {
icon = ContextCompat.getDrawable(context, R.drawable.ic_music)
ext = ".aiff"
}
Output:
I can think of 2 solutions for this. I am simply sharing ideas/approaches here and related code can easily be found.
Simpler approach would be to have this layout designed in your xml. Then create your custom class extending ViewGroup and in its constructor you can inflate the view xml and initialise things. Then you can define any helper method ,say setData() where you can pass the file extension info and/or thumb image. Then you can update your view right there.
Another approach would be to not create any xml but programmatically create them in your custom ViewGroup constructor. Then you can have a similar helper method as above to set values to various view components. After you have set everything, call requestLayout() at the end. You can then, if required, update views in onLayout() and perform any spacing/margin calculations. Then using these values draw them inside onDraw().
I am working on a paint tool for android and have attempted to implement a 'fill with color' tool. Where a user is able to fill a particular area with color depending on what they have selected. I have found examples of Flood Fill for Java but cant see anything similar for Kotlin.
I have tried the following - https://developer.android.com/reference/kotlin/android/graphics/Path.FillType
But have not had too much luck since it instead uses the surrounding colour and not what colour the user has selected.
EDIT:
A snippet of the code I am working with:
class DrawingView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private var mDrawPath: CustomPath? = null
private var mCanvasBitMap: Bitmap? = null
private var mDrawPaint: Paint? = null
private var mCanvasPaint: Paint? = null
private var mBrushSize: Float = 0.toFloat()
private var color = Color.BLACK
private var canvas: Canvas? = null
private val mPaths = ArrayList<CustomPath>()
private val mUndoPaths = ArrayList<CustomPath>()
private val mRedoPaths = ArrayList<CustomPath>()
private var counterUndo = 0
init {
setUpDrawing()
}
private fun setUpDrawing() {
mDrawPaint = Paint()
mDrawPath = CustomPath(color, mBrushSize)
mDrawPaint!!.color = color
mDrawPaint!!.style = Paint.Style.STROKE
mDrawPaint!!.strokeJoin = Paint.Join.ROUND
mDrawPaint!!.strokeCap = Paint.Cap.ROUND
mCanvasPaint = Paint(Paint.DITHER_FLAG)
}
//Change Canvas to Canvas? if fails
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(mCanvasBitMap!!, 0f,0f, mCanvasPaint)
for(path in mPaths){
mDrawPaint!!.strokeWidth = path.brushThickness
mDrawPaint!!.color = path.color
canvas.drawPath(path, mDrawPaint!!)
}
if(!mDrawPath!!.isEmpty) {
mDrawPaint!!.strokeWidth = mDrawPath!!.brushThickness
mDrawPaint!!.color = mDrawPath!!.color
canvas.drawPath(mDrawPath!!, mDrawPaint!!)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchX = event?.x
val touchY = event?.y
when(event?.action){
MotionEvent.ACTION_DOWN -> {
mDrawPath!!.color = color
mDrawPath!!.brushThickness = mBrushSize
mDrawPath!!.reset()
mDrawPath!!.moveTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_MOVE -> {
mDrawPath!!.lineTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_UP -> {
mPaths.add(mDrawPath!!)
mDrawPath = CustomPath(color, mBrushSize)
}
else -> return false
}
invalidate()
return true
}
fun setFill(){
mDrawPaint!!.style = Paint.Style.FILL
}
}
This is kind of a complicated subject - if you want to draw a Path with a particular fill and colour, you need to create a Paint and use setColor (and setStyle to make it Paint.Style.FILL.
But Paths are for vector graphics, they're all defined lines and curves. Unless you're creating the shapes that the user can fill in as paths in the first place, you'll have trouble defining one to fit an arbitrary area.
I'm guessing you're actually using bitmaps, like a normal paint program, and you want to tap on a pixel and have that change colour, and also recursively change the colour of surrounding pixels if they meet a certain threshold. So you'll have to get the Canvas's Bitmap, and work out how to move through the pixels, changing them as you go (e.g. with Bitmap.setPixel())
There are a lot of algorithms for doing this, you'll just need to pick one and implement it. You probably don't want to use Paths to do it though!
I'm trying to get the distance between the text and the left side of a TextView. It uses the property android:gravity="center".
I want to get the distance of the red bar (this red bar is not a part of the layout) to center the blue button. What should I do?
The dark area represents the bounds of the TextView.
I don't want to use a compoundDrawable because this view will change the color of the button randomly.
The code of the view (written in Kotlin):
class BallTextView: TextView {
private lateinit var ballPaint : Paint
private var ballRadius : Float = 10f
private var ballColor : Int = Color.BLACK
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
initializeAttributes(attrs)
configBall()
}
override fun onDraw(canvas: Canvas) {
canvas.drawCircle(ballRadius, height.toFloat()/2, ballRadius, ballPaint)
super.onDraw(canvas)
}
fun configBall() {
ballPaint = Paint()
ballPaint.isAntiAlias = true
ballPaint.color = ballColor
}
fun initializeAttributes(attrs: AttributeSet) {
val attributes = context.obtainStyledAttributes(attrs, R.styleable.ball_textview)
ballRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
attributes.getFloat(R.styleable.ball_textview_ball_radius, ballRadius),
context.resources.displayMetrics)
ballColor = attributes.getColor(R.styleable.ball_textview_ball_color, ballColor)
}
}
Thanks.
The TextView#getLineBounds(int, Rect) method is what you want.
The first parameter is the zero-based line number, and the second is a Rect object that will hold the bounds values of the given line after the call. The left field of the Rect will have the horizontal inset of the line, which you can use with the radius of your drawn circle to figure its center's x coordinate.
My another solution was:
override fun onDraw(canvas: Canvas) {
if (xPosition == 0f) {
xPosition = (width - paint.measureText(text.toString())) / 3
}
canvas.drawCircle(xPosition, height.toFloat()/2, ballRadius, ballPaint)
super.onDraw(canvas)
}
#MikeM. What do you think about this approach?