Updated PlayerFragment to MvRx
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
index 0a0a65d..de1c360 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
@@ -4,8 +4,6 @@
 import android.annotation.SuppressLint
 import android.app.AlertDialog
 import android.arch.lifecycle.Lifecycle
-import android.arch.lifecycle.Observer
-import android.arch.lifecycle.ViewModelProviders
 import android.graphics.Color
 import android.os.Bundle
 import android.support.design.widget.BottomSheetBehavior
@@ -29,7 +27,9 @@
 import com.airbnb.lottie.samples.views.BottomSheetItemView
 import com.airbnb.lottie.samples.views.BottomSheetItemViewModel_
 import com.airbnb.lottie.samples.views.ControlBarItemToggleView
-import com.airbnb.lottie.utils.MiscUtils
+import com.airbnb.mvrx.BaseMvRxFragment
+import com.airbnb.mvrx.fragmentViewModel
+import com.airbnb.mvrx.withState
 import com.github.mikephil.charting.components.LimitLine
 import com.github.mikephil.charting.components.YAxis
 import com.github.mikephil.charting.data.Entry
@@ -47,31 +47,10 @@
 import kotlinx.android.synthetic.main.fragment_player.*
 import kotlin.math.min
 import kotlin.math.roundToInt
-import kotlin.properties.ObservableProperty
-import kotlin.reflect.KProperty
 
