Android 跳动的TextView JumpingBeans

版权声明:本文为博主原创文章,未经博主允许不得转载。

在做项目的时候,经常碰到在加载数据时需要等待,这个时候就需要一个加载框。在项目开发中,碰到了这个这样的需求:只对界面中局部添加加载进度条,其他地方功能不受其影响。然后琢磨着就把JumpingBeans给用上了。本篇只讲JumpingBeans的使用。

第一步:前期准备工作

1、第一种方式: compile 'net.frakbot:jumpingbeans:1.3.0'
2、第二种方式:使用JumpingBeans,JumpingBeansSpan这个两个类就可以了。下面是代码:

A.JumpingBeans.java:

   /*
    * Copyright 2014 Frakbot (Sebastiano Poggi and Francesco Pontillo)
    *
    *    Licensed under the Apache License, Version 2.0 (the "License");
    *    you may not use this file except in compliance with the License.
    *    You may obtain a copy of the License at
    *
    *        http://www.apache.org/licenses/LICENSE-2.0
    *
    *    Unless required by applicable law or agreed to in writing, software
    *    distributed under the License is distributed on an "AS IS" BASIS,
    *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    *    See the License for the specific language governing permissions and
    *    limitations under the License.
    */

  package com.jieshun.jscarlife.widgets;
  import android.support.annotation.NonNull;
  import android.text.SpannableStringBuilder;
  import android.text.Spanned;
  import android.text.TextUtils;
  import android.widget.TextView;
  import java.lang.ref.WeakReference;


  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.65f;

/**
 * The default duration of a whole jumping animation loop, in milliseconds.
 */
public static final int DEFAULT_LOOP_DURATION = 1300;   // ms

public static final String ELLIPSIS_GLYPH = "…";
public static final String THREE_DOTS_ELLIPSIS = "...";
public static final int THREE_DOTS_ELLIPSIS_LENGTH = 3;

private final JumpingBeansSpan[] jumpingBeans;
private final WeakReference<TextView> textView;

private JumpingBeans(@NonNull JumpingBeansSpan[] beans, @NonNull 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 tv) {
    if (tv != null) {
        CharSequence text = tv.getText();
        if (text instanceof Spanned) {
            CharSequence cleanText = removeJumpingBeansSpansFrom((Spanned) text);
            tv.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;
         }

        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) {
         return TextUtils.equals(text.subSequence(text.length() - 1, text.length()), ELLIPSIS_GLYPH);
        }

       private static boolean endsWithThreeEllipsisDots(@NonNull CharSequence text) {
         if (text.length() < THREE_DOTS_ELLIPSIS_LENGTH) {
        // TODO we should try to normalize "invalid" ellipsis (e.g., ".." or "....")
        return false;
       }
          return TextUtils.equals(text.subSequence(text.length() - THREE_DOTS_ELLIPSIS_LENGTH, text.length()), THREE_DOTS_ELLIPSIS);
       }
        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;
       }
       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;

          /*package*/ Builder(TextView textView) {
        this.textView = textView;
       }

           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;
           }

           public Builder makeTextJump(int startPos, int endPos) {
              CharSequence text = textView.getText();
              ensureTextCanJump(startPos, endPos, text);

              this.text = text;
              this.wave = true;
              this.startPos = startPos;
              this.endPos = endPos;

              return this;
           }

            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;
           }
         public Builder setLoopDuration(int loopDuration) {
              if (loopDuration < 1) {
                  throw new IllegalArgumentException("The loop duration must be bigger than zero");
              }
              this.loopDuration = loopDuration;
              return this;
           }
           public Builder setWavePerCharDelay(int waveCharOffset) {
               if (waveCharOffset < 0) {
                  throw new IllegalArgumentException("The wave char offset must be non-negative");
              }
              this.waveCharDelay = waveCharOffset;
              return this;
           }         
           public Builder setIsWave(boolean wave) {
            this.wave = wave;
             return this;
           }
            public JumpingBeans build() {
               SpannableStringBuilder sbb = new SpannableStringBuilder(text);
              JumpingBeansSpan[] spans;
               if (wave) {
               spans = getJumpingBeansSpans(sbb);
              } else {
                  spans = buildSingleSpan(sbb);
               }
               textView.setText(sbb);
               return new JumpingBeans(spans, textView);
            }

           private JumpingBeansSpan[] getJumpingBeansSpans(SpannableStringBuilder sbb) {
               JumpingBeansSpan[] spans;
                if (waveCharDelay == -1) {
            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;
               }
           }
               }

B.JumpingBeansSpan.java

 /*
 * Copyright 2014 Frakbot (Sebastiano Poggi and Francesco Pontillo)
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.jieshun.jscarlife.widgets;

import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.os.Build;
import android.support.annotation.NonNull;
import android.text.TextPaint;
import android.text.style.SuperscriptSpan;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import java.lang.ref.WeakReference;

/*package*/
@SuppressLint("ParcelCreator")
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, 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(@NonNull TextPaint tp) {
    initIfNecessary(tp.ascent());
    tp.baselineShift = shift;
}

@Override
public void updateDrawState(@NonNull 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 run on main thread anyway
    TextView v = textView.get();
    if (v != null) {
        updateAnimationFor(animation, v);
    } else {
        cleanupAndComplainAboutUserBeingAFool();
    }
}

private void updateAnimationFor(@NonNull ValueAnimator animation, @NonNull 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
}

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 !!!");
}

/*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 65% of the full range.
 *
 * @see net.frakbot.jumpingbeans.JumpingBeans#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]
        double radians = (input / animRange) * Math.PI;
        double interpolatedValue = Math.max(0f, Math.sin(radians));
        return (float) interpolatedValue;
    }

}

}

第二步:具体使用

1、使用TextView布局,findViewById就可以。
2、声明一个JumpingBeans对象 :private JumpingBeans jumpingTvQry;
3、进行设置
jumpingTvQry = JumpingBeans.with(tvQrying)
.makeTextJump(0, tvQrying.getText().toString().length())
.setIsWave(true)
.setLoopDuration(1000)
.build();
with://把这个TextView绑定到JumpingBeans 中
makeTextJump://具体动作的哪几个字如传入(0,3)就是第1-第4个字有动画效果
setIsWave://是否允许有动画
setLoopDuration://效果持续的时间,毫秒为单位
build://判断一系列参数,构建效果并执行(重新对TextView的字符串内容进行拼接)

第三步:关闭开启的动画

@Override
protected void onDestroy()
{
jumpingTvQry.stopJumping();
jumpingTvQryDot.stopJumping();
super.onDestroy();
}

至此,整个JumpingBeans的使用就告一段落了。希望对你们有帮助。
补个效果图:


Paste_Image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,047评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,943评论 4 60
  • 从前有座庙,庙里有个老和尚。老和尚有个小儿子,小儿子叫做江流儿。 有一天江流儿出去玩耍,经过一座大山。大山...
    弈道者阅读 989评论 3 2
  • http://blog.sina.com.cn/s/blog_5971cdd00102vqgy.html
    楠Y阅读 234评论 0 0
  • 17考研的成绩下来那天,我全身发抖,害怕自己考的很差。中午午休的时候,我一个人偷偷摸摸的查了成绩,果然,291分...
    半夏夏半阅读 320评论 0 0