Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import com.example.gists.ui.theme.GistsTheme
import kotlin.math.*
import kotlin.random.Random
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
GistsTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
WavyBackground()
}
}
}
}
}
// these waves coordinates were actually created manually based on the %s of the screen
private val wave1 = listOf(
Offset(0.25f, -0.14f),
Offset(0.33f, 0f),
Offset(0.52f, 0.08f),
Offset(0.7f, 0.2f),
Offset(0.9f, 0.3f),
Offset(0.94f, 0.50f),
Offset(1.15f, 0.62f),
)
private val wave2 = listOf(
Offset(-0.1f, 0f),
Offset(0f, 0.1f),
Offset(0.2f, 0.085f),
Offset(0.3f, 0.2f),
Offset(0.6f, 0.3f),
Offset(0.9f, 0.57f),
Offset(1.1f, 0.65f),
)
private val wave3 = listOf(
Offset(0.25f, 1.1f),
Offset(0.30f, 1f),
Offset(0.38f, 0.95f),
Offset(0.60f, 0.9f),
Offset(0.80f, 0.8f),
Offset(1.2f, 0.82f),
)
private fun xChange() = Random.nextDouble(0.1, 0.15).toFloat()
private fun yChange() = Random.nextDouble(0.01, 0.07).toFloat()
private fun List<Offset>.buildFactors() = mapIndexed { i, _ ->
if (i == 0 || i == size - 1) Offset(0f, 0f)
else Offset(xChange(), yChange())
}
// we add some random changes to the waves
private val wave1changeFactors = wave1.buildFactors()
private val wave2changeFactors = wave2.buildFactors()
private val wave3changeFactors = wave3.buildFactors()
private val times = listOf(
1250 to 3000,
2000 to 4000,
2500 to 4250
)
@Composable
fun WavyBackground() {
val infiniteTransition = rememberInfiniteTransition()
val x1 by infiniteTransition.animateFloat(
initialValue = -0.3f,
targetValue = 0.3f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = times[0].first, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val y1 by infiniteTransition.animateFloat(
initialValue = -0.5f,
targetValue = 0.5f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = times[0].second, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val x2 by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = -0.3f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = times[1].first, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val y2 by infiniteTransition.animateFloat(
initialValue = 0.5f,
targetValue = -0.5f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = times[1].second, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val x3 by infiniteTransition.animateFloat(
initialValue = -0.3f,
targetValue = 0.3f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = times[2].first, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val y3 by infiniteTransition.animateFloat(
initialValue = -0.5f,
targetValue = 0.5f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = times[2].second, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Canvas(modifier = Modifier.fillMaxSize()) {
drawTopWaves(wave2, wave2changeFactors, x2, y2, Color(0xE9, 0xA0, 0xE0, 0xFF))
drawTopWaves(wave1, wave1changeFactors, x1, y1, Color(0x41, 0x2A, 0x4C, 0xFF))
drawBottomWaves(wave3, wave3changeFactors, x3, y3, Color(0xFD, 0xE5, 0xDC, 0xB2))
}
}
private fun DrawScope.drawTopWaves(wave: List<Offset>, factors: List<Offset>, xAnim: Float, yAnim: Float, color: Color) {
Path().apply {
val first = wave.first()
moveTo(first.x * size.width, first.y * size.height)
for (i in 1 until wave.size) {
smoothBezierTo(xAnim, yAnim, size, wave[i], i, wave, factors)
}
lineTo(size.width, -size.height)
close()
drawPath(
path = this,
color = color,
style = Fill
)
}
}
private fun DrawScope.drawBottomWaves(wave: List<Offset>, factors: List<Offset>, xAnim: Float, yAnim: Float, color: Color) {
Path().apply {
val first = wave.first()
moveTo(first.x * size.width, first.y * size.height)
for (i in 1 until wave.size) {
smoothBezierTo(xAnim, yAnim, size, wave[i], i, wave, factors)
}
lineTo(size.width, size.height)
close()
drawPath(
path = this,
color = color,
style = Fill
)
}
}
private fun Offset.multiplyBy(x: Float, y: Float) = copy(this.x * x, this.y * y)
// this method tries to select a control point for the bezier curve that would keep the overall shape of the wave smooth
private fun Path.smoothBezierTo(xAnim: Float, yAnim: Float, size: Size, point: Offset, index: Int, points: List<Offset>, factors: List<Offset>) {
val adjustedPoint = point + factors[index].multiplyBy(xAnim, yAnim)
val prevAdjustedPoint = points[index - 1] + factors[index - 1].multiplyBy(xAnim, yAnim)
val nextAdjustedPoint = points.getOrNull(index + 1)?.plus(factors.getOrElse(index + 1) { Offset.Zero }.multiplyBy(xAnim, yAnim))
val prevPrevAdjustedPoint = points.getOrNull(index - 2)?.plus(factors.getOrElse(index - 2) { Offset.Zero }.multiplyBy(xAnim, yAnim))
val startControlPoint = controlPoint(prevAdjustedPoint, prevPrevAdjustedPoint, adjustedPoint)
val endControlPoint = controlPoint(adjustedPoint, prevAdjustedPoint, nextAdjustedPoint, true)
cubicTo(
startControlPoint.x * size.width,
startControlPoint.y * size.height,
endControlPoint.x * size.width,
endControlPoint.y * size.height,
adjustedPoint.x * size.width,
adjustedPoint.y * size.height
)
}
private fun Offset.lengthTo(other: Offset) =
sqrt((other.x - this.x).pow(2f) + (other.y - this.y).pow(2f))
private fun Offset.angleTo(other: Offset) =
atan2((other.y - this.y), (other.x - this.x))
private const val smoothing = 0.2f
private fun controlPoint(current: Offset, previous: Offset?, next: Offset?, reverse: Boolean = false): Offset {
val prev = previous ?: current
val nxt = next ?: current
val angle = prev.angleTo(nxt) + if (reverse) PI.toFloat() else 0f
val length = prev.lengthTo(nxt) * smoothing
return Offset(
x = current.x + cos(angle) * length,
y = current.y + sin(angle) * length
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment