Canvas based custom progress view doesn't get round - android

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

Related

How to create a Radial Gradient with a glowing ring effect?

I am struggling with a custom view, I want it to have this effect.
But the problem is that the RadialGradient its very soft.
This is my class:
class CustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var radius = 0f
private var centerX = 0f
private var centerY = 0f
private val mainPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
}
private val gradientPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
radius = w.coerceAtMost(h) * 0.50f
if (radius < 0) return
centerX = w * 0.5f
centerY = h * 0.5f
gradientPaint.shader = RadialGradient(
centerX,
centerY,
radius,
Color.BLACK,
Color.TRANSPARENT,
Shader.TileMode.REPEAT
)
}
override fun onDraw(canvas: Canvas) {
canvas.drawCircle(centerX, centerY, radius, mainPaint)
canvas.drawCircle(centerX, centerY, radius, gradientPaint)
}
}
The only way I achieved this effect is by drawing a lot of circles (like 10 or more) in the canvas, but this way doesn't feel like the right approach and since I will be doing more stuff in this view, I don't want to have performance issues.
Hope you can help me to find a right way to achieve this. Thank you.

How to draw slices inside a circle without radio as center

I need to draw a circle and fill it according to a percentage value as image shows.
]2
I have tried to draw it using canvas:
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
rectF = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
val scaledValues = scale()
var sliceStartPoint = 0F
for (i in scaledValues.indices) {
slicePaint.color = ContextCompat.getColor(context, sliceColors[i])
canvas!!.drawArc(rectF!!, sliceStartPoint, scaledValues[i], true, slicePaint)
canvas.drawArc(rectF!!, sliceStartPoint, scaledValues[i], true, centerPaint)
sliceStartPoint += scaledValues[i]
}
}
The problem is that I got all lines centered to radio (as a Pie) instead of slices.
I tried to set usecenter:false but it was not successful result.
Any idea how to draw an arc without set the center point as vertice?
I think you just need rotate the canvas & a little math.
private val slicePaint = Paint()
private val centerPaint = Paint().apply {
color = 0x33333333
}
private val sliceColors = arrayListOf<Int>(Color.GRAY, Color.RED, Color.BLACK)
private var rectF: RectF? = null
private val margin = 0f
private var rotDeg = -120f
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rectF = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
canvas.save()
canvas.rotate(rotDeg, canvas.width /2.0f, canvas.height/2.0f)
// val scaledValues = scale()
// var sliceStartPoint = 0F
for (i in percent.indices) {
slicePaint.color = sliceColors[i] // ContextCompat.getColor(context, sliceColors[i])
var d = percent2degree(percent[i])
canvas!!.drawArc(rectF!!, d, -2 * d, false, slicePaint)
// canvas.drawArc(rectF!!, sliceStartPoint, scaledValues[i], true, centerPaint)
// sliceStartPoint += scaledValues[i]
}
canvas.restore()
}
private fun percent2degree(p:Float): Float {
return (acos((0.5 - p)/ 0.5) / Math.PI * 180).toFloat()
}
var percent = arrayOf(1.0f, 0.9f, 0.55f) // 0.5 + 0.3 + 0.2 = 100%

Draw simple XY graph ( plot ) with Kotlin without 3rd party library

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
)

How to get Canvas draw arc touch listener? (Android)

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.

Android drawing semi circle with canvas

Can anyone help me out on how I going to draw semicircle of the picture below with canvas and also how to detect the collection of the drawing object?
I had try to draw it using XML and i dont know how to detect the collision of it. I just want to detect the collision of the black part but not the whole circle.
Thank you.
I've just done something similar, with a custom View:
class SemiCircleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint: Paint = Paint()
private var rectangle: RectF? = null
private var margin: Float
init {
paint.isAntiAlias = true
paint.color = ContextCompat.getColor(context, R.color.colorAquamarine)
paint.style = Paint.Style.STROKE
paint.strokeWidth = 5.dpToPx()
margin = 3.dpToPx() // margin should be >= strokeWidth / 2 (otherwise the arc is cut)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (rectangle == null) {
rectangle = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
}
canvas.drawArc(rectangle!!, 90f, 180f, false, paint)
}
}
Add it you your XML layout like this:
<com.example.view.SemiCircleView
android:layout_width="120dp"
android:layout_height="120dp"/>
That's the result:
In my case I want to programmatically control the % of the circle being drawn, so I've added a method setArcProportion that controls that:
class SemiCircleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val mainPaint: Paint = Paint()
private val backgroundPaint: Paint = Paint()
private var rectangle: RectF? = null
private var margin: Float
private var arcProportion: Float = 0f
init {
mainPaint.isAntiAlias = true
mainPaint.color = ContextCompat.getColor(context, R.color.colorLutea)
mainPaint.style = Paint.Style.STROKE
mainPaint.strokeWidth = 5.dpToPx()
backgroundPaint.isAntiAlias = true
backgroundPaint.color = ContextCompat.getColor(context, R.color.black_08)
backgroundPaint.style = Paint.Style.STROKE
backgroundPaint.strokeWidth = 5.dpToPx()
margin = 3.dpToPx() // margin should be >= strokeWidth / 2 (otherwise the arc is cut)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (rectangle == null) {
rectangle = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
}
canvas.drawArc(rectangle!!, -90f, arcProportion * 360, false, mainPaint)
// This 2nd arc completes the circle. Remove it if you don't want it
canvas.drawArc(rectangle!!, -90f + arcProportion * 360, (1 - arcProportion) * 360, false, backgroundPaint)
}
/**
* #param arcProportion The proportion of the semi circle arc, from 0 to 1. Setting 0 makes the arc invisible, and 1
* makes a whole circle.
*/
fun setArcProportion(arcProportion: Float) {
this.arcProportion = arcProportion
invalidate()
}
}
So if I do semiCircleView.setArcProportion(0.62f) I have:
Bonus - To make the arc grow with an animation modify setArcProportion like this:
private const val ANIMATION_BASE_DURATION_MS: Long = 500 // milliseconds
fun setArcProportion(arcProportion: Float) {
ValueAnimator.ofFloat(0f, arcProportion).apply {
interpolator = DecelerateInterpolator()
// The animation duration is longer for a larger arc
duration = ANIMATION_BASE_DURATION_MS + (arcProportion * ANIMATION_BASE_DURATION_MS).toLong()
addUpdateListener { animator ->
this#SemiCircleView.arcProportion = animator.animatedValue as Float
this#SemiCircleView.invalidate()
}
start()
}
}
In your onDraw method draw a circle .
canvas.drawCircle(x, y, 10, paint);
Now draw a rect with your background colour
Paint fillBackgroundPaint = new Paint();
fillBackgroundPaint.setAntiAlias(true);
fillBackgroundPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(x,y+10,x+10,y-10);
This should serve the purpose.

Categories

Resources