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>
|
</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_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="30dp"
|
||||||
android:layout_gravity="center_vertical"/>
|
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>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -287,4 +287,20 @@
|
|||||||
<attr name="labeledEditText_background" format="color" />
|
<attr name="labeledEditText_background" format="color" />
|
||||||
<attr name="labeledEditText_textLayout" format="reference" />
|
<attr name="labeledEditText_textLayout" format="reference" />
|
||||||
</declare-styleable>
|
</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>
|
</resources>
|
||||||
|
@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.MediaSource;
|
|||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentServer;
|
import org.thoughtcrime.securesms.attachments.AttachmentServer;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||||
@ -150,7 +151,11 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
|
|
||||||
case Player.STATE_ENDED:
|
case Player.STATE_ENDED:
|
||||||
Log.i(TAG, "onComplete");
|
Log.i(TAG, "onComplete");
|
||||||
|
|
||||||
|
long millis = mediaPlayer.getDuration();
|
||||||
|
|
||||||
synchronized (AudioSlidePlayer.this) {
|
synchronized (AudioSlidePlayer.this) {
|
||||||
|
mediaPlayer.release();
|
||||||
mediaPlayer = null;
|
mediaPlayer = null;
|
||||||
|
|
||||||
if (audioAttachmentServer != null) {
|
if (audioAttachmentServer != null) {
|
||||||
@ -167,6 +172,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyOnProgress(1.0, millis);
|
||||||
notifyOnStop();
|
notifyOnStop();
|
||||||
progressEventHandler.removeMessages(0);
|
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) {
|
public void setListener(@NonNull Listener listener) {
|
||||||
this.listener = new WeakReference<>(listener);
|
this.listener = new WeakReference<>(listener);
|
||||||
|
|
||||||
@ -256,30 +278,15 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void notifyOnStart() {
|
private void notifyOnStart() {
|
||||||
Util.runOnMain(new Runnable() {
|
Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this));
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
getListener().onStart();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyOnStop() {
|
private void notifyOnStop() {
|
||||||
Util.runOnMain(new Runnable() {
|
Util.runOnMain(() -> getListener().onPlayerStop(AudioSlidePlayer.this));
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
getListener().onStop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyOnProgress(final double progress, final long millis) {
|
private void notifyOnProgress(final double progress, final long millis) {
|
||||||
Util.runOnMain(new Runnable() {
|
Util.runOnMain(() -> getListener().onPlayerProgress(AudioSlidePlayer.this, progress, millis));
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
getListener().onProgress(progress, millis);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull Listener getListener() {
|
private @NonNull Listener getListener() {
|
||||||
@ -288,11 +295,11 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
if (listener != null) return listener;
|
if (listener != null) return listener;
|
||||||
else return new Listener() {
|
else return new Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {}
|
public void onPlayerStart(@NotNull AudioSlidePlayer player) { }
|
||||||
@Override
|
@Override
|
||||||
public void onStop() {}
|
public void onPlayerStop(@NotNull AudioSlidePlayer player) { }
|
||||||
@Override
|
@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 {
|
public interface Listener {
|
||||||
void onStart();
|
void onPlayerStart(@NonNull AudioSlidePlayer player);
|
||||||
void onStop();
|
void onPlayerStop(@NonNull AudioSlidePlayer player);
|
||||||
void onProgress(double progress, long millis);
|
void onPlayerProgress(@NonNull AudioSlidePlayer player, double progress, long millis);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ProgressEventHandler extends Handler {
|
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.AudioSlide
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener
|
import org.thoughtcrime.securesms.mms.SlideClickListener
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
|||||||
private val pauseButton: ImageView
|
private val pauseButton: ImageView
|
||||||
private val downloadButton: ImageView
|
private val downloadButton: ImageView
|
||||||
private val downloadProgress: ProgressWheel
|
private val downloadProgress: ProgressWheel
|
||||||
private val seekBar: SeekBar
|
private val seekBar: WaveformSeekBar
|
||||||
private val timestamp: TextView
|
private val timestamp: TextView
|
||||||
|
|
||||||
private var downloadListener: SlideClickListener? = null
|
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) {
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
|
||||||
View.inflate(context, R.layout.audio_view, this)
|
View.inflate(context, R.layout.audio_view, this)
|
||||||
container = findViewById<View>(R.id.audio_widget_container) as ViewGroup
|
container = findViewById(R.id.audio_widget_container)
|
||||||
controlToggle = findViewById<View>(R.id.control_toggle) as AnimatingToggle
|
controlToggle = findViewById(R.id.control_toggle)
|
||||||
playButton = findViewById<View>(R.id.play) as ImageView
|
playButton = findViewById(R.id.play)
|
||||||
pauseButton = findViewById<View>(R.id.pause) as ImageView
|
pauseButton = findViewById(R.id.pause)
|
||||||
downloadButton = findViewById<View>(R.id.download) as ImageView
|
downloadButton = findViewById(R.id.download)
|
||||||
downloadProgress = findViewById<View>(R.id.download_progress) as ProgressWheel
|
downloadProgress = findViewById(R.id.download_progress)
|
||||||
seekBar = findViewById<View>(R.id.seek) as SeekBar
|
seekBar = findViewById(R.id.seek)
|
||||||
timestamp = findViewById<View>(R.id.timestamp) as TextView
|
timestamp = findViewById(R.id.timestamp)
|
||||||
|
|
||||||
playButton.setOnClickListener {
|
playButton.setOnClickListener {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "playbutton onClick")
|
Log.d(TAG, "playbutton onClick")
|
||||||
if (audioSlidePlayer != null) {
|
if (audioSlidePlayer != null) {
|
||||||
togglePlayToPause()
|
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) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, e)
|
Log.w(TAG, e)
|
||||||
@ -85,7 +90,17 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
|||||||
audioSlidePlayer!!.stop()
|
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))
|
playButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.play_icon))
|
||||||
pauseButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.pause_icon))
|
pauseButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.pause_icon))
|
||||||
@ -153,25 +168,41 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
|||||||
downloadProgress.barColor = foregroundTint
|
downloadProgress.barColor = foregroundTint
|
||||||
timestamp.setTextColor(foregroundTint)
|
timestamp.setTextColor(foregroundTint)
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onPlayerStart(player: AudioSlidePlayer) {
|
||||||
if (pauseButton.visibility != View.VISIBLE) {
|
if (pauseButton.visibility != View.VISIBLE) {
|
||||||
togglePlayToPause()
|
togglePlayToPause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onPlayerStop(player: AudioSlidePlayer) {
|
||||||
if (playButton.visibility != View.VISIBLE) {
|
if (playButton.visibility != View.VISIBLE) {
|
||||||
togglePauseToPlay()
|
togglePauseToPlay()
|
||||||
}
|
}
|
||||||
if (seekBar.progress + 5 >= seekBar.max) {
|
|
||||||
backwardsCounter = 4
|
// if (seekBar.progress + 5 >= seekBar.max) {
|
||||||
onProgress(0.0, 0)
|
// 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) {
|
override fun setFocusable(focusable: Boolean) {
|
||||||
@ -201,27 +232,6 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
|||||||
downloadButton.isEnabled = enabled
|
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() {
|
private fun togglePlayToPause() {
|
||||||
controlToggle.displayQuick(pauseButton)
|
controlToggle.displayQuick(pauseButton)
|
||||||
val playToPauseDrawable = ContextCompat.getDrawable(context, R.drawable.play_to_pause_animation) as AnimatedVectorDrawable
|
val playToPauseDrawable = ContextCompat.getDrawable(context, R.drawable.play_to_pause_animation) as AnimatedVectorDrawable
|
||||||
@ -236,27 +246,27 @@ class AudioView: FrameLayout, AudioSlidePlayer.Listener {
|
|||||||
pauseToPlayDrawable.start()
|
pauseToPlayDrawable.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class SeekBarModifiedListener : OnSeekBarChangeListener {
|
// private inner class SeekBarModifiedListener : OnSeekBarChangeListener {
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
// override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
||||||
|
//
|
||||||
@Synchronized
|
// @Synchronized
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
// override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||||
if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
|
// if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
|
||||||
audioSlidePlayer!!.stop()
|
// audioSlidePlayer!!.stop()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Synchronized
|
// @Synchronized
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
// override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
try {
|
// try {
|
||||||
if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
|
// if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
|
||||||
audioSlidePlayer!!.play(getProgress())
|
// audioSlidePlayer!!.play(getProgress())
|
||||||
}
|
// }
|
||||||
} catch (e: IOException) {
|
// } catch (e: IOException) {
|
||||||
Log.w(TAG, e)
|
// Log.w(TAG, e)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||||
fun onEventAsync(event: PartProgressEvent) {
|
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