| package com.airbnb.lottie.samples |
| |
| import android.Manifest |
| import android.app.Activity |
| import android.app.AlertDialog |
| import android.content.Context |
| import android.content.Intent |
| import android.content.pm.PackageManager |
| import android.graphics.Color |
| import android.graphics.Point |
| import android.net.Uri |
| import android.os.Build |
| import android.os.Bundle |
| import android.os.Handler |
| import android.provider.Settings |
| import android.support.v4.app.Fragment |
| import android.support.v7.app.AppCompatActivity |
| import android.text.TextUtils |
| import android.util.Log |
| import android.view.* |
| import android.widget.EditText |
| import android.widget.Toast |
| import com.airbnb.lottie.* |
| import com.airbnb.lottie.BuildConfig |
| import com.airbnb.lottie.model.KeyPath |
| import com.airbnb.lottie.value.LottieStaticValue |
| import com.github.mikephil.charting.components.LimitLine |
| import com.github.mikephil.charting.components.YAxis |
| import com.github.mikephil.charting.data.Entry |
| import com.github.mikephil.charting.data.LineData |
| import com.github.mikephil.charting.data.LineDataSet |
| import kotlinx.android.synthetic.main.fragment_animation.* |
| import kotlinx.android.synthetic.main.fragment_animation.view.* |
| import okhttp3.OkHttpClient |
| import okhttp3.Request |
| import org.json.JSONException |
| import org.json.JSONObject |
| import java.io.FileInputStream |
| import java.io.FileNotFoundException |
| import java.io.InputStream |
| import java.util.* |
| import kotlin.collections.ArrayList |
| |
| private inline fun consume(f: () -> Unit): Boolean { |
| f() |
| return true |
| } |
| |
| class AnimationFragment : Fragment() { |
| private val TAG = AnimationFragment::class.java.simpleName |
| private val RC_CAMERA = 1341 |
| private val RC_ASSET = 1337 |
| private val RC_FILE = 1338 |
| private val RC_QR = 1340 |
| private val SCALE_SLIDER_FACTOR = 50f |
| |
| private val assetFolders = HashMap<String, String>().apply { |
| put("WeAccept.json", "Images/WeAccept") |
| } |
| |
| private val handler = Handler() |
| private val client: OkHttpClient by lazy { OkHttpClient() } |
| private lateinit var myActivity: AppCompatActivity |
| private val application: ILottieApplication |
| get() = activity!!.application as ILottieApplication |
| private var renderTimeGraphRange = 4f |
| private val lineDataSet by lazy { |
| val entries = ArrayList<Entry>(101) |
| repeat(101) { i -> |
| entries.add(Entry(i.toFloat(), 0f)) |
| } |
| val dataSet = LineDataSet(entries, "Render Times") |
| dataSet.mode = LineDataSet.Mode.CUBIC_BEZIER |
| dataSet.cubicIntensity = 0.3f |
| dataSet.setDrawCircles(false) |
| dataSet.lineWidth = 1.8f |
| dataSet.color = Color.BLACK |
| dataSet |
| } |
| private val systemAnimationsAreDisabled by lazy { getAnimationScale(myActivity) == 0f } |
| private var lastAnimationAssetName: String? = null |
| |
| override fun onCreateView( |
| inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { |
| myActivity = activity!! as AppCompatActivity |
| val view = container!!.inflate(R.layout.fragment_animation, false) |
| |
| L.setTraceEnabled(true) |
| view.animationView.setPerformanceTrackingEnabled(true) |
| |
| myActivity.setSupportActionBar(view.toolbar) |
| view.toolbar.setNavigationIcon(R.drawable.ic_back) |
| view.toolbar.setNavigationOnClickListener { fragmentManager!!.popBackStack() } |
| setHasOptionsMenu(true) |
| postUpdatePlayButtonText() |
| |
| view.version.text = BuildConfig.VERSION_NAME |
| view.qrCode.setDrawableLeft(R.drawable.ic_qr_scan, myActivity) |
| view.sampleAnimations.setDrawableLeft(R.drawable.ic_assets, myActivity) |
| view.loadAnimation.setDrawableLeft(R.drawable.ic_file, myActivity) |
| view.loadFromJson.setDrawableLeft(R.drawable.ic_network, myActivity) |
| view.overflowMenu.setDrawableLeft(R.drawable.ic_more_vert, myActivity) |
| |
| view.animationView.addAnimatorListener(AnimatorListenerAdapter( |
| onStart = { startRecordingDroppedFrames() }, |
| onEnd = { |
| recordDroppedFrames() |
| postUpdatePlayButtonText() |
| animationView.performanceTracker?.logRenderTimes() |
| }, |
| onCancel = { postUpdatePlayButtonText() }, |
| onRepeat = { |
| animationView.performanceTracker?.logRenderTimes() |
| animationView.performanceTracker?.clearRenderTimes() |
| recordDroppedFrames() |
| startRecordingDroppedFrames() |
| } |
| )) |
| |
| view.animationView.addAnimatorUpdateListener { animation -> |
| if (animation.isRunning) { |
| seekBar.progress = (animation.animatedValue as Float * 100f).toInt() |
| } |
| } |
| |
| view.seekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter( |
| onProgressChanged = { _, progress, _ -> |
| if (!animationView.isAnimating) { |
| animationView.progress = progress / 100f |
| } |
| } |
| )) |
| |
| view.trimView.setCallback({ startProgress, endProgress -> |
| animationView.setMinAndMaxProgress(startProgress, endProgress) |
| }) |
| |
| view.scaleSeekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter( |
| onProgressChanged = { _, progress, _ -> |
| animationView.scale = progress / SCALE_SLIDER_FACTOR |
| scaleText.text = String.format(Locale.US, "%.2f", animationView.scale) |
| } |
| )) |
| |
| view.playButton.setOnClickListener { |
| if (animationView.isAnimating) { |
| animationView.pauseAnimation() |
| postUpdatePlayButtonText() |
| } else { |
| if (animationView.progress == 1f && !systemAnimationsAreDisabled) { |
| animationView.progress = 0f |
| } |
| animationView.resumeAnimation() |
| postUpdatePlayButtonText() |
| } |
| } |
| |
| view.loop.setOnClickListener { |
| view.loop.isActivated = !view.loop.isActivated |
| if (view.loop.isActivated) { |
| view.animationView.repeatCount = LottieDrawable.INFINITE |
| } else { |
| view.animationView.repeatCount = 0 |
| } |
| } |
| view.loop.callOnClick() |
| |
| view.qrScan.setOnClickListener { |
| animationView.cancelAnimation() |
| if (!Manifest.permission.CAMERA.hasPermission(myActivity)) { |
| requestPermissions(arrayOf(Manifest.permission.CAMERA), RC_CAMERA) |
| |
| } else { |
| startActivityForResult(Intent(context, QRScanActivity::class.java), RC_QR) |
| } |
| } |
| |
| view.invertColors.setOnClickListener { |
| animationContainer.isActivated = !animationContainer.isActivated |
| invertColors.isActivated = animationContainer.isActivated |
| } |
| |
| view.loadAsset.setOnClickListener { |
| animationView.cancelAnimation() |
| val assetFragment = ChooseAssetDialogFragment.newInstance(lastAnimationAssetName) |
| assetFragment.setTargetFragment(this, RC_ASSET) |
| assetFragment.show(fragmentManager, "assets") |
| } |
| |
| view.loadFile.setOnClickListener { |
| animationView.cancelAnimation() |
| val intent = Intent(Intent.ACTION_GET_CONTENT) |
| intent.type = "*/*" |
| intent.addCategory(Intent.CATEGORY_OPENABLE) |
| |
| try { |
| startActivityForResult(Intent.createChooser(intent, "Select a JSON file"), RC_FILE) |
| } catch (ex: android.content.ActivityNotFoundException) { |
| // Potentially direct the user to the Market with a Dialog |
| Toast.makeText(context, "Please install a File Manager.", Toast.LENGTH_SHORT).show() |
| } |
| } |
| |
| view.loadUrlOrJson.setOnClickListener { |
| animationView.cancelAnimation() |
| val urlOrJsonView = EditText(context) |
| AlertDialog.Builder(context) |
| .setTitle("Enter a URL or JSON string") |
| .setView(urlOrJsonView) |
| .setPositiveButton("Load") { _, _ -> loadUrlOrJson(urlOrJsonView.text.toString()) } |
| .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() } |
| .show() |
| } |
| |
| view.renderTimesGraph.axisRight.isEnabled = false |
| view.renderTimesGraph.xAxis.isEnabled = false |
| view.renderTimesGraph.legend.isEnabled = false |
| view.renderTimesGraph.description = null |
| view.renderTimesGraph.data = LineData(lineDataSet) |
| view.renderTimesGraph.axisLeft.setDrawGridLines(false) |
| view.renderTimesGraph.axisLeft.labelCount = 4 |
| val ll1 = LimitLine(16f, "60fps") |
| ll1.lineColor = Color.RED |
| ll1.lineWidth = 1.2f |
| ll1.textColor = Color.BLACK |
| ll1.textSize = 8f |
| view.renderTimesGraph.axisLeft.addLimitLine(ll1) |
| |
| val ll2 = LimitLine(32f, "30fps") |
| ll2.lineColor = Color.RED |
| ll2.lineWidth = 1.2f |
| ll2.textColor = Color.BLACK |
| ll2.textSize = 8f |
| view.renderTimesGraph.axisLeft.addLimitLine(ll2) |
| |
| return view |
| } |
| |
| override fun onStop() { |
| animationView.cancelAnimation() |
| super.onStop() |
| } |
| |
| override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = |
| inflater.inflate(R.menu.fragment_animation, menu) |
| |
| override fun onOptionsItemSelected(item: MenuItem): Boolean { |
| item.isChecked = !item.isChecked |
| return when (item.itemId) { |
| R.id.hardware_acceleration -> consume { |
| animationView.useHardwareAcceleration(item.isChecked) |
| } |
| R.id.merge_paths -> consume { |
| animationView.enableMergePathsForKitKatAndAbove(item.isChecked) |
| } |
| R.id.render_times_graph -> consume { |
| renderTimesGraphContainer.setVisibleIf(item.isChecked) |
| } |
| else -> super.onOptionsItemSelected(item) |
| } |
| } |
| |
| override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { |
| if (resultCode != Activity.RESULT_OK || data == null) { |
| return |
| } |
| |
| when (requestCode) { |
| RC_ASSET -> { |
| val assetName = data.getStringExtra(EXTRA_ANIMATION_NAME) |
| lastAnimationAssetName = assetName |
| animationView.imageAssetsFolder = assetFolders[assetName] |
| LottieComposition.Factory.fromAssetFileName(context, assetName, { composition -> |
| if (composition == null) { |
| onLoadError() |
| } else { |
| setComposition(composition, assetName) |
| } |
| }) |
| } |
| RC_FILE -> onFileLoaded(data.data) |
| RC_QR -> loadUrl(data.extras.getString(EXTRA_URL)) |
| } |
| } |
| |
| private fun setComposition(composition: LottieComposition, name: String) { |
| if (composition.hasImages() && TextUtils.isEmpty(animationView.imageAssetsFolder)) { |
| view!!.showSnackbarLong("This animation has images and no image folder was set") |
| return |
| } |
| instructions.visibility = View.GONE |
| seekBar.progress = 0 |
| animationView.setComposition(composition) |
| animationName.text = name |
| // make sure the animation doesn't start larger than the screen |
| val screenSize = Point() |
| val wm = myActivity.getSystemService(Context.WINDOW_SERVICE) as WindowManager |
| wm.defaultDisplay.getSize(screenSize) |
| val scale = screenSize.x / composition.bounds.width().toFloat() |
| animationView.scale = minOf(scale, 1f) |
| scaleText.text = String.format(Locale.US, "%.2f", animationView.scale) |
| scaleSeekBar.progress = (animationView.scale * SCALE_SLIDER_FACTOR).toInt() |
| setWarnings(composition.warnings) |
| renderTimeGraphRange = 8f |
| for (i in 1 until lineDataSet.entryCount) { |
| lineDataSet.getEntryForIndex(i).y = 0f |
| } |
| renderTimesGraph.invalidate() |
| animationView.performanceTracker?.addFrameListener { ms -> |
| if (renderTimesGraph == null) { |
| return@addFrameListener |
| } |
| lineDataSet.getEntryForIndex((animationView.progress * 100).toInt()).y = ms |
| renderTimeGraphRange = Math.max(renderTimeGraphRange, ms * 1.2f) |
| renderTimesGraph.setVisibleYRange(0f, renderTimeGraphRange, YAxis.AxisDependency.LEFT) |
| renderTimesGraph.invalidate() |
| } |
| } |
| |
| private fun setWarnings(warningsList: ArrayList<String>) { |
| val size = warningsList.size |
| warnings.visibility = if (size == 0) View.GONE else View.VISIBLE |
| warnings.text = resources.getQuantityString(R.plurals.warnings, size, size) |
| warnings.setOnClickListener { |
| WarningsDialogFragment.newInstance(warningsList).show(fragmentManager, null) |
| } |
| } |
| |
| override fun onRequestPermissionsResult( |
| requestCode: Int, permissions: Array<String>, grantResults: IntArray) { |
| if (requestCode == RC_CAMERA && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) { |
| startActivityForResult(Intent(context, QRScanActivity::class.java), RC_QR) |
| } else { |
| view!!.showSnackbarLong(R.string.permission_required) |
| } |
| } |
| |
| private fun postUpdatePlayButtonText() = handler.post { |
| if (playButton != null) { |
| updatePlayButtonText() |
| } |
| } |
| |
| private fun updatePlayButtonText() { |
| playButton.isActivated = animationView.isAnimating |
| } |
| |
| private fun onFileLoaded(uri: Uri) { |
| val fis: InputStream |
| |
| try { |
| when (uri.scheme) { |
| "file" -> fis = FileInputStream(uri.path) |
| "content" -> fis = myActivity.contentResolver.openInputStream(uri) |
| else -> { |
| onLoadError() |
| return |
| } |
| } |
| } catch (e: FileNotFoundException) { |
| onLoadError() |
| return |
| } |
| |
| LottieComposition.Factory.fromInputStream(context, fis, { composition -> |
| if (composition == null) { |
| onLoadError() |
| } else { |
| setComposition(composition, uri.path) |
| } |
| }) |
| } |
| |
| private fun loadUrlOrJson(text: String) { |
| if (text[0] == '{') { |
| // Assume JSON |
| loadJsonString(text) |
| } else { |
| loadUrl(text) |
| } |
| } |
| |
| private fun loadJsonString(jsonString: String?) { |
| if (jsonString == null) { |
| return |
| } |
| try { |
| val json = JSONObject(jsonString) |
| LottieComposition.Factory.fromJson(resources, json, { composition -> |
| if (composition == null) { |
| onLoadError() |
| } else { |
| setComposition(composition, "Animation") |
| } |
| }) |
| } catch (e: JSONException) { |
| onLoadError() |
| } |
| |
| } |
| |
| private fun loadUrl(url: String) { |
| val request: Request |
| try { |
| request = Request.Builder() |
| .url(url) |
| .build() |
| } catch (e: IllegalArgumentException) { |
| onLoadError() |
| return |
| } |
| client.newCall(request)?.enqueue(OkHttpCallback( |
| onFailure = { _, _ -> onLoadError() }, |
| onResponse = { _, response -> |
| if (!response.isSuccessful) { |
| onLoadError() |
| } else { |
| loadJsonString(response.body()?.string()) |
| } |
| |
| })) |
| } |
| |
| private fun onLoadError() = view!!.showSnackbarLong("Failed to load animation") |
| |
| private fun startRecordingDroppedFrames() = application.startRecordingDroppedFrames() |
| |
| private fun recordDroppedFrames() { |
| val droppedFrames = application.stopRecordingDroppedFrames() |
| Log.d(TAG, "Dropped frames: " + droppedFrames.first) |
| } |
| |
| private fun getAnimationScale(context: Context): Float { |
| return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { |
| Settings.Global.getFloat(context.contentResolver, |
| Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) |
| } else { |
| |
| @Suppress("DEPRECATION") |
| Settings.System.getFloat(context.contentResolver, |
| Settings.System.ANIMATOR_DURATION_SCALE, 1.0f) |
| } |
| } |
| |
| companion object { |
| @JvmField internal val EXTRA_ANIMATION_NAME = "animation_name" |
| @JvmField internal val EXTRA_URL = "json_url" |
| |
| internal fun newInstance(): AnimationFragment { |
| return AnimationFragment() |
| } |
| } |
| } |