Created
March 13, 2021 14:16
-
-
Save ZieIony/bc8fc618de7c6754cc9f67d72c8dc454 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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