Skip to content

Instantly share code, notes, and snippets.

@ZieIony
Created March 13, 2021 14:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ZieIony/bc8fc618de7c6754cc9f67d72c8dc454 to your computer and use it in GitHub Desktop.
Save ZieIony/bc8fc618de7c6754cc9f67d72c8dc454 to your computer and use it in GitHub Desktop.
package com.github.zieIony.dots
import android.animation.ArgbEvaluator
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.os.Bundle
import android.view.View
import carbon.internal.MathUtils.abs
import carbon.internal.MathUtils.min
import tk.zielony.carbonsamples.R
import tk.zielony.carbonsamples.SampleAnnotation
import tk.zielony.carbonsamples.ThemedActivity
class AnimatedDotsDemo(context: Context) : View(context) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val Float.dp: Float
get() {
return this * context.resources.displayMetrics.density
}
private fun Float.toPx(): Float{
return this
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(400.0f.dp.toInt(), dotComposableHeight.dp.toInt())
}
override fun onDraw(canvas: Canvas) {
drawDots(canvas, animator.animatedValue as Float, paint)
invalidate()
}
private val totalDotCount = 4
private val dotSpacing = 60f
private val dotComposableHeight = 200f
private val animator = ValueAnimator.ofFloat(1.0f, totalDotCount.toFloat()).apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.REVERSE
duration = 2000
start()
}
private fun drawDots(canvas: Canvas, position: Float, paint: Paint) {
val centerY = dotComposableHeight / 2
for (currentDotPosition in 1..totalDotCount) {
val dotSize = getDotSizeForPosition(position, currentDotPosition)
if (currentDotPosition < totalDotCount) {
// Draw a bridge between the current dot and the next dot
val nextDotPosition = currentDotPosition + 1
val nextDotSize = getDotSizeForPosition(position, nextDotPosition)
// Pick a direction to draw bridge from the smaller dot to the larger dot
val shouldFlip = nextDotSize > dotSize
val nextPositionDelta = -min(
1f,
abs(position - if (shouldFlip) nextDotPosition else currentDotPosition)
)
// Calculate the top-most and the bottom-most coordinates of current dot
val leftX = (currentDotPosition * dotSpacing).dp.toPx()
val leftYTop = (centerY - dotSize).dp.toPx()
val leftYBottom = (centerY + dotSize).dp.toPx()
// Calculate the top-most and the bottom-most coordinates of next dot
val rightX = (nextDotPosition * dotSpacing).dp.toPx()
val rightYTop = (centerY - nextDotSize).dp.toPx()
val rightYBottom = (centerY + nextDotSize).dp.toPx()
// Calculate the middle Y coordinate between two dots
val midX = ((currentDotPosition + 0.5f) * dotSpacing).dp.toPx()
val path = if (shouldFlip) {
// Calculate control point Y coordinates a bit inside the current dot
val bezierYTop = (centerY - dotSize - 5f * nextPositionDelta).dp.toPx()
val bezierYBottom = (centerY + dotSize + 5f * nextPositionDelta).dp.toPx()
getBridgePath(
rightX, rightYTop, rightYBottom, leftX, leftYTop, leftYBottom,
midX, bezierYTop, bezierYBottom, centerY.dp.toPx()
)
} else {
// Calculate control point Y coordinates a bit inside the next dot
val bezierYTop = (centerY - nextDotSize - 5f * nextPositionDelta).dp.toPx()
val bezierYBottom = (centerY + nextDotSize + 5f * nextPositionDelta).dp.toPx()
getBridgePath(
leftX, leftYTop, leftYBottom, rightX, rightYTop, rightYBottom,
midX, bezierYTop, bezierYBottom, centerY.dp.toPx()
)
}
paint.color = 0xff8eb4e6.toInt()
canvas.drawPath(path, paint)
}
// Draw the current dot
canvas.save()
canvas.translate((currentDotPosition * dotSpacing).dp.toPx(), 100f.dp.toPx())
paint.color = getDotColor(position, currentDotPosition)
canvas.drawCircle(
0.0f,
0.0f,
dotSize.dp.toPx(),
paint
)
canvas.restore()
}
}
/**
* Returns a path for a bridge between two dots drawn using two quadratic beziers.
*
* First bezier is drawn between (startX, startYTop) and (endX, endYTop) coordinates using
* (bezierX, bezierYTop) as control point.
* Second bezier is drawn between (startX, startYBottom) and (endX, endYBottom) coordinates using
* (bezierX, bezierYBottom) as control point.
*
* Then additional lines are drawn to make this a filled path.
*/
private fun getBridgePath(
startX: Float,
startYTop: Float,
startYBottom: Float,
endX: Float,
endYTop: Float,
endYBottom: Float,
bezierX: Float,
bezierYTop: Float,
bezierYBottom: Float,
midY: Float
): Path {
return Path().apply {
moveTo(startX, startYTop)
quadTo(bezierX, bezierYTop, endX, endYTop)
lineTo(endX, midY)
lineTo(startX, midY)
moveTo(startX, startYTop)
lineTo(startX, startYBottom)
quadTo(bezierX, bezierYBottom, endX, endYBottom)
lineTo(endX, midY)
lineTo(startX, midY)
}
}
private fun getDotColor(position: Float, dotIndex: Int): Int {
val fraction = min(abs(position - dotIndex), 1f)
return ArgbEvaluator().evaluate(fraction, 0xff1a73e8.toInt(), 0xff468ce8.toInt()) as Int
}
private fun getDotSizeForPosition(position: Float, dotIndex: Int): Float {
val positionDelta = abs(position - dotIndex)
return if (positionDelta < 1f) {
(10f + 20 * (1 - positionDelta))
} else {
10f
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment