Surface has already been released: when drawing with Canvas - android

My program draws an oval when we click on the screen and + changes the coordinates of the oval depending on the coordinates of the held finger. When we release the finger, the oval should disappear.
Now when I click on the screen I get my oval and + when I move my finger the oval is always next to it, but when I release my finger I get an error:
E/Surface: freeAllBuffers: 1 buffers were freed while being dequeued!
E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.example.lab10, PID: 24198
java.lang.IllegalStateException: Surface has already been released.
at android.view.Surface.checkNotReleasedLocked(Surface.java:774)
at android.view.Surface.unlockCanvasAndPost(Surface.java:473)
at android.view.SurfaceView$1.unlockCanvasAndPost(SurfaceView.java:1629)
at com.example.lab10.TaskSix$OvalsView.surfaceCreated$lambda$0(TaskSix.kt:136)
at com.example.lab10.TaskSix$OvalsView.$r8$lambda$fxYnyciEdLkjDqbt7r8jlbaJ-60(Unknown Source:0)
at com.example.lab10.TaskSix$OvalsView$$ExternalSyntheticLambda0.run(Unknown Source:2)
at java.lang.Thread.run(Thread.java:1012)
package com.example.lab10
import android.annotation.SuppressLint
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.DisplayMetrics
import android.util.Log
import android.view.MotionEvent
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.WindowManager
import androidx.constraintlayout.widget.ConstraintLayout
class TaskSix : AppCompatActivity() {
#SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_task_six)
title = "Task#6"
val layout = findViewById<ConstraintLayout>(R.id.layout)
val ovalsView = OvalsView(this)
layout.setOnTouchListener(View.OnTouchListener { view, motionEvent ->
when (motionEvent.getActionMasked()) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
Log.d("qqq", "down")
val ID: Int = motionEvent.getPointerId(motionEvent.getActionIndex())
Log.d("qqq", "element with id: $ID")
layout.addView(ovalsView)
}
MotionEvent.ACTION_MOVE -> {
Log.d("qqq", "move")
var idx = 0
while (idx < motionEvent.pointerCount) {
val ID: Int = motionEvent.getPointerId(idx) // pobranie unikalnego id dla każdego dotyku
idx++
ovalsView._top = motionEvent.y
ovalsView._left = motionEvent.x
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
Log.d("qqq", "up")
layout.removeView(ovalsView)
}
else -> Log.d("qqq", "unhandled")
}
return#OnTouchListener true
})
}
private class OvalsView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
private val mSurfaceHolder: SurfaceHolder
private val mPainter = Paint()
private var mDrawingThread: Thread? = null
private val mDisplay = DisplayMetrics()
private var mDisplayWidth: Int
private var mDisplayHeight: Int
private var mRotation = 0f
var _top: Float = 0f
var _left: Float = 0f
init {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
wm.defaultDisplay.getMetrics(mDisplay)
mDisplayWidth = mDisplay.widthPixels
mDisplayHeight = mDisplay.heightPixels
mPainter.isAntiAlias = true
mPainter.color = Color.RED
mSurfaceHolder = holder
mSurfaceHolder.addCallback(this)
}
private fun animateOvals(): Boolean {
mRotation += 1
return true
}
private fun drawWheel(canvas: Canvas) {
canvas.drawColor(Color.WHITE)
// canvas.rotate(mRotation, mDisplayWidth / 2f, mDisplayHeight / 2f)
//drawOval(float left, float top, float right, float bottom, #NonNull Paint paint)
// canvas.drawOval(mDisplayWidth/2f - 100f/2f - 100f, mDisplayHeight/2 + 100f/2f,
// mDisplayWidth/2f + 100f/2f + 100f, mDisplayHeight/2f - 100/2f, mPainter)
if (_top > 0f && _left > 0f)
canvas.drawOval(_left, _top, _left + 200f, _top + 350f, mPainter)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
mDisplayWidth = w
mDisplayHeight = h
super.onSizeChanged(w, h, oldw, oldh)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
if (mDrawingThread != null) {
mDrawingThread!!.interrupt()
}
}
override fun surfaceCreated(holder: SurfaceHolder) {
mDrawingThread = Thread(Runnable {
var MAX_FRAME_TIME = 1000/60
var frameStartTime = System.nanoTime();
var frameTime: Long = 0
var canvas: Canvas? = null
while (!Thread.currentThread().isInterrupted && animateOvals()) {
canvas = mSurfaceHolder.lockCanvas()
if (canvas != null) {
drawWheel(canvas)
mSurfaceHolder.unlockCanvasAndPost(canvas)
}
frameTime = (System.nanoTime() - frameStartTime) / 1000000
if (frameTime < MAX_FRAME_TIME) // faster than the max fps - limit the FPS
{
try {
Thread.sleep(MAX_FRAME_TIME - frameTime)
} catch (e: InterruptedException) {
// ignore
}
}
}
})
mDrawingThread!!.start()
}
}
}

