session-android/app/src/main/java/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt

315 lines
10 KiB
Kotlin
Raw Normal View History

package org.thoughtcrime.securesms.loki.views
2020-10-08 10:31:20 +02:00
import android.animation.ValueAnimator
import android.content.Context
2020-10-08 10:31:20 +02:00
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
2020-10-08 10:31:20 +02:00
import android.view.animation.DecelerateInterpolator
import androidx.core.math.MathUtils
import network.loki.messenger.R
import org.session.libsession.utilities.byteToNormalizedFloat
import kotlin.math.abs
2020-10-08 10:31:20 +02:00
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class WaveformSeekBar : View {
companion object {
@JvmStatic
2020-10-12 07:10:45 +02:00
fun dp(context: Context, dp: Float): Float {
return TypedValue.applyDimension(
2020-10-08 10:31:20 +02:00
TypedValue.COMPLEX_UNIT_DIP,
dp,
context.resources.displayMetrics
)
}
}
2020-10-08 10:31:20 +02:00
private val sampleDataHolder = SampleDataHolder(::invalidate)
/** An array of signed byte values representing the audio signal. */
var sampleData: ByteArray?
2020-10-08 10:31:20 +02:00
get() {
return sampleDataHolder.getSamples()
}
set(value) {
2020-10-08 10:31:20 +02:00
sampleDataHolder.setSamples(value)
invalidate()
}
/** Indicates whether the user is currently interacting with the view and performing a seeking gesture. */
private var userSeeking = false
private var _progress: Float = 0f
/** In [0..1] range. */
var progress: Float
set(value) {
// Do not let to modify the progress value from the outside
// when the user is currently interacting with the view.
if (userSeeking) return
_progress = value
invalidate()
progressChangeListener?.onProgressChanged(this, _progress, false)
}
get() {
return _progress
}
2020-10-08 10:31:20 +02:00
var barBackgroundColor: Int = Color.LTGRAY
set(value) {
field = value
invalidate()
}
2020-10-08 10:31:20 +02:00
var barProgressColor: Int = Color.WHITE
set(value) {
field = value
invalidate()
}
2020-10-08 10:31:20 +02:00
var barGap: Float = dp(context, 2f)
set(value) {
field = value
invalidate()
}
2020-10-08 10:31:20 +02:00
var barWidth: Float = dp(context, 5f)
set(value) {
field = value
invalidate()
}
2020-10-08 10:31:20 +02:00
var barMinHeight: Float = barWidth
set(value) {
field = value
invalidate()
}
2020-10-08 10:31:20 +02:00
var barCornerRadius: Float = dp(context, 2.5f)
set(value) {
field = value
invalidate()
}
2020-10-08 10:31:20 +02:00
var barGravity: WaveGravity = WaveGravity.CENTER
set(value) {
field = value
invalidate()
}
var progressChangeListener: ProgressChangeListener? = null
2020-10-08 10:31:20 +02:00
private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val barRect = RectF()
private var canvasWidth = 0
private var canvasHeight = 0
private var touchDownX = 0f
private var touchDownProgress: Float = 0f
private var scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
: super(context, attrs, defStyleAttr) {
2020-10-08 10:31:20 +02:00
val typedAttrs = context.obtainStyledAttributes(attrs, R.styleable.WaveformSeekBar)
barWidth = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_width, barWidth)
barGap = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_gap, barGap)
barCornerRadius = typedAttrs.getDimension(
R.styleable.WaveformSeekBar_bar_corner_radius,
barCornerRadius)
2020-10-08 10:31:20 +02:00
barMinHeight =
typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_min_height, barMinHeight)
barBackgroundColor = typedAttrs.getColor(
R.styleable.WaveformSeekBar_bar_background_color,
barBackgroundColor)
2020-10-08 10:31:20 +02:00
barProgressColor =
typedAttrs.getColor(R.styleable.WaveformSeekBar_bar_progress_color, barProgressColor)
progress = typedAttrs.getFloat(R.styleable.WaveformSeekBar_progress, progress)
barGravity = WaveGravity.fromString(
typedAttrs.getString(R.styleable.WaveformSeekBar_bar_gravity))
typedAttrs.recycle()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
canvasWidth = w
canvasHeight = h
2020-10-08 10:31:20 +02:00
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
2020-10-08 10:31:20 +02:00
val totalWidth = getAvailableWidth()
val barAmount = (totalWidth / (barWidth + barGap)).toInt()
2020-10-08 10:31:20 +02:00
var lastBarRight = paddingLeft.toFloat()
2020-10-08 10:31:20 +02:00
(0 until barAmount).forEach { barIdx ->
// Convert a signed byte to a [0..1] float.
val barValue = byteToNormalizedFloat(sampleDataHolder.computeBarValue(barIdx, barAmount))
2020-10-08 10:31:20 +02:00
val barHeight = max(barMinHeight, getAvailableHeight() * barValue)
2020-10-08 10:31:20 +02:00
val top: Float = when (barGravity) {
WaveGravity.TOP -> paddingTop.toFloat()
2020-10-08 10:31:20 +02:00
WaveGravity.CENTER -> paddingTop + getAvailableHeight() * 0.5f - barHeight * 0.5f
WaveGravity.BOTTOM -> canvasHeight - paddingBottom - barHeight
}
2020-10-08 10:31:20 +02:00
barRect.set(lastBarRight, top, lastBarRight + barWidth, top + barHeight)
2020-10-08 10:31:20 +02:00
barPaint.color = if (barRect.right <= totalWidth * progress)
barProgressColor else barBackgroundColor
2020-10-08 10:31:20 +02:00
canvas.drawRoundRect(barRect, barCornerRadius, barCornerRadius, barPaint)
2020-10-08 10:31:20 +02:00
lastBarRight = barRect.right + barGap
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isEnabled) return false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
userSeeking = true
touchDownX = event.x
touchDownProgress = progress
updateProgress(event, false)
}
MotionEvent.ACTION_MOVE -> {
// Prevent any parent scrolling if the user scrolled more
// than scaledTouchSlop on horizontal axis.
if (abs(event.x - touchDownX) > scaledTouchSlop) {
parent.requestDisallowInterceptTouchEvent(true)
}
updateProgress(event, false)
}
MotionEvent.ACTION_UP -> {
userSeeking = false
updateProgress(event, true)
performClick()
}
2020-10-08 06:42:32 +02:00
MotionEvent.ACTION_CANCEL -> {
updateProgress(touchDownProgress, false)
2020-10-08 06:42:32 +02:00
userSeeking = false
}
}
return true
}
private fun updateProgress(event: MotionEvent, notify: Boolean) {
2020-10-08 10:31:20 +02:00
updateProgress(event.x / getAvailableWidth(), notify)
}
private fun updateProgress(progress: Float, notify: Boolean) {
_progress = MathUtils.clamp(progress, 0f, 1f)
invalidate()
if (notify) {
progressChangeListener?.onProgressChanged(this, _progress, true)
}
}
override fun performClick(): Boolean {
super.performClick()
return true
}
2020-10-08 10:31:20 +02:00
private fun getAvailableWidth() = canvasWidth - paddingLeft - paddingRight
private fun getAvailableHeight() = canvasHeight - paddingTop - paddingBottom
2020-10-08 10:31:20 +02:00
private class SampleDataHolder(private val invalidateDelegate: () -> Any) {
private var sampleDataFrom: ByteArray? = null
private var sampleDataTo: ByteArray? = null
2020-10-08 10:31:20 +02:00
private var progress = 1f // Mix between from and to values.
private var animation: ValueAnimator? = null
fun computeBarValue(barIdx: Int, barAmount: Int): Byte {
/** @return The array's value at the interpolated index. */
fun getSampleValue(sampleData: ByteArray?): Byte {
2020-10-08 10:31:20 +02:00
if (sampleData == null || sampleData.isEmpty())
return Byte.MIN_VALUE
2020-10-08 10:31:20 +02:00
else {
val sampleIdx = (barIdx * (sampleData.size / barAmount.toFloat())).toInt()
return sampleData[sampleIdx]
}
}
if (progress == 1f) {
return getSampleValue(sampleDataTo)
}
val fromValue = getSampleValue(sampleDataFrom)
val toValue = getSampleValue(sampleDataTo)
val rawResultValue = fromValue * (1f - progress) + toValue * progress
return rawResultValue.roundToInt().toByte()
2020-10-08 10:31:20 +02:00
}
fun setSamples(sampleData: ByteArray?) {
/** @return a mix between [sampleDataFrom] and [sampleDataTo] arrays according to the current [progress] value. */
fun computeNewDataFromArray(): ByteArray? {
if (sampleDataTo == null) return null
if (sampleDataFrom == null) return sampleDataTo
val sampleSize = min(sampleDataFrom!!.size, sampleDataTo!!.size)
return ByteArray(sampleSize) { i -> computeBarValue(i, sampleSize) }
}
sampleDataFrom = computeNewDataFromArray()
2020-10-08 10:31:20 +02:00
sampleDataTo = sampleData
2020-10-12 08:36:58 +02:00
progress = 0f
2020-10-08 10:31:20 +02:00
animation?.cancel()
animation = ValueAnimator.ofFloat(0f, 1f).apply {
addUpdateListener { animation ->
progress = animation.animatedValue as Float
invalidateDelegate()
}
interpolator = DecelerateInterpolator(3f)
duration = 500
start()
}
}
fun getSamples(): ByteArray? {
2020-10-08 10:31:20 +02:00
return sampleDataTo
}
}
enum class WaveGravity {
TOP,
CENTER,
BOTTOM,
;
companion object {
@JvmStatic
fun fromString(gravity: String?): WaveGravity = when (gravity) {
"1" -> TOP
"2" -> CENTER
else -> BOTTOM
}
}
}
interface ProgressChangeListener {
fun onProgressChanged(waveformSeekBar: WaveformSeekBar, progress: Float, fromUser: Boolean)
}
}