();
+ mView = view;
+ }
+
+ /**
+ * @param context The application Context.
+ */
+ public DrawableAnimationSeries(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Convert a JSONArray containing only strings to a String[].
+ *
+ * @param jsonArray the array to convert.
+ * @return a String[] with the contents of the JSONArray.
+ * @throws org.json.JSONException
+ */
+ private static String[] jsonArrayToArray(JSONArray jsonArray) throws JSONException {
+ String[] array = new String[jsonArray.length()];
+ for (int i = 0; i < array.length; i++) {
+ array[i] = jsonArray.getString(i);
+ }
+ return array;
+ }
+
+ /**
+ * Creates a new DrawableAnimationSeries object from a json string.
+ * The document must have the following structure:
+ *
+ * {
+ * "section_name": {
+ * "oneshot": false,
+ * "frame_duration": 33,
+ * "frames": [
+ * "frame_01",
+ * "frame_02"
+ * ],
+ * "transitions_from": {
+ * "other_section_id": {
+ * "frame_duration": 33,
+ * "frames": [
+ * "spinner_intro_001",
+ * "spinner_intro_002"
+ * ]
+ * }
+ * }
+ * }
+ * "other_section_id": {
+ * "oneshot": true,
+ * "frames": [
+ * "other_frame_01"
+ * ]
+ * }
+ * }
+ *
+ * If "oneshot" is false, the animation will play in a loop instead of stopping at the last
+ * frame.
+ * "frame_duration" is the number of milliseconds that each frame in the "frame" list will play.
+ * It defaults to 33 if not given.
+ * "frames" is a list of string resource ID names that must correspond to a drawable resource.
+ * "transitions_from" is optional, and is a list of animations that play when transitioning to the
+ * current state from another given state.
+ *
+ * @param context The application Context.
+ * @param view If not null, animations will be set as the background of this view.
+ * @param resid The resource ID the the raw json document.
+ * @return A new DrawableAnimationSeries.
+ * @throws JSONException
+ */
+ public static DrawableAnimationSeries fromJsonResource(Context context, View view, int resid) throws JSONException, IOException {
+ // Read the resource into a string
+ BufferedReader r = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(resid)));
+ StringBuilder builder = new StringBuilder();
+ String line;
+ while ((line = r.readLine()) != null) {
+ builder.append(line);
+ }
+
+ // Parse
+ DrawableAnimationSeries drawableSeries = new DrawableAnimationSeries(context, view);
+ JSONObject root = new JSONObject(builder.toString());
+
+ // The root is a an object with keys that are sequence IDs
+ for (Iterator iter = root.keys(); iter.hasNext();) {
+ String id = iter.next();
+ JSONObject obj = root.getJSONObject(id);
+ int frameDuration = obj.optInt("frame_duration", DEFAULT_FRAME_DURATION);
+ boolean isOneShot = obj.optBoolean("oneshot", DEFAULT_ONESHOT_STATUS);
+ JSONArray frames = obj.getJSONArray("frames");
+ AnimationDrawableLoader loader = new AnimationDrawableLoader(context, frameDuration, isOneShot, jsonArrayToArray(frames));
+ AnimationSection section = new AnimationSection(id, loader);
+
+ JSONObject transitions_from;
+ if (obj.has("transitions_from")) {
+ transitions_from = obj.getJSONObject("transitions_from");
+ } else {
+ transitions_from = new JSONObject();
+ }
+
+ // The optional "transitions" entry is another list of objects
+ for (Iterator transition_iter = transitions_from.keys(); transition_iter.hasNext();) {
+ String from = transition_iter.next();
+
+ JSONObject t_obj = transitions_from.getJSONObject(from);
+ frameDuration = t_obj.optInt("frame_duration", DEFAULT_FRAME_DURATION);
+ frames = t_obj.getJSONArray("frames");
+ loader = new AnimationDrawableLoader(context, frameDuration, true, jsonArrayToArray(frames));
+ section.addTransition(from, loader);
+ }
+ drawableSeries.addSection(section);
+ }
+
+ return drawableSeries;
+ }
+
+ /**
+ * Create a DrawableAnimationSeries from a JSON resource without a connected View.
+ *
+ * @param context the Application context.
+ * @param resid The resource ID the the raw json document.
+ * @return A new DrawableAnimationSeries instance.
+ * @throws JSONException
+ * @throws IOException
+ */
+ public static DrawableAnimationSeries fromJsonResource(Context context, int resid) throws JSONException, IOException {
+ return DrawableAnimationSeries.fromJsonResource(context, null, resid);
+ }
+
+ /**
+ * Add an animation section to this series.
+ *
+ * @param section the section to add.
+ */
+ private void addSection(AnimationSection section) {
+ mSectionsById.put(section.getId(), section);
+ }
+
+ /**
+ * Returns the registered listener.
+ */
+ public AnimationSeriesListener getSeriesAnimationFinishedListener() {
+ return mListener;
+ }
+
+ /**
+ * Registers a listener that will be called when a running animation finishes. If the
+ * animation is continuous, the listener will be called every time the last frame of the
+ * animation is played.
+ *
+ * @param listener The listener to register.
+ */
+ public void setSeriesAnimationFinishedListener(AnimationSeriesListener listener) {
+ this.mListener = listener;
+ }
+
+ /**
+ * Calculates the total duration of the current animation section, including the transition
+ * if applicable. If the the animation is not a oneshot, the total will be for a single loop.
+ *
+ * @return The total animation duration, or 0 if no animation is playing.
+ */
+ public int currentSectionDuration() {
+ if (mCurrentSection == null) return 0;
+ return mCurrentSection.getDuration(mTransitioningFromId);
+ }
+
+ /**
+ * Returns the currently playing animation, or null if no animation has ever played.
+ */
+ public AnimationDrawable getCurrentDrawable() {
+ return mCurrentDrawable;
+ }
+
+ /**
+ * Return the ID of the current section if one is playing, or null otherwise.
+ */
+ public String getCurrentSectionId() {
+ return mCurrentSection == null ? null : mCurrentSection.getId();
+ }
+
+ /**
+ * Play an animation drawable.
+ *
+ * @param drawable The drawable to play.
+ */
+ @SuppressLint("NewApi")
+ private void playDrawable(NotifyingAnimationDrawable drawable) {
+ mCurrentDrawable = drawable;
+ mCurrentDrawable.setAnimationFinishedListener(this);
+
+ if (mListener != null) {
+ mListener.onAnimationStarting();
+ }
+
+ if (mView != null) {
+ if (Build.VERSION.SDK_INT >= 16) {
+ mView.setBackground(mCurrentDrawable);
+ } else {
+ mView.setBackgroundDrawable(mCurrentDrawable);
+ }
+ }
+ mCurrentDrawable.start();
+ }
+
+ /**
+ * Queues a section to start as soon as the current animation finishes.
+ * If no animation is playing, the queued animation will be started immediately
+ * if it is not the current animation.
+ */
+ public void queueTransition(String id) {
+ if (mCurrentSection == null ||
+ !getCurrentSectionId().equals(id) &&
+ mCurrentDrawable != null &&
+ mCurrentDrawable.isOneShot() &&
+ mCurrentDrawable.isFinished()) {
+ transitionNow(id);
+ } else {
+ mNextSection = mSectionsById.get(id);
+ }
+ }
+
+ /**
+ * Starts a specific section without waiting for the current animation to finish.
+ * If the last registered animation is currently playing, or no animations have been
+ * registered, no action is taken.
+ */
+ public void transitionNow(String id) {
+ AnimationSection newSection = mSectionsById.get(id);
+ if (newSection == null) {
+ throw new RuntimeException("transitionNow called with invalid id: " + id);
+ }
+
+ // If the section has a transition from the old section, play the
+ // transition before the main animation.
+ NotifyingAnimationDrawable transition = mCurrentSection == null ?
+ null : newSection.getTransition(mCurrentSection.getId());
+ if (transition != null) {
+ mCurrentDrawable = transition;
+ mTransitioningFromId = mCurrentSection.getId();
+ } else {
+ mCurrentDrawable = newSection.loadDrawable();
+ mTransitioningFromId = null;
+ }
+ mCurrentSection = newSection;
+ mNextSection = null;
+
+ playDrawable(mCurrentDrawable);
+ }
+
+ /**
+ * Calls the listener callback if one was registered and transitions to the next state.
+ */
+ @Override
+ public void onAnimationFinished() {
+ if (mListener != null) {
+ mListener.onAnimationFinished();
+ }
+ if (mTransitioningFromId != null) {
+ playDrawable(mCurrentSection.loadDrawable());
+ mTransitioningFromId = null;
+ } else if (mNextSection != null) {
+ transitionNow(mNextSection.getId());
+ }
+ }
+}
diff --git a/library/src/main/java/com/getkeepsafe/android/drawableanimationseries/NotifyingAnimationDrawable.java b/library/src/main/java/com/getkeepsafe/android/drawableanimationseries/NotifyingAnimationDrawable.java
new file mode 100755
index 0000000..180a781
--- /dev/null
+++ b/library/src/main/java/com/getkeepsafe/android/drawableanimationseries/NotifyingAnimationDrawable.java
@@ -0,0 +1,76 @@
+package com.getkeepsafe.android.drawableanimationseries;
+
+import android.graphics.drawable.AnimationDrawable;
+
+/**
+ * Extends AnimationDrawable to signal an event when the animation finishes.
+ * This class behaves identically to a normal AnimationDrawable, but contains a method for
+ * registering a callback that is called whenever the final frame of the animation is played.
+ * If the animation is continuous, the callback will be called repeatedly while the animation
+ * is running.
+ *
+ * @author AJ Alt
+ */
+public class NotifyingAnimationDrawable extends AnimationDrawable {
+ public interface OnAnimationFinishedListener {
+ void onAnimationFinished();
+ }
+
+ private boolean mFinished = false;
+ private OnAnimationFinishedListener mListener;
+
+ /**
+ * @param drawable The frames data from animation will be copied into this instance. The animation object will be unchanged.
+ */
+ public NotifyingAnimationDrawable(AnimationDrawable drawable) {
+ for (int i = 0; i < drawable.getNumberOfFrames(); i++) {
+ addFrame(drawable.getFrame(i), drawable.getDuration(i));
+ }
+ setOneShot(drawable.isOneShot());
+ }
+
+ public NotifyingAnimationDrawable() {
+ super();
+ }
+
+ /**
+ * @return The registered animation listener.
+ */
+ public OnAnimationFinishedListener getAnimationFinishedListener() {
+ return mListener;
+ }
+
+ /**
+ * Sets a listener that will be called when the last frame of the animation is rendered.
+ * If the animation is continuous, the listener will be called repeatedly while the animation
+ * is running.
+ *
+ * @param listener The listener to register.
+ */
+ public void setAnimationFinishedListener(OnAnimationFinishedListener listener) {
+ this.mListener = listener;
+ }
+
+ /**
+ * Indicates whether the animation has ever finished.
+ */
+ public boolean isFinished() {
+ return mFinished;
+ }
+
+ @Override
+ public boolean selectDrawable(int idx) {
+ boolean result = super.selectDrawable(idx);
+
+ if (idx != 0 && idx == getNumberOfFrames() - 1) {
+ if (!mFinished || !isOneShot()) {
+ mFinished = true;
+ if (mListener != null) {
+ mListener.onAnimationFinished();
+ }
+ }
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/samples/.gitignore b/samples/.gitignore
new file mode 100644
index 0000000..915c5f4
--- /dev/null
+++ b/samples/.gitignore
@@ -0,0 +1,2 @@
+/build
+/src/version/
diff --git a/samples/AndroidManifest.xml b/samples/AndroidManifest.xml
new file mode 100644
index 0000000..77ed15a
--- /dev/null
+++ b/samples/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/build.gradle b/samples/build.gradle
new file mode 100644
index 0000000..a7e7594
--- /dev/null
+++ b/samples/build.gradle
@@ -0,0 +1,37 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:1.1.0'
+ }
+}
+
+apply plugin: 'com.android.application'
+
+repositories {
+ mavenCentral()
+}
+
+android {
+ compileSdkVersion 21
+ buildToolsVersion "21.1.2"
+
+ defaultConfig {
+ applicationId "com.getkeepsafe.android.drawableanimationseries.samples"
+ versionCode 1
+ versionName "0.1.0"
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ res.srcDirs = ['res']
+ assets.srcDirs = ['assets']
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
diff --git a/samples/res/drawable-hdpi/ic_launcher.png b/samples/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
Binary files /dev/null and b/samples/res/drawable-hdpi/ic_launcher.png differ
diff --git a/samples/res/drawable-mdpi/ic_launcher.png b/samples/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
Binary files /dev/null and b/samples/res/drawable-mdpi/ic_launcher.png differ
diff --git a/samples/res/drawable-xhdpi/ic_launcher.png b/samples/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
Binary files /dev/null and b/samples/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/samples/res/values/strings.xml b/samples/res/values/strings.xml
new file mode 100644
index 0000000..8542005
--- /dev/null
+++ b/samples/res/values/strings.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/samples/res/values/styles.xml b/samples/res/values/styles.xml
new file mode 100644
index 0000000..bd5027f
--- /dev/null
+++ b/samples/res/values/styles.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..69de25d
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':library', ':samples'