I don't have time to dig into the Android source and find out exactly what's happening, but your crash is being generated by that Runnable interacting with the Surface's Canvas (creating the "Surface has been released" exception).
When you lift your finger, you do this:
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
layout.removeView(ovalsView)
}
And in the docs for removeView:
Note: do not invoke this method from View.draw(android.graphics.Canvas), View.onDraw(android.graphics.Canvas), dispatchDraw(android.graphics.Canvas) or any related method.
Which implies to me that calling removeView will cause things to happen to the View which mean they can no longer be drawn to, which is why you can't do it from inside one of the draw calls where you're actively drawing to their Canvas.
Since your drawing thread is constantly drawing to the Surface on a tick, you have a potential race condition where the Surface is released before the thread is interrupted. And you call interrupt() in surfaceDestroyed - but releasing happens before the Surface is actually destroyed, it just releases a reference and makes the Surface invalid.
So your drawing loop is probably still attempting to draw to that released, invalid Surface - there are lots of ways to get around this, but you might want to look at Surface#isValid (you can access a SurfaceView's underlying surface with getHolder()). And since you have multiple threads here, you might want to implement some kind of concurrent locking to avoid the UI thread invalidating the Surface with removeView while the drawing thread is running the Canvas-touching code, which could cause rare crashes

Related

how to properly add multiple SurfaceView objects to ConstraintLayout on finger press