-private class UiState(private val callback: () -> Unit) {
-
-    private inner class BooleanProperty(initialValue: Boolean) : ObservableProperty<Boolean>(initialValue) {
-        override fun afterChange(property: KProperty<*>, oldValue: Boolean, newValue: Boolean) {
-            callback()
-        }
-    }
-
-    var controls by BooleanProperty(true)
-    var controlBar by BooleanProperty(true)
-    var renderGraph by BooleanProperty(false)
-    var border by BooleanProperty(false)
-    var backgroundColor by BooleanProperty(false)
-    var scale by BooleanProperty(false)
-    var speed by BooleanProperty(false)
-    var trim by BooleanProperty(false)
-}
-
-class PlayerFragment : Fragment() {
+class PlayerFragment : BaseMvRxFragment() {
 
     private val transition = AutoTransition().apply { duration = 175 }
-    private val uiState = UiState { updateUiFromState() }
     private val renderTimesBehavior by lazy {
         BottomSheetBehavior.from(renderTimesBottomSheet).apply {
             peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
@@ -98,10 +77,6 @@
             color = Color.BLACK
         }
     }
-    private var composition: LottieComposition? = null
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
-            inflater.inflate(R.layout.fragment_player, container, false)
 
     private val animatorListener = AnimatorListenerAdapter(
             onStart = { playButton.isActivated = true },
@@ -109,20 +84,20 @@
                 playButton.isActivated = false
                 animationView.performanceTracker?.logRenderTimes()
                 updateRenderTimesPerLayer()
-                updateWarnings()
             },
             onCancel = {
                 playButton.isActivated = false
-                updateWarnings()
             },
             onRepeat = {
                 animationView.performanceTracker?.logRenderTimes()
                 updateRenderTimesPerLayer()
-                updateWarnings()
             }
     )
 
-    private val viewModel by lazy { ViewModelProviders.of(this).get(PlayerViewModel::class.java) }
+    private val viewModel: PlayerViewModel by fragmentViewModel()
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+            inflater.inflate(R.layout.fragment_player, container, false)
 
     @SuppressLint("SetTextI18n")
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -135,45 +110,150 @@
 
         lottieVersionView.text = getString(R.string.lottie_version, com.airbnb.lottie.BuildConfig.VERSION_NAME)
 
-        val args = arguments?.getParcelable<CompositionArgs>(EXTRA_ANIMATION_ARGS) ?: throw IllegalArgumentException("No composition args specified")
+        val args = arguments?.getParcelable<CompositionArgs>(EXTRA_ANIMATION_ARGS)
+                ?: throw IllegalArgumentException("No composition args specified")
         args.animationData?.bgColorInt()?.let {
             backgroundButton1.setBackgroundColor(it)
             animationContainer.setBackgroundColor(it)
             invertColor(it)
         }
 
-        viewModel.composition.observe(this, Observer {
+        minFrameView.setOnClickListener {
+            val minFrameView = EditText(context)
+            minFrameView.setText(animationView.minFrame.toInt().toString())
+            AlertDialog.Builder(context)
+                    .setTitle(R.string.min_frame_dialog)
+                    .setView(minFrameView)
+                    .setPositiveButton("Load") { _, _ ->
+                        viewModel.setMinFrame(minFrameView.text.toString().toIntOrNull() ?: 0)
+                    }
+                    .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
+                    .show()
+        }
+
+        maxFrameView.setOnClickListener {
+            val maxFrameView = EditText(context)
+            maxFrameView.setText(animationView.maxFrame.toInt().toString())
+            AlertDialog.Builder(context)
+                    .setTitle(R.string.max_frame_dialog)
+                    .setView(maxFrameView)
+                    .setPositiveButton("Load") { _, _ ->
+                        viewModel.setMaxFrame(maxFrameView.text.toString().toIntOrNull() ?: 0)
+                    }
+                    .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
+                    .show()
+        }
+
+        viewModel.selectSubscribe(PlayerState::minFrame, PlayerState::maxFrame) { minFrame, maxFrame ->
+            animationView.setMinAndMaxFrame(minFrame, maxFrame)
+            // I think this is a lint bug. It complains about int being <ErrorType>
+            //noinspection StringFormatMatches
+            minFrameView.setText(resources.getString(R.string.min_frame, animationView.minFrame.toInt()))
+            //noinspection StringFormatMatches
+            maxFrameView.setText(resources.getString(R.string.max_frame, animationView.maxFrame.toInt()))
+        }
+
+        viewModel.fetchAnimation(args)
+        viewModel.asyncSubscribe(PlayerState::composition, onFail = {
+            Snackbar.make(coordinatorLayout, R.string.composition_load_error, Snackbar.LENGTH_LONG).show()
+            Log.w(L.TAG, "Error loading composition.", it);
+        }) {
             loadingView.isVisible = false
             onCompositionLoaded(it)
-        })
-        viewModel.error.observe(this, Observer {
-            Snackbar.make(coordinatorLayout, R.string.composition_load_error, Snackbar.LENGTH_LONG).show()
-            Log.w(L.TAG, "Error loading composition.", viewModel.error.value);
-        })
-        viewModel.fetchAnimation(args)
-
-        borderToggle.setOnClickListener { uiState.border++ }
-        backgroundColorToggle.setOnClickListener { uiState.backgroundColor++ }
-        scaleToggle.setOnClickListener { uiState.scale++ }
-        speedToggle.setOnClickListener { uiState.speed++ }
-        trimToggle.setOnClickListener { uiState.trim++ }
-        renderGraphToggle.setOnClickListener { uiState.renderGraph++ }
-
-        closeBackgroundColorButton.setOnClickListener { uiState.backgroundColor = false }
-        closeScaleButton.setOnClickListener { uiState.scale = false }
-        closeSpeedButton.setOnClickListener { uiState.speed = false }
-        closeTrimButton.setOnClickListener { uiState.trim = false }
-
-        hardwareAccelerationToggle.setOnClickListener {
-            animationView.useHardwareAcceleration(!animationView.useHardwareAcceleration)
-            updateUiFromState()
         }
 
-        mergePathsToggle.setOnClickListener {
-            animationView.enableMergePathsForKitKatAndAbove(!animationView.isMergePathsEnabledForKitKatAndAbove)
-            updateUiFromState()
+        borderToggle.setOnClickListener { viewModel.toggleBorderVisible() }
+        viewModel.selectSubscribe(PlayerState::borderVisible) {
+            borderToggle.isActivated = it
+            borderToggle.setImageResource(
+                    if (it) R.drawable.ic_border_on
+                    else R.drawable.ic_border_off
+            )
+            animationView.setBackgroundResource(if (it) R.drawable.outline else 0)
         }
 
+        viewModel.selectSubscribe(PlayerState::controlsVisible) { controlsContainer.animateVisible(it) }
+
+        viewModel.selectSubscribe(PlayerState::controlBarVisible) { controlBar.animateVisible(it) }
+
+        renderGraphToggle.setOnClickListener { viewModel.toggleRenderGraphVisible() }
+        viewModel.selectSubscribe(PlayerState::renderGraphVisible) {
+            renderGraphToggle.isActivated = it
+            renderTimesGraphContainer.animateVisible(it)
+            renderTimesPerLayerButton.animateVisible(it)
+            lottieVersionView.animateVisible(!it)
+        }
+
+        backgroundColorToggle.setOnClickListener { viewModel.toggleBackgroundColorVisible() }
+        closeBackgroundColorButton.setOnClickListener { viewModel.setBackgroundColorVisible(false) }
+        viewModel.selectSubscribe(PlayerState::backgroundColorVisible) {
+            backgroundColorToggle.isActivated = it
+            backgroundColorContainer.animateVisible(it)
+        }
+
+        scaleToggle.setOnClickListener { viewModel.toggleScaleVisible() }
+        closeScaleButton.setOnClickListener { viewModel.setScaleVisible(false) }
+        viewModel.selectSubscribe(PlayerState::scaleVisible) {
+            scaleToggle.isActivated = it
+            scaleContainer.animateVisible(it)
+        }
+
+        trimToggle.setOnClickListener { viewModel.toggleTrimVisible() }
+        closeTrimButton.setOnClickListener { viewModel.setTrimVisible(false) }
+        viewModel.selectSubscribe(PlayerState::trimVisible) {
+            trimToggle.isActivated = it
+            trimContainer.animateVisible(it)
+        }
+
+        hardwareAccelerationToggle.setOnClickListener { viewModel.toggleHardwareAcceleration() }
+        viewModel.selectSubscribe(PlayerState::useHardwareAcceleration) {
+            hardwareAccelerationToggle.isActivated = it
+            animationView.useHardwareAcceleration(it)
+        }
+
+        mergePathsToggle.setOnClickListener { viewModel.toggleMergePaths() }
+        viewModel.selectSubscribe(PlayerState::useMergePaths) {
+            animationView.enableMergePathsForKitKatAndAbove(it)
+            mergePathsToggle.isActivated = it
+        }
+
+        speedToggle.setOnClickListener { viewModel.toggleSpeedVisible() }
+        closeSpeedButton.setOnClickListener { viewModel.setSpeedVisible(false) }
+        viewModel.selectSubscribe(PlayerState::speedVisible) {
+            speedToggle.isActivated = it
+            speedContainer.isVisible = it
+        }
+        viewModel.selectSubscribe(PlayerState::speed) {
+            animationView.speed = it
+            speedButtonsContainer
+                    .children
+                    .filterIsInstance<ControlBarItemToggleView>()
+                    .forEach { toggleView ->
+                        toggleView.isActivated = toggleView.getText().replace("x", "").toFloat() == animationView.speed
+                    }
+        }
+        speedButtonsContainer
+                .children
+                .filterIsInstance(ControlBarItemToggleView::class.java)
+                .forEach { child ->
+                    child.setOnClickListener {
+                        val speed = (it as ControlBarItemToggleView)
+                                .getText()
+                                .replace("x", "")
+                                .toFloat()
+                        viewModel.setSpeed(speed)
+                    }
+                }
+
+
+        loopButton.setOnClickListener { viewModel.toggleLoop() }
+        viewModel.selectSubscribe(PlayerState::repeatCount) {
+            animationView.repeatCount = it
+            loopButton.isActivated = animationView.repeatCount == ValueAnimator.INFINITE
+        }
+
+        playButton.isActivated = animationView.isAnimating
+
         seekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
                 onProgressChanged = { _, progress, _ ->
                     if (seekBar.isPressed && progress in 1..4) {
@@ -185,8 +265,6 @@
                 }
         ))
 
-        animationView.repeatCount = ValueAnimator.INFINITE
-
         animationView.addAnimatorUpdateListener {
             currentFrameView.text = updateFramesAndDurationLabel(animationView)
 
@@ -196,13 +274,7 @@
         animationView.addAnimatorListener(animatorListener)
         playButton.setOnClickListener {
             if (animationView.isAnimating) animationView.pauseAnimation() else animationView.resumeAnimation()
-            updateUiFromState()
-        }
-
-        loopButton.setOnClickListener {
-            val repeatCount = if (animationView.repeatCount == ValueAnimator.INFINITE) 0 else ValueAnimator.INFINITE
-            animationView.repeatCount = repeatCount
-            updateUiFromState()
+            postInvalidate()
         }
 
         scaleSeekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
@@ -215,39 +287,6 @@
                 }
         ))
 
