Skip to content

Instantly share code, notes, and snippets.

@Pooh3Mobi
Last active March 18, 2018 02:00
Show Gist options
  • Save Pooh3Mobi/21a7290baf1c69bb4fb624fcc0e79432 to your computer and use it in GitHub Desktop.
Save Pooh3Mobi/21a7290baf1c69bb4fb624fcc0e79432 to your computer and use it in GitHub Desktop.
Kotlin-FRP Metronome sample code
class Formatters {
fun formatBpmInfo(msec: Long) = "$msec msec\n${(60000/msec).toFloat()} BPM"
}
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="mobi.pooh3.frpstudydummy.metronome.FRPSeekBarMetronomeFragment">
<SeekBar
android:id="@+id/seekBar"
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:max="1000"
android:progress="500"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/textView"
app:layout_constraintTop_toBottomOf="@+id/pulse_img"/>
<TextView
android:id="@+id/out_interval"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/seekBar"
app:layout_constraintEnd_toStartOf="@+id/pulse_img"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="interval"/>
<ImageView
android:id="@+id/pulse_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:src="@mipmap/ic_launcher_round"
app:layout_constraintBottom_toBottomOf="@+id/out_interval"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/out_interval"
app:layout_constraintTop_toTopOf="@+id/out_interval"/>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tepmpo"
app:layout_constraintBottom_toBottomOf="@+id/seekBar"
app:layout_constraintEnd_toStartOf="@+id/seekBar"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/seekBar"/>
</android.support.constraint.ConstraintLayout>
import android.media.ToneGenerator
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.jakewharton.rxbinding2.widget.text
import com.jakewharton.rxbinding2.widget.userChanges
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.withLatestFrom
import io.reactivex.subjects.BehaviorSubject
import kotlinx.android.synthetic.main.fragment_metronome.*
import mobi.pooh3.frpstudydummy.R
import java.util.concurrent.TimeUnit
class FRPSeekBarMetronomeFragment : Fragment() {
override fun onCreateView(inf: LayoutInflater?, ctr: ViewGroup?, savedInstanceState: Bundle?): View?
= inf!!.inflate(R.layout.fragment_rx_seekbar_interval, ctr, false)
override fun onViewCreated(v: View?, bdl: Bundle?) {
val sProgress = seekBar.userChanges()
.map { if (it <= 50) 50 else it }.share()
// inputs to outputs
val outputs = create(Inputs(
seekBar.progress.toLong(),
sProgress,
sProgress.debounce(500, TimeUnit.MILLISECONDS)
))
outputs.bpmText.subscribe(out_interval.text())
outputs.duration
.subscribe { duration ->
pulse_img?.also {
it.fadeOut(newDuration = duration)
ToneGeneratorHolder.instance()
.startTone(ToneGenerator.TONE_PROP_PROMPT)
}
}
}
fun create(inputs: Inputs): Outputs {
val mn = Metronome(
inputs.sProgress,
inputs.sDebouncedProgress,
inputs.defaultDuration)
return Outputs(
bpmText = mn.bpmText.map { Formatters().formatBpmInfo(it) },
duration = mn.duration
)
}
companion object {
fun newInstance(): FRPSeekBarMetronomeFragment {
return FRPSeekBarMetronomeFragment()
}
}
}
data class Inputs(val defaultDuration: Long, val sProgress: Observable<Int>, val sDebouncedProgress: Observable<Int>)
data class Outputs(
val duration: Observable<Long> = BehaviorSubject.createDefault(0L),
val bpmText: Observable<String> = BehaviorSubject.createDefault("")
)
class Metronome(sProgress: Observable<Int>, sDebouncedProgress: Observable<Int>, defaultDuration: Long) {
val bpmText: Observable<Long>
val duration: Observable<Long>
init {
val bpmTextValue = BehaviorSubject.createDefault(defaultDuration)
bpmText = bpmTextValue
sProgress.map { it.toLong() }.subscribe(bpmTextValue)
val durationValue = BehaviorSubject.createDefault(defaultDuration)
duration = durationValue
.switchInterval()
sDebouncedProgress
.map { it.toLong() }
.subscribe(durationValue)
}
}
private fun View.fadeOut(from: Float = 1f, to: Float = 0f, newDuration: Long) {
this.apply { this.alpha = from }
.animate()
.apply { duration = newDuration }
.alpha(to)
}
fun Observable<Long>.switchInterval(): Observable<Long> =
this.switchMap {
Observable.interval(it, TimeUnit.MILLISECONDS)
.withLatestFrom(this, {_, duration_ -> duration_})
.observeOn(AndroidSchedulers.mainThread())
}
import android.media.AudioManager
import android.media.ToneGenerator
object ToneGeneratorHolder {
private val toneGenerator: ToneGenerator by lazy { ToneGenerator(AudioManager.STREAM_SYSTEM, ToneGenerator.MAX_VOLUME) }
fun instance() = toneGenerator
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment