直入主题!!!
在你的build.gradle 引入
compile 'com.android.support:support-annotations:23.1.1
android-support-annotations是Android官方提供的一个注解库,它提供了许多有用的注解,这些注解的生命周期为源码时期,也就是在编译之后则不再保留,通常用于辅助代码上的静态检查。
但是如果你已经通过这种方式依赖了support-v4库的话,则不必再显式声明依赖它,因为support-v4也依赖于这个库,由于Gradle的传递依赖的特性,你的项目如果依赖了support-v4库的话,也会依赖到这个注解库。
详细介绍
使用
1.让 Jumping 跳跃起来
@Override
protected void onResume() {
super.onResume();
// those instead)
final TextView textView1 = (TextView) findViewById(R.id.jumping_text_1);
jumpingBeans1 = JumpingBeans.with(textView1)
.appendJumpingDots()
.build();
}
@Override
protected void onPause() {
super.onPause();
jumpingBeans1.stopJumping();
jumpingBeans2.stopJumping();
}
2.让。。。跳跃起来
final TextView textView2 = (TextView) findViewById(R.id.jumping_text_2);
jumpingBeans2 = JumpingBeans.with(textView2)
.makeTextJump(0, textView2.getText().toString().indexOf(' '))
.setIsWave(false)
.setLoopDuration(1000)
.build();
功能Bean
1.JumpingBeans
public final class JumpingBeans {
private static final String ELLIPSIS_GLYPH = "…";
private static final String THREE_DOTS_ELLIPSIS = "...";
private static final int THREE_DOTS_ELLIPSIS_LENGTH = 3;
private final JumpingBeansSpan[] jumpingBeans;
private final WeakReference<TextView> textView;
private JumpingBeans(JumpingBeansSpan[] beans, TextView textView) {
this.jumpingBeans = beans;
this.textView = new WeakReference<>(textView);
}
/**
* Create an instance of the {@link net.frakbot.jumpingbeans.JumpingBeans.Builder}
* applied to the provided {@code TextView}.
*
* @param textView The TextView to apply the JumpingBeans to
* @return the {@link net.frakbot.jumpingbeans.JumpingBeans.Builder}
*/
public static Builder with(@NonNull TextView textView) {
return new Builder(textView);
}
/**
* Stops the jumping animation and frees up the animations.
*/
public void stopJumping() {
for (JumpingBeansSpan bean : jumpingBeans) {
if (bean != null) {
bean.teardown();
}
}
cleanupSpansFrom(textView.get());
}
private static void cleanupSpansFrom(TextView textView) {
if (textView == null) {
return;
}
CharSequence text = textView.getText();
if (text instanceof Spanned) {
CharSequence cleanText = removeJumpingBeansSpansFrom((Spanned) text);
textView.setText(cleanText);
}
}
private static CharSequence removeJumpingBeansSpansFrom(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 = JumpingBeans.with(myTextView)
* .appendJumpingDots()
* .setLoopDuration(1500)
* .build();
* </pre>
*
* @see JumpingBeans#with(TextView)
*/
public static class Builder {
private static final float DEFAULT_ANIMATION_DUTY_CYCLE = 0.65f;
private static final int DEFAULT_LOOP_DURATION = 1300; // ms
private static final int DEFAULT_WAVE_CHAR_DELAY = -1;
private final TextView textView;
private int startPos;
private int endPos;
private float animRange = DEFAULT_ANIMATION_DUTY_CYCLE;
private int loopDuration = DEFAULT_LOOP_DURATION;
private int waveCharDelay = DEFAULT_WAVE_CHAR_DELAY;
private CharSequence text;
private boolean wave;
Builder(TextView textView) {
this.textView = textView;
}
/**
* 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}.
*
* @see #setIsWave(boolean)
*/
@NonNull
public Builder appendJumpingDots() {
CharSequence text = appendThreeDotsEllipsisTo(textView);
this.text = text;
this.wave = true;
this.startPos = text.length() - THREE_DOTS_ELLIPSIS_LENGTH;
this.endPos = text.length();
return this;
}
private static CharSequence appendThreeDotsEllipsisTo(TextView textView) {
CharSequence text = getTextSafe(textView);
if (text.length() > 0 && endsWithEllipsisGlyph(text)) {
text = text.subSequence(0, text.length() - 1);
}
if (!endsWithThreeEllipsisDots(text)) {
text = new SpannableStringBuilder(text).append(THREE_DOTS_ELLIPSIS); // Preserve spans in original text
}
return text;
}
private static CharSequence getTextSafe(TextView textView) {
return !TextUtils.isEmpty(textView.getText()) ? textView.getText() : "";
}
private static boolean endsWithEllipsisGlyph(CharSequence text) {
CharSequence lastChar = text.subSequence(text.length() - 1, text.length());
return ELLIPSIS_GLYPH.equals(lastChar);
}
@SuppressWarnings("SimplifiableIfStatement") // For readability
private static boolean endsWithThreeEllipsisDots(CharSequence text) {
if (text.length() < THREE_DOTS_ELLIPSIS_LENGTH) {
// TODO we should try to normalize "invalid" ellipsis (e.g., ".." or "....")
return false;
}
CharSequence lastThreeChars = text.subSequence(text.length() - THREE_DOTS_ELLIPSIS_LENGTH, text.length());
return THREE_DOTS_ELLIPSIS.equals(lastThreeChars);
}
/**
* 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 startPos The position of the first character to animate
* @param endPos The position after the one the animated range ends at
* (just like in {@link String#substring(int)})
* @see #setIsWave(boolean)
*/
@NonNull
public Builder makeTextJump(@IntRange(from = 0) int startPos, @IntRange(from = 0) int endPos) {
CharSequence text = textView.getText();
ensureTextCanJump(startPos, endPos, text);
this.text = text;
this.wave = true;
this.startPos = startPos;
this.endPos = endPos;
return this;
}
private static CharSequence ensureTextCanJump(int startPos, int endPos, CharSequence text) {
if (text == null) {
throw new NullPointerException("The textView 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");
}
if (endPos > text.length()) {
throw new IndexOutOfBoundsException("The end position must be smaller than the text length");
}
return text;
}
/**
* Sets the fraction of the animation loop time spent actually animating.
* The rest of the time will be spent "resting".
*
* @param animatedRange The fraction of the animation loop time spent
* actually animating the characters
*/
@NonNull
public Builder setAnimatedDutyCycle(@FloatRange(from = 0f, to = 1f, fromInclusive = false) 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.
*
* @param loopDuration The jumping animation loop duration, in milliseconds
*/
@NonNull
public Builder setLoopDuration(@IntRange(from = 1) 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)
*/
@NonNull
public Builder setWavePerCharDelay(@IntRange(from = 0) 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)
*/
@NonNull
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.
*/
@NonNull
public JumpingBeans build() {
SpannableStringBuilder sbb = new SpannableStringBuilder(text);
JumpingBeansSpan[] spans;
if (wave) {
spans = buildWavingSpans(sbb);
} else {
spans = buildSingleSpan(sbb);
}
textView.setText(sbb);
return new JumpingBeans(spans, textView);
}
@SuppressWarnings("Range") // Lint bug: the if makes sure waveCharDelay >= 0
private JumpingBeansSpan[] buildWavingSpans(SpannableStringBuilder sbb) {
JumpingBeansSpan[] spans;
if (waveCharDelay == DEFAULT_WAVE_CHAR_DELAY) {
waveCharDelay = loopDuration / (3 * (endPos - startPos));
}
spans = 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);
spans[pos - startPos] = jumpingBean;
}
return spans;
}
private JumpingBeansSpan[] buildSingleSpan(SpannableStringBuilder sbb) {
JumpingBeansSpan[] spans;
spans = new JumpingBeansSpan[]{new JumpingBeansSpan(textView, loopDuration, 0, 0, animRange)};
sbb.setSpan(spans[0], startPos, endPos, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return spans;
}
}
}
2 . JumpingBeansSpan
final class JumpingBeansSpan extends SuperscriptSpan implements ValueAnimator.AnimatorUpdateListener {
private final WeakReference<TextView> textView;
private final int delay;
private final int loopDuration;
private final float animatedRange;
private int shift;
private ValueAnimator jumpAnimator;
public JumpingBeansSpan(@NonNull TextView textView,
@IntRange(from = 1) int loopDuration,
@IntRange(from = 0) int position,
@IntRange(from = 0) int waveCharOffset,
@FloatRange(from = 0, to = 1, fromInclusive = false) 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.ascent());
tp.baselineShift = shift;
}
@Override
public void updateDrawState(TextPaint tp) {
initIfNecessary(tp.ascent());
tp.baselineShift = shift;
}
private void initIfNecessary(float ascent) {
if (jumpAnimator != null) {
return;
}
this.shift = 0;
int maxShift = (int) ascent / 2;
jumpAnimator = ValueAnimator.ofInt(0, maxShift);
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 runs on main thread anyway
TextView v = textView.get();
if (v != null) {
updateAnimationFor(animation, v);
} else {
cleanupAndComplainAboutUserBeingAFool();
}
}
private void updateAnimationFor(ValueAnimator animation, TextView v) {
if (isAttachedToHierarchy(v)) {
shift = (int) animation.getAnimatedValue();
v.invalidate();
}
}
private static boolean isAttachedToHierarchy(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return v.isAttachedToWindow();
}
return v.getParent() != null; // Best-effort fallback (without adding support-v4 just for this...)
}
private void cleanupAndComplainAboutUserBeingAFool() {
// The textview has been destroyed and teardown() hasn't been called
teardown();
Log.w("JumpingBeans", "!!! Remember to call JumpingBeans.stopJumping() when appropriate !!!");
}
public 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 65% of the full range.
*
* @see JumpingBeans.Builder#DEFAULT_ANIMATION_DUTY_CYCLE
*/
private static class JumpInterpolator implements TimeInterpolator {
private final float animRange;
public JumpInterpolator(float animatedRange) {
animRange = Math.abs(animatedRange);
}
@Override
public float getInterpolation(float input) {
// We want to map the [0, PI] sine range onto [0, animRange]
if (input > animRange) {
return 0f;
}
double radians = (input / animRange) * Math.PI;
return (float) Math.sin(radians);
}
}
}