-        minFrame.setOnClickListener {
-            val minFrameView = EditText(context)
-            minFrameView.setText(animationView.minFrame.toInt().toString())
-            AlertDialog.Builder(context)
-                    .setTitle(R.string.min_frame_dialog)
-                    .setView(minFrameView)
-                    .setPositiveButton("Load") { _, _ ->
-                        var frame = minFrameView.text.toString().toFloatOrNull() ?: 0f
-                        frame = MiscUtils.clamp(frame, composition?.startFrame ?: frame, animationView.maxFrame)
-
-                        animationView.setMinFrame(frame.toInt())
-                        updateUiFromState()
-                    }
-                    .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
-                    .show()
-        }
-
-        maxFrame.setOnClickListener {
-            val maxFrameView = EditText(context)
-            maxFrameView.setText(animationView.maxFrame.toInt().toString())
-            AlertDialog.Builder(context)
-                    .setTitle(R.string.max_frame_dialog)
-                    .setView(maxFrameView)
-                    .setPositiveButton("Load") { _, _ ->
-                        var frame = maxFrameView.text.toString().toFloatOrNull() ?: 0f
-                        frame = MiscUtils.clamp(frame, animationView.minFrame, composition?.endFrame ?: frame)
-                        animationView.setMaxFrame(frame.toInt())
-                        updateUiFromState()
-                    }
-                    .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
-                    .show()
-        }
-
         arrayOf<BackgroundColorView>(
                 backgroundButton1,
                 backgroundButton2,
@@ -286,18 +325,6 @@
             axisLeft.addLimitLine(ll2)
         }
 
