Last active
August 29, 2015 14:06
-
-
Save SeniorZhai/ae1338d6d13c5870d913 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.ref.WeakReference; | |
import android.text.SpannableStringBuilder; | |
import android.text.Spanned; | |
import android.text.TextUtils; | |
import android.widget.TextView; | |
public final class JumpingBeans { | |
/** | |
* The default fraction of the whole animation time spent actually animating. | |
* The rest of the range will be spent in "resting" state. | |
* This the "duty cycle" of the jumping animation. | |
*/ | |
public static final float DEFAULT_ANIMATION_DUTY_CYCLE = 0.5f; | |
/** | |
* The default duration of a whole jumping animation loop, in milliseconds. | |
*/ | |
public static final int DEFAULT_LOOP_DURATION = 1500; | |
private JumpingBeansSpan[] jumpingBeans; | |
private WeakReference<TextView> textView; | |
private JumpingBeans(JumpingBeansSpan[] beans, TextView textView) { | |
// Clients will have to use the builder | |
this.jumpingBeans = beans; | |
this.textView = new WeakReference<TextView>(textView); | |
} | |
/** | |
* Stops the jumping animation and frees up the animations. | |
*/ | |
public void stopJumping() { | |
for (JumpingBeansSpan bean : jumpingBeans) { | |
if (bean != null) { | |
bean.teardown(); | |
} | |
} | |
TextView tv = textView.get(); | |
if (tv != null) { | |
CharSequence text = tv.getText(); | |
if (text instanceof Spanned) { | |
CharSequence cleanText = removeJumpingBeansSpans((Spanned) text); | |
tv.setText(cleanText); | |
} | |
} | |
} | |
private static CharSequence removeJumpingBeansSpans(Spanned text) { | |
SpannableStringBuilder sbb = new SpannableStringBuilder(text.toString()); | |
Object[] spans = text.getSpans(0, text.length(), Object.class); | |
for (Object span : spans) { | |
if (!(span instanceof JumpingBeansSpan)) { | |
sbb.setSpan(span, text.getSpanStart(span), | |
text.getSpanEnd(span), text.getSpanFlags(span)); | |
} | |
} | |
return sbb; | |
} | |
/** | |
* Builder class for {@link net.frakbot.jumpingbeans.JumpingBeans} objects. | |
* <p/> | |
* Provides a way to set the fields of a {@link JumpingBeans} and generate | |
* the desired jumping beans effect. With this builder you can easily append | |
* a Hangouts-style trio of jumping suspension points to any TextView, or | |
* apply the effect to any other subset of a TextView's text. | |
* <p/> | |
* <p>Example: | |
* <p/> | |
* <pre class="prettyprint"> | |
* JumpingBeans jumpingBeans = new JumpingBeans.Builder() | |
* .appendJumpingDots(myTextView) | |
* .setLoopDuration(1500) | |
* .build(); | |
* </pre> | |
*/ | |
public static class Builder { | |
private int startPos, endPos; | |
private float animRange = DEFAULT_ANIMATION_DUTY_CYCLE; | |
private int loopDuration = DEFAULT_LOOP_DURATION; | |
private int waveCharDelay = -1; | |
private CharSequence text; | |
private TextView textView; | |
private boolean wave; | |
/** | |
* Appends three jumping dots to the end of a TextView text. | |
* <p/> | |
* This implies that the animation will by default be a wave. | |
* <p/> | |
* If the TextView has no text, the resulting TextView text will | |
* consist of the three dots only. | |
* <p/> | |
* The TextView text is cached to the current value at | |
* this time and set again in the {@link #build()} method, so any | |
* change to the TextView text done in the meantime will be lost. | |
* This means that <b>you should do all changes to the TextView text | |
* <i>before</i> you begin using this builder.</b> | |
* <p/> | |
* Call the {@link #build()} method once you're done to get the | |
* resulting {@link net.frakbot.jumpingbeans.JumpingBeans}. | |
* | |
* @param textView The TextView to append the dots to | |
* @see #setIsWave(boolean) | |
*/ | |
public Builder appendJumpingDots(TextView textView) { | |
if (textView == null) { | |
throw new NullPointerException("The textView must not be null"); | |
} | |
CharSequence text = !TextUtils.isEmpty(textView.getText()) ? textView.getText() : ""; | |
if (text.length() > 0 && text.subSequence(text.length() - 1, text.length()).equals("…")) { | |
text = text.subSequence(0, text.length() - 1); | |
} | |
if (text.length() < 3 || !TextUtils.equals(text.subSequence(text.length() - 3, text.length()), "...")) { | |
text = new SpannableStringBuilder(text).append("..."); // Preserve spans in original text | |
} | |
this.text = text; | |
this.wave = true; | |
this.textView = textView; | |
this.startPos = this.text.length() - 3; | |
this.endPos = this.text.length(); | |
return this; | |
} | |
/** | |
* Appends three jumping dots to the end of a TextView text. | |
* <p/> | |
* This implies that the animation will by default be a wave. | |
* <p/> | |
* If the TextView has no text, the resulting TextView text will | |
* consist of the three dots only. | |
* <p/> | |
* The TextView text is cached to the current value at | |
* this time and set again in the {@link #build()} method, so any | |
* change to the TextView text done in the meantime will be lost. | |
* This means that <b>you should do all changes to the TextView text | |
* <i>before</i> you begin using this builder.</b> | |
* <p/> | |
* Call the {@link #build()} method once you're done to get the | |
* resulting {@link net.frakbot.jumpingbeans.JumpingBeans}. | |
* | |
* @param textView The TextView whose text is to be animated | |
* @param startPos The position of the first character to animate | |
* @param endPos The position after the one the animated range ends at | |
* (just like in String#substring()) | |
* @see #setIsWave(boolean) | |
*/ | |
public Builder makeTextJump(TextView textView, int startPos, int endPos) { | |
if (textView == null || textView.getText() == null) { | |
throw new NullPointerException("The textView and its text must not be null"); | |
} | |
if (endPos < startPos) { | |
throw new IllegalArgumentException("The start position must be smaller than the end position"); | |
} | |
if (startPos < 0) { | |
throw new IndexOutOfBoundsException("The start position must be non-negative"); | |
} | |
this.text = textView.getText(); | |
if (endPos > text.length()) { | |
throw new IndexOutOfBoundsException("The end position must be smaller than the text length"); | |
} | |
this.wave = true; | |
this.textView = textView; | |
this.startPos = startPos; | |
this.endPos = endPos; | |
return this; | |
} | |
/** | |
* Sets the fraction of the animation loop time spent actually animating. | |
* The rest of the time will be spent "resting". | |
* The default value is | |
* {@link net.frakbot.jumpingbeans.JumpingBeans#DEFAULT_ANIMATION_DUTY_CYCLE}. | |
* | |
* @param animatedRange The fraction of the animation loop time spent | |
* actually animating the characters | |
*/ | |
public Builder setAnimatedDutyCycle(float animatedRange) { | |
if (animatedRange <= 0f || animatedRange > 1f) { | |
throw new IllegalArgumentException("The animated range must be in the (0, 1] range"); | |
} | |
this.animRange = animatedRange; | |
return this; | |
} | |
/** | |
* Sets the jumping loop duration. The default value is | |
* {@link net.frakbot.jumpingbeans.JumpingBeans#DEFAULT_LOOP_DURATION}. | |
* | |
* @param loopDuration The jumping animation loop duration, in milliseconds | |
*/ | |
public Builder setLoopDuration(int loopDuration) { | |
if (loopDuration < 1) { | |
throw new IllegalArgumentException("The loop duration must be bigger than zero"); | |
} | |
this.loopDuration = loopDuration; | |
return this; | |
} | |
/** | |
* Sets the delay for starting the animation of every single dot over the | |
* start of the previous one, in milliseconds. The default value is | |
* the loop length divided by three times the number of character animated | |
* by this instance of JumpingBeans. | |
* <p/> | |
* Only has a meaning when the animation is a wave. | |
* | |
* @param waveCharOffset The start delay for the animation of every single | |
* character over the previous one, in milliseconds | |
* @see #setIsWave(boolean) | |
*/ | |
public Builder setWavePerCharDelay(int waveCharOffset) { | |
if (waveCharOffset < 0) { | |
throw new IllegalArgumentException("The wave char offset must be non-negative"); | |
} | |
this.waveCharDelay = waveCharOffset; | |
return this; | |
} | |
/** | |
* Sets a flag that determines if the characters will jump in a wave | |
* (i.e., with a delay between each other) or all at the same | |
* time. | |
* | |
* @param wave If true, the animation is going to be a wave; if | |
* false, all characters will jump ay the same time | |
* @see #setWavePerCharDelay(int) | |
*/ | |
public Builder setIsWave(boolean wave) { | |
this.wave = wave; | |
return this; | |
} | |
/** | |
* Combine all of the options that have been set and return a new | |
* {@link net.frakbot.jumpingbeans.JumpingBeans} instance. | |
* <p/> | |
* Remember to call the {@link #stopJumping()} method once you're done | |
* using the JumpingBeans (that is, when you detach the TextView from | |
* the view tree, you hide it, or the parent Activity/Fragment goes in | |
* the paused status). This will allow to release the animations and | |
* free up memory and CPU that would be otherwise wasted. | |
*/ | |
public JumpingBeans build() { | |
SpannableStringBuilder sbb = new SpannableStringBuilder(text); | |
JumpingBeansSpan[] jumpingBeans; | |
if (!wave) { | |
jumpingBeans = new JumpingBeansSpan[]{new JumpingBeansSpan(textView, loopDuration, 0, 0, animRange)}; | |
sbb.setSpan(jumpingBeans[0], startPos, endPos, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | |
} else { | |
if (waveCharDelay == -1) { | |
waveCharDelay = loopDuration / (3 * (endPos - startPos)); | |
} | |
jumpingBeans = new JumpingBeansSpan[endPos - startPos]; | |
for (int pos = startPos; pos < endPos; pos++) { | |
JumpingBeansSpan jumpingBean = | |
new JumpingBeansSpan(textView, loopDuration, pos - startPos, waveCharDelay, animRange); | |
sbb.setSpan(jumpingBean, pos, pos + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | |
jumpingBeans[pos - startPos] = jumpingBean; | |
} | |
} | |
textView.setText(sbb); | |
return new JumpingBeans(jumpingBeans, textView); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.ref.WeakReference; | |
import android.animation.TimeInterpolator; | |
import android.animation.ValueAnimator; | |
import android.os.Build; | |
import android.text.TextPaint; | |
import android.text.style.SuperscriptSpan; | |
import android.util.Log; | |
import android.view.View; | |
import android.widget.TextView; | |
public final class JumpingBeansSpan extends SuperscriptSpan implements ValueAnimator.AnimatorUpdateListener { | |
private ValueAnimator jumpAnimator; | |
private WeakReference<TextView> textView; | |
private int shift; | |
private int delay; | |
private int loopDuration; | |
private float animatedRange; | |
public JumpingBeansSpan(TextView textView, int loopDuration, int position, int waveCharOffset, | |
float animatedRange) { | |
this.textView = new WeakReference<>(textView); | |
this.delay = waveCharOffset * position; | |
this.loopDuration = loopDuration; | |
this.animatedRange = animatedRange; | |
} | |
@Override | |
public void updateMeasureState(TextPaint tp) { | |
initIfNecessary(tp); | |
tp.baselineShift = shift; | |
} | |
@Override | |
public void updateDrawState(TextPaint tp) { | |
initIfNecessary(tp); | |
tp.baselineShift = shift; | |
} | |
private void initIfNecessary(TextPaint tp) { | |
if (jumpAnimator != null) { | |
return; | |
} | |
shift = (int) tp.ascent() / 2; | |
jumpAnimator = ValueAnimator.ofInt(0, shift, 0); | |
jumpAnimator | |
.setDuration(loopDuration) | |
.setStartDelay(delay); | |
jumpAnimator.setInterpolator(new JumpInterpolator(animatedRange)); | |
jumpAnimator.setRepeatCount(ValueAnimator.INFINITE); | |
jumpAnimator.setRepeatMode(ValueAnimator.RESTART); | |
jumpAnimator.addUpdateListener(this); | |
jumpAnimator.start(); | |
} | |
@Override | |
public void onAnimationUpdate(ValueAnimator animation) { | |
// No need for synchronization as this always run on main thread anyway | |
TextView v = textView.get(); | |
if (v != null) { | |
if (isAttachedToHierarchy(v)) { | |
shift = (int) animation.getAnimatedValue(); | |
v.invalidate(); | |
} else { | |
animation.setCurrentPlayTime(0); | |
animation.start(); | |
} | |
} else { | |
// The textview has been destroyed and teardown() hasn't been called | |
teardown(); | |
if (BuildConfig.DEBUG) { | |
Log.e("JumpingBeans", "!!! Remember to call JumpingBeans.stopJumping() when appropriate !!!"); | |
} | |
} | |
} | |
private boolean isAttachedToHierarchy(View v) { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | |
return v.isAttachedToWindow(); | |
} else { | |
return v.getParent() != null; // Best-effort fallback | |
} | |
} | |
/*package*/ void teardown() { | |
if (jumpAnimator != null) { | |
jumpAnimator.cancel(); | |
jumpAnimator.removeAllListeners(); | |
} | |
if (textView.get() != null) { | |
textView.clear(); | |
} | |
} | |
/** | |
* A tweaked {@link android.view.animation.AccelerateDecelerateInterpolator} | |
* that covers the full range in a fraction of its input range, and holds on | |
* the final value on the rest of the input range. By default, this fraction | |
* is half of the full range. | |
* | |
* @see net.frakbot.jumpingbeans.JumpingBeans#DEFAULT_ANIMATION_DUTY_CYCLE | |
*/ | |
private class JumpInterpolator implements TimeInterpolator { | |
private float animRange; | |
public JumpInterpolator(float animatedRange) { | |
animRange = Math.abs(animatedRange); | |
} | |
@Override | |
public float getInterpolation(float input) { | |
if (input <= animRange) { | |
return (float) (Math.cos((input / animRange + 1) * Math.PI) / 2f) + 0.5f; | |
} | |
return 1.0f; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment