Skip to content

Commit

Permalink
Add Android implementation code and add links to its source code
Browse files Browse the repository at this point in the history
  • Loading branch information
mahozad committed Feb 2, 2024
1 parent f10e4bb commit e616c83
Show file tree
Hide file tree
Showing 2 changed files with 268 additions and 1 deletion.
267 changes: 266 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,276 @@ fun MyComposable() {
```

## Related projects
- Android squiggly progress:
+ <details>
<summary>Current implementation (as of 2024-02-02)</summary>

```kotlin
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.systemui.media.controls.ui

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.os.SystemClock
import android.util.MathUtils.lerp
import android.util.MathUtils.lerpInv
import android.util.MathUtils.lerpInvSat
import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators
import com.android.internal.graphics.ColorUtils
import kotlin.math.abs
import kotlin.math.cos

private const val TAG = "Squiggly"

private const val TWO_PI = (Math.PI * 2f).toFloat()
@VisibleForTesting internal const val DISABLED_ALPHA = 77

class SquigglyProgress : Drawable() {

private val wavePaint = Paint()
private val linePaint = Paint()
private val path = Path()
private var heightFraction = 0f
private var heightAnimator: ValueAnimator? = null
private var phaseOffset = 0f
private var lastFrameTime = -1L

/* distance over which amplitude drops to zero, measured in wavelengths */
private val transitionPeriods = 1.5f
/* wave endpoint as percentage of bar when play position is zero */
private val minWaveEndpoint = 0.2f
/* wave endpoint as percentage of bar when play position matches wave endpoint */
private val matchedWaveEndpoint = 0.6f

// Horizontal length of the sine wave
var waveLength = 0f
// Height of each peak of the sine wave
var lineAmplitude = 0f
// Line speed in px per second
var phaseSpeed = 0f
// Progress stroke width, both for wave and solid line
var strokeWidth = 0f
set(value) {
if (field == value) {
return
}
field = value
wavePaint.strokeWidth = value
linePaint.strokeWidth = value
}

// Enables a transition region where the amplitude
// of the wave is reduced linearly across it.
var transitionEnabled = true
set(value) {
field = value
invalidateSelf()
}

init {
wavePaint.strokeCap = Paint.Cap.ROUND
linePaint.strokeCap = Paint.Cap.ROUND
linePaint.style = Paint.Style.STROKE
wavePaint.style = Paint.Style.STROKE
linePaint.alpha = DISABLED_ALPHA
}

var animate: Boolean = false
set(value) {
if (field == value) {
return
}
field = value
if (field) {
lastFrameTime = SystemClock.uptimeMillis()
}
heightAnimator?.cancel()
heightAnimator =
ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply {
if (animate) {
startDelay = 60
duration = 800
interpolator = Interpolators.EMPHASIZED_DECELERATE
} else {
duration = 550
interpolator = Interpolators.STANDARD_DECELERATE
}
addUpdateListener {
heightFraction = it.animatedValue as Float
invalidateSelf()
}
addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
heightAnimator = null
}
}
)
start()
}
}

override fun draw(canvas: Canvas) {
if (animate) {
invalidateSelf()
val now = SystemClock.uptimeMillis()
phaseOffset += (now - lastFrameTime) / 1000f * phaseSpeed
phaseOffset %= waveLength
lastFrameTime = now
}

val progress = level / 10_000f
val totalWidth = bounds.width().toFloat()
val totalProgressPx = totalWidth * progress
val waveProgressPx =
totalWidth *
(if (!transitionEnabled || progress > matchedWaveEndpoint) progress
else
lerp(
minWaveEndpoint,
matchedWaveEndpoint,
lerpInv(0f, matchedWaveEndpoint, progress)
))

// Build Wiggly Path
val waveStart = -phaseOffset - waveLength / 2f
val waveEnd = if (transitionEnabled) totalWidth else waveProgressPx

// helper function, computes amplitude for wave segment
val computeAmplitude: (Float, Float) -> Float = { x, sign ->
if (transitionEnabled) {
val length = transitionPeriods * waveLength
val coeff =
lerpInvSat(waveProgressPx + length / 2f, waveProgressPx - length / 2f, x)
sign * heightFraction * lineAmplitude * coeff
} else {
sign * heightFraction * lineAmplitude
}
}

// Reset path object to the start
path.rewind()
path.moveTo(waveStart, 0f)

// Build the wave, incrementing by half the wavelength each time
var currentX = waveStart
var waveSign = 1f
var currentAmp = computeAmplitude(currentX, waveSign)
val dist = waveLength / 2f
while (currentX < waveEnd) {
waveSign = -waveSign
val nextX = currentX + dist
val midX = currentX + dist / 2
val nextAmp = computeAmplitude(nextX, waveSign)
path.cubicTo(midX, currentAmp, midX, nextAmp, nextX, nextAmp)
currentAmp = nextAmp
currentX = nextX
}

// translate to the start position of the progress bar for all draw commands
val clipTop = lineAmplitude + strokeWidth
canvas.save()
canvas.translate(bounds.left.toFloat(), bounds.centerY().toFloat())

// Draw path up to progress position
canvas.save()
canvas.clipRect(0f, -1f * clipTop, totalProgressPx, clipTop)
canvas.drawPath(path, wavePaint)
canvas.restore()

if (transitionEnabled) {
// If there's a smooth transition, we draw the rest of the
// path in a different color (using different clip params)
canvas.save()
canvas.clipRect(totalProgressPx, -1f * clipTop, totalWidth, clipTop)
canvas.drawPath(path, linePaint)
canvas.restore()
} else {
// No transition, just draw a flat line to the end of the region.
// The discontinuity is hidden by the progress bar thumb shape.
canvas.drawLine(totalProgressPx, 0f, totalWidth, 0f, linePaint)
}

// Draw round line cap at the beginning of the wave
val startAmp = cos(abs(waveStart) / waveLength * TWO_PI)
canvas.drawPoint(0f, startAmp * lineAmplitude * heightFraction, wavePaint)

canvas.restore()
}

