Flood Fill for Kotlin - android

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!

Related

Comparing two user-drawn Canvas lines in Kotlin

I need to compare, for example, two straight lines. One of these lines will be set by me initially (or drawn), will have certain coordinates and thickness. The second one will be drawn by the user. Finally, I need to see the result of comparing these lines as a percentage. Can it be implement like in the following code below? I had an idea to draw my own line, put its coordinates from Path into an array somehow, and then compare it with the new one. But i don't know.
And, and yet, can you tell me how to make the background transparent? I am not satisfied with the replacement of a white background with a static picture, the animation will take place in the far background
Thank you!
enter image description here
import android.content.Context
import android.graphics.*
import android.util.Log
import android.view.View
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.core.content.res.ResourcesCompat
class SomeDraw(context: Context) : View(context) {
// Holds the path you are currently drawing.
private var path = Path()
private val drawColor = ResourcesCompat.getColor(resources, R.color.purple_700, null)
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.transparent, null)
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
private lateinit var frame: Rect
// Set up the paint with which to draw.
private val paint = Paint().apply {
color = drawColor
// Smooths out edges of what is drawn without affecting shape.
isAntiAlias = true
// Dithering affects how colors with higher-precision than the device are down-sampled.
isDither = true
style = Paint.Style.STROKE // default: FILL
strokeJoin = Paint.Join.ROUND // default: MITER
strokeCap = Paint.Cap.ROUND // default: BUTT
strokeWidth = 70.0F // default: Hairline-width (really thin)
}
/**
* Don't draw every single pixel.
* If the finger has has moved less than this distance, don't draw. scaledTouchSlop, returns
* the distance in pixels a touch can wander before we think the user is scrolling.
*/
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop
private var currentX = 0f
private var currentY = 0f
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
/**
* Called whenever the view changes size.
* Since the view starts out with no size, this is also called after
* the view has been inflated and has a valid size.
*/
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldWidth, oldHeight)
if (::extraBitmap.isInitialized) extraBitmap.recycle()
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(extraBitmap)
extraCanvas.drawColor(backgroundColor)
// Calculate a rectangular frame around the picture.
val inset = 0
frame = Rect(inset, inset, width - inset, height - inset)
}
override fun onDraw(canvas: Canvas) {
// Draw the bitmap that has the saved path.
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
// Draw a frame around the canvas.
extraCanvas.drawRect(frame, paint)
}
/**
* No need to call and implement MyCanvasView#performClick, because MyCanvasView custom view
* does not handle click actions.
*/
override fun onTouchEvent(event: MotionEvent): Boolean {
motionTouchEventX = event.x
motionTouchEventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> touchStart()
MotionEvent.ACTION_MOVE -> touchMove()
MotionEvent.ACTION_UP -> touchUp()
}
return true
}
/**
* The following methods factor out what happens for different touch events,
* as determined by the onTouchEvent() when statement.
* This keeps the when conditional block
* concise and makes it easier to change what happens for each event.
* No need to call invalidate because we are not drawing anything.
*/
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}
private fun touchMove() {
val dx = Math.abs(motionTouchEventX - currentX)
val dy = Math.abs(motionTouchEventY - currentY)
if (dx >= touchTolerance || dy >= touchTolerance) {
// QuadTo() adds a quadratic bezier from the last point,
// approaching control point (x1,y1), and ending at (x2,y2).
path.quadTo(currentX, currentY, (motionTouchEventX + currentX) / 2, (motionTouchEventY + currentY) / 2)
currentX = motionTouchEventX
currentY = motionTouchEventY
// Draw the path in the extra bitmap to save it.
extraCanvas.drawPath(path, paint)
val pathTrue = path
Log.d("AAA", ""+pathTrue.equals(path))
}
// Invalidate() is inside the touchMove() under ACTION_MOVE because there are many other
// types of motion events passed into this listener, and we don't want to invalidate the
// view for those.
invalidate()
}
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}
}
I tried to work with Path but nothing worked. Tried various ways to set transparency to the background (color, apply alpha, delete) - nothing helped

What is wrong with my code. I am unable to draw on the screen

