Create a custom audio waveform view.
This commit is contained in:
parent
a75e2d5fa8
commit
ab2001cd7f
|
@ -2,14 +2,14 @@
|
|||
<resources>
|
||||
|
||||
<style name="VoicePlaybackWaveform">
|
||||
<item name="chunkColor">?vctr_content_secondary</item>
|
||||
<item name="chunkAlignTo">center</item>
|
||||
<item name="chunkMinHeight">1dp</item>
|
||||
<item name="chunkRoundedCorners">true</item>
|
||||
<item name="chunkSoftTransition">true</item>
|
||||
<item name="chunkSpace">2dp</item>
|
||||
<item name="chunkWidth">2dp</item>
|
||||
<item name="direction">rightToLeft</item>
|
||||
<item name="alignment">center</item>
|
||||
<item name="flow">leftToRight</item>
|
||||
<item name="verticalPadding">4dp</item>
|
||||
<item name="horizontalPadding">4dp</item>
|
||||
<item name="barWidth">2dp</item>
|
||||
<item name="barSpace">2dp</item>
|
||||
<item name="barMinHeight">1dp</item>
|
||||
<item name="isBarRounded">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -221,7 +221,9 @@ class VoiceMessageHelper @Inject constructor(
|
|||
private fun onPlaybackTick(id: String) {
|
||||
if (mediaPlayer?.isPlaying.orFalse()) {
|
||||
val currentPosition = mediaPlayer?.currentPosition ?: 0
|
||||
playbackTracker.updateCurrentPlaybackTime(id, currentPosition)
|
||||
val totalDuration = mediaPlayer?.duration ?: 0
|
||||
val percentage = currentPosition.toFloat() / totalDuration
|
||||
playbackTracker.updateCurrentPlaybackTime(id, currentPosition, percentage)
|
||||
} else {
|
||||
playbackTracker.stopPlayback(id)
|
||||
stopPlaybackTicker()
|
||||
|
|
|
@ -27,7 +27,6 @@ import androidx.core.view.doOnLayout
|
|||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.visualizer.amplitude.AudioRecordView
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setAttributeBackground
|
||||
import im.vector.app.core.extensions.setAttributeTintedBackground
|
||||
|
@ -37,6 +36,8 @@ import im.vector.app.databinding.ViewVoiceMessageRecorderBinding
|
|||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.DraggingState
|
||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.app.features.voice.AudioWaveformView
|
||||
|
||||
class VoiceMessageViews(
|
||||
private val resources: Resources,
|
||||
|
@ -284,7 +285,7 @@ class VoiceMessageViews(
|
|||
hideRecordingViews(RecordingUiState.Idle)
|
||||
views.voiceMessageMicButton.isVisible = true
|
||||
views.voiceMessageSendButton.isVisible = false
|
||||
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }
|
||||
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.clear() }
|
||||
}
|
||||
|
||||
fun renderPlaying(state: VoiceMessagePlaybackTracker.Listener.State.Playing) {
|
||||
|
@ -292,11 +293,15 @@ class VoiceMessageViews(
|
|||
views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_pause_voice_message)
|
||||
val formattedTimerText = DateUtils.formatElapsedTime((state.playbackTime / 1000).toLong())
|
||||
views.voicePlaybackTime.text = formattedTimerText
|
||||
val waveformColorIdle = ThemeUtils.getColor(views.voicePlaybackWaveform.context, R.attr.vctr_content_quaternary)
|
||||
val waveformColorPlayed = ThemeUtils.getColor(views.voicePlaybackWaveform.context, R.attr.vctr_content_secondary)
|
||||
views.voicePlaybackWaveform.updateColors(state.percentage, waveformColorPlayed, waveformColorIdle)
|
||||
}
|
||||
|
||||
fun renderIdle() {
|
||||
views.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
views.voicePlaybackControlButton.contentDescription = resources.getString(R.string.a11y_play_voice_message)
|
||||
views.voicePlaybackWaveform.summarize()
|
||||
}
|
||||
|
||||
fun renderToast(message: String) {
|
||||
|
@ -327,8 +332,9 @@ class VoiceMessageViews(
|
|||
|
||||
fun renderRecordingWaveform(amplitudeList: Array<Int>) {
|
||||
views.voicePlaybackWaveform.doOnLayout { waveFormView ->
|
||||
val waveformColor = ThemeUtils.getColor(waveFormView.context, R.attr.vctr_content_secondary)
|
||||
amplitudeList.iterator().forEach {
|
||||
(waveFormView as AudioRecordView).update(it)
|
||||
(waveFormView as AudioWaveformView).add(AudioWaveformView.FFT(it.toFloat(), waveformColor))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ import im.vector.app.features.location.toLocationData
|
|||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.voice.AudioWaveformView
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||
|
@ -688,8 +689,7 @@ class MessageItemFactory @Inject constructor(
|
|||
return this
|
||||
?.filterNotNull()
|
||||
?.map {
|
||||
// Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec
|
||||
it * 22760 / 1024
|
||||
it * AudioWaveformView.MAX_FFT / 1024
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,8 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
|
|||
|
||||
fun startPlayback(id: String) {
|
||||
val currentPlaybackTime = getPlaybackTime(id)
|
||||
val currentState = Listener.State.Playing(currentPlaybackTime)
|
||||
val currentPercentage = getPercentage(id)
|
||||
val currentState = Listener.State.Playing(currentPlaybackTime, currentPercentage)
|
||||
setState(id, currentState)
|
||||
// Pause any active playback
|
||||
states
|
||||
|
@ -87,15 +88,16 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
|
|||
|
||||
fun pausePlayback(id: String) {
|
||||
val currentPlaybackTime = getPlaybackTime(id)
|
||||
setState(id, Listener.State.Paused(currentPlaybackTime))
|
||||
val currentPercentage = getPercentage(id)
|
||||
setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage))
|
||||
}
|
||||
|
||||
fun stopPlayback(id: String) {
|
||||
setState(id, Listener.State.Idle)
|
||||
}
|
||||
|
||||
fun updateCurrentPlaybackTime(id: String, time: Int) {
|
||||
setState(id, Listener.State.Playing(time))
|
||||
fun updateCurrentPlaybackTime(id: String, time: Int, percentage: Float) {
|
||||
setState(id, Listener.State.Playing(time, percentage))
|
||||
}
|
||||
|
||||
fun updateCurrentRecording(id: String, amplitudeList: List<Int>) {
|
||||
|
@ -113,6 +115,15 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getPercentage(id: String): Float {
|
||||
return when (val state = states[id]) {
|
||||
is Listener.State.Playing -> state.percentage
|
||||
is Listener.State.Paused -> state.percentage
|
||||
/* Listener.State.Idle, */
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
listeners.forEach {
|
||||
it.value.onUpdate(Listener.State.Idle)
|
||||
|
@ -131,8 +142,8 @@ class VoiceMessagePlaybackTracker @Inject constructor() {
|
|||
|
||||
sealed class State {
|
||||
object Idle : State()
|
||||
data class Playing(val playbackTime: Int) : State()
|
||||
data class Paused(val playbackTime: Int) : State()
|
||||
data class Playing(val playbackTime: Int, val percentage: Float) : State()
|
||||
data class Paused(val playbackTime: Int, val percentage: Float) : State()
|
||||
data class Recording(val amplitudeList: List<Int>) : State()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ import android.widget.TextView
|
|||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.visualizer.amplitude.AudioRecordView
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
|
@ -34,6 +33,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStat
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.app.features.voice.AudioWaveformView
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
||||
|
@ -78,11 +78,15 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
|
||||
holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
|
||||
val waveformColorIdle = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quaternary)
|
||||
val waveformColorPlayed = ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_secondary)
|
||||
|
||||
holder.voicePlaybackWaveform.post {
|
||||
holder.voicePlaybackWaveform.recreate()
|
||||
holder.voicePlaybackWaveform.clear()
|
||||
waveform.forEach { amplitude ->
|
||||
holder.voicePlaybackWaveform.update(amplitude)
|
||||
holder.voicePlaybackWaveform.add(AudioWaveformView.FFT(amplitude.toFloat(), waveformColorIdle))
|
||||
}
|
||||
holder.voicePlaybackWaveform.summarize()
|
||||
}
|
||||
|
||||
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
|
||||
|
@ -93,33 +97,39 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
|
||||
holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
|
||||
|
||||
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||
// Don't track and don't try to update UI before view is present
|
||||
holder.view.post {
|
||||
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is VoiceMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderIdleState(holder: Holder) {
|
||||
private fun renderIdleState(holder: Holder, idleColor: Int, playedColor: Int) {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message)
|
||||
holder.voicePlaybackTime.text = formatPlaybackTime(duration)
|
||||
holder.voicePlaybackWaveform.updateColors(0f, playedColor, idleColor)
|
||||
}
|
||||
|
||||
private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing) {
|
||||
private fun renderPlayingState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Playing, idleColor: Int, playedColor: Int) {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_pause_voice_message)
|
||||
holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||
holder.voicePlaybackWaveform.updateColors(state.percentage, playedColor, idleColor)
|
||||
}
|
||||
|
||||
private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused) {
|
||||
private fun renderPausedState(holder: Holder, state: VoiceMessagePlaybackTracker.Listener.State.Paused, idleColor: Int, playedColor: Int) {
|
||||
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message)
|
||||
holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
|
||||
holder.voicePlaybackWaveform.updateColors(state.percentage, playedColor, idleColor)
|
||||
}
|
||||
|
||||
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
|
||||
|
@ -138,7 +148,7 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
val voiceLayout by bind<ViewGroup>(R.id.voiceLayout)
|
||||
val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton)
|
||||
val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime)
|
||||
val voicePlaybackWaveform by bind<AudioRecordView>(R.id.voicePlaybackWaveform)
|
||||
val voicePlaybackWaveform by bind<AudioWaveformView>(R.id.voicePlaybackWaveform)
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.app.features.voice
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import im.vector.app.R
|
||||
import kotlin.math.max
|
||||
import kotlin.random.Random
|
||||
|
||||
class AudioWaveformView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private enum class Alignment(var value: Int) {
|
||||
CENTER(0),
|
||||
BOTTOM(1),
|
||||
TOP(2)
|
||||
}
|
||||
|
||||
private enum class Flow(var value: Int) {
|
||||
LTR(0),
|
||||
RTL(1)
|
||||
}
|
||||
|
||||
data class FFT(val value: Float, var color: Int)
|
||||
|
||||
private fun Int.dp() = this * Resources.getSystem().displayMetrics.density
|
||||
|
||||
// Configuration fields
|
||||
private var alignment = Alignment.CENTER
|
||||
private var flow = Flow.LTR
|
||||
private var verticalPadding = 4.dp()
|
||||
private var horizontalPadding = 4.dp()
|
||||
private var barWidth = 2.dp()
|
||||
private var barSpace = 1.dp()
|
||||
private var barMinHeight = 1.dp()
|
||||
private var isBarRounded = true
|
||||
|
||||
private val rawFftList = mutableListOf<FFT>()
|
||||
private var visibleBarHeights = mutableListOf<FFT>()
|
||||
|
||||
private val barPaint = Paint()
|
||||
|
||||
init {
|
||||
attrs?.let {
|
||||
context
|
||||
.theme
|
||||
.obtainStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.AudioWaveformView,
|
||||
0,
|
||||
0
|
||||
)
|
||||
.apply {
|
||||
alignment = Alignment.values().find { it.value == getInt(R.styleable.AudioWaveformView_alignment, alignment.value) }!!
|
||||
flow = Flow.values().find { it.value == getInt(R.styleable.AudioWaveformView_flow, alignment.value) }!!
|
||||
verticalPadding = getDimension(R.styleable.AudioWaveformView_verticalPadding, verticalPadding)
|
||||
horizontalPadding = getDimension(R.styleable.AudioWaveformView_horizontalPadding, horizontalPadding)
|
||||
barWidth = getDimension(R.styleable.AudioWaveformView_barWidth, barWidth)
|
||||
barSpace = getDimension(R.styleable.AudioWaveformView_barSpace, barSpace)
|
||||
barMinHeight = getDimension(R.styleable.AudioWaveformView_barMinHeight, barMinHeight)
|
||||
isBarRounded = getBoolean(R.styleable.AudioWaveformView_isBarRounded, isBarRounded)
|
||||
setWillNotDraw(false)
|
||||
barPaint.isAntiAlias = true
|
||||
}
|
||||
.apply { recycle() }
|
||||
.also {
|
||||
barPaint.strokeWidth = barWidth
|
||||
barPaint.strokeCap = if (isBarRounded) Paint.Cap.ROUND else Paint.Cap.BUTT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initialize(fftList: List<FFT>) {
|
||||
handleNewFftList(fftList)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun add(fft: FFT) {
|
||||
handleNewFftList(listOf(fft))
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun summarize() {
|
||||
if (rawFftList.isEmpty()) return
|
||||
|
||||
val maxVisibleBarCount = getMaxVisibleBarCount()
|
||||
val summarizedFftList = rawFftList.summarize(maxVisibleBarCount)
|
||||
clear()
|
||||
handleNewFftList(summarizedFftList)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun updateColors(limitPercentage: Float, colorBefore: Int, colorAfter: Int) {
|
||||
val size = visibleBarHeights.size
|
||||
val limitIndex = (size * limitPercentage).toInt()
|
||||
visibleBarHeights.forEachIndexed { index, fft ->
|
||||
fft.color = if (index < limitIndex) {
|
||||
colorBefore
|
||||
} else {
|
||||
colorAfter
|
||||
}
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
rawFftList.clear()
|
||||
visibleBarHeights.clear()
|
||||
}
|
||||
|
||||
private fun List<FFT>.summarize(target: Int): List<FFT> {
|
||||
val result = mutableListOf<FFT>()
|
||||
if (size <= target) {
|
||||
result.addAll(this)
|
||||
val missingItemCount = target - size
|
||||
repeat(missingItemCount) {
|
||||
val index = Random.nextInt(result.size)
|
||||
result.add(index, result[index])
|
||||
}
|
||||
} else {
|
||||
val step = (size.toDouble() - 1) / (target - 1)
|
||||
var index = 0.0
|
||||
while (index < size) {
|
||||
result.add(get(index.toInt()))
|
||||
index += step
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun handleNewFftList(fftList: List<FFT>) {
|
||||
val maxVisibleBarCount = getMaxVisibleBarCount()
|
||||
fftList.forEach { fft ->
|
||||
rawFftList.add(fft)
|
||||
val barHeight = max(fft.value / MAX_FFT * (height - verticalPadding * 2), barMinHeight)
|
||||
visibleBarHeights.add(FFT(barHeight, fft.color))
|
||||
if (visibleBarHeights.size > maxVisibleBarCount) {
|
||||
visibleBarHeights = visibleBarHeights.subList(visibleBarHeights.size - maxVisibleBarCount, visibleBarHeights.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaxVisibleBarCount() = ((width - horizontalPadding * 2) / (barWidth + barSpace)).toInt()
|
||||
|
||||
private fun drawBars(canvas: Canvas) {
|
||||
var currentX = horizontalPadding
|
||||
visibleBarHeights.forEach {
|
||||
barPaint.color = it.color
|
||||
// TODO. Support flow
|
||||
when (alignment) {
|
||||
Alignment.BOTTOM -> {
|
||||
val startY = height - verticalPadding
|
||||
val stopY = startY - it.value
|
||||
canvas.drawLine(currentX, startY, currentX, stopY, barPaint)
|
||||
}
|
||||
Alignment.CENTER -> {
|
||||
val startY = (height - it.value) / 2
|
||||
val stopY = startY + it.value
|
||||
canvas.drawLine(currentX, startY, currentX, stopY, barPaint)
|
||||
}
|
||||
Alignment.TOP -> {
|
||||
val startY = verticalPadding
|
||||
val stopY = startY + it.value
|
||||
canvas.drawLine(currentX, startY, currentX, stopY, barPaint)
|
||||
}
|
||||
}
|
||||
currentX += barWidth + barSpace
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
drawBars(canvas)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_FFT = 32760f
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@
|
|||
app:layout_constraintTop_toTopOf="@id/voicePlaybackControlButton"
|
||||
tools:text="0:23" />
|
||||
|
||||
<com.visualizer.amplitude.AudioRecordView
|
||||
<im.vector.app.features.voice.AudioWaveformView
|
||||
android:id="@+id/voicePlaybackWaveform"
|
||||
style="@style/VoicePlaybackWaveform"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -208,7 +208,7 @@
|
|||
app:layout_goneMarginStart="24dp"
|
||||
tools:text="0:23" />
|
||||
|
||||
<com.visualizer.amplitude.AudioRecordView
|
||||
<im.vector.app.features.voice.AudioWaveformView
|
||||
android:id="@+id/voicePlaybackWaveform"
|
||||
style="@style/VoicePlaybackWaveform"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="AudioWaveformView">
|
||||
|
||||
<attr name="alignment" format="enum">
|
||||
<enum name="center" value="0" />
|
||||
<enum name="bottom" value="1" />
|
||||
<enum name="top" value="2" />
|
||||
</attr>
|
||||
<attr name="flow" format="enum">
|
||||
<enum name="leftToRight" value="0" />
|
||||
<enum name="rightToLeft" value="1" />
|
||||
</attr>
|
||||
<attr name="verticalPadding" format="dimension" />
|
||||
<attr name="horizontalPadding" format="dimension" />
|
||||
|
||||
<attr name="barWidth" format="dimension" />
|
||||
<attr name="barSpace" format="dimension" />
|
||||
<attr name="barMinHeight" format="dimension" />
|
||||
<attr name="isBarRounded" format="boolean" />
|
||||
</declare-styleable>
|
||||
</resources>
|
Loading…
Reference in New Issue