At this class i draw simple ground for Tic-Tae-Toe. It consists of intercepted lines and "X" in the center of the cell.
So when User touches the cell, then textColor in it should be changed.
I use invalidate(rect) to redraw concrete cell, but in this case every cell changes it's textColor.
According to Romain Guy words, the canvas with whole view Rect comes
for drawing. The DisplayList will find interceptions between drawing commands and your dirty Rect, and only those commands will be drawn. But seems, that it doesn't work so way.
Partial invalidation in custom Android view with hardware acceleration
And also i found strange code change between 4.4 - 5.0 Android. So you can see, that mCurrentDirty disappeared from code at all.
Android View.invalidate(Rect) different behavior between two devices
P.S for SA this logic works correctly, and only dirty Rect is changed.
package com.eugeneshapovalov.fizmigclient
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import timber.log.Timber
class TicTacToeView : View, View.OnTouchListener {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
companion object {
const val CELL_SIZE_RATIO = 1 / 3f
const val LINE_SIZE = 2f
const val CELL_COUNT = 3
}
val linePaint = Paint()
val textPaint = Paint()
val dirtyCell = Rect()
var colorNumber: Int
init {
setOnTouchListener(this)
colorNumber = 0
linePaint.strokeWidth = resources.displayMetrics.density * LINE_SIZE
textPaint.textSize = 60f
}
private lateinit var cells: Array<Array<Rect>>
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
initCells()
}
private fun initCells() {
cells = Array(CELL_COUNT, { Array(CELL_COUNT, { Rect() }) })
val xCell = (width * CELL_SIZE_RATIO).toInt()
val yCell = (height * CELL_SIZE_RATIO).toInt()
for (i in 0 until CELL_COUNT) {
for (j in 0 until CELL_COUNT) {
cells[i][j].left = (x + j * xCell).toInt()
cells[i][j].top = (y + i * yCell).toInt()
cells[i][j].right = (x + (j + 1) * xCell).toInt()
cells[i][j].bottom = (y + (i + 1) * yCell).toInt()
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawLines(canvas)
drawText(canvas)
}
private fun drawLines(canvas: Canvas) {
// Vertical lines
canvas.drawLine(x + width * CELL_SIZE_RATIO, y, x + width * CELL_SIZE_RATIO, y + height, linePaint)
canvas.drawLine(x + width * 2 * CELL_SIZE_RATIO, y, x + width * 2 * CELL_SIZE_RATIO, y + height, linePaint)
// Horizontal lines
canvas.drawLine(x, y + height * CELL_SIZE_RATIO, x + width, y + height * CELL_SIZE_RATIO, linePaint)
canvas.drawLine(x, y + height * 2 * CELL_SIZE_RATIO, x + width, y + height * 2 * CELL_SIZE_RATIO, linePaint)
}
private fun drawText(canvas: Canvas) {
textPaint.color = when (colorNumber % 5) {
0 -> Color.BLACK
1 -> Color.BLUE
2 -> Color.RED
3 -> Color.GRAY
4 -> Color.YELLOW
else -> Color.GREEN
}
for (i in 0 until CELL_COUNT) {
for (j in 0 until CELL_COUNT) {
val rect = cells[i][j]
canvas.drawText("X", rect.exactCenterX(), rect.exactCenterY(), textPaint)
}
}
}
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_UP -> {
for (i in 0 until CELL_COUNT) {
for (j in 0 until CELL_COUNT) {
val rect = cells[i][j]
if (rect.contains(event.x.toInt(), event.y.toInt())) {
colorNumber += (j + 7)
Timber.d("Rect: ${rect.flattenToString()}")
invalidate(rect)
}
}
}
}
}
return true
}
}
Related
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()
}
}
}
When I add ImageView using animation it works smoothly fine. But when I set up the player (astronaut) to be moved with interaction, the ImageView keeps trembling when not moving.
Problem
I have attached all the related codes in Kotlin below
MyGameObject class
open class MyGameObject(var x:Int, var y:Int, var dx:Int, var dy:Int, var image:Drawable) {
var width:Int = 300
var height:Int = 300
open fun move(canvas:Canvas)
{
x += dx
y += dy
if(x > (canvas.width - width) || x < 0)
dx = -dx
if(y > (canvas.height - height) || y < 0)
dy = -dy
image.setBounds(x, y, x+width, y+width)
image.draw(canvas)
}
}
Astronaut class (player)
import android.graphics.Canvas
import android.graphics.drawable.Drawable
class Astronaut(x:Int, y:Int, dx:Int, dy:Int, image: Drawable) : MyGameObject(x, y,dx, dy, image) {
var px:Int=0
var py:Int=0
override fun move(canvas: Canvas) {
if (px > x)
x += 5
else
x -= 5
if (py > y)
y += 5
else
y -= 5
image.setBounds(x, y, x + width, y + height)
image.draw(canvas)
}
}
MySurfaceView class
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.*
class MySurfaceView(context: Context?, attrs: AttributeSet?) : SurfaceView(context, attrs), Runnable {
var paint = Paint()
var isRunning = true
lateinit var myThread: Thread
lateinit var myHolder: SurfaceHolder
var myGameObjects = ArrayList<MyGameObject>()
val astronautImage = context!!.resources.getDrawable(R.drawable.astronaut, null)
val astronaut = Astronaut(100, 100, 0, 0, astronautImage)
init {
val asteroid = context!!.resources.getDrawable(R.drawable.asteroid, null)
myGameObjects.add(MyGameObject(100, 100, 10, 10, asteroid))
myGameObjects.add(astronaut)
myThread = Thread(this)
myThread.start()
myHolder = holder // controls access to canvas
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
//return super.onTouchEvent(event)
astronaut.px = event!!.x.toInt()
astronaut.py = event!!.y.toInt()
return true
}
override fun run() {
while(isRunning)
{
if(!myHolder.surface.isValid)
{
continue
}
val canvas: Canvas = myHolder.lockCanvas() // prevent other threads using this section
canvas.drawRect(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), paint)
for(gameObject in myGameObjects)
{
gameObject.move(canvas)
}
myHolder.unlockCanvasAndPost(canvas)
}
}
}
I have already tried to change values in astronaut class and MySurfaceView class but that did not work.
I think it is because you haven't set a fallback option in case astronaut is not moving. When px is equal to astronaut's x, astronaut goes into the else block because the px > x is false, then it moves 5 pixels to the left, then it goes back to initial position because now px is greater than x.
So, based by your logic, the condition should be:
x = x + when {
px > x -> 5
px == x -> 0
px < x -> -5
}
same with the y coordinate, you need a case when the astronaut is NOT moving... That's why the bitmap *trembles* in a diagonal path.
Hope this helps!
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
I want to draw the LudoDeck on top of the LudoBoard. I have create a custom view group and disable the willNotDraw and setup the child view position and size, but it somewhat does not rendered to the screen. I saw the log for the LudoDeck onDraw in logcat, but I'm not sure why it does not drawn, is it because I have not set the view size correctly?
Can someone help me figuring where is my mistakes? Thanks.
LudoBoard.kt
package io.github.andraantariksa.ludo
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
const val RATIO = 1.0f
class LudoBoard(context: Context, attributeSet: AttributeSet):
ViewGroup(context, attributeSet) {
private val ludoPawnsInLane = arrayOf<LudoPawn>()
private val totalGridToTarget = 6
private var gridSideSize = 0F
private val colors = arrayOf(Color.RED, Color.BLUE, Color.YELLOW, Color.GREEN)
private val deck = arrayOf<LudoDeck>(
LudoDeck(context))
init {
setWillNotDraw(false)
deck.forEach {
addView(it)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
var width = measuredWidth
var height = measuredHeight
val widthWithoutPadding = width - paddingLeft - paddingRight
val heightWithoutPadding = height - paddingTop - paddingBottom
val maxWidth = (heightWithoutPadding * RATIO).toInt()
val maxHeight = (widthWithoutPadding / RATIO).toInt()
if (widthWithoutPadding > maxWidth) {
width = maxWidth + paddingLeft + paddingRight
} else {
height = maxHeight + paddingTop + paddingBottom
}
gridSideSize = width / (totalGridToTarget * 2 + 3).toFloat()
val deckSideSize = gridSideSize * 6F
deck.forEach {
it.measure(deckSideSize.toInt(), deckSideSize.toInt())
it.x = 0F
it.y = 0F
}
setMeasuredDimension(width, height)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// some code to draw the board
}
}
LudoDeck.kt
package io.github.andraantariksa.ludo
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
class LudoDeck(context: Context): View(context) {
private var totalPawn = 4
init {
setWillNotDraw(false)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
}
override fun onDraw(canvas: Canvas) {
Log.d("zzzzz", "Draw")
val p = Paint()
p.color = Color.BLACK
val rect = Rect(0, 0, measuredWidth, measuredHeight)
canvas.drawRect(rect, p)
}
}
So basically I tweaked your code a bit and in the process got to learn about ViewGroups also. Thanks for that!
I have commented the explanations of the changes made in code you can refer to that and if any doubt please feel free to ask..
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.ViewGroup
const val RATIO = 1.0f
class LudoBoard1(context: Context, attributeSet: AttributeSet):
ViewGroup(context, attributeSet) {
// private val ludoPawnsInLane = arrayOf<LudoPawn>()
private val totalGridToTarget = 6
private var gridSideSize = 0F
private val colors = arrayOf(Color.RED, Color.BLUE, Color.YELLOW, Color.GREEN)
private val deck = arrayOf<LudoDeck>(
LudoDeck(context, attributeSet), LudoDeck(context, attributeSet))
init {
//setting this will call the onDraw method of the viewGroup. If we just want to treat this as a container
//then set this as 'true'. This way it will not draw anything.
setWillNotDraw(false)
//Add all your custom views here in the beginning.
deck.forEach {
addView(it)
}
}
/**
* This method is used to measure the size of the view itself and also the size of the children.
* Here we calculate the size and allocate it to the children also by calling their "measure()"
* method.
* It's extremely important to call the "setMeasuredDimension()" at the end as this method will
* allocated the measured width and the height.
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
var width = measuredWidth
var height = measuredHeight
val widthWithoutPadding = width - paddingLeft - paddingRight
val heightWithoutPadding = height - paddingTop - paddingBottom
val maxWidth = (heightWithoutPadding * RATIO).toInt()
val maxHeight = (widthWithoutPadding / RATIO).toInt()
if (widthWithoutPadding > maxWidth) {
width = maxWidth + paddingLeft + paddingRight
} else {
height = maxHeight + paddingTop + paddingBottom
}
gridSideSize = width / (totalGridToTarget * 2 + 3).toFloat()
val deckSideSize = gridSideSize * 6F
deck.forEach {
it.measure(deckSideSize.toInt(), deckSideSize.toInt())
}
setMeasuredDimension(width, height)
}
/**
* For a viewGroup, its better if we don't draw anything, but still if we have to, then we can.
* The view group is designed as a container where it determines it's own size, it's children's size
* and their positions.
*/
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// some code to draw the board
}
/**
* This is the method where we calculate the positions for every child. Here we determine the
* starting and ending point for every child element of this viewGroup.
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
/*
Here you will determine the position for every child. Calculate the left top right bottom for every child
and allocate it to the layout.
For now, i am just positioning the ludo deck besides each other.
*/
var previousXStartPoint = 0
deck.forEachIndexed { index, it ->
it.layout(previousXStartPoint , 0, previousXStartPoint.plus(it.measuredWidth), (it.measuredHeight))
previousXStartPoint = it.right + 20
}
}
}
And this is the LudoDeck class:
class LudoDeck(context: Context, attrs: AttributeSet?): View(context, attrs) {
private var totalPawn = 4
private val rect = Rect(0, 0, 50, 50)
private val p = Paint()
/*
As we are drawing something in this view, it's appropriate to set call this method in the beginning.
*/
init {
setWillNotDraw(false)
}
/**
* Its advised not to initialize anything in the onMeasure() method. So, we have already initialized
* the rect in the beginning and now we will just update its params.
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
rect.right = widthMeasureSpec
rect.bottom = heightMeasureSpec
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
}
/**
* Its also advised not to initialize anything in onDraw() method, so we have already created the paint
* object. Here we simply draw!
*/
override fun onDraw(canvas: Canvas) {
p.color = Color.BLACK
canvas.drawRect(rect, p)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
//Uncomment to check whether the dims set in onLayout of Ludo Board are properly allocated. :)
//Log.e("ludoDeck", "left: $left, top: $top, right: $right, bottom: $bottom")
}
Lastly, the source where i was able to understand a few things and get this working is : https://academy.realm.io/posts/360andev-huyen-tue-dao-measure-layout-draw-repeat-custom-views-and-viewgroups-android/
Let me know how it works out!
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"/>