I have checked my code several times and don't know where is it going wrong. i am trying to build a drawing app and after doing all this code I am unable to get any output. Not able to draw on the canvas.
please help me get rid of this issue.
This is my DrawingView.kt
package com.example.drawingapp
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.content.res.ResourcesCompat
class DrawingView(context : Context, attrs:AttributeSet): View(context, attrs){
private var myDrawPath: CustomPath? = null
private var myCanvasBitmap: Bitmap? = null
private var myDrawPaint: Paint? = null
private var myCanvasPaint: Paint? = null
private var myCanvas: Canvas?= null
private var myBrushSize :Float = 0.toFloat()
private var myColor = Color.BLACK
init{
drawingSetUp()
}
private fun drawingSetUp(){
myDrawPaint = Paint()
myDrawPath = CustomPath(myColor,myBrushSize)
myDrawPaint!!.color= myColor
myDrawPaint!!.style = Paint.Style.STROKE
myDrawPaint!!.strokeJoin =Paint.Join.ROUND
myDrawPaint!!.strokeCap =Paint.Cap.ROUND
myCanvasPaint= Paint(Paint.DITHER_FLAG)
myBrushSize=20.toFloat()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
myCanvasBitmap= Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
myCanvas = Canvas(myCanvasBitmap!!)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
myCanvas!!.drawBitmap(myCanvasBitmap!!, 0f,0f , myCanvasPaint)
if (!myDrawPath!!.isEmpty){
myDrawPaint!!.strokeWidth = myDrawPath!!.brushThickness
myDrawPaint!!.color= myDrawPath!!.color
myCanvas!!.drawPath(myDrawPath!!, myDrawPaint!!)
}
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchX = event?.x
val touchY = event?.y
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
myDrawPath!!.color = myColor
myDrawPath!!.brushThickness = myBrushSize
myDrawPath!!.reset()
if (touchX != null) {
if (touchY != null) {
myDrawPath!!.moveTo(touchX, touchX)
}
}
}
MotionEvent.ACTION_MOVE -> {
if (touchX != null) {
if (touchY != null) {
myDrawPath!!.lineTo(touchX, touchY)
}
}
}
MotionEvent.ACTION_UP->{
myDrawPath = CustomPath(myColor, myBrushSize)
}
else->return false
}
invalidate()
return true
}
internal inner class CustomPath(var color:Int, var brushThickness:Float): Path(){
}
}
This is my activity_main.xml file
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.drawingapp.DrawingView
android:id="#+id/drawing_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
And this is my MainActivity.kt file
package com.example.drawingapp
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
You're drawing to the wrong Canvas. You need to draw to the one passed in to onDraw, that's the one linked to what your View will actually display.
When you do this:
myCanvasBitmap= Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
myCanvas = Canvas(myCanvasBitmap!!)
you're creating your own, personal Bitmap, and creating your own personal Canvas to draw stuff on that bitmap. You can do that - but unless you draw that bitmap to the canvas onDraw gives you at some point, you'll never see it.
I'm assuming you wanted to do this:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// draw your stored bitmap to the view's canvas
canvas.drawBitmap(myCanvasBitmap!!, 0f,0f , myCanvasPaint)
if (!myDrawPath!!.isEmpty){
myDrawPaint!!.strokeWidth = myDrawPath!!.brushThickness
myDrawPaint!!.color= myDrawPath!!.color
// draw this stored path to the view's canvas, on top of
// the stored bitmap you just drew
canvas.drawPath(myDrawPath!!, myDrawPaint!!)
}
}
i.e. drawing to canvas and not myCanvas. I don't know where you're actually drawing stuff to myCanvas though (to build up your stored image), you still need to do that so you have something to paint on the view's canvas. I'm assuming that happens elsewhere and the path you're drawing in onDraw is like a temporary overlay that isn't part of the stored image yet
Also just as a piece of advice, you need to avoid that !! stuff everywhere - that's a big sign you've made something nullable that shouldn't be nullable, and now you have to fight with the null-checking system because you're sure it can't be null but the system thinks it could be.
If you just move all your drawingSetup code into init, it'll see that you're assigning everything a value and you won't need to make them null. It's because you're assigning them through another function called from init that it can't be sure you're doing that - it's a limitation of the language unfortunately. Or you could just assign them directly:
private var myDrawPath = CustomPath(myColor,myBrushSize)
private lateinit var myCanvasBitmap: Bitmap
private var myDrawPaint = Paint().apply {
color= myColor
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
}
// etc
If you do want to keep that initialisation function (e.g. because you want to reset everything to normal) at least make your vars lateinit non-nullable types - if they're never going to be null when they're accessed, don't make them nullable! You just have to make sure lateinit stuff is assigned a value before something tries to read it - but the same is true for making something null and doing !! every time you access it, aka "trust me bro it's not null"

Custom view will not update if run after onViewCreated()

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()
}
}

Is it possible to get the text Rects of a TextView?

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

Get space between text and bound of TextView

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?

Categories

Resources