How to draw slices inside a circle without radio as center - android

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%

Related

How can I animate a dial to swich between On and OFF in Android/Kotlin, and retaining the position of the knob after each click?

I want it to function like this: when the screen is opened then the dial points off. The user clicks it, the dial turns to ON and then remains there. When I click it again, the reverse happens. It is unclear to me why it snaps back to another value after the animation is done. I tried tweaking the angle but I had no luck. Thanks in advance!
OFF(R.string.fan_off),
ON(R.string.fan_on);
fun next() = when (this) {
OFF -> ON
ON -> OFF
}
}
private const val RADIUS_OFFSET_LABEL = 75
private const val RADIUS_OFFSET_INDICATOR = -85
class KnobView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var radius = 0.0f // Radius of the circle.
private var socketOnOffSwitch = SocketMainSwich.OFF // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
// MAIN IMPORTANCE
// This one is actually used for the nice knob and cannot be used with the small black button
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
setBackground(ContextCompat.getDrawable(context, R.drawable.socket_transp2))
}
// used for text and for the black circle
private val paint2 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
textAlign = Paint.Align.CENTER
textSize = 55.0f
typeface = Typeface.create("", Typeface.NORMAL)
}
init {
isClickable = true
}
override fun performClick(): Boolean {
if (super.performClick()) return true
// socketOnOffSwitch = socketOnOffSwitch.next()
// contentDescription = resources.getString(socketOnOffSwitch.label)
invalidate()
return true
}
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
private fun PointF.computeXYForSpeed(pos: SocketMainSwich, radius: Float) {
// Angles are in radians. // tweak the angle by adding 0.5 at start angle (9 + 0.5) and subtractig the same from angle
val startAngle = Math.PI * (9.5 / 8.0)
val angle = startAngle + pos.ordinal * (2.5 * Math.PI/ 4)
x = (radius * cos(angle)).toFloat() + width / 2
y = (radius * sin(angle)).toFloat() + height / 2
}
private val paint3 = Paint()
// this angle in correlation with the canvas.rotate method in onDraw realizes the first image of the dial
private var angle = 0f
private var rotateAnimation: RotateAnimation? = null
init {
// Set the paint color and stroke width
paint3.color = resources.getColor(androidx.appcompat.R.color.material_blue_grey_800)
paint3.strokeWidth = 4f
// Create a rotation animation
setOnClickListener {
clearAnimation()
if(angle == -60f) {
rotateAnimation = RotateAnimation(-60f, 60f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotateAnimation?.duration = 3000
rotateAnimation?.interpolator = AccelerateDecelerateInterpolator()
startAnimation(rotateAnimation)
angle = 60f
} else {
rotateAnimation = RotateAnimation(60f, -60f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotateAnimation?.duration = 3000
rotateAnimation?.interpolator = AccelerateDecelerateInterpolator()
startAnimation(rotateAnimation)
angle = -60f
}
}
// as above we need to switch to the next value
socketOnOffSwitch = socketOnOffSwitch.next()
contentDescription = resources.getString(socketOnOffSwitch.label)
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// maybe we can get a rotation
// // Save the current canvas state
// canvas.save()
canvas.rotate(angle, width / 2f, height / 2f)
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR - 15
pointPosition.computeXYForSpeed(socketOnOffSwitch, markerRadius)
paint2.setColor(resources.getColor(R.color.cream))
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/10, paint2)
}```

How can I make a dial stick to it's latest position in Android Kotlin?

A simple On/OFF dial that snaps back to ON after animation is complete, instead of remaining OFF.
What I've tried: I used rotation Animator, onDraw() with Canvas, I tweaked the angles for 2 days now,
I redraw the bitmap in onDraw, I rotate the canvas. I used animation listeners, it still snaps back.
The actual code in onDraw - at the moment it has no effect at all. Why?
I attach a video maybe it will help somebody give a solution. https://youtu.be/v6OiHyLhZv8
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var radius = 0.0f // Radius of the circle.
private var socketOnOffSwitch = SocketMainSwich.OFF // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
// MAIN IMPORTANCE
// This one is actually used for the nice knob and cannot be used with the small black button
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
setBackground(ContextCompat.getDrawable(context, R.drawable.socket_transp3_cubuton))
}
// used for text and for the black circle
private val paint2 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
textAlign = Paint.Align.CENTER
textSize = 55.0f
typeface = Typeface.create("", Typeface.NORMAL)
}
private val paint3 = Paint()
// this angle in correlation with the canvas.rotate method in onDraw realises the first image of the dial
private var angle = -60f
private var rotateAnimation: RotateAnimation? = null
init {
isClickable = true
}
override fun performClick(): Boolean {
if (super.performClick()) return true
socketOnOffSwitch = socketOnOffSwitch.next()
contentDescription = resources.getString(socketOnOffSwitch.label)
paint3.color = resources.getColor(androidx.appcompat.R.color.material_blue_grey_800)
paint3.strokeWidth = 4f
// first - from OFF to ON - from left to right -the origin of the canvas is top right corner so we have to put
// from -120 degrees to 0
if(angle == -60f) {
// we create the animation -- the canvas will rotate to generate the ON position
createAnimation( 0f, 120f)
println("dra - in onclicklistener la if angle = -60f")
angle = 60f
// here the canvas was rotated with 120 so we have to alter the angles more.
} else {
createAnimation( 120f, 0f)
println("dra - in onclicklistener la if angle este altceva decat -60f")
angle = -60f
}
return true
}
fun createAnimation(fromDegrees: Float, toDegrees: Float) {
rotateAnimation = RotateAnimation(fromDegrees, toDegrees, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotateAnimation?.duration = 3000
rotateAnimation?.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation!!.isFillEnabled = true
startAnimation(rotateAnimation)
}
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
private fun PointF.computeXYForSpeed(pos: SocketMainSwich, radius: Float) {
// Angles are in radians. // tweak the angle by adding 0.5 at start angle (9 + 0.5) and subtractig the same from angle
val startAngle = Math.PI * (9.5 / 8.0)
val angle = startAngle + pos.ordinal * (2.5 * Math.PI/ 4)
x = (radius * cos(angle)).toFloat() + width / 2
y = (radius * sin(angle)).toFloat() + height / 2
}
val icon = BitmapFactory.decodeResource(
context.resources,
R.drawable.socket_transp3_cubuton
)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// if (socketOnOffSwitch == SocketMainSwich.OFF) {
//
// canvas.drawBitmap(icon,width / 2f, height / 2f,paint )
// canvas.rotate(120f, width / 2f, height / 2f)
// // check if finished and rotate the canvas
// if (rotateAnimation?.hasEnded() == true) {
// canvas.rotate(120f, width / 2f, height / 2f)
// println("dra - animation finished at OFF")
// }
//
//
// } else {
// canvas.drawBitmap(icon,width / 2f, height / 2f,paint )
// canvas.rotate(120f, width / 2f, height / 2f)
//
// if (rotateAnimation?.hasEnded() == true) {
// canvas.rotate(120f, width / 2f, height / 2f)
// println("dra - animation finished at ON")
//
// }
// }
// Text color depending on dial state
paint2.color = when (socketOnOffSwitch) {
SocketMainSwich.ON -> Color.BLACK
SocketMainSwich.OFF -> Color.BLACK
}
}
}```
Thank you!

The border or shadow cannot be set after BottomNavigationView sets the top bulge

I set the bulge for BottomNavigationView through the set to edge method of materialShapeDrawable, because the color of the page is similar to that of BottomNavigationView, so I need to set a border or shadow on top of the control, but I can't set the border or shadow on top of the control normally.
In addition, bottomNavigationView.invalidateOutline() seems to have some effect, but it seems to be only valid on Android12, and the lower version of Android cannot be used.
val materialShapeDrawable = bottomNavigationView.background as MaterialShapeDrawable materialShapeDrawable.shapeAppearanceModel = materialShapeDrawable.shapeAppearanceModel
.toBuilder()
.setTopEdge(CutoutCircleEdgeTreatment(resources, 70.toFloat(), 10.toFloat()))
.build()
val backgroundDrawable =
MaterialShapeDrawable(materialShapeDrawable.shapeAppearanceModel).apply {
setTint(Color.WHITE)
paintStyle = Paint.Style.FILL
shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
initializeElevationOverlay(this#MainActivity)
setShadowColor(Color.RED)
elevation = 100F
}
(bottomNavigationView.parent as? ViewGroup)?.clipChildren = false
bottomNavigationView.background = backgroundDrawable
bottomNavigationView.invalidateOutline()
class CutoutCircleEdgeTreatment(
res: Resources,
circleDiameterDp: Float,
circleLeftRightOffsetDp: Float
) : EdgeTreatment() {
private val fabDiameter: Float
private val offset: Float
init {
fabDiameter = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
circleDiameterDp,
res.displayMetrics
)
offset = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
circleLeftRightOffsetDp,
res.displayMetrics
)
}
override fun getEdgePath(
length: Float,
center: Float,
interpolation: Float,
shapePath: ShapePath
) {
//凸起半径
val r = length / 10 // == center
val halfLine = length / 2 - r
//值越大转角处就越顺滑,但是必须小于等于r,这个大小可以多试试
val offset = 30f
//第一部分,直线,画到凹陷开始的地方
shapePath.lineTo(halfLine - offset, 0f)
//凸起开始的地方的转角,不处理的话看起来很尖锐
shapePath.quadToPoint(halfLine, 0f, halfLine + offset, -offset)
//凸起部分
shapePath.quadToPoint(center, -r, halfLine + 2 * r - offset, -offset)
//凸起结束的地方的转角
shapePath.quadToPoint(halfLine + 2 * r, 0f, halfLine + 2 * r + offset, 0f)
// 最后一部分的直线,画到底
shapePath.lineTo(length, 0f)
}
}
enter image description here
enter image description here

