<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