mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-24 10:35:19 +00:00
Use waveform seek bar for audio message view.
This commit is contained in:
parent
fdaadcb2b0
commit
e07cb716c0
@ -70,10 +70,18 @@
|
||||
|
||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
||||
|
||||
<SeekBar android:id="@+id/seek"
|
||||
<!-- TODO: Extract styling attributes into a theme. -->
|
||||
<org.thoughtcrime.securesms.components.WaveformSeekBar
|
||||
android:id="@+id/seek"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
app:wave_background_color="#bbb"
|
||||
app:wave_progress_color="?colorPrimary"
|
||||
app:wave_gravity="center"
|
||||
app:wave_width="4dp"
|
||||
app:wave_corner_radius="2dp"
|
||||
app:wave_gap="1dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -287,4 +287,20 @@
|
||||
<attr name="labeledEditText_background" format="color" />
|
||||
<attr name="labeledEditText_textLayout" format="reference" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="WaveformSeekBar">
|
||||
<attr name="wave_progress" format="float"/>
|
||||
<attr name="wave_width" format="dimension"/>
|
||||
<attr name="wave_gap" format="dimension"/>
|
||||
<attr name="wave_min_height" format="dimension"/>
|
||||
<attr name="wave_corner_radius" format="dimension"/>
|
||||
<attr name="wave_background_color" format="color"/>
|
||||
<attr name="wave_progress_color" format="color"/>
|
||||
<!-- Corresponds to WaveformSeekBar.WaveGravity enum. -->
|
||||
<attr name="wave_gravity" format="enum">
|
||||
<enum name="top" value="1" />
|
||||
<enum name="center" value="2" />
|
||||
<enum name="bottom" value="3" />
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
|
@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentServer;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
@ -150,7 +151,11 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
|
||||
case Player.STATE_ENDED:
|
||||
Log.i(TAG, "onComplete");
|
||||
|
||||
long millis = mediaPlayer.getDuration();
|
||||
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
mediaPlayer.release();
|
||||
mediaPlayer = null;
|
||||
|
||||
if (audioAttachmentServer != null) {
|
||||
@ -167,6 +172,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
notifyOnProgress(1.0, millis);
|
||||
notifyOnStop();
|
||||
progressEventHandler.removeMessages(0);
|
||||
}
|
||||
@ -233,6 +239,22 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isReady() {
|
||||
if (mediaPlayer == null) return false;
|
||||
|
||||
return mediaPlayer.getPlaybackState() == Player.STATE_READY && mediaPlayer.getPlayWhenReady();
|
||||
}
|
||||
|
||||
public synchronized void seekTo(double progress) throws IOException {
|
||||
if (mediaPlayer == null) return;
|
||||
|
||||
if (isReady()) {
|
||||
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
|
||||
} else {
|
||||
play(progress);
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(@NonNull Listener listener) {
|
||||
this.listener = new WeakReference<>(listener);
|
||||
|
||||
@ -256,30 +278,15 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
}
|
||||
|
||||
private void notifyOnStart() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStart();
|
||||
}
|
||||
});
|
||||
Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this));
|
||||
}
|
||||
|
||||
private void notifyOnStop() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStop();
|
||||
}
|
||||
});
|
||||
Util.runOnMain(() -> getListener().onPlayerStop(AudioSlidePlayer.this));
|
||||
}
|
||||
|
||||
private void notifyOnProgress(final double progress, final long millis) {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onProgress(progress, millis);
|
||||
}
|
||||
});
|
||||
Util.runOnMain(() -> getListener().onPlayerProgress(AudioSlidePlayer.this, progress, millis));
|
||||
}
|
||||
|
||||
private @NonNull Listener getListener() {
|
||||
@ -288,11 +295,11 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
if (listener != null) return listener;
|
||||
else return new Listener() {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
public void onPlayerStart(@NotNull AudioSlidePlayer player) { }
|
||||
@Override
|
||||
public void onStop() {}
|
||||
public void onPlayerStop(@NotNull AudioSlidePlayer player) { }
|
||||
@Override
|
||||
public void onProgress(double progress, long millis) {}
|
||||
public void onPlayerProgress(@NotNull AudioSlidePlayer player, double progress, long millis) { }
|
||||
};
|
||||
}
|
||||
|
||||
@ -355,9 +362,9 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onStart();
|
||||
void onStop();
|
||||
void onProgress(double progress, long millis);
|
||||
void onPlayerStart(@NonNull AudioSlidePlayer player);
|
||||
void onPlayerStop(@NonNull AudioSlidePlayer player);
|
||||
void onPlayerProgress(@NonNull AudioSlidePlayer player, double progress, long millis);
|
||||
}
|
||||
|
||||
private static class ProgressEventHandler extends Handler {
|
||||
|
@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.floor
|
||||
|
||||
@ -45,7 +46,7 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
||||
private val pauseButton: ImageView
|
||||
private val downloadButton: ImageView
|
||||
private val downloadProgress: ProgressWheel
|
||||
private val seekBar: SeekBar
|
||||
private val seekBar: WaveformSeekBar
|
||||
private val timestamp: TextView
|
||||
|
||||
private var downloadListener: SlideClickListener? = null
|
||||
@ -58,21 +59,25 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
|
||||
View.inflate(context, R.layout.audio_view, this)
|
||||
container = findViewById<View>(R.id.audio_widget_container) as ViewGroup
|
||||
controlToggle = findViewById<View>(R.id.control_toggle) as AnimatingToggle
|
||||
playButton = findViewById<View>(R.id.play) as ImageView
|
||||
pauseButton = findViewById<View>(R.id.pause) as ImageView
|
||||
downloadButton = findViewById<View>(R.id.download) as ImageView
|
||||
downloadProgress = findViewById<View>(R.id.download_progress) as ProgressWheel
|
||||
seekBar = findViewById<View>(R.id.seek) as SeekBar
|
||||
timestamp = findViewById<View>(R.id.timestamp) as TextView
|
||||
container = findViewById(R.id.audio_widget_container)
|
||||
controlToggle = findViewById(R.id.control_toggle)
|
||||
playButton = findViewById(R.id.play)
|
||||
pauseButton = findViewById(R.id.pause)
|
||||
downloadButton = findViewById(R.id.download)
|
||||
downloadProgress = findViewById(R.id.download_progress)
|
||||
seekBar = findViewById(R.id.seek)
|
||||
timestamp = findViewById(R.id.timestamp)
|
||||
|
||||
playButton.setOnClickListener {
|
||||
try {
|
||||
Log.d(TAG, "playbutton onClick")
|
||||
if (audioSlidePlayer != null) {
|
||||
togglePlayToPause()
|
||||
audioSlidePlayer!!.play(getProgress())
|
||||
|
||||
// Restart the playback if progress bar is near at the end.
|
||||
val progress = if (seekBar.progress < 0.99f) seekBar.progress.toDouble() else 0.0
|
||||
|
||||
audioSlidePlayer!!.play(progress)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
@ -85,7 +90,17 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
||||
audioSlidePlayer!!.stop()
|
||||
}
|
||||
}
|
||||
seekBar.setOnSeekBarChangeListener(SeekBarModifiedListener())
|
||||
seekBar.progressChangeListener = object : WaveformSeekBar.ProgressChangeListener {
|
||||
override fun onProgressChanged(waveformSeekBar: WaveformSeekBar, progress: Float, fromUser: Boolean) {
|
||||
if (fromUser && audioSlidePlayer != null) {
|
||||
synchronized(audioSlidePlayer!!) {
|
||||
audioSlidePlayer!!.seekTo(progress.toDouble())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO Remove this.
|
||||
seekBar.sample = Random().let { (0 until 64).map { i -> it.nextFloat() }.toFloatArray() }
|
||||
|
||||
playButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.play_icon))
|
||||
pauseButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.pause_icon))
|
||||
@ -153,25 +168,41 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
||||
downloadProgress.barColor = foregroundTint
|
||||
timestamp.setTextColor(foregroundTint)
|
||||
|
||||
val colorFilter = createBlendModeColorFilterCompat(foregroundTint, BlendModeCompat.SRC_IN)
|
||||
seekBar.progressDrawable.colorFilter = colorFilter
|
||||
seekBar.thumb.colorFilter = colorFilter
|
||||
// val colorFilter = createBlendModeColorFilterCompat(foregroundTint, BlendModeCompat.SRC_IN)
|
||||
// seekBar.progressDrawable.colorFilter = colorFilter
|
||||
// seekBar.thumb.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
override fun onPlayerStart(player: AudioSlidePlayer) {
|
||||
if (pauseButton.visibility != View.VISIBLE) {
|
||||
togglePlayToPause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
override fun onPlayerStop(player: AudioSlidePlayer) {
|
||||
if (playButton.visibility != View.VISIBLE) {
|
||||
togglePauseToPlay()
|
||||
}
|
||||
if (seekBar.progress + 5 >= seekBar.max) {
|
||||
backwardsCounter = 4
|
||||
onProgress(0.0, 0)
|
||||
}
|
||||
|
||||
// if (seekBar.progress + 5 >= seekBar.max) {
|
||||
// backwardsCounter = 4
|
||||
// onProgress(0.0, 0)
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, millis: Long) {
|
||||
// val seekProgress = floor(progress * seekBar.max).toInt()
|
||||
//TODO Update text.
|
||||
seekBar.progress = progress.toFloat()
|
||||
// if (/*seekProgress > 1f || */backwardsCounter > 3) {
|
||||
// backwardsCounter = 0
|
||||
// seekBar.progress = 1f
|
||||
// timestamp.text = String.format("%02d:%02d",
|
||||
// TimeUnit.MILLISECONDS.toMinutes(millis),
|
||||
// TimeUnit.MILLISECONDS.toSeconds(millis))
|
||||
// } else {
|
||||
// backwardsCounter++
|
||||
// }
|
||||
}
|
||||
|
||||
override fun setFocusable(focusable: Boolean) {
|
||||
@ -201,27 +232,6 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
||||
downloadButton.isEnabled = enabled
|
||||
}
|
||||
|
||||
override fun onProgress(progress: Double, millis: Long) {
|
||||
val seekProgress = floor(progress * seekBar.max).toInt()
|
||||
if (seekProgress > seekBar.progress || backwardsCounter > 3) {
|
||||
backwardsCounter = 0
|
||||
seekBar.progress = seekProgress
|
||||
timestamp.text = String.format("%02d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(millis),
|
||||
TimeUnit.MILLISECONDS.toSeconds(millis))
|
||||
} else {
|
||||
backwardsCounter++
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgress(): Double {
|
||||
return if (seekBar.progress <= 0 || seekBar.max <= 0) {
|
||||
0.0
|
||||
} else {
|
||||
seekBar.progress.toDouble() / seekBar.max.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
private fun togglePlayToPause() {
|
||||
controlToggle.displayQuick(pauseButton)
|
||||
val playToPauseDrawable = ContextCompat.getDrawable(context, R.drawable.play_to_pause_animation) as AnimatedVectorDrawable
|
||||
@ -236,27 +246,27 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
||||
pauseToPlayDrawable.start()
|
||||
}
|
||||
|
||||
private inner class SeekBarModifiedListener : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
||||
|
||||
@Synchronized
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
|
||||
audioSlidePlayer!!.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
try {
|
||||
if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
|
||||
audioSlidePlayer!!.play(getProgress())
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
// private inner class SeekBarModifiedListener : OnSeekBarChangeListener {
|
||||
// override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
||||
//
|
||||
// @Synchronized
|
||||
// override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
// if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
|
||||
// audioSlidePlayer!!.stop()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Synchronized
|
||||
// override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
// try {
|
||||
// if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
|
||||
// audioSlidePlayer!!.play(getProgress())
|
||||
// }
|
||||
// } catch (e: IOException) {
|
||||
// Log.w(TAG, e)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
fun onEventAsync(event: PartProgressEvent) {
|
||||
|
317
src/org/thoughtcrime/securesms/components/WaveformSeekBar.kt
Normal file
317
src/org/thoughtcrime/securesms/components/WaveformSeekBar.kt
Normal file
@ -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
Block a user