Waveform change animation.

This commit is contained in:
Anton Chekulaev 2020-10-08 19:31:20 +11:00
parent 8cbb34f174
commit c7d89985a1
4 changed files with 129 additions and 80 deletions

View File

@ -79,12 +79,13 @@
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
app:wave_gravity="center" app:bar_gravity="center"
app:wave_width="4dp" app:bar_width="4dp"
app:wave_corner_radius="2dp" app:bar_corner_radius="2dp"
app:wave_gap="1dp" app:bar_gap="1dp"
tools:wave_background_color="#bbb" tools:progress="0.5"
tools:wave_progress_color="?colorPrimary"/> tools:bar_background_color="#bbb"
tools:bar_progress_color="?colorPrimary"/>
<TextView android:id="@+id/total_duration" <TextView android:id="@+id/total_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -289,15 +289,15 @@
</declare-styleable> </declare-styleable>
<declare-styleable name="WaveformSeekBar"> <declare-styleable name="WaveformSeekBar">
<attr name="wave_progress" format="float"/> <attr name="progress" format="float"/>
<attr name="wave_width" format="dimension"/> <attr name="bar_width" format="dimension"/>
<attr name="wave_gap" format="dimension"/> <attr name="bar_gap" format="dimension"/>
<attr name="wave_min_height" format="dimension"/> <attr name="bar_min_height" format="dimension"/>
<attr name="wave_corner_radius" format="dimension"/> <attr name="bar_corner_radius" format="dimension"/>
<attr name="wave_background_color" format="color"/> <attr name="bar_background_color" format="color"/>
<attr name="wave_progress_color" format="color"/> <attr name="bar_progress_color" format="color"/>
<!-- Corresponds to WaveformSeekBar.WaveGravity enum. --> <!-- Corresponds to WaveformSeekBar.WaveGravity enum. -->
<attr name="wave_gravity" format="enum"> <attr name="bar_gravity" format="enum">
<enum name="top" value="1" /> <enum name="top" value="1" />
<enum name="center" value="2" /> <enum name="center" value="2" />
<enum name="bottom" value="3" /> <enum name="bottom" value="3" />

View File