My program now draws an oval at the touch of a finger and moves it along with the finger.
I want to add a multi-tap so that without releasing the first oval, pressing with the second finger will add the next one.
How my code works now:
Here I create 20 ovals:
var ovalsViews = Array(20) { OvalsView(this) }
In the function MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN (here I put my finger on the screen) I add ovals from the ovals table to the layout (ConstraintLayout):
layout.addView(ovalsViews[ID])
In the MotionEvent.ACTION_MOVE function (here I move my finger on the screen) I change the coordinates of each oval that I hold with my finger:
ovalsViews[ID]._top = motionEvent.getY(ID)
ovalsViews[ID]._left = motionEvent.getX(ID)
And finally, in the function MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP (here I release my finger from the screen) I want to remove the oval that I am holding with my finger:
layout.removeView(ovalsViews[ID])
Now my program is working strangely, when I click once everything works ok, but the next clicks do not add new ovals. Also, when I release the first finger, the oval disappears and in order to get an oval for the first time, now you will need to hit it twice, then three, and so on. What am I doing wrong?
Here is the whole code:
package com.example.lab10
import android.annotation.SuppressLint
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.DisplayMetrics
import android.util.Log
import android.view.MotionEvent
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.WindowManager
import androidx.constraintlayout.widget.ConstraintLayout
class TaskSevenNEW : AppCompatActivity() {
#SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_task_seven_new)
title = "Task#7 NEW"
val layout = findViewById<ConstraintLayout>(R.id.layout_7_NEW)
var ovalsViews = Array(20) { OvalsView(this) }
layout.setOnTouchListener(View.OnTouchListener { view, motionEvent ->
when (motionEvent.getActionMasked()) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
Log.d("touchInfo", "down")
val ID: Int = motionEvent.getPointerId(motionEvent.getActionIndex())
Log.d("touchInfo", "down ID: " + ID)
layout.addView(ovalsViews[ID])
}
MotionEvent.ACTION_MOVE -> {
Log.d("touchInfo", "move")
var idx = 0
while (idx < motionEvent.pointerCount) {
val ID: Int = motionEvent.getPointerId(idx) // pobranie unikalnego id dla każdego dotyku
idx++
Log.d("touchInfo", "move ID: " + ID)
ovalsViews[ID]._top = motionEvent.getY(ID)
ovalsViews[ID]._left = motionEvent.getX(ID)
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
Log.d("touchInfo", "up")
val ID: Int = motionEvent.getPointerId(motionEvent.getActionIndex())
Log.d("touchInfo", "up ID: " + ID)
layout.removeView(ovalsViews[ID])
}
else -> Log.d("touchInfo", "unhandled")
}
return#OnTouchListener true
})
}
private class OvalsView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
private val mSurfaceHolder: SurfaceHolder
private val mPainter = Paint()
private var mDrawingThread: Thread? = null
private val mDisplay = DisplayMetrics()
private var mDisplayWidth: Int
private var mDisplayHeight: Int
private var mRotation = 0f
private var running = true
var _top: Float = 0f
var _left: Float = 0f
init {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
wm.defaultDisplay.getMetrics(mDisplay)
mDisplayWidth = mDisplay.widthPixels
mDisplayHeight = mDisplay.heightPixels
mPainter.isAntiAlias = true
mPainter.color = Color.RED
mSurfaceHolder = holder
mSurfaceHolder.addCallback(this)
}
private fun animateOvals(): Boolean {
mRotation += 1
return true
}
private fun drawWheel(canvas: Canvas) {
canvas.drawColor(Color.WHITE)
// canvas.rotate(mRotation, mDisplayWidth / 2f, mDisplayHeight / 2f)
//drawOval(float left, float top, float right, float bottom, #NonNull Paint paint)
// canvas.drawOval(mDisplayWidth/2f - 100f/2f - 100f, mDisplayHeight/2 + 100f/2f,
// mDisplayWidth/2f + 100f/2f + 100f, mDisplayHeight/2f - 100/2f, mPainter)
if (_top > 0f && _left > 0f)
canvas.drawOval(_left, _top, _left + 200f, _top + 350f, mPainter)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
mDisplayWidth = w
mDisplayHeight = h
super.onSizeChanged(w, h, oldw, oldh)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
if (mDrawingThread != null) {
mDrawingThread!!.interrupt()
running = false
mDrawingThread!!.join()
Log.e("qqq", "Thread done")
}
}
override fun surfaceCreated(holder: SurfaceHolder) {
mDrawingThread = Thread(Runnable {
var frameStartTime = System.nanoTime();
var frameTime: Long = 0
var canvas: Canvas? = null
while (!Thread.currentThread().isInterrupted && animateOvals() && running) {
canvas = mSurfaceHolder.lockCanvas()
if (canvas != null) {
drawWheel(canvas)
mSurfaceHolder.unlockCanvasAndPost(canvas)
}
frameTime = (System.nanoTime() - frameStartTime) / 1000000
if (frameTime < MAX_FRAME_TIME) // faster than the max fps - limit the FPS
{
try {
Thread.sleep(MAX_FRAME_TIME - frameTime)
} catch (e: InterruptedException) {
// ignore
}
}
}
})
mDrawingThread!!.start()
}
}
}

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

How to create following types of layout in android

