From c7d89985a1a00fe099cb3e4b30bb580c45f3b705 Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Thu, 8 Oct 2020 19:31:20 +1100 Subject: [PATCH] Waveform change animation. --- res/layout/message_audio_view.xml | 13 +- res/values/attrs.xml | 16 +- .../securesms/loki/views/MessageAudioView.kt | 6 +- .../securesms/loki/views/WaveformSeekBar.kt | 174 +++++++++++------- 4 files changed, 129 insertions(+), 80 deletions(-) diff --git a/res/layout/message_audio_view.xml b/res/layout/message_audio_view.xml index 8ec2d8b64e..6f14afd919 100644 --- a/res/layout/message_audio_view.xml +++ b/res/layout/message_audio_view.xml @@ -79,12 +79,13 @@ android:layout_gravity="center_vertical" android:layout_marginStart="4dp" android:layout_marginEnd="4dp" - app:wave_gravity="center" - app:wave_width="4dp" - app:wave_corner_radius="2dp" - app:wave_gap="1dp" - tools:wave_background_color="#bbb" - tools:wave_progress_color="?colorPrimary"/> + app:bar_gravity="center" + app:bar_width="4dp" + app:bar_corner_radius="2dp" + app:bar_gap="1dp" + tools:progress="0.5" + tools:bar_background_color="#bbb" + tools:bar_progress_color="?colorPrimary"/> - - - - - - - + + + + + + + - + diff --git a/src/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt b/src/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt index aaf1b7518e..19a1d930ec 100644 --- a/src/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt +++ b/src/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt @@ -192,8 +192,8 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { // val colorFilter = createBlendModeColorFilterCompat(foregroundTint, BlendModeCompat.SRC_IN) // seekBar.progressDrawable.colorFilter = colorFilter // seekBar.thumb.colorFilter = colorFilter - seekBar.waveProgressColor = foregroundTint - seekBar.waveBackgroundColor = ColorUtils.blendARGB(foregroundTint, backgroundTint, 0.75f) + seekBar.barProgressColor = foregroundTint + seekBar.barBackgroundColor = ColorUtils.blendARGB(foregroundTint, backgroundTint, 0.75f) } override fun onPlayerStart(player: AudioSlidePlayer) { @@ -313,7 +313,7 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { android.util.Log.d(TAG, "RMS: ${rmsValues.joinToString()}") post { - seekBar.sample = rmsValues + seekBar.sampleData = rmsValues if (totalDurationMs > 0) { totalDuration.visibility = View.VISIBLE diff --git a/src/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt b/src/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt index a064bad4f2..5a283890ba 100644 --- a/src/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt +++ b/src/org/thoughtcrime/securesms/loki/views/WaveformSeekBar.kt @@ -1,17 +1,23 @@ package org.thoughtcrime.securesms.loki.views +import android.animation.ValueAnimator 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.Looper import android.util.AttributeSet +import android.util.Log import android.util.TypedValue import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration +import android.view.animation.DecelerateInterpolator import network.loki.messenger.R -import java.lang.IllegalArgumentException import java.lang.Math.abs +import kotlin.math.max class WaveformSeekBar : View { @@ -19,17 +25,20 @@ class WaveformSeekBar : View { @JvmStatic inline fun dp(context: Context, dp: Float): Float { return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - dp, - context.resources.displayMetrics + TypedValue.COMPLEX_UNIT_DIP, + dp, + context.resources.displayMetrics ) } } - var sample: FloatArray = floatArrayOf(0f) + private val sampleDataHolder = SampleDataHolder(::invalidate) + var sampleData: FloatArray? + get() { + return sampleDataHolder.getSamples() + } set(value) { - if (value.isEmpty()) throw IllegalArgumentException("Sample array cannot be empty") - field = value + sampleDataHolder.setSamples(value) invalidate() } @@ -51,44 +60,43 @@ class WaveformSeekBar : View { return _progress } - var waveBackgroundColor: Int = Color.LTGRAY + var barBackgroundColor: Int = Color.LTGRAY set(value) { field = value invalidate() } - var waveProgressColor: Int = Color.WHITE + var barProgressColor: Int = Color.WHITE set(value) { field = value invalidate() } - var waveGap: Float = dp(context, 2f) + var barGap: Float = dp(context, 2f) set(value) { field = value invalidate() } - var waveWidth: Float = dp(context, 5f) + var barWidth: Float = dp(context, 5f) set(value) { field = value invalidate() } - var waveMinHeight: Float = waveWidth + var barMinHeight: Float = barWidth set(value) { field = value invalidate() } - var waveCornerRadius: Float = dp(context, 2.5f) + var barCornerRadius: Float = dp(context, 2.5f) set(value) { field = value invalidate() } - var waveGravity: WaveGravity = - WaveGravity.CENTER + var barGravity: WaveGravity = WaveGravity.CENTER set(value) { field = value invalidate() @@ -101,8 +109,8 @@ class WaveformSeekBar : View { progressChangeListener?.onProgressChanged(this, progress, true) } - private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG) - private val waveRect = RectF() + private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val barRect = RectF() private val progressCanvas = Canvas() private var canvasWidth = 0 @@ -117,28 +125,25 @@ class WaveformSeekBar : View { constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - val typedAttrs = context.obtainStyledAttributes(attrs, - R.styleable.WaveformSeekBar + 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 ) - - waveWidth = typedAttrs.getDimension(R.styleable.WaveformSeekBar_wave_width, waveWidth) - waveGap = typedAttrs.getDimension(R.styleable.WaveformSeekBar_wave_gap, waveGap) - waveCornerRadius = typedAttrs.getDimension( - R.styleable.WaveformSeekBar_wave_corner_radius, - waveCornerRadius + barMinHeight = + typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_min_height, barMinHeight) + barBackgroundColor = typedAttrs.getColor( + R.styleable.WaveformSeekBar_bar_background_color, + barBackgroundColor ) - waveMinHeight = - typedAttrs.getDimension(R.styleable.WaveformSeekBar_wave_min_height, waveMinHeight) - waveBackgroundColor = typedAttrs.getColor( - R.styleable.WaveformSeekBar_wave_background_color, - waveBackgroundColor - ) - waveProgressColor = - typedAttrs.getColor(R.styleable.WaveformSeekBar_wave_progress_color, waveProgressColor) - progress = typedAttrs.getFloat(R.styleable.WaveformSeekBar_wave_progress, progress) - waveGravity = + 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_wave_gravity) + typedAttrs.getString(R.styleable.WaveformSeekBar_bar_gravity) ) typedAttrs.recycle() @@ -146,47 +151,39 @@ class WaveformSeekBar : View { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) + canvasWidth = w canvasHeight = h + invalidate() } override fun onDraw(canvas: 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 - while (i < sample.size) { + val barHeight = max(barMinHeight, getAvailableHeight() * barValue) - var waveHeight = getAvailableHeight() * sample[i.toInt()] - - if (waveHeight < waveMinHeight) { - waveHeight = waveMinHeight - } - - val top: Float = when (waveGravity) { + val top: Float = when (barGravity) { WaveGravity.TOP -> paddingTop.toFloat() - WaveGravity.CENTER -> paddingTop + getAvailableHeight() / 2f - waveHeight / 2f - WaveGravity.BOTTOM -> canvasHeight - paddingBottom - waveHeight + WaveGravity.CENTER -> paddingTop + getAvailableHeight() * 0.5f - barHeight * 0.5f + 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) - waveProgressColor else waveBackgroundColor + barPaint.color = if (barRect.right <= totalWidth * progress) + barProgressColor else barBackgroundColor - canvas.drawRoundRect(waveRect, waveCornerRadius, waveCornerRadius, wavePaint) + canvas.drawRoundRect(barRect, barCornerRadius, barCornerRadius, barPaint) - lastWaveRight = waveRect.right + waveGap - - if (lastWaveRight + waveWidth > totalWidth + paddingLeft) - break - - i += 1f / step + lastBarRight = barRect.right + barGap } } @@ -240,7 +237,7 @@ class WaveformSeekBar : View { } private fun updateProgress(event: MotionEvent, notify: Boolean) { - updateProgress(event.x / getAvailableWith(), notify) + updateProgress(event.x / getAvailableWidth(), notify) } private fun updateProgress(progress: Float, notify: Boolean) { @@ -270,9 +267,60 @@ class WaveformSeekBar : View { return true } - private fun getAvailableWith() = canvasWidth - paddingLeft - paddingRight + private fun getAvailableWidth() = canvasWidth - paddingLeft - paddingRight 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 { TOP, CENTER,