-
-
Save daividssilverio/68e7293d38c3c170cb60855f7ea714ed to your computer and use it in GitHub Desktop.
This file contains 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
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