I have tried using ProgressBar or SeekBar but how to add left cut on one side and right side also
I've created a sample custom ConstraintLayout for you to achive what you want.it will behave as a regular constraintLayout but just with a custom background shape.
you have to change numbers and logic according to ui ofcourse, but it will cut your imageView inside it and you can also put progressbar and textViews to create your own ui
package com.customView
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
/**
* Created by Payam Monsef
* At: 2022/Jun/27
*/
class CustomConstraintLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ConstraintLayout(context, attrs) {
private var strokeWidth = 4F
private var strokeColor = Color.CYAN
private val cutPosition = CutPosition.TOP_LEFT
//like 0.2 of width will be reduced from cut position
private val reduceWidthMultiplier = 0.2
private var mPath: Path? = null
private var mPathBorder: Path? = null
private var mPaint: Paint? = null
init {
mPath = Path()
mPathBorder = Path()
mPaint = Paint()
mPaint?.style = Paint.Style.STROKE
mPaint?.color = strokeColor
mPaint?.strokeWidth = strokeWidth
setBackgroundColor(Color.TRANSPARENT)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val mW = width.toFloat()
val mH = height.toFloat()
mPathBorder?.apply {
reset()
if (cutPosition == CutPosition.BOTTOM_RIGHT) {
moveTo(0F, 0F)
lineTo(mW, 0F)
lineTo(mW - (reduceWidthMultiplier * mW).toFloat(), mH)
lineTo(0F, mH)
close()
} else {
moveTo((reduceWidthMultiplier * mW).toFloat(), 0F)
lineTo(mW, 0F)
lineTo(mW, mH)
lineTo(0F, mH)
close()
}
}
val offset = strokeWidth / 2
mPath?.apply {
reset()
if (cutPosition == CutPosition.BOTTOM_RIGHT) {
moveTo(offset, offset)
lineTo(mW - offset, offset)
lineTo(mW - (reduceWidthMultiplier * mW).toFloat(), mH - offset)
lineTo(offset, mH - offset)
close()
} else {
moveTo((reduceWidthMultiplier * mW).toFloat() , offset)
lineTo(mW - offset, offset)
lineTo(mW - offset, mH - offset)
lineTo(offset, mH - offset)
close()
}
}
}
override fun onDraw(canvas: Canvas) {
mPathBorder?.let { p ->
mPaint?.let { paint ->
canvas.drawPath(p, paint)
}
}
mPath?.let {
canvas.clipPath(it)
}
}
private enum class CutPosition {
TOP_LEFT, BOTTOM_RIGHT
}
}
Its better than you define above variables as custom attributes.you can read the Google documents to do that

Pixel art creator: loading saved pixel data from Room causes strange Canvas bug/glitch

