Use waveform seek bar for audio message view.
parent
fdaadcb2b0
commit
e07cb716c0
@ -0,0 +1,317 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import network.loki.messenger.R
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.Math.abs
|
||||
|
||||
class WaveformSeekBar : View {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
inline fun dp(context: Context, dp: Float): Float {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
dp,
|
||||
context.resources.displayMetrics
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
inline fun smooth(values: FloatArray, neighborWeight: Float = 1f): FloatArray {
|
||||
if (values.size < 3) return values
|
||||
|
||||
val result = FloatArray(values.size)
|
||||
result[0] = values[0]
|
||||
result[values.size - 1] == values[values.size - 1]
|
||||
for (i in 1 until values.size - 1) {
|
||||
result[i] =
|
||||
(values[i] + values[i - 1] * neighborWeight + values[i + 1] * neighborWeight) / (1f + neighborWeight * 2f)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
var sample: FloatArray = floatArrayOf(0f)
|
||||
set(value) {
|
||||
if (value.isEmpty()) throw IllegalArgumentException("Sample array cannot be empty")
|
||||
|
||||
// field = smooth(value, 0.25f)
|
||||
field = 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
|
||||
}
|
||||
|
||||
var waveBackgroundColor: Int = Color.LTGRAY
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var waveProgressColor: Int = Color.WHITE
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var waveGap: Float =
|
||||
dp(
|
||||
context,
|
||||
2f
|
||||
)
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var waveWidth: Float =
|
||||
dp(
|
||||
context,
|
||||
5f
|
||||
)
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var waveMinHeight: Float = waveWidth
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var waveCornerRadius: Float =
|
||||
dp(
|
||||
context,
|
||||
2.5f
|
||||
)
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var waveGravity: WaveGravity =
|
||||
WaveGravity.CENTER
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var progressChangeListener: ProgressChangeListener? = null
|
||||
|
||||
private val postponedProgressUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private val postponedProgressUpdateRunnable = Runnable {
|
||||
progressChangeListener?.onProgressChanged(this, progress, true)
|
||||
}
|
||||
|
||||
private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val waveRect = RectF()
|
||||
private val progressCanvas = Canvas()
|
||||
|
||||
private var canvasWidth = 0
|
||||
private var canvasHeight = 0
|
||||
private var maxValue =
|
||||
dp(
|
||||
context,
|
||||
2f
|
||||
)
|
||||
private var touchDownX = 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) {
|
||||
|
||||
val typedAttrs = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.WaveformSeekBar
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
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 =
|
||||
WaveGravity.fromString(
|
||||
typedAttrs.getString(R.styleable.WaveformSeekBar_wave_gravity)
|
||||
)
|
||||
|
||||
typedAttrs.recycle()
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
canvasWidth = w
|
||||
canvasHeight = h
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
val totalWidth = getAvailableWith()
|
||||
|
||||
maxValue = sample.max()!!
|
||||
val step = (totalWidth / (waveGap + waveWidth)) / sample.size
|
||||
|
||||
var lastWaveRight = paddingLeft.toFloat()
|
||||
|
||||
var i = 0f
|
||||
while (i < sample.size) {
|
||||
|
||||
var waveHeight = if (maxValue != 0f) {
|
||||
getAvailableHeight() * (sample[i.toInt()] / maxValue)
|
||||
} else {
|
||||
waveMinHeight
|
||||
}
|
||||
|
||||
if (waveHeight < waveMinHeight) {
|
||||
waveHeight = waveMinHeight
|
||||
}
|
||||
|
||||
val top: Float = when (waveGravity) {
|
||||
WaveGravity.TOP -> paddingTop.toFloat()
|
||||
WaveGravity.CENTER -> paddingTop + getAvailableHeight() / 2f - waveHeight / 2f
|
||||
WaveGravity.BOTTOM -> canvasHeight - paddingBottom - waveHeight
|
||||
}
|
||||
|
||||
waveRect.set(lastWaveRight, top, lastWaveRight + waveWidth, top + waveHeight)
|
||||
|
||||
wavePaint.color = if (waveRect.right <= totalWidth * progress)
|
||||
waveProgressColor else waveBackgroundColor
|
||||
|
||||
canvas.drawRoundRect(waveRect, waveCornerRadius, waveCornerRadius, wavePaint)
|
||||
|
||||
lastWaveRight = waveRect.right + waveGap
|
||||
|
||||
if (lastWaveRight + waveWidth > totalWidth + paddingLeft)
|
||||
break
|
||||
|
||||
i += 1f / step
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (!isEnabled) return false
|
||||
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
userSeeking = true
|
||||
if (isParentScrolling()) {
|
||||
touchDownX = event.x
|
||||
} else {
|
||||
updateProgress(event, true)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
updateProgress(event, true)
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
userSeeking = false
|
||||
if (abs(event.x - touchDownX) > scaledTouchSlop) {
|
||||
updateProgress(event, false)
|
||||
}
|
||||
|
||||
performClick()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isParentScrolling(): Boolean {
|
||||
var parent = parent as View
|
||||
val root = rootView
|
||||
|
||||
while (true) {
|
||||
when {
|
||||
parent.canScrollHorizontally(+1) -> return true
|
||||
parent.canScrollHorizontally(-1) -> return true
|
||||
parent.canScrollVertically(+1) -> return true
|
||||
parent.canScrollVertically(-1) -> return true
|
||||
}
|
||||
|
||||
if (parent == root) return false
|
||||
|
||||
parent = parent.parent as View
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProgress(event: MotionEvent, delayNotification: Boolean) {
|
||||
_progress = event.x / getAvailableWith()
|
||||
invalidate()
|
||||
|
||||
postponedProgressUpdateHandler.removeCallbacks(postponedProgressUpdateRunnable)
|
||||
if (delayNotification) {
|
||||
// Re-post delayed user update notification to throttle a bit.
|
||||
postponedProgressUpdateHandler.postDelayed(postponedProgressUpdateRunnable, 150)
|
||||
} else {
|
||||
postponedProgressUpdateRunnable.run()
|
||||
}
|
||||
}
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
super.performClick()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getAvailableWith() = canvasWidth - paddingLeft - paddingRight
|
||||
private fun getAvailableHeight() = canvasHeight - paddingTop - paddingBottom
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue