blob: 40f12f40d468d35761caf2bf0eecf9d8131ef348 [file] [log] [blame]
package com.airbnb.lottie.samples;
import android.Manifest;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v4.util.Pair;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.AppCompatSeekBar;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieComposition;
import com.airbnb.lottie.OnCompositionLoadedListener;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class AnimationFragment extends Fragment {
private static final String TAG = AnimationFragment.class.getSimpleName();
private static final int RC_ASSET = 1337;
private static final int RC_FILE = 1338;
private static final int RC_URL = 1339;
private static final int RC_QR = 1340;
private static final int RC_CAMERA = 1341;
static final String EXTRA_ANIMATION_NAME = "animation_name";
static final String EXTRA_URL = "json_url";
private static final float SCALE_SLIDER_FACTOR = 50f;
static AnimationFragment newInstance() {
return new AnimationFragment();
}
private final Map<String, String> assetFolders = new HashMap<String, String>() {{
put("WeAccept.json", "Images/WeAccept");
}};
private OkHttpClient client;
@BindView(R.id.toolbar) Toolbar toolbar;
@BindView(R.id.instructions) ViewGroup instructionsContainer;
@BindView(R.id.animation_container) ViewGroup animationContainer;
@BindView(R.id.animation_view) LottieAnimationView animationView;
@BindView(R.id.seek_bar) AppCompatSeekBar seekBar;
@BindView(R.id.scale_seek_bar) AppCompatSeekBar scaleSeekBar;
@BindView(R.id.scale_text) TextView scaleTextView;
@BindView(R.id.invert_colors) ImageButton invertButton;
@BindView(R.id.play_button) ImageButton playButton;
@BindView(R.id.loop) ImageButton loopButton;
@BindView(R.id.animation_name) TextView animationNameView;
@BindView(R.id.qr_code) TextView qrCodeTextView;
@BindView(R.id.sample_animations) TextView sampleAnimationsTextView;
@BindView(R.id.load_animation) TextView loadAnimationTextView;
@BindView(R.id.load_from_json) TextView loadFromJsonTextView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_animation, container, false);
ButterKnife.bind(this, view);
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
toolbar.setNavigationIcon(R.drawable.ic_back);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
getFragmentManager().popBackStack();
}
});
setHasOptionsMenu(true);
postUpdatePlayButtonText();
onLoopChanged();
setDrawableLeft(qrCodeTextView, R.drawable.ic_qr_scan);
setDrawableLeft(sampleAnimationsTextView, R.drawable.ic_assets);
setDrawableLeft(loadAnimationTextView, R.drawable.ic_file);
setDrawableLeft(loadFromJsonTextView, R.drawable.ic_network);
animationView.addAnimatorListener(new Animator.AnimatorListener() {
@Override public void onAnimationStart(Animator animation) {
startRecordingDroppedFrames();
}
@Override public void onAnimationEnd(Animator animation) {
recordDroppedFrames();
postUpdatePlayButtonText();
}
@Override public void onAnimationCancel(Animator animation) {
postUpdatePlayButtonText();
}
@Override public void onAnimationRepeat(Animator animation) {
recordDroppedFrames();
startRecordingDroppedFrames();
}
});
animationView.addAnimatorUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
seekBar.setProgress((int) (animation.getAnimatedFraction() * 100));
}
});
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (!animationView.isAnimating()) {
animationView.setProgress(progress / 100f);
}
}
@Override public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override public void onStopTrackingTouch(SeekBar seekBar) {
}
});
scaleSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
animationView.setScale(progress / SCALE_SLIDER_FACTOR);
scaleTextView.setText(String.format(Locale.US, "%.2f", animationView.getScale()));
}
@Override public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override public void onStopTrackingTouch(SeekBar seekBar) {
}
});
return view;
}
private void setDrawableLeft(TextView textView, @DrawableRes int resId) {
Drawable drawable = VectorDrawableCompat.create(getResources(), resId, getActivity().getTheme());
textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
}
@Override public void onStop() {
animationView.cancelAnimation();
super.onStop();
}
@Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.fragment_animation, menu);
}
@Override public boolean onOptionsItemSelected(MenuItem item) {
if (item.isCheckable()) {
item.setChecked(!item.isChecked());
}
switch (item.getItemId()) {
case R.id.hardware_acceleration:
animationView.useExperimentalHardwareAcceleration(item.isChecked());
return true;
case R.id.merge_paths:
animationView.enableMergePathsForKitKatAndAbove(item.isChecked());
return true;
}
return super.onOptionsItemSelected(item);
}
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
switch (requestCode) {
case RC_ASSET:
final String assetName = data.getStringExtra(EXTRA_ANIMATION_NAME);
animationView.setImageAssetsFolder(assetFolders.get(assetName));
LottieComposition.Factory.fromAssetFileName(getContext(), assetName,
new OnCompositionLoadedListener() {
@Override public void onCompositionLoaded(@Nullable LottieComposition composition) {
if (composition == null) {
onLoadError();
return;
}
setComposition(composition, assetName);
}
});
break;
case RC_FILE:
onFileLoaded(data.getData());
break;
case RC_URL:
break;
case RC_QR:
loadUrl(data.getExtras().getString(EXTRA_URL));
break;
}
}
private void setComposition(LottieComposition composition, String name) {
if (composition.hasImages() && TextUtils.isEmpty(animationView.getImageAssetsFolder())) {
//noinspection ConstantConditions
Snackbar.make(
getView(),
"This animation has images and no image folder was set",
Snackbar.LENGTH_LONG).show();
return;
}
instructionsContainer.setVisibility(View.GONE);
seekBar.setProgress(0);
animationView.setComposition(composition);
animationNameView.setText(name);
scaleTextView.setText(String.format(Locale.US, "%.2f", animationView.getScale()));
scaleSeekBar.setProgress((int) (animationView.getScale() * SCALE_SLIDER_FACTOR));
}
@OnClick(R.id.play_button)
void onPlayClicked() {
if (animationView.isAnimating()) {
animationView.pauseAnimation();
postUpdatePlayButtonText();
} else {
if (animationView.getProgress() == 1f) {
animationView.setProgress(0f);
}
animationView.resumeAnimation();
postUpdatePlayButtonText();
}
}
@OnClick(R.id.loop)
void onLoopChanged() {
loopButton.setActivated(!loopButton.isActivated());
animationView.loop(loopButton.isActivated());
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == RC_CAMERA && grantResults.length > 0 && grantResults[0] == PackageManager
.PERMISSION_GRANTED) {
startActivityForResult(new Intent(getContext(), QRScanActivity.class), RC_QR);
} else {
Toast.makeText(getContext(), R.string.permission_required, Toast.LENGTH_LONG).show();
}
}
@OnClick(R.id.qrscan)
void onQRScanClicked() {
animationView.cancelAnimation();
if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.CAMERA) !=
PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.CAMERA}, RC_CAMERA);
} else {
startActivityForResult(new Intent(getContext(), QRScanActivity.class), RC_QR);
}
}
@OnClick(R.id.restart)
void onRestartClicked() {
boolean restart = animationView.isAnimating();
animationView.cancelAnimation();
animationView.setProgress(0f);
if (restart) {
animationView.playAnimation();
}
}
@OnClick(R.id.invert_colors)
void onInvertClicked() {
animationContainer.setActivated(!animationContainer.isActivated());
invertButton.setActivated(animationContainer.isActivated());
}
@OnClick(R.id.load_asset)
void onLoadAssetClicked() {
animationView.cancelAnimation();
android.support.v4.app.DialogFragment assetFragment = ChooseAssetDialogFragment.newInstance();
assetFragment.setTargetFragment(this, RC_ASSET);
assetFragment.show(getFragmentManager(), "assets");
}
@OnClick(R.id.load_file)
void onLoadFileClicked() {
animationView.cancelAnimation();
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
try {
startActivityForResult(Intent.createChooser(intent, "Select a JSON file"), RC_FILE);
} catch (android.content.ActivityNotFoundException ex) {
// Potentially direct the user to the Market with a Dialog
Toast.makeText(getContext(), "Please install a File Manager.", Toast.LENGTH_SHORT).show();
}
}
@OnClick(R.id.load_url_or_json)
void onLoadUrlOrJsonClicked() {
animationView.cancelAnimation();
final EditText urlOrJsonView = new EditText(getContext());
new AlertDialog.Builder(getContext())
.setTitle("Enter a URL or JSON string")
.setView(urlOrJsonView)
.setPositiveButton("Load", new DialogInterface.OnClickListener() {
@Override public void onClick(DialogInterface dialog, int which) {
loadUrlOrJson(urlOrJsonView.getText().toString());
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
}
private void postUpdatePlayButtonText() {
new Handler().post(new Runnable() {
@Override public void run() {
updatePlayButtonText();
}
});
}
private void updatePlayButtonText() {
playButton.setActivated(animationView.isAnimating());
}
private void onFileLoaded(final Uri uri) {
InputStream fis;
try {
switch (uri.getScheme()) {
case "file":
fis = new FileInputStream(uri.getPath());
break;
case "content":
fis = getContext().getContentResolver().openInputStream(uri);
break;
default:
onLoadError();
return;
}
} catch (FileNotFoundException e) {
onLoadError();
return;
}
LottieComposition.Factory
.fromInputStream(getContext(), fis, new OnCompositionLoadedListener() {
@Override public void onCompositionLoaded(@Nullable LottieComposition composition) {
if (composition == null) {
onLoadError();
return;
}
setComposition(composition, uri.getPath());
}
});
}
private void loadUrlOrJson(String text) {
if (text.charAt(0) == '{') {
// Assume JSON
loadJsonString(text);
return;
}
loadUrl(text);
}
private void loadJsonString(String jsonString) {
try {
JSONObject json = new JSONObject(jsonString);
LottieComposition.Factory
.fromJson(getResources(), json, new OnCompositionLoadedListener() {
@Override public void onCompositionLoaded(@Nullable LottieComposition composition) {
if (composition == null) {
onLoadError();
return;
}
setComposition(composition, "Animation");
}
});
} catch (JSONException e) {
onLoadError();
}
}
private void loadUrl(String url) {
Request request;
try {
request = new Request.Builder()
.url(url)
.build();
} catch (IllegalArgumentException e) {
onLoadError();
return;
}
if (client == null) {
client = new OkHttpClient();
}
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Call call, IOException e) {
onLoadError();
}
@Override public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
onLoadError();
}
loadJsonString(response.body().string());
}
});
}
private void onLoadError() {
//noinspection ConstantConditions
Snackbar.make(getView(), "Failed to load animation", Snackbar.LENGTH_LONG).show();
}
private void startRecordingDroppedFrames() {
getApplication().startRecordingDroppedFrames();
}
private void recordDroppedFrames() {
Pair<Integer, Long> droppedFrames = getApplication().stopRecordingDroppedFrames();
Log.d(TAG, "Dropped frames: " + droppedFrames.first);
}
private ILottieApplication getApplication() {
return (ILottieApplication) getActivity().getApplication();
}
}