I have a small application that intends to make an animation, consisting of a graphic with 3 elements.
The application consists of:
mainactivity that will instantiate an object of the AnimatedCanvas class and that will send a vector of 3 pointF objects.
the AnimatedCanvas class that inherits from view and manages the canvas
An abstract class called basicItems, which represents the minimum information to draw basic elements of a canvas, such as circles, lines...
I come across two situations. In the first one, if the elements that I want to draw and animate are variables of the AnimatedCanvas class, both the drawing and the animation are done as expected.
The second is when I get the coordinates sent from the mainactivity that either not all animations (or none) are performed or some element is not initialized correctly.
The idea is:
draw a sphere for each of the elements in the graph, to be represented, making an animation that makes the spheres go from alpha 0f to 1f
perform an animation on the y axis, so that these spheres move to a certain point on the axis
at the same time that the spheres are animated, draw a line that animates and moves along the y-axis, along with its sphere
Heres my code...
`
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">``your text``
<com.example.prueba_circulo_01.AnimatedCanvasView
android:id="#+id/canvas"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="13dp"
/>
</RelativeLayout>
</layout>
import android.os.Bundle
import android.os.Handler
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.example.prueba_circulo_01.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var animateC : AnimatedCanvasView
private val p1 = GraphicPoint("enero", 1f, "uno")
private val p2 = GraphicPoint("enero",2f,"dos")
private val p3 = GraphicPoint("enero", 3f, "tres")
private var listAux = listOf<GraphicPoint>(p1,p2,p3)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.apply {
canvas.addDataPoints(listAux,GraphicType.Default)
}
}
}
BasicItem class
abstract class BasicItem (var x : Float,
var y : Float,
var yFinal : Float,
var paint : Paint,
var alpha: Float
){
abstract fun updateY(value: Float)
abstract fun updateYFinal(final : Float)
abstract fun updateAlpha(value: Float)
}
//----------------------------------------------------------
class VerticalLine1 (x : Float,
y : Float,
yFinal : Float,
paint : Paint,
alpha: Float):BasicItem(x,y,yFinal,paint,alpha){
override fun updateY(value: Float) {
y = value
}
override fun updateYFinal(final: Float) {
yFinal = final
}
override fun updateAlpha(value: Float){
alpha = value
paint.alpha = (255*alpha).toInt()
}
}
//----------------------------------------------------------
class Circle1 ( x : Float,
y : Float,
yFinal : Float,
var radius : Float,
paint : Paint,
alpha : Float):BasicItem(x,y,yFinal,paint,alpha) {
override fun updateY(value: Float) {
y = value
}
override fun updateYFinal(final: Float) {
yFinal = final
}
override fun updateAlpha(value: Float){
alpha = value
paint.alpha = (255*alpha).toInt()
}
}
//----------------------------------------------------------
class StringItem(x : Float,
y : Float,
yFinal : Float,
paint : Paint,
var string: String,
alpha : Float):BasicItem(x,y,yFinal,paint,alpha) {
override fun updateY(value: Float) {
y = value
}
override fun updateYFinal(final: Float) {
yFinal = final
}
override fun updateAlpha(value: Float){
alpha = value
paint.alpha = (255*alpha).toInt()
}
}
AnimatedCanvasView class
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import kotlin.math.abs
import kotlin.math.min
class AnimatedCanvasView(context: Context, attrs: AttributeSet) : View(context, attrs), ValueAnimator.AnimatorUpdateListener {
private val data = mutableListOf<DataPoint>()
private var points = mutableListOf<PointF>()
private val conPoint1 = mutableListOf<PointF>()
private val conPoint2 = mutableListOf<PointF>()
private val graphicPoints = mutableListOf<GraphicPoint>()
private val primaryColorPaint = Paint()
private val borderPathPaint = Paint()
private val blueCircle = Paint()
private val pathPaint = Paint()
private val barPaint = Paint()
private val path = Path()
private val borderPath = Path()
private val barWidth by lazy {
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 1f, resources.displayMetrics
)
}
private val borderPathWidth by lazy {
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics
)
}
private val startX = 0f
private var delay = 500L
private val standardDuration = 3000L
private val standardDelay = 1000L
private val secondDuration = 2000L
private val animatorCircle1 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle2 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle3 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle4 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle5 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle6 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle10 = ValueAnimator.ofFloat(0f,1f)
private lateinit var circle10 : Circle
private lateinit var animatorVerticalLine1 : ValueAnimator
private lateinit var animatorVerticalLine2 : ValueAnimator
private lateinit var animatorVerticalLine3 : ValueAnimator
private var animatorVerticalLine4 : ValueAnimator? = null
private lateinit var animatorVerticalLine5 : ValueAnimator
private lateinit var animatorVerticalLine6 : ValueAnimator
private var curveTopMargin = 32
private val externalRadiusNormal = 30f
private val externalRadiusFinal = 35f
private val internalRadiusNormal = 23f
private val internalRadiusFinal = 33f
private val secondInternalRadius = 15f
private lateinit var circle1 : Circle
private lateinit var circle2 : Circle
private lateinit var circle3 : Circle
private lateinit var circle4 : Circle
private lateinit var circle5 : Circle
private lateinit var circle6 : Circle
private lateinit var internalCircle1 : Circle
private lateinit var internalCircle2 : Circle
private lateinit var internalCircle3 : Circle
private lateinit var secondInternalCircle : Circle
private lateinit var verticalLine1 : VerticalLine
private lateinit var verticalLine2 : VerticalLine
private lateinit var verticalLine3 : VerticalLine
private lateinit var verticalLine4 : VerticalLine
private lateinit var verticalLine5 : VerticalLine
private lateinit var verticalLine6 : VerticalLine
private lateinit var stringCircle1 : StringItem
private lateinit var stringCircle2 : StringItem
private lateinit var stringCircle3 : StringItem
private lateinit var bezierCurve: BezierCurve
private val paintCircleBlack = Paint()
private val paintCircleNormal = Paint()
private val paintCircleFinal = Paint()
private val paintInternal = Paint()
private val paintLine = Paint()
private val paintVerticalLineGray = Paint()
private val paintVerticalLineGreen = Paint()
private val paintSecondInternalRadius = Paint()
private val paintText = Paint()
private val paintBezier = Paint()
private var empezado = false
private val images = mutableListOf<ImageCoordinates>()
val images2 = mutableListOf<imgCoor>()
//lista de animaciones...
private var animatorList = mutableListOf<ValueAnimator>()
private lateinit var animator2 : ValueAnimator
private lateinit var animator3 : ValueAnimator
private lateinit var basicItem: BasicItem
private lateinit var basicItem2: BasicItem
init {
initPaint()
initItemsToDraw()
initAnimators()
}
override fun onAnimationUpdate(animation: ValueAnimator?) {
animation?.apply {
startDelay = standardDelay
addUpdateListener {
basicItem.updateAlpha(it.animatedValue as Float)
invalidate()
}
addListener(object:AnimatorListenerAdapter(){
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
ValueAnimator.ofFloat(basicItem.y,basicItem.yFinal).apply {
duration=secondDuration
addUpdateListener {
basicItem.updateY(it.animatedValue as Float)
invalidate()
}
start()
}
}
})
//start()
}
}
fun starAnim(){
animator2 = ValueAnimator.ofFloat(0f,1f)
animator2.duration = 1000
animator2.addUpdateListener(this)
animator2.start()
}
private fun initPaint(){
paintCircleBlack.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.BLACK
alpha = 255
}
paintCircleNormal.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GREEN
alpha = 0
}
paintCircleFinal.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.DKGRAY
alpha = 0
}
paintInternal.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.WHITE
alpha = 0
}
paintLine.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GREEN
strokeWidth = 3f
}
paintVerticalLineGray.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GRAY
strokeWidth = 1f
alpha = 1
}
paintVerticalLineGreen.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GREEN
strokeWidth = 3f
// alpha = 255
alpha = 255
}
paintSecondInternalRadius.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.DKGRAY
alpha = 0
}
paintText.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.BLACK
textSize = 50f
alpha = 0
}
paintBezier.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GREEN
strokeWidth = 3f
}
borderPathPaint.apply {
isAntiAlias = true
strokeWidth = borderPathWidth
style = Paint.Style.STROKE
color = ContextCompat.getColor(context, R.color.blue_green_alpha_50)
}
barPaint.apply {
isAntiAlias = true
strokeWidth = barWidth
}
pathPaint.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.WHITE
}
blueCircle.apply {
isAntiAlias = true
color = ContextCompat.getColor(context, R.color.purple_700)
}
primaryColorPaint.apply {
isAntiAlias = true
color = ContextCompat.getColor(context, R.color.blue_green)
}
}
//----------------------------------------------------------
private fun initItemsToDraw(){
circle1=Circle(200f,600f,400f,externalRadiusNormal,paintCircleNormal,0f)
circle2=Circle(400f,600f,500f,externalRadiusNormal,paintCircleNormal,0f)
circle3=Circle(600f,600f,300f,externalRadiusFinal,paintCircleFinal,0f)
//circle4=Circle(400f,1500f,600f,externalRadiusFinal,paintCircleFinal,0f)
internalCircle1=Circle(200f,600f,400f,internalRadiusNormal,paintInternal,0f)
internalCircle2=Circle(400f,600f,500f,internalRadiusNormal,paintInternal,0f)
internalCircle3=Circle(600f,600f,300f,internalRadiusFinal,paintInternal,0f)
secondInternalCircle = Circle(600f,600f,300f,secondInternalRadius,paintSecondInternalRadius, 0f)
verticalLine1 = VerticalLine(
x = 200f,
y = 600f,
yFinal = 400f+circle1.radius,
paint = paintVerticalLineGray,
alpha = 1f
)
verticalLine2 = VerticalLine(
x = 400f,
y = 600f,
yFinal = 500f+circle2.radius,
paint = paintVerticalLineGray,
alpha = 1f
)
verticalLine3 = VerticalLine(
x = 600f,
y = 600f,
yFinal = 300f+circle3.radius,
paint = paintVerticalLineGreen,
alpha = 1f
)
stringCircle1 = StringItem(
x = 200f,
y = 600f,
yFinal = 400f+circle1.radius,
paint = paintText,
string = "hola",
alpha = 1f)
bezierCurve = BezierCurve(200f,400f,600f,paintBezier,1f,400f,500f,600f,300f)
}
private fun initAnimators(){
animatorVerticalLine1 = ValueAnimator.ofFloat(verticalLine1.y, verticalLine1.yFinal)
animatorVerticalLine2 = ValueAnimator.ofFloat(verticalLine2.y, verticalLine2.yFinal)
animatorVerticalLine3 = ValueAnimator.ofFloat(verticalLine3.y, verticalLine3.yFinal)
setAnimator(animatorCircle1, circle1)
setAnimator(animatorCircle1, internalCircle1)
setAnimator(animatorCircle2, circle2)
setAnimator(animatorCircle2, internalCircle2)
setAnimator(animatorCircle3, circle3)
setAnimator(animatorCircle3, internalCircle3)
setAnimator(animatorCircle1, secondInternalCircle)
setAnimatorVerticalLine(animatorVerticalLine1, verticalLine1)
setAnimatorVerticalLine(animatorVerticalLine2, verticalLine2)
setAnimatorVerticalLine(animatorVerticalLine3, verticalLine3)
}
private fun initPrueba(){
//basicItem = Circle(600f,1500f,1100f,40f,paintCircleNormal,1f )
basicItem = Circle(points[1].x,height.toFloat() - 16f.toDP(),1100f,40f,paintCircleNormal,1f )
basicItem2 = Circle(points[2].x,height.toFloat() - 16f.toDP(),1100f,70f,paintCircleNormal,1f )
}
private fun initAnimators2(){
try {
//if (animatorVerticalLine4==null) animatorVerticalLine4 = ValueAnimator.ofFloat(verticalLine4.y, verticalLine4.yFinal)
if (animatorVerticalLine4==null) animatorVerticalLine4 = ValueAnimator.ofFloat(1500f, 1000f)
animatorVerticalLine5 = ValueAnimator.ofFloat(verticalLine5.y, verticalLine5.yFinal)
animatorVerticalLine6 = ValueAnimator.ofFloat(verticalLine6.y, verticalLine6.yFinal)
}catch (ex:Exception){}
}
//----------------------------------------------------------
private fun setElementsToAnimators(){
try {
setAnimatorVerticalLine(animatorVerticalLine4, verticalLine4)
setAnimatorVerticalLine(animatorVerticalLine5, verticalLine5)
setAnimatorVerticalLine(animatorVerticalLine6, verticalLine6)
}catch (ex:Exception){}
}
private fun drawHorizontalLine(canvas: Canvas?, paint: Paint) {
canvas?.drawLine(startX, height.toFloat() - 20f.toDP(), width.toFloat(), height.toFloat() - 20f.toDP(), paint)
}
private fun drawBezierCurve(canvas: Canvas?) {
try {
if (points.isEmpty() && conPoint1.isEmpty() && conPoint2.isEmpty()) return
val pointsToDraw = points.filterIndexed { index, _ -> index != 0 }
path.reset()
path.moveTo(pointsToDraw.first().x, pointsToDraw.first().y)
for (i in 1 until pointsToDraw.size) {
path.cubicTo(
conPoint1[i - 1].x, conPoint1[i - 1].y, conPoint2[i - 1].x, conPoint2[i - 1].y,
pointsToDraw[i].x, pointsToDraw[i].y
)
}
borderPath.set(path)
canvas?.drawPath(path, pathPaint.apply {
})
canvas?.drawPath(borderPath, borderPathPaint)
} catch (e: Exception) {
}
}
private fun setAnimator(animatorToSet: ValueAnimator, basicItem: BasicItem){
animatorToSet.apply {
duration = standardDuration
startDelay = standardDelay
addUpdateListener {
basicItem.updateAlpha(it.animatedValue as Float)
invalidate()
}
addListener(object:AnimatorListenerAdapter(){
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
ValueAnimator.ofFloat(basicItem.y,basicItem.yFinal).apply {
duration=secondDuration
addUpdateListener {
basicItem.updateY(it.animatedValue as Float)
invalidate()
}
start()
}
}
})
start()
}
}
private fun setAnimatorVerticalLine(animatorToSet: ValueAnimator?, verticalLine: VerticalLine){
animatorToSet?.apply {
duration = secondDuration
startDelay = standardDelay + standardDuration
repeatCount=0
addUpdateListener {
verticalLine.yFinal = it.animatedValue as Float
verticalLine.paint.alpha = 255
invalidate()
}
start()
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
try {
canvas?.drawCircle(basicItem.x,basicItem.y,40f,basicItem.paint)
canvas?.drawCircle(basicItem2.x,basicItem2.y,70f,basicItem2.paint)
}catch (ex:Exception){
println(ex)}
drawFirstDraw(canvas)
}
//----------------------------------------------------------
private fun drawFirstDraw(canvas: Canvas?){
canvas?.drawCircle(circle1.x,circle1.y, circle1.radius,paintCircleNormal)
canvas?.drawCircle(circle2.x,circle2.y, circle2.radius,paintCircleNormal)
canvas?.drawCircle(circle3.x,circle3.y, circle3.radius,paintCircleNormal)
canvas?.drawCircle(internalCircle1.x,internalCircle1.y, internalCircle1.radius,paintInternal)
canvas?.drawCircle(internalCircle2.x,internalCircle2.y, internalCircle2.radius,paintInternal)
canvas?.drawCircle(internalCircle3.x,internalCircle3.y, internalCircle3.radius,paintInternal)
canvas?.drawCircle(secondInternalCircle.x,secondInternalCircle.y, secondInternalCircle.radius,paintSecondInternalRadius)
canvas?.drawLine(100f,600f,700f,600f,paintLine)
drawHorizontalLine(canvas,paintLine)
canvas?.drawLine(verticalLine1.x,verticalLine1.y,verticalLine1.x,verticalLine1.yFinal,paintVerticalLineGray)
canvas?.drawLine(verticalLine2.x,verticalLine2.y,verticalLine2.x,verticalLine2.yFinal,paintVerticalLineGray)
canvas?.drawLine(verticalLine3.x,verticalLine3.y,verticalLine3.x,verticalLine3.yFinal,paintVerticalLineGreen)
}
private fun resetDataPoints() {
this.data.clear()
points.clear()
graphicPoints.clear()
conPoint1.clear()
conPoint2.clear()
}
private fun calculatePointsForData() {
if (data.isEmpty()) return
val bottomY = height - 20f.toDP()
val xDiff =
(width.toFloat() / (data.size - 1)) //subtract -1 because we want to include position at right side
val maxData = (data.maxByOrNull { it.amount }?.amount ?: 100f) + 10
for (i in 0 until data.size) {
val y = bottomY - (data[i].amount / maxData * (bottomY - curveTopMargin))
points.add(
PointF(
when (i) {
0 -> xDiff * i + 30
data.size - 1 -> xDiff * i - 30
else ->
xDiff * i
}, y
)
)
}
}
private fun calculateConnectionPointsForBezierCurve() {
try {
val pointsToConnectPoints = points.filterIndexed { index, _ -> index != 0 }
for (i in 1 until pointsToConnectPoints.size) {
val a = pointsToConnectPoints[i - 1]
val b = pointsToConnectPoints[i]
conPoint1.add(
PointF(
a.x + (0.3f * (b.x - a.x)),
a.y + 0.3f * (b.y - a.y) / 6
)
)
conPoint2.add(
PointF(
b.x - (0.3f * (b.x - a.x)),
b.y - 0.3f * (b.y - a.y) / 6
)
)
}
} catch (e: Exception) {
}
}
private fun Float.toDP(): Float =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, resources.displayMetrics)
fun addDataPoints(data: List<GraphicPoint>, graphicType: GraphicType = GraphicType.Default) {
post {
Thread(Runnable {
resetDataPoints()
primaryColorPaint.apply {
color = ContextCompat.getColor(context, graphicType.primaryColor)
}
borderPathPaint.apply {
color = ContextCompat.getColor(context, graphicType.borderColor)
}
graphicPoints.add(GraphicPoint("", 0f, ""))
graphicPoints.addAll(data)
val max = graphicPoints.maxByOrNull { it.amount }?.amount ?: 30f
val dataPoints = graphicPoints.map { graphicPoint ->
DataPoint(if(graphicPoint.amount<=0) 0f else (graphicPoint.amount * 30f) / max)
}.toList()
graphicPoints.maxByOrNull { it.amount }
val oldPoints = points.toList()
if (oldPoints.isEmpty()) {
this.data.addAll(dataPoints)
calculatePointsForData()
calculateConnectionPointsForBezierCurve()
initPrueba()
postInvalidate()
return#Runnable
}
resetDataPoints()
this.data.addAll(dataPoints)
calculatePointsForData()
calculateConnectionPointsForBezierCurve()
val newPoints = points.toList()
val size = oldPoints.size
var maxDiffY = 0f
for (i in 0 until size) {
val abs = abs(oldPoints[i].y - newPoints[i].y)
if (abs > maxDiffY) maxDiffY = abs
}
val loopCount = maxDiffY / 16
val tempPointsForAnimation = mutableListOf<MutableList<PointF>>()
for (i in 0 until size) {
val old = oldPoints[i]
val new = newPoints[i]
val plusOrMinusAmount = abs(new.y - old.y) / maxDiffY * 16
var tempY = old.y
val tempList = mutableListOf<PointF>()
for (j in 0..loopCount.toInt()) {
if (tempY == new.y) {
tempList.add(PointF(new.x, new.y))
} else {
if (new.y > old.y) {
tempY += plusOrMinusAmount
tempY = min(tempY, new.y)
tempList.add(PointF(new.x, tempY))
} else {
tempY -= plusOrMinusAmount
tempY = kotlin.math.max(tempY, new.y)
tempList.add(PointF(new.x, tempY))
}
}
}
tempPointsForAnimation.add(tempList)
}
if (tempPointsForAnimation.isEmpty()) return#Runnable
val first = tempPointsForAnimation[0]
val length = first.size
for (i in 0 until length) {
conPoint1.clear()
conPoint2.clear()
points.clear()
points.addAll(tempPointsForAnimation.map { it[i] })
calculateConnectionPointsForBezierCurve()
postInvalidate()
Thread.sleep(16)
}
}).start()
}
}
data class ImageCoordinates(
var x: Float,
var y: Float,
var alpha:Float,
var drawable:Int)
data class imgCoor(
var x: Float,
var y:Float,
var finalY:Float
)
data class GraphicPoint(
val month: String,
val amount: Float,
val amountText: String,
)
data class DataPoint(val amount: Float)
sealed class GraphicType(val primaryColor: Int, val borderColor: Int) {
object Credit : GraphicType(R.color.trans_color_4, R.color.trans_color_4_alpha_50)
object Default : GraphicType(R.color.blue_green, R.color.blue_green_alpha_50)
}
`
was wondering if anyone knows how to produce elliptical/arched list in Compose?
Something along these lines:
Not sure If I'm overlooking an 'easy' way of doing it in Compose. Cheers!
I have an article here that shows how to do this. It is not a LazyList in that it computes all the items (but only renders the visible ones); you can use this as a starting point to build upon.
The full code is below as well:
data class CircularListConfig(
val contentHeight: Float = 0f,
val numItems: Int = 0,
val visibleItems: Int = 0,
val circularFraction: Float = 1f,
val overshootItems: Int = 0,
)
#Stable
interface CircularListState {
val verticalOffset: Float
val firstVisibleItem: Int
val lastVisibleItem: Int
suspend fun snapTo(value: Float)
suspend fun decayTo(velocity: Float, value: Float)
suspend fun stop()
fun offsetFor(index: Int): IntOffset
fun setup(config: CircularListConfig)
}
class CircularListStateImpl(
currentOffset: Float = 0f,
) : CircularListState {
private val animatable = Animatable(currentOffset)
private var itemHeight = 0f
private var config = CircularListConfig()
private var initialOffset = 0f
private val decayAnimationSpec = FloatSpringSpec(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow,
)
private val minOffset: Float
get() = -(config.numItems - 1) * itemHeight
override val verticalOffset: Float
get() = animatable.value
override val firstVisibleItem: Int
get() = ((-verticalOffset - initialOffset) / itemHeight).toInt().coerceAtLeast(0)
override val lastVisibleItem: Int
get() = (((-verticalOffset - initialOffset) / itemHeight).toInt() + config.visibleItems)
.coerceAtMost(config.numItems - 1)
override suspend fun snapTo(value: Float) {
val minOvershoot = -(config.numItems - 1 + config.overshootItems) * itemHeight
val maxOvershoot = config.overshootItems * itemHeight
animatable.snapTo(value.coerceIn(minOvershoot, maxOvershoot))
}
override suspend fun decayTo(velocity: Float, value: Float) {
val constrainedValue = value.coerceIn(minOffset, 0f).absoluteValue
val remainder = (constrainedValue / itemHeight) - (constrainedValue / itemHeight).toInt()
val extra = if (remainder <= 0.5f) 0 else 1
val target =((constrainedValue / itemHeight).toInt() + extra) * itemHeight
animatable.animateTo(
targetValue = -target,
initialVelocity = velocity,
animationSpec = decayAnimationSpec,
)
}
override suspend fun stop() {
animatable.stop()
}
override fun setup(config: CircularListConfig) {
this.config = config
itemHeight = config.contentHeight / config.visibleItems
initialOffset = (config.contentHeight - itemHeight) / 2f
}
override fun offsetFor(index: Int): IntOffset {
val maxOffset = config.contentHeight / 2f + itemHeight / 2f
val y = (verticalOffset + initialOffset + index * itemHeight)
val deltaFromCenter = (y - initialOffset)
val radius = config.contentHeight / 2f
val scaledY = deltaFromCenter.absoluteValue * (config.contentHeight / 2f / maxOffset)
val x = if (scaledY < radius) {
sqrt((radius * radius - scaledY * scaledY))
} else {
0f
}
return IntOffset(
x = (x * config.circularFraction).roundToInt(),
y = y.roundToInt()
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CircularListStateImpl
if (animatable.value != other.animatable.value) return false
if (itemHeight != other.itemHeight) return false
if (config != other.config) return false
if (initialOffset != other.initialOffset) return false
if (decayAnimationSpec != other.decayAnimationSpec) return false
return true
}
override fun hashCode(): Int {
var result = animatable.value.hashCode()
result = 31 * result + itemHeight.hashCode()
result = 31 * result + config.hashCode()
result = 31 * result + initialOffset.hashCode()
result = 31 * result + decayAnimationSpec.hashCode()
return result
}
companion object {
val Saver = Saver<CircularListStateImpl, List<Any>>(
save = { listOf(it.verticalOffset) },
restore = {
CircularListStateImpl(it[0] as Float)
}
)
}
}
#Composable
fun rememberCircularListState(): CircularListState {
val state = rememberSaveable(saver = CircularListStateImpl.Saver) {
CircularListStateImpl()
}
return state
}
#Composable
fun CircularList(
visibleItems: Int,
modifier: Modifier = Modifier,
state: CircularListState = rememberCircularListState(),
circularFraction: Float = 1f,
overshootItems: Int = 3,
content: #Composable () -> Unit,
) {
check(visibleItems > 0) { "Visible items must be positive" }
check(circularFraction > 0f) { "Circular fraction must be positive" }
Layout(
modifier = modifier.clipToBounds().drag(state),
content = content,
) { measurables, constraints ->
val itemHeight = constraints.maxHeight / visibleItems
val itemConstraints = Constraints.fixed(width = constraints.maxWidth, height = itemHeight)
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
state.setup(
CircularListConfig(
contentHeight = constraints.maxHeight.toFloat(),
numItems = placeables.size,
visibleItems = visibleItems,
circularFraction = circularFraction,
overshootItems = overshootItems,
)
)
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
for (i in state.firstVisibleItem..state.lastVisibleItem) {
placeables[i].placeRelative(state.offsetFor(i))
}
}
}
}
private fun Modifier.drag(
state: CircularListState,
) = pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
coroutineScope {
while (true) {
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
state.stop()
val tracker = VelocityTracker()
awaitPointerEventScope {
verticalDrag(pointerId) { change ->
val verticalDragOffset = state.verticalOffset + change.positionChange().y
launch {
state.snapTo(verticalDragOffset)
}
tracker.addPosition(change.uptimeMillis, change.position)
change.consumePositionChange()
}
}
val velocity = tracker.calculateVelocity().y
val targetValue = decay.calculateTargetValue(state.verticalOffset, velocity)
launch {
state.decayTo(velocity, targetValue)
}
}
}
}
I have to make following Ui, In which when I play this small circle will move in an arc, when I stop it will stop. I can also set the timing of rotation of the small blue circle.
Till now I have implemented the following code: this gives me an arc of the circle, but I am not able to rotate the smaller circler over the bigger one.
public class CustoCustomProgressBar : View{
private var path : Path ? =null
private var mPrimaryPaint: Paint? = null
private var mSecondaryPaint: Paint? = null
private var mBackgroundPaint: Paint? = null
private var mTextPaint: TextPaint? = null
private var mProgressDrawable : Drawable ? = null
private var mRectF: RectF? = null
private var mDrawText = false
private var mTextColor = 0
private var mSecondaryProgressColor = 0
private var mPrimaryProgressColor = 0
private var mBackgroundColor = 0
private var mStrokeWidth = 0
private var mProgress = 0
var secodaryProgress = 0
private set
private var firstArcprogress = 0
private var secondArcProgress = 0
private var thirdArcProgress = 0
private var mFristArcCapSize = 0
private var mSecondArcCapSize = 0
private var mThirdArcCapSize = 0
private var isFristCapVisible = false
private var isSecondCapVisible = false
private var isThirdCapVisible = false
private var capColor = 0
private var mPrimaryCapSize = 0
private var mSecondaryCapSize = 0
var isPrimaryCapVisible = false
var isSecondaryCapVisible = false
private var x = 0
private var y = 0
private var mWidth = 0
private var mHeight = 0
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}
fun init(context: Context, attrs: AttributeSet?) {
val a: TypedArray
a = if (attrs != null) {
context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CustomProgressBar,
0, 0
)
} else {
throw IllegalArgumentException("Must have to pass the attributes")
}
try {
mProgressDrawable = resources.getDrawable(R.drawable.test)
mDrawText = a.getBoolean(R.styleable.CustomProgressBar_showProgressText, false)
mBackgroundColor =
a.getColor(
R.styleable.CustomProgressBar_backgroundColor,
resources.getColor(R.color.white)
)
mPrimaryProgressColor =
a.getColor(
R.styleable.CustomProgressBar_progressColor,
resources.getColor(R.color.white)
)
mSecondaryProgressColor =
a.getColor(
R.styleable.CustomProgressBar_secondaryProgressColor,
resources.getColor(R.color.black)
)
capColor =
a.getColor(
R.styleable.CustomProgressBar_capColor,
resources.getColor(R.color.color_9bc6e6_mind)
)
firstArcprogress =a.getInt(R.styleable.CustomProgressBar_firstArcProgress, 0)
mProgress = a.getInt(R.styleable.CustomProgressBar_progress, 0)
secodaryProgress = a.getInt(R.styleable.CustomProgressBar_secondaryProgress, 0)
mStrokeWidth = a.getDimensionPixelSize(R.styleable.CustomProgressBar_strokeWidth, 10)
mTextColor = a.getColor(
R.styleable.CustomProgressBar_textColor,
resources.getColor(R.color.black)
)
mPrimaryCapSize = a.getInt(R.styleable.CustomProgressBar_primaryCapSize, 20)
mSecondaryCapSize = a.getInt(R.styleable.CustomProgressBar_secodaryCapSize, 20)
isPrimaryCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_primaryCapVisibility, true)
isSecondaryCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_secodaryCapVisibility, true)
isFristCapVisible = a.getBoolean(R.styleable.CustomProgressBar_firstCapVisibility, true)
isSecondCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_secodaryCapVisibility, false)
isThirdCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_thirdCapVisibility, false)
} finally {
a.recycle()
}
mBackgroundPaint = Paint()
mBackgroundPaint!!.setAntiAlias(true)
mBackgroundPaint!!.setStyle(Paint.Style.STROKE)
mBackgroundPaint!!.setStrokeWidth(mStrokeWidth.toFloat())
mBackgroundPaint!!.setColor(mBackgroundColor)
mPrimaryPaint = Paint()
mPrimaryPaint!!.setAntiAlias(true)
mPrimaryPaint!!.setStyle(Paint.Style.STROKE)
mPrimaryPaint!!.setStrokeWidth(mStrokeWidth.toFloat())
mPrimaryPaint!!.setColor(capColor)
mSecondaryPaint = Paint()
mSecondaryPaint!!.setAntiAlias(true)
mSecondaryPaint!!.setStyle(Paint.Style.STROKE)
mSecondaryPaint!!.setStrokeWidth((mStrokeWidth - 2).toFloat())
mSecondaryPaint!!.setColor(mSecondaryProgressColor)
mTextPaint = TextPaint()
mTextPaint!!.color = mTextColor
mRectF = RectF()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mRectF!![paddingLeft.toFloat(), paddingTop.toFloat(), (w - paddingRight).toFloat()] =
(h - paddingBottom).toFloat()
mTextPaint!!.textSize = (w / 5).toFloat()
x = w / 2 - (mTextPaint!!.measureText("$mProgress%") / 2).toInt()
y = (h / 2 - (mTextPaint!!.descent() + mTextPaint!!.ascent()) / 2).toInt()
mWidth = w
mHeight = h
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
mPrimaryPaint?.setStyle(Paint.Style.STROKE)
mSecondaryPaint?.setStyle(Paint.Style.STROKE)
// for drawing a full progress .. The background circle
mRectF?.let {
mBackgroundPaint?.let { it1 -> canvas.drawArc(it, 270F, 180F, false, it1) } }
path = Path()
path?.arcTo(mRectF,270F,180F,true)
mRectF?.let {
mBackgroundPaint?.let { it1 ->
canvas.drawArc(it, 95F, 80F, false, it1)
}
}
mRectF?.let {
mBackgroundPaint?.let { it1 ->
canvas.drawArc(it, 180F, 80F, false, it1)
}
}
// // for drawing a secondary progress circle
// val secondarySwipeangle = secodaryProgress * 360 / 100
// mRectF?.let { mSecondaryPaint?.let { it1 ->
// canvas.drawArc(it, 270F, secondarySwipeangle.toFloat(), false,
// it1
// )
// } }
//
val firstArcProgresSwipeAngle = firstArcprogress * 180 / 100
// mRectF?.let {
// mPrimaryPaint?.let { it1 ->
// canvas.drawArc(
// it, 270F, firstArcProgresSwipeAngle.toFloat(), false,
// it1
// )
// }
// }
// // for drawing a main progress circle
// val primarySwipeangle = mProgress * 360 / 100
// mRectF?.let { mPrimaryPaint?.let { it1 ->
// canvas.drawArc(it, 270F, primarySwipeangle.toFloat(), false,
// it1
// )
// } }
// // for cap of secondary progress
// val r = (height - paddingLeft * 2) / 2 // Calculated from canvas width
// var trad = (secondarySwipeangle - 90) * (Math.PI / 180.0) // = 5.1051
// var x = (r * Math.cos(trad)).toInt()
// var y = (r * Math.sin(trad)).toInt()
// mSecondaryPaint?.setStyle(Paint.Style.FILL)
// if (isSecondaryCapVisible) mSecondaryPaint?.let {
// canvas.drawCircle(
// (x + mWidth / 2).toFloat(),
// (y + mHeight / 2).toFloat(),
// mSecondaryCapSize.toFloat(),
// it
// )
// }
val r = (height - paddingLeft * 2) / 2
// for cap of primary progress
var trad = (firstArcProgresSwipeAngle - 90) * (Math.PI / 180.0) // = 5.1051
x = (r * Math.cos(trad)).toInt()
y = (r * Math.sin(trad)).toInt()
mPrimaryPaint?.setStyle(Paint.Style.FILL)
if (isPrimaryCapVisible) mPrimaryPaint?.let {
canvas.drawCircle(
(x + mWidth / 2).toFloat(),
(y + mHeight / 2).toFloat(),
mPrimaryCapSize.toFloat(),
it
)
}
if (mDrawText) mTextPaint?.let {
canvas.drawText(
"$mProgress%", x.toFloat(), y.toFloat(),
it
)
}
}
fun setDrawText(mDrawText: Boolean) {
this.mDrawText = mDrawText
invalidate()
}
override fun setBackgroundColor(mBackgroundColor: Int) {
this.mBackgroundColor = mBackgroundColor
mBackgroundPaint?.setColor(mBackgroundColor)
invalidate()
}
fun setStrokeWidth(mStrokeWidth: Int) {
this.mStrokeWidth = mStrokeWidth
invalidate()
}
fun setSecondaryProgress(mSecondaryProgress: Int) {
secodaryProgress = mSecondaryProgress
invalidate()
}
fun setTextColor(mTextColor: Int) {
this.mTextColor = mTextColor
mTextPaint!!.color = mTextColor
invalidate()
}
var secondaryProgressColor: Int
get() = mSecondaryProgressColor
set(mSecondaryProgressColor) {
this.mSecondaryProgressColor = mSecondaryProgressColor
mSecondaryPaint?.setColor(mSecondaryProgressColor)
invalidate()
}
var primaryProgressColor: Int
get() = mPrimaryProgressColor
set(mPrimaryProgressColor) {
this.mPrimaryProgressColor = mPrimaryProgressColor
mPrimaryPaint?.setColor(mPrimaryProgressColor)
invalidate()
}
var progress: Int
get() = mProgress
set(mProgress) {
while (this.mProgress <= mProgress) {
postInvalidateDelayed(150)
this.mProgress++
}
}
fun getBackgroundColor(): Int {
return mBackgroundColor
}
var primaryCapSize: Int
get() = mPrimaryCapSize
set(mPrimaryCapSize) {
this.mPrimaryCapSize = mPrimaryCapSize
invalidate()
}
var secondaryCapSize: Int
get() = mSecondaryCapSize
set(mSecondaryCapSize) {
this.mSecondaryCapSize = mSecondaryCapSize
invalidate()
}
var arcprogress: Int
get() = firstArcprogress
set(firstArcprogress) {
while (this.firstArcprogress <= firstArcprogress) {
postInvalidateDelayed(150)
this.firstArcprogress = firstArcprogress
}
}
fun getPath(): Path? {
return path
}
fun getXCOORDINTE(): Float{
return x.toFloat()
}
fun getYCoordinate(): Float{
return y.toFloat()
}
}
I create an override method called draw in SurfaceView. I want to see the paint I set in my SurfaceView but nothing shows up when I touched the screen and trying to draw a line. What should I do to make this work?
private var mPaint: Paint
private val mPaths: ArrayList<Path> = ArrayList<Path>()
private val mEraserPath: Path = Path()
init {
mPaint = Paint()
mPaint.isAntiAlias = true
mPaint.isDither = true
mPaint.style = Paint.Style.STROKE
mPaint.strokeJoin = Paint.Join.ROUND
mPaint.strokeCap = Paint.Cap.ROUND
mPaint.strokeWidth = 3f
mPaint.alpha = 255
mPaint.color = android.graphics.Color.BLACK
mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
override fun draw(canvas: Canvas) {
canvas.drawPaint(mPaint)
val action: EditAction? = this.getEditAction()
for (path: Path in mPaths) {
when (action) {
EditAction.COLOR -> {
setPaintColor(this.getStrokeColor()) // android.graphics.Color.BLACK
setPaintSize(this.getStrokeSize()) // 5f
canvas.drawPath(path, mPaint)
}
EditAction.SIZE -> {
setPaintColor(this.getStrokeColor()) // android.graphics.Color.BLACK
setPaintSize(this.getStrokeSize()) // 5f
canvas.drawPath(path, mPaint)
}
EditAction.ERASER -> {
}
}
}
canvas.drawPath(mEraserPath, mPaint)
super.draw(canvas)
}
Instead of using draw, use the SurfaceHolder.Callback functions instead, as shown below. I have mof
class SlowSurfaceView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: SurfaceView(context, attrs, defStyleAttr), SurfaceHolder.Callback {
private var mPaint: Paint = Paint()
init {
holder.addCallback(this)
mPaint.isAntiAlias = true
mPaint.isDither = true
mPaint.style = Paint.Style.STROKE
mPaint.strokeJoin = Paint.Join.ROUND
mPaint.strokeCap = Paint.Cap.ROUND
mPaint.strokeWidth = 3f
mPaint.alpha = 255
mPaint.color = android.graphics.Color.RED
mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
// Do nothing for now
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
override fun surfaceCreated(holder: SurfaceHolder) {
if (isAttachedToWindow) {
val canvas = holder.lockCanvas()
canvas?.let {
it.drawRect(Rect(100, 100, 200, 200), mPaint)
holder.unlockCanvasAndPost(it)
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom
setMeasuredDimension(View.resolveSize(desiredWidth, widthMeasureSpec),
View.resolveSize(desiredHeight, heightMeasureSpec))
}
}
Refer to the above modify the code, and hopefully you should get what you want.