@ -192,8 +192,8 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener {
// val colorFilter = createBlendModeColorFilterCompat(foregroundTint, BlendModeCompat.SRC_IN) // val colorFilter = createBlendModeColorFilterCompat(foregroundTint, BlendModeCompat.SRC_IN)
// seekBar.progressDrawable.colorFilter = colorFilter // seekBar.progressDrawable.colorFilter = colorFilter
// seekBar.thumb.colorFilter = colorFilter // seekBar.thumb.colorFilter = colorFilter
seekBar.waveProgressColor = foregroundTint seekBar.barProgressColor = foregroundTint
seekBar.waveBackgroundColor = ColorUtils.blendARGB(foregroundTint, backgroundTint, 0.75f) seekBar.barBackgroundColor = ColorUtils.blendARGB(foregroundTint, backgroundTint, 0.75f)
} }
override fun onPlayerStart(player: AudioSlidePlayer) { override fun onPlayerStart(player: AudioSlidePlayer) {
@ -313,7 +313,7 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener {
android.util.Log.d(TAG, "RMS: ${rmsValues.joinToString()}") android.util.Log.d(TAG, "RMS: ${rmsValues.joinToString()}")
post { post {
seekBar.sample = rmsValues seekBar.sampleData = rmsValues
if (totalDurationMs > 0) { if (totalDurationMs > 0) {
totalDuration.visibility = View.VISIBLE totalDuration.visibility = View.VISIBLE

View File

@ -1,17 +1,23 @@
package org.thoughtcrime.securesms.loki.views package org.thoughtcrime.securesms.loki.views
import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.graphics.* import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
import android.view.animation.DecelerateInterpolator
import network.loki.messenger.R import network.loki.messenger.R
import java.lang.IllegalArgumentException
import java.lang.Math.abs import java.lang.Math.abs
import kotlin.math.max
class WaveformSeekBar : View { class WaveformSeekBar : View {
@ -19,17 +25,20 @@ class WaveformSeekBar : View {
@JvmStatic @JvmStatic
inline fun dp(context: Context, dp: Float): Float { inline fun dp(context: Context, dp: Float): Float {
return TypedValue.applyDimension( return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, TypedValue.COMPLEX_UNIT_DIP,
dp, dp,
context.resources.displayMetrics context.resources.displayMetrics
) )
} }
} }
var sample: FloatArray = floatArrayOf(0f) private val sampleDataHolder = SampleDataHolder(::invalidate)
var sampleData: FloatArray?
get() {
return sampleDataHolder.getSamples()
}
set(value) { set(value) {
if (value.isEmpty()) throw IllegalArgumentException("Sample array cannot be empty") sampleDataHolder.setSamples(value)
field = value
invalidate() invalidate()
} }
@ -51,44 +60,43 @@ class WaveformSeekBar : View {
return _progress return _progress
} }
var waveBackgroundColor: Int = Color.LTGRAY var barBackgroundColor: Int = Color.LTGRAY
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var waveProgressColor: Int = Color.WHITE var barProgressColor: Int = Color.WHITE
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var waveGap: Float = dp(context, 2f) var barGap: Float = dp(context, 2f)
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var waveWidth: Float = dp(context, 5f) var barWidth: Float = dp(context, 5f)
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var waveMinHeight: Float = waveWidth var barMinHeight: Float = barWidth
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var waveCornerRadius: Float = dp(context, 2.5f) var barCornerRadius: Float = dp(context, 2.5f)
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var waveGravity: WaveGravity = var barGravity: WaveGravity = WaveGravity.CENTER
WaveGravity.CENTER
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
@ -101,8 +109,8 @@ class WaveformSeekBar : View {
progressChangeListener?.onProgressChanged(this, progress, true) progressChangeListener?.onProgressChanged(this, progress, true)
} }
private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG) private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val waveRect = RectF() private val barRect = RectF()
private val progressCanvas = Canvas() private val progressCanvas = Canvas()
private var canvasWidth = 0 private var canvasWidth = 0
@ -117,28 +125,25 @@ class WaveformSeekBar : View {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
: super(context, attrs, defStyleAttr) { : super(context, attrs, defStyleAttr) {
val typedAttrs = context.obtainStyledAttributes(attrs, val typedAttrs = context.obtainStyledAttributes(attrs, R.styleable.WaveformSeekBar)
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
) )
barMinHeight =
waveWidth = typedAttrs.getDimension(R.styleable.WaveformSeekBar_wave_width, waveWidth) typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_min_height, barMinHeight)
waveGap = typedAttrs.getDimension(R.styleable.WaveformSeekBar_wave_gap, waveGap) barBackgroundColor = typedAttrs.getColor(
waveCornerRadius = typedAttrs.getDimension( R.styleable.WaveformSeekBar_bar_background_color,
R.styleable.WaveformSeekBar_wave_corner_radius, barBackgroundColor
waveCornerRadius
) )
waveMinHeight = barProgressColor =
typedAttrs.getDimension(R.styleable.WaveformSeekBar_wave_min_height, waveMinHeight) typedAttrs.getColor(R.styleable.WaveformSeekBar_bar_progress_color, barProgressColor)
waveBackgroundColor = typedAttrs.getColor( progress = typedAttrs.getFloat(R.styleable.WaveformSeekBar_progress, progress)
R.styleable.WaveformSeekBar_wave_background_color, barGravity =
waveBackgroundColor
)
waveProgressColor =
typedAttrs.getColor(R.styleable.WaveformSeekBar_wave_progress_color, waveProgressColor)
progress = typedAttrs.getFloat(R.styleable.WaveformSeekBar_wave_progress, progress)
waveGravity =
WaveGravity.fromString( WaveGravity.fromString(
typedAttrs.getString(R.styleable.WaveformSeekBar_wave_gravity) typedAttrs.getString(R.styleable.WaveformSeekBar_bar_gravity)
) )
typedAttrs.recycle() typedAttrs.recycle()
@ -146,47 +151,39 @@ class WaveformSeekBar : View {
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
canvasWidth = w canvasWidth = w
canvasHeight = h canvasHeight = h
invalidate()
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
val totalWidth = getAvailableWith() val totalWidth = getAvailableWidth()
val barAmount = (totalWidth / (barWidth + barGap)).toInt()
val step = (totalWidth / (waveGap + waveWidth)) / sample.size var lastBarRight = paddingLeft.toFloat()
var lastWaveRight = paddingLeft.toFloat() (0 until barAmount).forEach { barIdx ->
val barValue = sampleDataHolder.computeBarValue(barIdx, barAmount)
var i = 0f val barHeight = max(barMinHeight, getAvailableHeight() * barValue)
while (i < sample.size) {
var waveHeight = getAvailableHeight() * sample[i.toInt()] val top: Float = when (barGravity) {
if (waveHeight < waveMinHeight) {
waveHeight = waveMinHeight
}
val top: Float = when (waveGravity) {
WaveGravity.TOP -> paddingTop.toFloat() WaveGravity.TOP -> paddingTop.toFloat()
WaveGravity.CENTER -> paddingTop + getAvailableHeight() / 2f - waveHeight / 2f WaveGravity.CENTER -> paddingTop + getAvailableHeight() * 0.5f - barHeight * 0.5f
WaveGravity.BOTTOM -> canvasHeight - paddingBottom - waveHeight WaveGravity.BOTTOM -> canvasHeight - paddingBottom - barHeight
} }
waveRect.set(lastWaveRight, top, lastWaveRight + waveWidth, top + waveHeight) barRect.set(lastBarRight, top, lastBarRight + barWidth, top + barHeight)
wavePaint.color = if (waveRect.right <= totalWidth * progress) barPaint.color = if (barRect.right <= totalWidth * progress)
waveProgressColor else waveBackgroundColor barProgressColor else barBackgroundColor
canvas.drawRoundRect(waveRect, waveCornerRadius, waveCornerRadius, wavePaint) canvas.drawRoundRect(barRect, barCornerRadius, barCornerRadius, barPaint)
lastWaveRight = waveRect.right + waveGap lastBarRight = barRect.right + barGap
if (lastWaveRight + waveWidth > totalWidth + paddingLeft)
break
i += 1f / step
} }
} }
@ -240,7 +237,7 @@ class WaveformSeekBar : View {
} }
private fun updateProgress(event: MotionEvent, notify: Boolean) { private fun updateProgress(event: MotionEvent, notify: Boolean) {
updateProgress(event.x / getAvailableWith(), notify) updateProgress(event.x / getAvailableWidth(), notify)
} }
private fun updateProgress(progress: Float, notify: Boolean) { private fun updateProgress(progress: Float, notify: Boolean) {
@ -270,9 +267,60 @@ class WaveformSeekBar : View {
return true return true
} }
private fun getAvailableWith() = canvasWidth - paddingLeft - paddingRight private fun getAvailableWidth() = canvasWidth - paddingLeft - paddingRight
private fun getAvailableHeight() = canvasHeight - paddingTop - paddingBottom private fun getAvailableHeight() = canvasHeight - paddingTop - paddingBottom
private class SampleDataHolder(private val invalidateDelegate: () -> Any) {
private var sampleDataFrom: FloatArray? = null
private var sampleDataTo: FloatArray? = null
private var progress = 1f // Mix between from and to values.
private var animation: ValueAnimator? = null
fun computeBarValue(barIdx: Int, barAmount: Int): Float {
fun getSampleValue(sampleData: FloatArray?): Float {
if (sampleData == null || sampleData.isEmpty())
return 0f
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)
return fromValue * (1f - progress) + toValue * progress
}
fun setSamples(sampleData: FloatArray?) {
//TODO Animate from the current value.
sampleDataFrom = sampleDataTo
sampleDataTo = sampleData
animation?.cancel()
animation = ValueAnimator.ofFloat(0f, 1f).apply {
addUpdateListener { animation ->
progress = animation.animatedValue as Float
Log.d("MTPHR", "Progress: $progress")
invalidateDelegate()
}
interpolator = DecelerateInterpolator(3f)
duration = 500
start()
}
}
fun getSamples(): FloatArray? {
return sampleDataTo
}
}
enum class WaveGravity { enum class WaveGravity {
TOP, TOP,
CENTER, CENTER,