I want to draw a simple XY chart using my data parsed from JSON, but every answer here is redirecting to using some sort of library. I want to draw it without any library usage, is there is a possible way to do this in Kotlin ?
PS No, it's NOT a homework or smth.
There is one simple way to integrate a graph by writing custom view
( original:https://github.com/SupahSoftware/AndroidExampleGraphView )
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class GraphView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private val dataSet = mutableListOf<DataPoint>()
private var xMin = 0
private var xMax = 0
private var yMin = 0
private var yMax = 0
private val dataPointPaint = Paint().apply {
color = Color.BLUE
strokeWidth = 7f
style = Paint.Style.STROKE
}
private val dataPointFillPaint = Paint().apply {
color = Color.WHITE
}
private val dataPointLinePaint = Paint().apply {
color = Color.BLUE
strokeWidth = 7f
isAntiAlias = true
}
private val axisLinePaint = Paint().apply {
color = Color.RED
strokeWidth = 10f
}
fun setData(newDataSet: List<DataPoint>) {
xMin = newDataSet.minBy { it.xVal }?.xVal ?: 0
xMax = newDataSet.maxBy { it.xVal }?.xVal ?: 0
yMin = newDataSet.minBy { it.yVal }?.yVal ?: 0
yMax = newDataSet.maxBy { it.yVal }?.yVal ?: 0
dataSet.clear()
dataSet.addAll(newDataSet)
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
dataSet.forEachIndexed { index, currentDataPoint ->
val realX = currentDataPoint.xVal.toRealX()
val realY = currentDataPoint.yVal.toRealY()
if (index < dataSet.size - 1) {
val nextDataPoint = dataSet[index + 1]
val startX = currentDataPoint.xVal.toRealX()
val startY = currentDataPoint.yVal.toRealY()
val endX = nextDataPoint.xVal.toRealX()
val endY = nextDataPoint.yVal.toRealY()
canvas.drawLine(startX, startY, endX, endY, dataPointLinePaint)
}
canvas.drawCircle(realX, realY, 7f, dataPointFillPaint)
canvas.drawCircle(realX, realY, 7f, dataPointPaint)
}
canvas.drawLine(0f, 0f, 0f, height.toFloat(), axisLinePaint)
canvas.drawLine(0f, height.toFloat(), width.toFloat(), height.toFloat(), axisLinePaint)
}
private fun Int.toRealX() = toFloat() / xMax * width
private fun Int.toRealY() = toFloat() / yMax * height
}
data class DataPoint(
val xVal: Int,
val yVal: Int
)
Related
I wanted to implement a drawing app written in Jetpack compose, I wanted to achieve goal like below in kotlin canvas:
I have tried to build a simple app base on Compose Multiplatform Canvas, but the functionability is pretty bad, My experimental code is below. What I want to know are:
(1) How can I detect mouse entering the drown shapes?
(2) Is there any canvas API can draw rotated line on canvas?
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.input.pointer.pointerInput
import kotlin.math.cos
import kotlin.math.sin
data class PathProperties(val Angle: Float, val length: Float, val startPoint: Pair<Float, Float>, val endPoint: Pair<Float, Float>)
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun customCanvas(){
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
var previousPosition by remember { mutableStateOf(Offset.Unspecified) }
val randomAngle = listOf(45f, -45f)
val paths = remember { mutableStateListOf<Pair<Path, PathProperties>>() }
var currentPath by remember { mutableStateOf(Path()) }
val lineLength = 30f
var cPaths = remember { mutableStateListOf<Rect>() }
var dotList = remember { mutableStateListOf<Color>() }
Canvas(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Gray)
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown().also {
currentPosition = it.position
previousPosition = currentPosition
currentPath.moveTo(currentPosition.x, currentPosition.y)
val angle = randomAngle.random()
val startPoint = Pair(currentPosition.x, currentPosition.y)
val endPoint = getPointByAngle(lineLength, angle, startPoint)
currentPath.lineTo(endPoint.first, endPoint.second)
paths.add(Pair(currentPath, PathProperties(angle, 30f, startPoint, endPoint)))
cPaths.add(Rect(
left = currentPosition.x - 4,
right = currentPosition.x + 4,
top = currentPosition.y - 4,
bottom = currentPosition.y + 4,
))
dotList.add(Color.Cyan)
}
}
}
}
.onPointerEvent(PointerEventType.Move) {
val position = it.changes.first().position
for ((idx, rect) in cPaths.withIndex()) {
if (rect.contains(position)) {
dotList[idx] = Color.Black
break
} else {
dotList[idx] = Color.Cyan
}
}
}
){
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
paths.forEachIndexed() { idx,it: Pair<Path, PathProperties> ->
drawPath(
color = Color.Black,
path = it.first,
style = Stroke(
width = 3f,
cap = StrokeCap.Round,
join = StrokeJoin.Round,
)
)
drawCircle(
color = dotList[idx],
radius = 8f,
center = Offset(it.second.startPoint.first, it.second.startPoint.second),
)
drawCircle(
color = dotList[idx],
radius = 8f,
center = Offset(it.second.endPoint.first, it.second.endPoint.second),
)
}
}
}
}
//calculate the end point x and y coordinate by cos() and sin()
fun getPointByAngle(length: Float, angle: Float, startPoint: Pair<Float, Float>): Pair<Float, Float> {
return Pair(startPoint.first + length * cos(angle), startPoint.second + length * sin(angle))
}
I have a matrix of cells placed in a custom view.
I want to select the cell when it is clicked to change the colour from red to green.
Matrix
Board
class Board : View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet): super(context, attrs)
// Paint object for coloring and styling
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val board_col = Color.CYAN
private val size = 1000
private lateinit var board : Array<Array<Cell>>
override fun onDraw(canvas: Canvas) {
// call the super method to keep any drawing from the parent side.
super.onDraw(canvas)
board(canvas)
initGrid(canvas)
grid(canvas)
}
private fun board(canvas: Canvas){
paint.color = board_col
paint.style = Paint.Style.FILL
canvas.save()
val board_rec = Rect(0,0,size,size)
canvas.drawRect(board_rec, paint)
canvas.restore()
}
private fun initGrid(canvas: Canvas): Array<Array<Cell>> {
val array = Array(10) { Array(10){Cell(0,0,0,0,0,0, canvas)} }
var dim = size / 10
for(i in 0 until array.size) {
for (j in 0 until array[i].size) {
var px = i * dim
var py = j * dim
array [i][j] = Cell(i, j, px, py, px + dim, py + dim, canvas)
}
}
Log.i("TAG", "initGrid: " + array.size.toString())
return array
}
private fun grid(canvas: Canvas){
board = initGrid(canvas)
for(i in 0 until board.size) {
for (j in 0 until board.size) {
board[i][j].show()
}
}
}
//TODO: 19:48 View Pressed
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
for(i in 0 until board.size) {
for (j in 0 until board.size) {
if (board[i][j].contain(board[i][j].coorX(), board[i][j].coorY())) {
Log.i("TAG", "initGrid: " + board[i][j].coorX())
}
}
}
invalidate()
}
}
return super.onTouchEvent(event)
}
}
This is the Board class where I populate the Cells.
The cells have the following attributes
i,j: location in matrix
px,py: where the cell is drawn
px + dim, py + dim: how big the cell is displayed
Cell
class Cell(var col : Int , var row : Int, x : Int, y: Int, w: Int, h: Int,canvas: Canvas){
var x = x
var y = y
var w= w
var canvas = canvas
// Paint object for coloring and styling
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
var r = Rect(x,y,w,h)
fun show(){
paint.color = Color.BLACK
paint.style = Paint.Style.FILL
canvas.save()
canvas.drawRect(r,paint)
canvas.restore()
paint.color = Color.WHITE
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.0f
canvas.save()
canvas.drawRect(r,paint)
canvas.restore()
}
fun contain(x: Int, y: Int) : Boolean{
Log.i("TAG", "Location: " + x)
return (x > this.x && x < this.x + this.w && y > this.y && y < this.y + this.w)
}
fun changeBtn(){
paint.color = Color.RED
paint.style = Paint.Style.FILL
canvas.drawRect(r,paint)
canvas.restore()
}
fun coorX(): Int {
return x
}
fun coorY(): Int {
return y
}
}
I am following this tutorial but it is in JS, the time wher I am is 19:30
https://www.youtube.com/watch?v=LFU5ZlrR21E&t=1188s
When my onTouchEvent() is called it checks the the cell function contains() and is supposed to return the coordinates of the cell (according to the video) which I could then use to change the colour of the button. But it does not work in Kotlin here is the list of the first 10 coordinates.
How can I change the Cell colour when it is clicked on?
Coordinates
I created a custom view to draw a line, but progressively. I tried to use PathMeasure and getSegment, but the effect doesn't work. It just keeps drawing the line already with the final size.
private val paint = Paint().apply {
isAntiAlias = true
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 10f
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val path = Path().apply {
moveTo(width/2.toFloat(), height/2.toFloat())
lineTo(width/2.toFloat(), height/4.toFloat())
}
val measure = PathMeasure(path, false)
val length = measure.length
val partialPath = Path()
measure.getSegment(0.0f, length, partialPath, true)
partialPath.rLineTo(0.0f, 0.0f)
canvas!!.drawPath(partialPath, paint)
}
you can do this with DashPathEffect
DashPathEffect dashPathEffect = new DashPathEffect(new float[]{1000.0f,9999999},0);
Paint.setPathEffect(dashPathEffect);
change 1000 to your length ("on" parts in Dash)
and set 99999999 to your max ("off" parts in Dash)
play with this parameters and read this article please
Here's how I made it, as #mohandes explained:
private var path = Path()
private var paint = Paint()
private val dashes = floatArrayOf(125f, 125f)
init {
paint = Paint().apply {
isAntiAlias = true
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 10.0f
pathEffect = CornerPathEffect(8f)
}
path = Path().apply {
moveTo(312f, 475f)
lineTo(312f, 375f)
}
val lineAnim = ValueAnimator.ofFloat(100f, 0f)
lineAnim.interpolator = LinearInterpolator()
lineAnim.addUpdateListener {
paint.pathEffect = ComposePathEffect(DashPathEffect(dashes, lineAnim.animatedValue as Float), CornerPathEffect(8f))
invalidate()
}
lineAnim.duration = 1000
lineAnim.start()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas!!.drawPath(path, paint)
}
How to get canvas arc touch listener.
I am creating a pie chart with dynamic arcs. I need to perform some task when the arc is clicked (for that I need to know which arch was clicked).
onTouchEvent of View just gives event from which we can get the x & y coordinated but here the arc has thickness.
How can I get the click listener for each arc?
NOTE - Please don't suggest any library
Need to create this kind of pie chart
https://camo.githubusercontent.com/7e8a4a3c938c21d032d44d999edd781b6e146f2a/68747470733a2f2f7261772e6769746875622e636f6d2f5068696c4a61792f4d50416e64726f696443686172742f6d61737465722f73637265656e73686f74732f73696d706c6564657369676e5f7069656368617274312e706e67
My current implementation
private lateinit var mRectF: RectF
private lateinit var mRectFInner: RectF
private lateinit var mPaint: Paint
private lateinit var mCanvas: Canvas
private var isTouched = false
private lateinit var mBitmap:Bitmap
private var pieChartItemList = arrayListOf<PieChartItem>()
private var innerOuterCircleGap: Float = 0F
constructor(context: Context) : super(context) {
init(null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(attrs)
}
private fun init(#Nullable set: AttributeSet?) {
mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
var typedArray = context.obtainStyledAttributes(set, R.styleable.MyCustomView)
innerOuterCircleGap = typedArray.getFloat(R.styleable.MyCustomView_innerOuterCircleGap, 0F)
}
//onDraw is called several times - so don't
override fun onDraw(canvas: Canvas?) {
drawPieChart(canvas)
}
private fun drawPieChart(canvas: Canvas?) {
var unselectedConstant = 10
mPaint.color = Color.BLACK
canvas?.drawRect(0F, 0F, width.toFloat() - unselectedConstant, width.toFloat() - unselectedConstant, mPaint)
mRectF = RectF(0F, 0F, width.toFloat() - unselectedConstant, height.toFloat() - unselectedConstant)
mRectFInner = RectF(innerOuterCircleGap, innerOuterCircleGap, width.toFloat() - innerOuterCircleGap - unselectedConstant,
height.toFloat() - innerOuterCircleGap - unselectedConstant)
var startAngle = 0F
var sweepAngle: Float
var radius: Float = width.toFloat() / 2
Log.e("ANKUSH", "width = $width height = $height radius = $radius")
for (i in 0 until pieChartItemList.size) {
sweepAngle = (pieChartItemList[i].percent * 3.6).toFloat()
Log.e("ANKUSH - SweepAngle $i", sweepAngle.toString())
mPaint.color = pieChartItemList[i].color
canvas?.drawArc(mRectF, startAngle, sweepAngle, true, mPaint)
startAngle += sweepAngle
}
if (isTouched) {
mRectF = RectF(0F, 0F, width.toFloat() - unselectedConstant, height.toFloat() - unselectedConstant)
mPaint.color = Color.RED
canvas?.drawArc(mRectF, 0F, 45F, true, mPaint)
}
mPaint.color = Color.WHITE
canvas?.drawArc(mRectFInner, 0F, 360F, true, mPaint)
}
fun setPieChartItems(itemList: ArrayList<PieChartItem>) {
pieChartItemList = itemList
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val w = MeasureSpec.getSize(widthMeasureSpec)
val h = MeasureSpec.getSize(heightMeasureSpec)
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
mCanvas = Canvas()
mCanvas.setBitmap(mBitmap)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN) {
isTouched = true
Log.e("ANKUSH", mBitmap?.getPixel(event.x.toInt(), event.y.toInt()).toString())
invalidate()
}
return super.onTouchEvent(event)
}
Face same problem. Finally found a solution using this function for convert click coordinates to angle.
private fun convertTouchEventPointToAngle(xPos: Float, yPos: Float): Double {
var x = xPos - (width * 0.5f)
val y = yPos - (height * 0.5f)
var angle = Math.toDegrees(atan2(y.toDouble(), x.toDouble()) + Math.PI / 2)
angle = if (angle < 0) angle + 360 else angle
return angle
}
Found on article https://enginebai.com/2018/05/07/android-custom-view/ so might be helpful.
You need to store the event and draw it accordling something like:
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN) {
isTouched = true
touchedAtX = event.x.toInt()
touchedAtY = event.x.toInt()
Log.e("ANKUSH", mBitmap?.getPixel(event.x.toInt(), event.y.toInt()).toString())
invalidate()
}
return super.onTouchEvent(event)
}
And modify the onDraw
for (i in 0 until pieChartItemList.size) {
sweepAngle = (pieChartItemList[i].percent * 3.6).toFloat()
Log.e("ANKUSH - SweepAngle $i", sweepAngle.toString())
mPaint.color = pieChartItemList[i].color
canvas?.drawArc(mRectF, startAngle, sweepAngle, true, mPaint)
startAngle += sweepAngle
if (isTouched && mRectF.contains(touchedAtX, touchedAtY) {
canvas?.drawArc(mRectF, startAngle - 15, sweepAngle + 15, true, mPaint)
}
}
Note that my code doesn't account for the startAngle and sweepAngle while doing collision detection, but you will need to know based on those which specific arch is touched by its coordinates.
I have developed a custom progress view using android canvas. Currently I am facing a issue where if the progress value below 3 it does not round the edge as in the image.5% to 100% all good. Following is my code. I have attached the image too hope can someone can help.
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.support.v4.content.ContextCompat
import android.util.AttributeSet
import android.view.View
class DevProgressIndicator #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
View(context, attrs, defStyleAttr) {
private var color: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var bgColor: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
var percentage = 0f
set(value) {
if (value in 0f..100f) field = value
else throw IllegalArgumentException("Value should be more than 0 and less or equal 100")
}
init {
color.apply {
color = Color.RED
style = Paint.Style.FILL
}
bgColor.apply {
color = ContextCompat.getColor(context, R.color.material_grey_100)
style = Paint.Style.FILL
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (canvas == null) {
return
}
val centerY = (height / 2).toFloat()
val radius = centerY
val leftCircleX = radius
val rightCircleX = width - radius
canvas.drawCircle(leftCircleX, centerY, radius, bgColor)
canvas.drawCircle(rightCircleX, centerY, radius, bgColor)
canvas.drawRect(leftCircleX, 0f, rightCircleX, height.toFloat(), bgColor)
if (percentage == 0f) {
return
}
val leftIndicatorX = width - (width * percentage) / 100
canvas.drawCircle(rightCircleX, centerY, radius, color)
canvas.drawCircle(leftIndicatorX + radius, centerY, radius, color)
canvas.drawRect(leftIndicatorX + radius, 0f, rightCircleX, height.toFloat(), color)
}
fun setColor(colorId: Int) {
color.color = ContextCompat.getColor(context, colorId)
invalidate()
requestLayout()
}
}
If percentage = 0 then leftIndicatorX must be equal to rightCircleX - radius (so that left and right circles coincide). Try to replace
val leftIndicatorX = width - (width * percentage) / 100
with
val leftIndicatorX = rightCircleX - radius - (rightCircleX - leftCircleX) * percentage / 100