override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}

override fun setColorFilter(colorFilter: ColorFilter?) {
wavePaint.colorFilter = colorFilter
linePaint.colorFilter = colorFilter
}

override fun setAlpha(alpha: Int) {
updateColors(wavePaint.color, alpha)
}

override fun getAlpha(): Int {
return wavePaint.alpha
}

override fun setTint(tintColor: Int) {
updateColors(tintColor, alpha)
}

override fun onLevelChange(level: Int): Boolean {
return animate
}

override fun setTintList(tint: ColorStateList?) {
if (tint == null) {
return
}
updateColors(tint.defaultColor, alpha)
}

private fun updateColors(tintColor: Int, alpha: Int) {
wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha)
linePaint.color =
ColorUtils.setAlphaComponent(tintColor, (DISABLED_ALPHA * (alpha / 255f)).toInt())
}
}
```
</details>
+ [Main branch](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt)
+ [Android 14](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android14-release/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt)
+ [Android 13](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android13-release/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt)
+ [Android Music app](https://android.googlesource.com/platform/packages/apps/Music/)
+ [Everything you see in Android that's not an app](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/packages/SystemUI/)
- Wave slider (similar to ours): https://github.com/galaxygoldfish/waveslider
- Squiggly seekbar (Flutter): https://github.com/hannesgith/squiggly_slider
- Sliders with custom styles: https://github.com/krottv/compose-sliders
- Squiggly text underlines: https://github.com/saket/ExtendedSpans
- Android 13 music app: https://android.googlesource.com/platform/packages/apps/Music/
[Kotlin version]: https://img.shields.io/badge/Kotlin-1.9.22-303030.svg?labelColor=303030&logo=data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxOC45MyAxOC45MiIgd2lkdGg9IjE4IiBoZWlnaHQ9IjE4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyYWRpYWxHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHI9IjIxLjY3OSIgY3g9IjIyLjQzMiIgY3k9IjMuNDkzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIDEgLTQuMTMgLTIuNzE4KSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICAgPHN0b3Agc3RvcC1jb2xvcj0iI2U0NDg1NyIgb2Zmc2V0PSIuMDAzIi8+CiAgICA8c3RvcCBzdG9wLWNvbG9yPSIjYzcxMWUxIiBvZmZzZXQ9Ii40NjkiLz4KICAgIDxzdG9wIHN0b3AtY29sb3I9IiM3ZjUyZmYiIG9mZnNldD0iMSIvPgogIDwvcmFkaWFsR3JhZGllbnQ+CiAgPHBhdGggZmlsbD0idXJsKCNncmFkaWVudCkiIGQ9Ik0gMTguOTMsMTguOTIgSCAwIFYgMCBIIDE4LjkzIEwgOS4yNyw5LjMyIFoiLz4KPC9zdmc+Cg==
[Compose Multiplatform version]: https://img.shields.io/badge/Compose_Multiplatform-1.5.12-303030.svg?labelColor=303030&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAj5JREFUOI2Vk0FIVFEUhv9znBllplBIF7loK1jtJKhFNG/EVtYicNkmKghCMpJGq0HoPcWQVi2KUMqdixaJi0KdXVBILQojs4wCaTGC4LyX+N47fwtFpnEKOnDh3p//fudeDr+QRK3KukGHCscAwCjXi4PphVo+qQZkhzaa61J6m8RhAfpisS01HQOwZin0F29kftYEdDxCsqnkX6HgIonR+YHM00pjzg26oXRBPrNw30ixgM1dgDMcnFFyyIAphpn7xQI2Tw6XW5LQO0L+isPQKxaa1rNDaJCkf02BHhMpzOfTzxUA1GyCxEcFxjcOIu50/b4kZQnkZQJ9mkwuOV5wqaUdYSIhTwBZFto4AOj2R+S7qEwZMNtU8lcoGAPximZHDegAsCjgw7XP/rJFnDHBhEB+AABIIueW35FEdsQ/67hl5jz/AklUrpxX7nfcMp27wYnKO/rHCAwhANDkffW4DPJhZxtV6lpt/N+qCRCND+3RDHs0AEhUHii6KIxXSZnq9PxJTUhetrQ+VrsH4TlAvlgUfd3zAgMau0aD1uLNhm8WBm0CjBDoiSN8ijReJHBaRAYtTB8pFvaXukaDVgMadwFC6bWIM47n54GWaHYgM5CwunaASwBe1yXQNptPewDgeH7eIs4IpXcXMDeYnl5vzhxTINCUv+B4/vkXtxpWQEwK8Phlf3o15wbdmvLfCFgfh5njc4Pp6e3mVWHqHN44AOidnTC9NVpJRE+BKP0zTNW1HWc8IMxIvfq3OP8GvjkzgYHHZZMAAAAASUVORK5CYII=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import androidx.compose.ui.unit.dp
import ir.mahozad.multiplatform.wavyslider.WaveDirection.TAIL
import kotlin.math.*

// For the Android 13/14 implementation, see the main README file.

/**
* The horizontal movement (shift) of the whole wave.
*/
Expand Down

0 comments on commit e616c83

Please sign in to comment.