• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 package com.airbnb.lottie.samples
2 
3 import android.animation.ValueAnimator
4 import android.annotation.SuppressLint
5 import android.app.AlertDialog
6 import android.graphics.Color
7 import android.graphics.Typeface
8 import android.os.Bundle
9 import android.util.Log
10 import android.view.Menu
11 import android.view.MenuInflater
12 import android.view.MenuItem
13 import android.view.View
14 import android.widget.EditText
15 import androidx.appcompat.app.AppCompatActivity
16 import androidx.core.content.ContextCompat
17 import androidx.core.view.MenuHost
18 import androidx.core.view.MenuProvider
19 import androidx.core.view.children
20 import androidx.core.view.isVisible
21 import androidx.fragment.app.Fragment
22 import androidx.lifecycle.Lifecycle
23 import androidx.transition.AutoTransition
24 import androidx.transition.TransitionManager
25 import com.airbnb.epoxy.EpoxyController
26 import com.airbnb.epoxy.EpoxyRecyclerView
27 import com.airbnb.lottie.FontAssetDelegate
28 import com.airbnb.lottie.L
29 import com.airbnb.lottie.LottieAnimationView
30 import com.airbnb.lottie.LottieComposition
31 import com.airbnb.lottie.RenderMode
32 import com.airbnb.lottie.model.KeyPath
33 import com.airbnb.lottie.samples.databinding.PlayerFragmentBinding
34 import com.airbnb.lottie.samples.model.CompositionArgs
35 import com.airbnb.lottie.samples.utils.BaseFragment
36 import com.airbnb.lottie.samples.utils.getParcelableCompat
37 import com.airbnb.lottie.samples.utils.viewBinding
38 import com.airbnb.lottie.samples.views.BottomSheetItemView
39 import com.airbnb.lottie.samples.views.BottomSheetItemViewModel_
40 import com.airbnb.lottie.samples.views.ControlBarItemToggleView
41 import com.airbnb.mvrx.fragmentViewModel
42 import com.airbnb.mvrx.withState
43 import com.github.mikephil.charting.components.LimitLine
44 import com.github.mikephil.charting.components.YAxis
45 import com.github.mikephil.charting.data.Entry
46 import com.github.mikephil.charting.data.LineData
47 import com.github.mikephil.charting.data.LineDataSet
48 import com.google.android.material.bottomsheet.BottomSheetBehavior
49 import com.google.android.material.snackbar.Snackbar
50 import kotlin.math.abs
51 import kotlin.math.roundToInt
52 
53 class PlayerFragment : BaseFragment(R.layout.player_fragment) {
54     private val binding: PlayerFragmentBinding by viewBinding()
55     private val viewModel: PlayerViewModel by fragmentViewModel()
56 
57     private val transition = AutoTransition().apply { duration = 175 }
58     private val renderTimesBehavior by lazy {
59         BottomSheetBehavior.from(binding.bottomSheetRenderTimes.root).apply {
60             peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
61         }
62     }
63     private val warningsBehavior by lazy {
64         BottomSheetBehavior.from(binding.bottomSheetWarnings.root).apply {
65             peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
66         }
67     }
68     private val keyPathsBehavior by lazy {
69         BottomSheetBehavior.from(binding.bottomSheetKeyPaths.root).apply {
70             peekHeight = resources.getDimensionPixelSize(R.dimen.bottom_bar_peek_height)
71         }
72     }
73     private val lineDataSet by lazy {
74         val entries = ArrayList<Entry>(101)
75         repeat(101) { i -> entries.add(Entry(i.toFloat(), 0f)) }
76         LineDataSet(entries, "Render Times").apply {
77             mode = LineDataSet.Mode.CUBIC_BEZIER
78             cubicIntensity = 0.3f
79             setDrawCircles(false)
80             lineWidth = 1.8f
81             color = Color.BLACK
82         }
83     }
84 
85     private val animatorListener = AnimatorListenerAdapter(
86         onStart = { binding.controlBarPlayerControls.playButton.isActivated = true },
87         onEnd = {
88             binding.controlBarPlayerControls.playButton.isActivated = false
89             binding.animationView.performanceTracker?.logRenderTimes()
90             updateRenderTimesPerLayer()
91         },
92         onCancel = {
93             binding.controlBarPlayerControls.playButton.isActivated = false
94         },
95         onRepeat = {
96             binding.animationView.performanceTracker?.logRenderTimes()
97             updateRenderTimesPerLayer()
98         }
99     )
100 
101     @SuppressLint("SetTextI18n")
102     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
103         super.onViewCreated(view, savedInstanceState)
104         (requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
105         (requireActivity() as AppCompatActivity).supportActionBar?.setDisplayShowTitleEnabled(false)
106         (requireActivity() as MenuHost).addMenuProvider(object : MenuProvider {
107 
108             override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
109                 menuInflater.inflate(R.menu.fragment_player, menu)
110             }
111 
112             override fun onMenuItemSelected(item: MenuItem): Boolean {
113                 if (item.isCheckable) item.isChecked = !item.isChecked
114                 when (item.itemId) {
115                     android.R.id.home -> requireActivity().finish()
116                     R.id.visibility -> {
117                         viewModel.setDistractionFree(item.isChecked)
118                         val menuIcon = if (item.isChecked) R.drawable.ic_eye_teal else R.drawable.ic_eye_selector
119                         item.icon = ContextCompat.getDrawable(requireContext(), menuIcon)
120                     }
121                 }
122                 return true
123             }
124         }, viewLifecycleOwner, Lifecycle.State.RESUMED)
125 
126         binding.controlBarPlayerControls.lottieVersionView.text = getString(R.string.lottie_version, BuildConfig.VERSION_NAME)
127 
128         binding.animationView.setFontAssetDelegate(object : FontAssetDelegate() {
129             override fun fetchFont(fontFamily: String?, fontStyle: String?, fontName: String?): Typeface {
130                 return Typeface.DEFAULT
131             }
132         })
133 
134         val args = arguments?.getParcelableCompat(EXTRA_ANIMATION_ARGS, CompositionArgs::class.java)
135             ?: throw IllegalArgumentException("No composition args specified")
136 
137         binding.controlBarTrim.minFrameView.setOnClickListener { showMinFrameDialog() }
138         binding.controlBarTrim.maxFrameView.setOnClickListener { showMaxFrameDialog() }
139         viewModel.onEach(PlayerState::minFrame, PlayerState::maxFrame) { minFrame, maxFrame ->
140             binding.animationView.setMinAndMaxFrame(minFrame, maxFrame)
141             // I think this is a lint bug. It complains about int being <ErrorType>
142             //noinspection StringFormatMatches
143             binding.controlBarTrim.minFrameView.setText(resources.getString(R.string.min_frame, binding.animationView.minFrame.toInt()))
144             //noinspection StringFormatMatches
145             binding.controlBarTrim.maxFrameView.setText(resources.getString(R.string.max_frame, binding.animationView.maxFrame.toInt()))
146         }
147 
148         viewModel.fetchAnimation(args)
149         viewModel.onAsync(PlayerState::composition, onFail = {
150             Snackbar.make(binding.coordinatorLayout, R.string.composition_load_error, Snackbar.LENGTH_LONG).show()
151             Log.w(L.TAG, "Error loading composition.", it)
152         }) {
153             binding.loadingView.isVisible = false
154             onCompositionLoaded(it)
155         }
156 
157         binding.controlBar.borderToggle.setOnClickListener { viewModel.toggleBorderVisible() }
158         viewModel.onEach(PlayerState::borderVisible) {
159             binding.controlBar.borderToggle.isActivated = it
160             binding.controlBar.borderToggle.setImageResource(
161                 if (it) R.drawable.ic_border_on
162                 else R.drawable.ic_border_off
163             )
164             binding.animationView.setBackgroundResource(if (it) R.drawable.outline else 0)
165         }
166 
167         binding.controlBar.hardwareAccelerationToggle.setOnClickListener {
168             val renderMode = if (binding.animationView.renderMode == RenderMode.HARDWARE) {
169                 RenderMode.SOFTWARE
170             } else {
171                 RenderMode.HARDWARE
172             }
173             binding.animationView.renderMode = renderMode
174             binding.controlBar.hardwareAccelerationToggle.isActivated = binding.animationView.renderMode == RenderMode.HARDWARE
175         }
176 
177         binding.controlBar.enableApplyingOpacityToLayers.setOnClickListener {
178             val isApplyingOpacityToLayersEnabled = !binding.controlBar.enableApplyingOpacityToLayers.isActivated
179             binding.animationView.setApplyingOpacityToLayersEnabled(isApplyingOpacityToLayersEnabled)
180             binding.controlBar.enableApplyingOpacityToLayers.isActivated = isApplyingOpacityToLayersEnabled
181         }
182 
183         viewModel.onEach(PlayerState::controlsVisible) { binding.controlBarPlayerControls.controlsContainer.animateVisible(it) }
184 
185         viewModel.onEach(PlayerState::controlBarVisible) { binding.controlBar.root.animateVisible(it) }
186 
187         binding.controlBar.renderGraphToggle.setOnClickListener { viewModel.toggleRenderGraphVisible() }
188         viewModel.onEach(PlayerState::renderGraphVisible) {
189             binding.controlBar.renderGraphToggle.isActivated = it
190             binding.controlBarPlayerControls.renderTimesGraphContainer.animateVisible(it)
191             binding.controlBarPlayerControls.renderTimesPerLayerButton.animateVisible(it)
192             binding.controlBarPlayerControls.lottieVersionView.animateVisible(!it)
193         }
194 
195         binding.controlBar.masksAndMattesToggle.setOnClickListener { viewModel.toggleOutlineMasksAndMattes() }
196         viewModel.onEach(PlayerState::outlineMasksAndMattes) {
197             binding.controlBar.masksAndMattesToggle.isActivated = it
198             binding.animationView.setOutlineMasksAndMattes(it)
199         }
200 
201         binding.controlBar.backgroundColorToggle.setOnClickListener { viewModel.toggleBackgroundColorVisible() }
202         binding.controlBarBackgroundColor.closeBackgroundColorButton.setOnClickListener { viewModel.setBackgroundColorVisible(false) }
203         viewModel.onEach(PlayerState::backgroundColorVisible) {
204             binding.controlBar.backgroundColorToggle.isActivated = it
205             binding.controlBarBackgroundColor.backgroundColorContainer.animateVisible(it)
206         }
207 
208         binding.controlBar.trimToggle.setOnClickListener { viewModel.toggleTrimVisible() }
209         binding.controlBarTrim.closeTrimButton.setOnClickListener { viewModel.setTrimVisible(false) }
210         viewModel.onEach(PlayerState::trimVisible) {
211             binding.controlBar.trimToggle.isActivated = it
212             binding.controlBarTrim.trimContainer.animateVisible(it)
213         }
214 
215         binding.controlBar.mergePathsToggle.setOnClickListener { viewModel.toggleMergePaths() }
216         viewModel.onEach(PlayerState::useMergePaths) {
217             binding.animationView.enableMergePathsForKitKatAndAbove(it)
218             binding.controlBar.mergePathsToggle.isActivated = it
219         }
220 
221         binding.controlBar.speedToggle.setOnClickListener { viewModel.toggleSpeedVisible() }
222         binding.controlBarSpeed.closeSpeedButton.setOnClickListener { viewModel.setSpeedVisible(false) }
223         viewModel.onEach(PlayerState::speedVisible) {
224             binding.controlBar.speedToggle.isActivated = it
225             binding.controlBarSpeed.speedContainer.isVisible = it
226         }
227         viewModel.onEach(PlayerState::speed) {
228             binding.animationView.speed = it
229             binding.controlBarSpeed.speedButtonsContainer
230                 .children
231                 .filterIsInstance<ControlBarItemToggleView>()
232                 .forEach { toggleView ->
233                     toggleView.isActivated = toggleView.getText().replace("x", "").toFloat() == binding.animationView.speed
234                 }
235         }
236         binding.controlBarSpeed.speedButtonsContainer
237             .children
238             .filterIsInstance(ControlBarItemToggleView::class.java)
239             .forEach { child ->
240                 child.setOnClickListener {
241                     val speed = (it as ControlBarItemToggleView)
242                         .getText()
243                         .replace("x", "")
244                         .toFloat()
245                     viewModel.setSpeed(speed)
246                 }
247             }
248 
249 
250         binding.controlBarPlayerControls.loopButton.setOnClickListener { viewModel.toggleLoop() }
251         viewModel.onEach(PlayerState::repeatCount) {
252             binding.animationView.repeatCount = it
253             binding.controlBarPlayerControls.loopButton.isActivated = binding.animationView.repeatCount == ValueAnimator.INFINITE
254         }
255 
256         binding.controlBarPlayerControls.playButton.isActivated = binding.animationView.isAnimating
257 
258         binding.controlBarPlayerControls.seekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
259             onProgressChanged = { _, progress, _ ->
260                 if (binding.controlBarPlayerControls.seekBar.isPressed && progress in 1..4) {
261                     binding.controlBarPlayerControls.seekBar.progress = 0
262                     return@OnSeekBarChangeListenerAdapter
263                 }
264                 if (binding.animationView.isAnimating) return@OnSeekBarChangeListenerAdapter
265                 binding.animationView.progress = progress / binding.controlBarPlayerControls.seekBar.max.toFloat()
266             }
267         ))
268 
269         binding.animationView.addAnimatorUpdateListener {
270             binding.controlBarPlayerControls.currentFrameView.text = updateFramesAndDurationLabel(binding.animationView)
271 
272             if (binding.controlBarPlayerControls.seekBar.isPressed) return@addAnimatorUpdateListener
273             binding.controlBarPlayerControls.seekBar.progress =
274                 ((it.animatedValue as Float) * binding.controlBarPlayerControls.seekBar.max).roundToInt()
275         }
276         binding.animationView.addAnimatorListener(animatorListener)
277         binding.controlBarPlayerControls.playButton.setOnClickListener {
278             if (binding.animationView.isAnimating) binding.animationView.pauseAnimation() else binding.animationView.resumeAnimation()
279             binding.controlBarPlayerControls.playButton.isActivated = binding.animationView.isAnimating
280             postInvalidate()
281         }
282 
283         binding.animationView.setOnClickListener {
284             // Click the animation view to re-render it for debugging purposes.
285             binding.animationView.invalidate()
286         }
287 
288         arrayOf(
289             binding.controlBarBackgroundColor.backgroundButton1,
290             binding.controlBarBackgroundColor.backgroundButton2,
291             binding.controlBarBackgroundColor.backgroundButton3,
292             binding.controlBarBackgroundColor.backgroundButton4,
293             binding.controlBarBackgroundColor.backgroundButton5,
294             binding.controlBarBackgroundColor.backgroundButton6
295         ).forEach { bb ->
296             bb.setOnClickListener {
297                 binding.animationContainer.setBackgroundColor(bb.getColor())
298                 invertColor(bb.getColor())
299             }
300         }
301 
302         binding.controlBarPlayerControls.renderTimesGraph.apply {
303             setTouchEnabled(false)
304             axisRight.isEnabled = false
305             xAxis.isEnabled = false
306             legend.isEnabled = false
307             description = null
308             data = LineData(lineDataSet)
309             axisLeft.setDrawGridLines(false)
310             axisLeft.labelCount = 4
311             val ll1 = LimitLine(16f, "60fps")
312             ll1.lineColor = Color.RED
313             ll1.lineWidth = 1.2f
314             ll1.textColor = Color.BLACK
315             ll1.textSize = 8f
316             axisLeft.addLimitLine(ll1)
317 
318             val ll2 = LimitLine(32f, "30fps")
319             ll2.lineColor = Color.RED
320             ll2.lineWidth = 1.2f
321             ll2.textColor = Color.BLACK
322             ll2.textSize = 8f
323             axisLeft.addLimitLine(ll2)
324         }
325 
326         binding.controlBarPlayerControls.renderTimesPerLayerButton.setOnClickListener {
327             updateRenderTimesPerLayer()
328             renderTimesBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
329         }
330 
331         binding.bottomSheetRenderTimes.closeRenderTimesBottomSheetButton.setOnClickListener {
332             renderTimesBehavior.state = BottomSheetBehavior.STATE_HIDDEN
333         }
334         renderTimesBehavior.state = BottomSheetBehavior.STATE_HIDDEN
335 
336         binding.controlBar.warningsButton.setOnClickListener {
337             withState(viewModel) { state ->
338                 if (state.composition()?.warnings?.isEmpty() != true) {
339                     warningsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
340 
341                 }
342             }
343         }
344 
345         binding.bottomSheetWarnings.closeWarningsBottomSheetButton.setOnClickListener {
346             warningsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
347         }
348         warningsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
349 
350         binding.controlBar.keyPathsToggle.setOnClickListener {
351             keyPathsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
352         }
353 
354         binding.bottomSheetKeyPaths.closeKeyPathsBottomSheetButton.setOnClickListener {
355             keyPathsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
356         }
357         keyPathsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
358     }
359 
360     private fun showMinFrameDialog() {
361         val minFrameView = EditText(context)
362         minFrameView.setText(binding.animationView.minFrame.toInt().toString())
363         AlertDialog.Builder(context)
364             .setTitle(R.string.min_frame_dialog)
365             .setView(minFrameView)
366             .setPositiveButton("Load") { _, _ ->
367                 viewModel.setMinFrame(minFrameView.text.toString().toIntOrNull() ?: 0)
368             }
369             .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
370             .show()
371     }
372 
373     private fun showMaxFrameDialog() {
374         val maxFrameView = EditText(context)
375         maxFrameView.setText(binding.animationView.maxFrame.toInt().toString())
376         AlertDialog.Builder(context)
377             .setTitle(R.string.max_frame_dialog)
378             .setView(maxFrameView)
379             .setPositiveButton("Load") { _, _ ->
380                 viewModel.setMaxFrame(maxFrameView.text.toString().toIntOrNull() ?: 0)
381             }
382             .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
383             .show()
384     }
385 
386     private fun View.animateVisible(visible: Boolean) {
387         beginDelayedTransition()
388         isVisible = visible
389     }
390 
391     private fun invertColor(color: Int) {
392         val isDarkBg = color.isDark()
393         binding.animationView.isActivated = isDarkBg
394         binding.toolbar.isActivated = isDarkBg
395     }
396 
397     private fun Int.isDark(): Boolean {
398         val y = (299 * Color.red(this) + 587 * Color.green(this) + 114 * Color.blue(this)) / 1000
399         return y < 128
400     }
401 
402     override fun onDestroyView() {
403         binding.animationView.removeAnimatorListener(animatorListener)
404         super.onDestroyView()
405     }
406 
407     private fun onCompositionLoaded(composition: LottieComposition?) {
408         composition ?: return
409 
410         // If the composition is missing any images, return the original image or null.
411         if (composition.images.any { (_, asset) -> !asset.hasBitmap() }) {
412             binding.animationView.setImageAssetDelegate { it.bitmap }
413         }
414 
415         binding.animationView.setComposition(composition)
416         binding.controlBar.hardwareAccelerationToggle.isActivated = binding.animationView.renderMode == RenderMode.HARDWARE
417         binding.animationView.setPerformanceTrackingEnabled(true)
418         var renderTimeGraphRange = 4f
419         binding.animationView.performanceTracker?.addFrameListener { ms ->
420             if (lifecycle.currentState != Lifecycle.State.RESUMED) return@addFrameListener
421             lineDataSet.getEntryForIndex((binding.animationView.progress * 100).toInt()).y = ms
422             renderTimeGraphRange = renderTimeGraphRange.coerceAtLeast(ms * 1.2f)
423             binding.controlBarPlayerControls.renderTimesGraph.setVisibleYRange(0f, renderTimeGraphRange, YAxis.AxisDependency.LEFT)
424             binding.controlBarPlayerControls.renderTimesGraph.invalidate()
425         }
426 
427         binding.bottomSheetKeyPaths.keyPathsRecyclerView.buildModelsWith(object : EpoxyRecyclerView.ModelBuilderCallback {
428             override fun buildModels(controller: EpoxyController) {
429                 binding.animationView.resolveKeyPath(KeyPath("**")).forEachIndexed { index, keyPath ->
430                     BottomSheetItemViewModel_()
431                         .id(index)
432                         .text(keyPath.keysToString())
433                         .addTo(controller)
434                 }
435             }
436         })
437 
438         updateWarnings()
439     }
440 
441     override fun invalidate() {
442     }
443 
444     private fun updateRenderTimesPerLayer() {
445         binding.bottomSheetRenderTimes.renderTimesContainer.removeAllViews()
446         binding.animationView.performanceTracker?.sortedRenderTimes?.forEach {
447             val view = BottomSheetItemView(requireContext()).apply {
448                 set(
449                     it.first!!.replace("__container", "Total"),
450                     "%.2f ms".format(it.second!!)
451                 )
452             }
453             binding.bottomSheetRenderTimes.renderTimesContainer.addView(view)
454         }
455         binding.animationView.performanceTracker?.clearRenderTimes()
456     }
457 
458     private fun updateWarnings() = withState(viewModel) { state ->
459         // Force warning to update
460         binding.bottomSheetWarnings.warningsContainer.removeAllViews()
461 
462         val warnings = state.composition()?.warnings ?: emptySet<String>()
463         if (!warnings.isEmpty() && warnings.size == binding.bottomSheetWarnings.warningsContainer.childCount) return@withState
464 
465         binding.bottomSheetWarnings.warningsContainer.removeAllViews()
466         warnings.forEach {
467             val view = BottomSheetItemView(requireContext()).apply {
468                 set(it)
469             }
470             binding.bottomSheetWarnings.warningsContainer.addView(view)
471         }
472 
473         val size = warnings.size
474         binding.controlBar.warningsButton.setText(resources.getQuantityString(R.plurals.warnings, size, size))
475         binding.controlBar.warningsButton.setImageResource(
476             if (warnings.isEmpty()) R.drawable.ic_sentiment_satisfied
477             else R.drawable.ic_sentiment_dissatisfied
478         )
479     }
480 
481     private fun beginDelayedTransition() = TransitionManager.beginDelayedTransition(binding.container, transition)
482 
483     companion object {
484         const val EXTRA_ANIMATION_ARGS = "animation_args"
485 
486         fun forAsset(args: CompositionArgs): Fragment {
487             return PlayerFragment().apply {
488                 arguments = Bundle().apply {
489                     putParcelable(EXTRA_ANIMATION_ARGS, args)
490                 }
491             }
492         }
493     }
494 
495     private fun updateFramesAndDurationLabel(animation: LottieAnimationView): String {
496         val currentFrame = animation.frame.toString()
497         val totalFrames = ("%.0f").format(animation.maxFrame)
498 
499         val animationSpeed: Float = abs(animation.speed)
500 
501         val totalTime = ((animation.duration / animationSpeed) / 1000.0)
502         val totalTimeFormatted = ("%.1f").format(totalTime)
503 
504         val progress = (totalTime / 100.0) * ((animation.progress * 100.0).roundToInt())
505         val progressFormatted = ("%.1f").format(progress)
506 
507         return "$currentFrame/$totalFrames\n$progressFormatted/$totalTimeFormatted"
508     }
509 }
510