-        speedButtonsContainer.children.forEach {
-            if (it is ControlBarItemToggleView) {
-                it.setOnClickListener {
-                    animationView.speed = (it as ControlBarItemToggleView)
-                            .getText()
-                            .replace("x", "")
-                            .toFloat()
-                    updateUiFromState()
-                }
-            }
-        }
-
         renderTimesPerLayerButton.setOnClickListener {
             updateRenderTimesPerLayer()
             renderTimesBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
@@ -309,9 +336,11 @@
         renderTimesBehavior.state = BottomSheetBehavior.STATE_HIDDEN
 
         warningsButton.setOnClickListener {
-            updateWarnings()
-            if (composition?.warnings?.isEmpty() != true) {
-                warningsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
+            withState(viewModel) { state ->
+                if (state.composition()?.warnings?.isEmpty() != true) {
+                    warningsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
+
+                }
             }
         }
 
@@ -328,20 +357,11 @@
             keyPathsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
         }
         keyPathsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
+    }
 
-        keyPathsRecyclerView.buildModelsWith { controller ->
-            composition?.let {
-                animationView.resolveKeyPath(KeyPath("**")).forEachIndexed { index, keyPath ->
-                    BottomSheetItemViewModel_()
-                            .id(index)
-                            .text(keyPath.keysToString())
-                            .addTo(controller)
-
-                }
-            }
-        }
-
-        updateUiFromState()
+    private fun View.animateVisible(visible: Boolean) {
+        beginDelayedTransition()
+        isVisible = visible
     }
 
     private fun invertColor(color: Int) {
@@ -371,15 +391,7 @@
             android.R.id.home -> requireActivity().finish()
             R.id.info -> Unit
             R.id.visibility -> {
-                uiState.controls = !item.isChecked
-                uiState.controlBar = !item.isChecked
-                uiState.renderGraph = false
-                uiState.border = false
-                uiState.backgroundColor = false
-                uiState.scale = false
-                uiState.speed = false
-                uiState.trim = false
-                updateUiFromState()
+                viewModel.setDistractionFree(item.isChecked)
                 val menuIcon = if (item.isChecked) R.drawable.ic_eye_teal else R.drawable.ic_eye_selector
                 item.icon = ContextCompat.getDrawable(requireContext(), menuIcon)
             }
@@ -389,10 +401,8 @@
 
     private fun onCompositionLoaded(composition: LottieComposition?) {
         composition ?: return
-        this.composition = composition
 
         animationView.setComposition(composition)
-        animationView.setMinAndMaxFrame(composition.startFrame.toInt(), composition.endFrame.toInt())
         animationView.setPerformanceTrackingEnabled(true)
         var renderTimeGraphRange = 4f
         animationView.performanceTracker?.addFrameListener { ms ->
@@ -403,61 +413,23 @@
             renderTimesGraph.invalidate()
         }
 
-        // Force warning to update
-        warningsContainer.removeAllViews()
-        updateWarnings()
-
         // Scale up to fill the screen
         scaleSeekBar.progress = 100
 
-        keyPathsRecyclerView.requestModelBuild()
-    }
+        keyPathsRecyclerView.buildModelsWith { controller ->
+            animationView.resolveKeyPath(KeyPath("**")).forEachIndexed { index, keyPath ->
+                BottomSheetItemViewModel_()
+                        .id(index)
+                        .text(keyPath.keysToString())
+                        .addTo(controller)
 
-    private fun updateUiFromState() {
-        beginDelayedTransition()
-
-        controlsContainer.isVisible = uiState.controls
-        controlBar.isVisible = uiState.controlBar
-
-        renderGraphToggle.isActivated = uiState.renderGraph
-        renderTimesGraphContainer.isVisible = uiState.renderGraph
-        renderTimesPerLayerButton.isVisible = uiState.renderGraph
-        lottieVersionView.isVisible = !uiState.renderGraph
-
-        borderToggle.isActivated = uiState.border
-        borderToggle.setImageResource(
-                if (uiState.border) R.drawable.ic_border_on
-                else R.drawable.ic_border_off
-        )
-        animationView.setBackgroundResource(if (borderToggle.isActivated) R.drawable.outline else 0)
-
-        backgroundColorToggle.isActivated = uiState.backgroundColor
-        backgroundColorContainer.isVisible = uiState.backgroundColor
-
-        scaleToggle.isActivated = uiState.scale
-        scaleContainer.isVisible = uiState.scale
-
-        trimToggle.isActivated = uiState.trim
-        trimContainer.isVisible = uiState.trim
-        // I think this is a lint bug. It complains about int being <ErrorType>
-        //noinspection StringFormatMatches
-        minFrame.setText(resources.getString(R.string.min_frame, animationView.minFrame.toInt()))
-        //noinspection StringFormatMatches
-        maxFrame.setText(resources.getString(R.string.max_frame, animationView.maxFrame.toInt()))
-
-        hardwareAccelerationToggle.isActivated = animationView.useHardwareAcceleration
-        mergePathsToggle.isActivated = animationView.isMergePathsEnabledForKitKatAndAbove
-
-        speedToggle.isActivated = uiState.speed
-        speedContainer.isVisible = uiState.speed
-        speedButtonsContainer.children.forEach {
-            if (it is ControlBarItemToggleView) {
-                it.isActivated = it.getText().replace("x", "").toFloat() == animationView.speed
             }
         }
 
-        loopButton.isActivated = animationView.repeatCount == ValueAnimator.INFINITE
-        playButton.isActivated = animationView.isAnimating
+        updateWarnings()
+    }
+
+    override fun invalidate() {
     }
 
     private fun updateRenderTimesPerLayer() {
@@ -473,9 +445,12 @@
         }
     }
 
-    private fun updateWarnings() {
-        val warnings = composition?.warnings ?: emptySet<String>()
-        if (!warnings.isEmpty() && warnings.size == warningsContainer.childCount) return
+    private fun updateWarnings() = withState(viewModel) { state ->
+        // Force warning to update
+        warningsContainer.removeAllViews()
+
+        val warnings = state.composition()?.warnings ?: emptySet<String>()
+        if (!warnings.isEmpty() && warnings.size == warningsContainer.childCount) return@withState
 
         warningsContainer.removeAllViews()
         warnings.forEach {
@@ -495,12 +470,13 @@
 
     private fun minScale() = 0.15f
 
-    private fun maxScale(): Float {
+    private fun maxScale(): Float = withState(viewModel) { state ->
         val screenWidth = resources.displayMetrics.widthPixels.toFloat()
         val screenHeight = resources.displayMetrics.heightPixels.toFloat()
-        return min(
-                screenWidth / (composition?.bounds?.width()?.toFloat() ?: screenWidth),
-                screenHeight / (composition?.bounds?.height()?.toFloat() ?: screenHeight)
+        val bounds = state.composition()?.bounds
+        return@withState min(
+                screenWidth / (bounds?.width()?.toFloat() ?: screenWidth),
+                screenHeight / (bounds?.height()?.toFloat() ?: screenHeight)
         )
     }
 
@@ -524,7 +500,7 @@
 
         val animationSpeed: Float = Math.abs(animation.speed)
 
-		val totalTime = ((animation.duration / animationSpeed) / 1000.0)
+        val totalTime = ((animation.duration / animationSpeed) / 1000.0)
         val totalTimeFormatted = ("%.1f").format(totalTime)
 
         val progress = (totalTime / 100.0) * (Math.round(animation.progress * 100.0))
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
index ac90cc8..f70d592 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
@@ -1,54 +1,122 @@
 package com.airbnb.lottie.samples
 
+import android.animation.ValueAnimator
 import android.app.Application
-import android.arch.lifecycle.AndroidViewModel
-import android.arch.lifecycle.MutableLiveData
 import android.net.Uri
-import android.os.Handler
-import android.os.Looper
+import android.support.v4.app.FragmentActivity
 import com.airbnb.lottie.LottieComposition
 import com.airbnb.lottie.LottieCompositionFactory
 import com.airbnb.lottie.LottieTask
 import com.airbnb.lottie.samples.model.CompositionArgs
+import com.airbnb.mvrx.*
 import java.io.FileInputStream
 import java.io.FileNotFoundException
 
-class PlayerViewModel(application: Application) : AndroidViewModel(application) {
+data class PlayerState(
+        val composition: Async<LottieComposition> = Uninitialized,
+        val controlsVisible: Boolean = true,
+        val controlBarVisible: Boolean = true,
+        val renderGraphVisible: Boolean = false,
+        val borderVisible: Boolean = false,
+        val backgroundColorVisible: Boolean = false,
+        val scaleVisible: Boolean = false,
+        val speedVisible: Boolean = false,
+        val trimVisible: Boolean = false,
+        val useHardwareAcceleration: Boolean = false,
+        val useMergePaths: Boolean = false,
+        val minFrame: Int = 0,
+        val maxFrame: Int = 0,
+        val speed: Float = 1f,
+        val repeatCount: Int = ValueAnimator.INFINITE
+) : MvRxState
 
-    private val handler = Handler(Looper.getMainLooper())
-
-    val composition = MutableLiveData<LottieComposition>()
-    val error = MutableLiveData<Throwable>()
+class PlayerViewModel(
+        initialState: PlayerState,
+        private val application: Application
+) : MvRxViewModel<PlayerState>(initialState) {
 
     fun fetchAnimation(args: CompositionArgs) {
         val url = args.url ?: args.animationData?.lottieLink
 
-        val task = when {
-            url != null -> LottieCompositionFactory.fromUrl(getApplication(), url)
+        when {
+            url != null -> LottieCompositionFactory.fromUrl(application, url)
             args.fileUri != null -> taskForUri(args.fileUri)
-            args.asset != null -> LottieCompositionFactory.fromAsset(getApplication(), args.asset)
+            args.asset != null -> LottieCompositionFactory.fromAsset(application, args.asset)
             else -> throw IllegalArgumentException("Don't know how to fetch animation for $args")
         }
-        registerTask(task)
+                .addListener {
+                    setState {
+                        copy(composition = Success(it), minFrame = it.startFrame.toInt(), maxFrame = it.endFrame.toInt())
+                    }
+                }
+                .addFailureListener { setState { copy(composition = Fail(it)) } }
     }
 
     private fun taskForUri(uri: Uri): LottieTask<LottieComposition> {
         val fis = try {
             when (uri.scheme) {
                 "file" -> FileInputStream(uri.path)
-                "content" -> getApplication<LottieApplication>().contentResolver.openInputStream(uri)
+                "content" -> application.contentResolver.openInputStream(uri)
                 else -> return LottieTask() { throw IllegalArgumentException("Unknown scheme ${uri.scheme}") }
             }
         } catch (e: FileNotFoundException) {
-            return LottieTask() { throw e }
+            return LottieTask { throw e }
         }
 
         return LottieCompositionFactory.fromJsonInputStream(fis, uri.toString())
     }
 
-    private fun registerTask(task: LottieTask<LottieComposition>) {
-        task
-                .addListener { composition.value = it }
-                .addFailureListener { error.value = it }
+    fun toggleRenderGraphVisible() = setState { copy(renderGraphVisible = !renderGraphVisible) }
+
+    fun toggleBorderVisible() = setState { copy(borderVisible = !borderVisible) }
+
+    fun toggleBackgroundColorVisible() = setState { copy(backgroundColorVisible = !backgroundColorVisible) }
+
+    fun setBackgroundColorVisible(visible: Boolean) = setState { copy(backgroundColorVisible = visible) }
+
+    fun toggleScaleVisible() = setState { copy(scaleVisible = !scaleVisible) }
+
+    fun setScaleVisible(visible: Boolean) = setState { copy(scaleVisible = visible) }
+
+    fun toggleSpeedVisible() = setState { copy(speedVisible = !speedVisible) }
+
+    fun setSpeedVisible(visible: Boolean) = setState { copy(speedVisible = visible) }
+
+    fun toggleTrimVisible() = setState { copy(trimVisible = !trimVisible) }
+
+    fun setTrimVisible(visible: Boolean) = setState { copy(trimVisible = visible) }
+
+    fun toggleHardwareAcceleration() = setState { copy(useHardwareAcceleration = !useHardwareAcceleration) }
+
+    fun toggleMergePaths() = setState { copy(useMergePaths = !useMergePaths) }
+
+    fun setMinFrame(minFrame: Int) = setState {
+        copy(minFrame = Math.max(minFrame, composition()?.startFrame?.toInt() ?: 0))
+    }
+
+    fun setMaxFrame(maxFrame: Int) = setState {
+        copy(maxFrame = Math.min(maxFrame, composition()?.endFrame?.toInt() ?: 0))
+    }
+
+    fun setSpeed(speed: Float) = setState { copy(speed = speed) }
+
+    fun toggleLoop() = setState { copy(repeatCount = if (repeatCount == ValueAnimator.INFINITE) 0 else ValueAnimator.INFINITE) }
+
+    fun setDistractionFree(distractionFree: Boolean) = setState {
+        copy(
+                controlsVisible = !distractionFree,
+                controlBarVisible = !distractionFree,
+                renderGraphVisible = false,
+                borderVisible = false,
+                backgroundColorVisible = false,
+                scaleVisible = false,
+                speedVisible = false,
+                trimVisible = false
+        )
+    }
+
+    companion object : MvRxViewModelFactory<PlayerState> {
+        @JvmStatic
+        override fun create(activity: FragmentActivity, state: PlayerState) = PlayerViewModel(state, activity.application)
     }
 }
\ No newline at end of file
diff --git a/LottieSample/src/main/res/layout/control_bar.xml b/LottieSample/src/main/res/layout/control_bar.xml
index d4f61ed..c8a0d1d 100644
--- a/LottieSample/src/main/res/layout/control_bar.xml
+++ b/LottieSample/src/main/res/layout/control_bar.xml
@@ -22,9 +22,7 @@
             app:text="@string/control_bar_render_graph" />
         <com.airbnb.lottie.samples.views.ControlBarItemToggleView
             android:id="@+id/warningsButton"
-            style="@style/ControlBarItem"
-            app:src="@drawable/ic_device"
-            app:text="@string/control_bar_no_warnings" />
+            style="@style/ControlBarItem" />
         <com.airbnb.lottie.samples.views.ControlBarItemToggleView
             android:id="@+id/borderToggle"
             style="@style/ControlBarItem"
diff --git a/LottieSample/src/main/res/layout/control_bar_player_controls.xml b/LottieSample/src/main/res/layout/control_bar_player_controls.xml
index f909bb2..d7dc7f4 100644
--- a/LottieSample/src/main/res/layout/control_bar_player_controls.xml
+++ b/LottieSample/src/main/res/layout/control_bar_player_controls.xml
@@ -90,7 +90,8 @@
         android:padding="4dp"
         android:text="@string/render_times_per_layer_button"
         android:textColor="@color/item_selected_teal"
-        android:textSize="10sp"/>
+        android:textSize="10sp"
+        android:visibility="gone"/>
 
     <TextView
         android:id="@+id/lottieVersionView"
diff --git a/LottieSample/src/main/res/layout/control_bar_speed.xml b/LottieSample/src/main/res/layout/control_bar_speed.xml
index 107a739..34e54f3 100644
--- a/LottieSample/src/main/res/layout/control_bar_speed.xml
+++ b/LottieSample/src/main/res/layout/control_bar_speed.xml
@@ -4,7 +4,8 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/speedContainer"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:visibility="gone">
 
     <LinearLayout
         android:id="@+id/speedButtonsContainer"
diff --git a/LottieSample/src/main/res/layout/control_bar_trim.xml b/LottieSample/src/main/res/layout/control_bar_trim.xml
index b850b44..15a547c 100644
--- a/LottieSample/src/main/res/layout/control_bar_trim.xml
+++ b/LottieSample/src/main/res/layout/control_bar_trim.xml
@@ -4,7 +4,8 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/trimContainer"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:visibility="gone">
 
     <LinearLayout
         android:layout_width="wrap_content"
@@ -22,7 +23,7 @@
             android:layout_weight="0.5"/>
 
         <com.airbnb.lottie.samples.views.ControlBarItemToggleView
-            android:id="@+id/minFrame"
+            android:id="@+id/minFrameView"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"/>
 
@@ -32,7 +33,7 @@
             android:layout_weight="1"/>
 
         <com.airbnb.lottie.samples.views.ControlBarItemToggleView
-            android:id="@+id/maxFrame"
+            android:id="@+id/maxFrameView"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"/>
 
diff --git a/LottieSample/src/main/res/values/plurals.xml b/LottieSample/src/main/res/values/plurals.xml
index eda632d..42309d3 100644
--- a/LottieSample/src/main/res/values/plurals.xml
+++ b/LottieSample/src/main/res/values/plurals.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <plurals name="warnings">
-        <item quantity="zero">%d warnings</item>
+        <item quantity="zero">No warnings</item>
         <item quantity="one">%d warning</item>
         <item quantity="other">%d warnings</item>
     </plurals>
diff --git a/LottieSample/src/main/res/values/strings.xml b/LottieSample/src/main/res/values/strings.xml
index b213819..6dc70df 100644
--- a/LottieSample/src/main/res/values/strings.xml
+++ b/LottieSample/src/main/res/values/strings.xml
@@ -48,7 +48,6 @@
     <string name="control_bar_scale">Scale</string>
     <string name="control_bar_trim">Trim</string>
     <string name="control_bar_speed">Speed</string>
-    <string name="control_bar_no_warnings">No Warnings</string>
     <string name="control_bar_key_paths">Show KeyPaths</string>
     <string name="control_bar_merge_paths">Merge Paths (KitKat+)</string>
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 13a2093..eb1c7a2 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -326,6 +326,9 @@
    *    anything about that.
    */
   public void useHardwareAcceleration(boolean use) {
+    if (useHardwareLayer == use) {
+      return;
+    }
     useHardwareLayer = use;
     enableOrDisableHardwareLayer();
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index c97b94f..a2b3e4a 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -132,6 +132,10 @@
    * instead of using merge paths.
    */
   public void enableMergePathsForKitKatAndAbove(boolean enable) {
+    if (enableMergePaths == enable) {
+      return;
+    }
+
     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
       Log.w(TAG, "Merge paths are not supported pre-Kit Kat.");
       return;