Note: my question is very specific, apologies if the title isn't clear enough as to what the problem is.
I'm creating a pixel art editor application using Canvas, and the pixel art data is saved into a Room database.
Here's the canvas code:
package com.realtomjoney.pyxlmoose.customviews
import android.content.Context
import android.graphics.*
import android.util.Log
import android.view.MotionEvent
import android.view.View
import androidx.lifecycle.LifecycleOwner
import com.realtomjoney.pyxlmoose.activities.canvas.*
import com.realtomjoney.pyxlmoose.converters.JsonConverter
import com.realtomjoney.pyxlmoose.database.AppData
import com.realtomjoney.pyxlmoose.listeners.CanvasFragmentListener
import com.realtomjoney.pyxlmoose.models.Pixel
import kotlin.math.sqrt
class MyCanvasView(context: Context, val spanCount: Double) : View(context) {
lateinit var extraCanvas: Canvas
lateinit var extraBitmap: Bitmap
val rectangles = mutableMapOf<RectF, Paint?>()
private lateinit var caller: CanvasFragmentListener
private var thisWidth: Int = 0
private var scale: Double = 0.0
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
thisWidth = w
caller = context as CanvasFragmentListener
if (::extraBitmap.isInitialized) extraBitmap.recycle()
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(extraBitmap)
scale = (w / spanCount)
for (i in 0 until spanCount.toInt()) {
for (i_2 in 0 until spanCount.toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; color = Color.WHITE })
}
}
}
private fun drawRectAt(x: Float, y: Float) {
for (rect in rectangles.keys) {
if (rect.contains(x, y)) {
caller.onPixelTapped(this, rect)
invalidate()
}
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_MOVE -> drawRectAt(event.x, event.y)
MotionEvent.ACTION_DOWN -> drawRectAt(event.x, event.y)
}
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
fun saveData(): List<Pixel> {
val data = mutableListOf<Pixel>()
for (pair in rectangles) {
data.add(Pixel(pair.value?.color))
}
return data
}
fun loadData(context: LifecycleOwner, index: Int) {
AppData.db.pixelArtCreationsDao().getAllPixelArtCreations().observe(context, {
currentPixelArtObj = it[index]
val localPixelData = JsonConverter.convertJsonStringToPixelList(currentPixelArtObj.pixelData)
var index = 0
for (i in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
for (i_2 in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE })
rectangles[rectangles.keys.toList()[index]] = Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE }
index++
}
}
})
}
}
Here's an example of how a 10 by 10 canvas may look like:
The pixel data is saved into a Room database as a Json String, and whenever we want to access this data we convert the Json String back to a List<Pixel>, et cetera:
Dao:
#Dao
interface PixelArtCreationsDao {
#Insert
suspend fun insertPixelArt(pixelArt: PixelArt)
#Query("SELECT * FROM PixelArt ")
fun getAllPixelArtCreations(): LiveData<List<PixelArt>>
#Query("DELETE FROM PixelArt WHERE objId=:pixelArtId")
fun deletePixelArtCreation(pixelArtId: Int)
#Query("UPDATE PixelArt SET item_bitmap=:bitmap WHERE objId=:id_t")
fun updatePixelArtCreationBitmap(bitmap: String, id_t: Int): Int
#Query("UPDATE PixelArt SET item_pixel_data=:pixelData WHERE objId=:id_t")
fun updatePixelArtCreationPixelData(pixelData: String, id_t: Int): Int
#Query("UPDATE PixelArt SET item_favourited=:favorited WHERE objId=:id_t")
fun updatePixelArtCreationFavorited(favorited: Boolean, id_t: Int): Int
}
PixelArt database:
#Database(entities = [PixelArt::class], version = 1)
abstract class PixelArtDatabase: RoomDatabase() {
abstract fun pixelArtCreationsDao(): PixelArtCreationsDao
companion object {
private var instance: PixelArtDatabase? = null
fun getDatabase(context: Context): PixelArtDatabase {
if (instance == null) {
synchronized(PixelArtDatabase::class) {
if (instance == null) instance = Room.databaseBuilder(context.applicationContext, PixelArtDatabase::class.java, AppData.dbFileName).allowMainThreadQueries().build()
}
}
return instance!!
}
}
}
AppData:
class AppData {
companion object {
var dbFileName = "pixel_art_db"
lateinit var db: PixelArtDatabase
}
}
Model:
#Entity
data class PixelArt(
#ColumnInfo(name = "item_bitmap") var bitmap: String,
#ColumnInfo(name = "item_title") var title: String,
#ColumnInfo(name = "item_pixel_data") var pixelData: String,
#ColumnInfo(name = "item_favourited") var favourited: Boolean,
#ColumnInfo(name = "item_date_created") var dateCreated: String = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now())) {
#PrimaryKey(autoGenerate = true) var objId = 0
}
Now, say we have two projects like so with two different spanCount values:
Once we click on the first item, the following occurs:
For some reason it's setting the grid size to be equal to that of the second item, and I'm really trying to understand why this is the case. I've tried for a couple of hours to fix this weird glitch and have had no luck doing so.
But, for some reason when we go to our second item it renders properly:
If we create a new 80 x 80 canvas and then go back to the second creation it will render like so:
I'm assuming that it's setting the spanCount to that of the latest item in the database, but I'm unsure why this is happening.
I suspect it has something to do with the code that takes the List<Pixel> and draws it onscreen:
fun loadData(context: LifecycleOwner, index: Int) {
AppData.db.pixelArtCreationsDao().getAllPixelArtCreations().observe(context, {
currentPixelArtObj = it[index]
val localPixelData = JsonConverter.convertJsonStringToPixelList(currentPixelArtObj.pixelData)
var index = 0
for (i in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
for (i_2 in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE })
rectangles[rectangles.keys.toList()[index]] = Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE }
index++
}
}
})
}
Although I'm not entirely sure where the source of the bug is coming from because it seems I'm doing everything right. It's honestly been a brainfuck trying to fix this lol
Any help would be appreciated to fix this annoying glitch so I can finish my pixel art editor app.
This bug was fixed by calling invalidate() on the Fragment's Canvas property after the user taps the back button. It took me a couple of days to get to fix this, so I'm posting an answer here in case someone has a similar bug.
fun CanvasActivity.extendedOnBackPressed() {
canvasFragmentInstance.myCanvasViewInstance.invalidate()
startActivity(Intent(context, MainActivity::class.java))
}

How to draw sine wave curve using sin() function and canvas in android?

I am new to android and I am trying to draw a sine wave on the screen on the trigger of accelerometer values change. I just need a plain sine wave dynamically drawn on the screen(confined to screen). I can draw the coordinates on Canvas and validate screen dimensions but I am not able to think of how to convert sine values(ranging from 0 to 1) to the screen coordinates.
I was trying something like this in onSensorChanged():
tvarY = sin(tvarX)*2.0; // tvarX and tvarY are double values
tvarX= (tvarX+ 2); // 2.0 is for magnifying
xPosition = (float)tvarX;
yPosition = (float)tvarY;
But the values of tvarx using this approach are always switching between back and forth from infinity to 0. Can anybody please suggest me any approach to change the values and convert them into screen coordinates for drawing a proper sine wave?
Thanks :-)
I think you can use function:
path.rQuadTo(float dx1, float dy1, float dx2, float dy2)
Same as quadTo, but the coordinates are considered relative to the last point on this contour.
I wrote a sentence for your reference:
Path mpath = new Path();
mpath.moveTo(0, 100);
mpath.rQuadTo(20, 5, 40, 0);
mpath.rQuadTo(20, -5, 40, 0);
You can try once, then you will get a sine wave like this picture:
I think this method will be easier comparing with converting value of sin to coordinate.
Create custom view:
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
class WaveView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var amplitude = 30f.toDp() // scale
private var speed = 0f
private val path = Path()
private var paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var animator: ValueAnimator? = null
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
animator?.cancel()
animator = createAnimator().apply { start() }
}
override fun onDraw(c: Canvas) = c.drawPath(path, paint)
private fun createAnimator(): ValueAnimator {
return ValueAnimator.ofFloat(0f, Float.MAX_VALUE).apply {
repeatCount = ValueAnimator.INFINITE
addUpdateListener {
speed -= WAVE_SPEED
createPath()
invalidate()
}
}
}
private fun createPath() {
path.reset()
paint.color = Color.parseColor("#1da6f9")
path.moveTo(0f, height.toFloat())
path.lineTo(0f, amplitude)
var i = 0
while (i < width + 10) {
val wx = i.toFloat()
val wy = amplitude * 2 + amplitude * Math.sin((i + 10) * Math.PI / WAVE_AMOUNT_ON_SCREEN + speed).toFloat()
path.lineTo(wx, wy)
i += 10
}
path.lineTo(width.toFloat(), height.toFloat())
path.close()
}
override fun onDetachedFromWindow() {
animator?.cancel()
super.onDetachedFromWindow()
}
companion object {
const val WAVE_SPEED = 0.3f
const val WAVE_AMOUNT_ON_SCREEN = 350
}
private fun Float.toDp() = this * context.resources.displayMetrics.density
}
In activity layout(ConstraintLayout):
<com.uvn.test.WaveView
android:layout_width="0dp"
android:layout_height="350dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>

Categories

Resources