Draw rounded button instead of rectangular in Android

I'm developing a function to draw a rounded button in my android application. The problem is that the final results is rectangular instead of something rounded on the corners. What am I missing?
fun draw(canvas: Canvas?) {
canvas?.let {
if (strokeColor != -1) {
shaderBorderPaint.color = strokeColor
shaderBorderPaint.shader = null
} else {
shaderBorderPaint.shader = shaderFactory.resize(it.width, it.height)
}
val cornerRadius = 8f
val halfStrokeSize: Float = shaderBorderPaint.strokeWidth / 2
val rect = RectF(halfStrokeSize, it.height - halfStrokeSize, it.width - halfStrokeSize, halfStrokeSize)
it.drawRoundRect(rect, cornerRadius, cornerRadius, shaderBorderPaint)
}
}
Try this instead.
val corners = floatArrayOf(
80f, 80f, // Top left radius in px
80f, 80f, // Top right radius in px
0f, 0f, // Bottom right radius in px
0f, 0f // Bottom left radius in px
)
val path = Path()
path.addRoundRect(rect, corners, Path.Direction.CW)
canvas.drawPath(path, mPaint)
or this,
public static void makeRoundCorner(int bgcolor,int radius,View v,int strokeWidth,int strokeColor)
{
GradientDrawable gdDefault = new GradientDrawable();
gdDefault.setColor(bgcolor);
gdDefault.setCornerRadius(radius);
gdDefault.setStroke(strokeWidth, strokeColor);
v.setBackgroundDrawable(gdDefault);
}
because val cornerRadius = 8f. try to set cornerRadius = heightRect / 2

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