自定義Span

1 簡介

之前已經(jīng)講過TextView的基礎(chǔ)知識、段落級別的Span和字符級別的Span捧颅,分析了Android提供的一些Span的源碼景图,這篇文字講解如何自定義Span。
這篇文章中碉哑,由于段落級別的Span比較簡單挚币,在這不講述這個類型的自定義Span。這篇著重講述字符級的Span扣典,并且結(jié)合Android提供動畫機制制作出十分酷炫的動畫Span妆毕。


2 FrameSpan

FrameSpan實現(xiàn)給相應(yīng)的字符序列添加邊框的效果,整體思路其實比較簡單贮尖。

  1. 計算字符序列的寬度笛粘;
  2. 根據(jù)計算的寬度、上下坐標(biāo)湿硝、起始坐標(biāo)繪制矩形薪前;
  3. 繪制文字

展現(xiàn)效果如下所示:


FrameSpan

再來看一下代碼,其實代碼十分簡單关斜。

public class FrameSpan extends ReplacementSpan {

    private final Paint mPaint;
    private int mWidth;

    public FrameSpan() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.BLUE);
        mPaint.setAntiAlias(true);
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        //return text with relative to the Paint
        mWidth = (int) paint.measureText(text, start, end);
        return mWidth;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        //draw the frame with custom Paint
        canvas.drawRect(x, top, x + mWidth, bottom, mPaint);
        canvas.drawText(text, start, end, x, y, paint);
    }
}

在這再次說明一下draw方法里面的參數(shù)的意義示括。

  1. canvas:用來繪制的畫布;
  2. text:整個text痢畜;
  3. start:這個Span起始字符在text中的位置垛膝;
  4. end:這個Span結(jié)束字符在text中的位置鳍侣;
  5. x:這個Span的其實水平坐標(biāo);
  6. y:這個Span的baseline的垂直坐標(biāo)吼拥;
  7. top:這個Span的起始垂直坐標(biāo)倚聚;
  8. bottom:這個Span的結(jié)束垂直坐標(biāo);
  9. paint:畫筆

3 VerticalImageSpan

Google提供的ImageSpan和DynamicDrawableSpan只能實現(xiàn)圖片和文字底部對齊或者是baseline對齊凿可,現(xiàn)在VerticalImageSpan可以實現(xiàn)圖片和文字居中對齊惑折。


VerticalImageSpan

圖中的圖片保持了和文字居中對齊,現(xiàn)在來看看VerticalImageSpan的源碼枯跑。

public class VerticalImageSpan extends ImageSpan {

    private Drawable drawable;
    public VerticalImageSpan(Drawable drawable) {
        super(drawable);
        this.drawable=drawable;
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetricsInt) {
        Drawable drawable = getDrawable();
        if(drawable==null){
            drawable= this.drawable;
        }
        Rect rect = drawable.getBounds();
        if (fontMetricsInt != null) {
            Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
            int fontHeight = fmPaint.bottom - fmPaint.top;
            int drHeight = rect.bottom - rect.top;

            int top = drHeight / 2 - fontHeight / 4;
            int bottom = drHeight / 2 + fontHeight / 4;

            fontMetricsInt.ascent = -bottom;
            fontMetricsInt.top = -bottom;
            fontMetricsInt.bottom = top;
            fontMetricsInt.descent = top;
        }
        return rect.right;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        Drawable drawable = getDrawable();
        canvas.save();
        int transY = ((bottom - top) - drawable.getBounds().bottom) / 2 + top;
        canvas.translate(x, transY);
        drawable.draw(canvas);
        canvas.restore();
    }
}

在geSize方法中通過fontMetricsInt設(shè)置從而實現(xiàn)圖片和文字居中對齊唬复,其實計算的根本為計算baseline的位置,因為TextView是按照baseline對齊的全肮。
分析getSize方法可以知道這個圖片的baseline為圖片中央往下fontHeight / 2敞咧,這樣也就實現(xiàn)了圖片和文字的居中對齊。
draw方法用來繪制圖片辜腺,繪制x坐標(biāo)為span的其實坐標(biāo)休建,繪制y坐標(biāo)可以通過計算得到,具體計算請看上面的源碼评疗。


4 AnimateForegroundColorSpan

先講述一個簡單的動畫Span的例子测砂,這個動畫是用來改變文字顏色的。


AnimateForegroundColorSpan

源代碼如下:

private void animateColorSpan() {
    MutableForegroundColorSpan span = new MutableForegroundColorSpan(255, mTextColor);
    mSpans.add(span);

    WordPosition wordPosition = getWordPosition(mBaconIpsum);
    mBaconIpsumSpannableString.setSpan(span, wordPosition.start, wordPosition.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    ObjectAnimator objectAnimator = ObjectAnimator.ofInt(span, MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY, Color.BLACK, Color.RED);
    objectAnimator.setEvaluator(new ArgbEvaluator());
    objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //refresh
            mText.setText(mBaconIpsumSpannableString);
        }
    });
    objectAnimator.setInterpolator(mSmoothInterpolator);
    objectAnimator.setDuration(600);
    objectAnimator.start();
}

private static final Property<MutableForegroundColorSpan, Integer> MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY =
        new Property<MutableForegroundColorSpan, Integer>(Integer.class, "MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY") {

            @Override
            public void set(MutableForegroundColorSpan alphaForegroundColorSpanGroup, Integer value) {
                alphaForegroundColorSpanGroup.setForegroundColor(value);
            }

            @Override
            public Integer get(MutableForegroundColorSpan span) {
                return span.getForegroundColor();
            }
        };

其實整個邏輯比較簡單百匆,通過Property不斷給span更換顏色砌些,然后動畫update的時候給TextView重新設(shè)置Span。


5 RainbowSpan

彩虹樣的Span加匈,其實實現(xiàn)起來也是很簡單的存璃,主要是用到了Paint的Shader技術(shù),效果如下所示:



源代碼如下所示:

private static class RainbowSpan extends CharacterStyle implements UpdateAppearance {
private final int[] colors;

public RainbowSpan(Context context) {
  colors = context.getResources().getIntArray(R.array.rainbow);
}

@Override
public void updateDrawState(TextPaint paint) {
  paint.setStyle(Paint.Style.FILL);
  Shader shader = new LinearGradient(0, 0, 0, paint.getTextSize() * colors.length, colors, null,
      Shader.TileMode.MIRROR);
  Matrix matrix = new Matrix();
  matrix.setRotate(90);
  shader.setLocalMatrix(matrix);
  paint.setShader(shader);
}
}

由于paint使用shader是從上到下進行繪制雕拼,因此這里需要用到矩陣纵东,然后將矩陣旋轉(zhuǎn)90度。


6 AnimatedRainbowSpan

AnimatedRainbowSpan

如果要實現(xiàn)一個動畫的彩虹樣式啥寇,那么該如何實現(xiàn)呢偎球?
其實結(jié)合上面的RainbowSpan和AnimateForegroundColorSpan的例子便可以實現(xiàn)AnimatedRainbowSpan。
實現(xiàn)思路:通過ObjectAnimator動畫調(diào)整RainbowSpan中矩陣的平移辑甜,從而實現(xiàn)動畫彩虹的效果衰絮。
代碼如下所示:

public class AnimatedRainbowSpanActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_animated_rainbow_span);

    final TextView textView = (TextView) findViewById(R.id.text);
    String text = textView.getText().toString();

    AnimatedColorSpan span = new AnimatedColorSpan(this);

    final SpannableString spannableString = new SpannableString(text);
    String substring = getString(R.string.animated_rainbow_span).toLowerCase();
    int start = text.toLowerCase().indexOf(substring);
    int end = start + substring.length();
    spannableString.setSpan(span, start, end, 0);

    ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(
        span, ANIMATED_COLOR_SPAN_FLOAT_PROPERTY, 0, 100);
    objectAnimator.setEvaluator(new FloatEvaluator());
    objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        textView.setText(spannableString);
      }
    });
    objectAnimator.setInterpolator(new LinearInterpolator());
    objectAnimator.setDuration(DateUtils.MINUTE_IN_MILLIS * 3);
    objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
    objectAnimator.start();
  }

  private static final Property<AnimatedColorSpan, Float> ANIMATED_COLOR_SPAN_FLOAT_PROPERTY
      = new Property<AnimatedColorSpan, Float>(Float.class, "ANIMATED_COLOR_SPAN_FLOAT_PROPERTY") {
    @Override
    public void set(AnimatedColorSpan span, Float value) {
      span.setTranslateXPercentage(value);
    }
    @Override
    public Float get(AnimatedColorSpan span) {
      return span.getTranslateXPercentage();
    }
  };

  private static class AnimatedColorSpan extends CharacterStyle implements UpdateAppearance {
    private final int[] colors;
    private Shader shader = null;
    private Matrix matrix = new Matrix();
    private float translateXPercentage = 0;

    public AnimatedColorSpan(Context context) {
      colors = context.getResources().getIntArray(R.array.rainbow);
    }

    public void setTranslateXPercentage(float percentage) {
      translateXPercentage = percentage;
    }

    public float getTranslateXPercentage() {
      return translateXPercentage;
    }

    @Override
    public void updateDrawState(TextPaint paint) {
      paint.setStyle(Paint.Style.FILL);
      float width = paint.getTextSize() * colors.length;
      if (shader == null) {
        shader = new LinearGradient(0, 0, 0, width, colors, null,
            Shader.TileMode.MIRROR);
      }
      matrix.reset();
      matrix.setRotate(90);
      matrix.postTranslate(width * translateXPercentage, 0);
      shader.setLocalMatrix(matrix);
      paint.setShader(shader);
    }
  }
}

7 FireworksSpan

FireworksSpan

“煙火”動畫是讓文字隨機淡入。首先磷醋,把文字切斷成多個spans(例如猫牡,一個character的span),淡入spans后再淡入其它的spans子檀。用 前面介紹的MutableForegroundColorSpan镊掖,我們將創(chuàng)建一組特殊的span對象。在span組調(diào)用對應(yīng)的setAlpha方法褂痰,我 們隨機設(shè)置每個span的透明度亩进。

private static final class FireworksSpanGroup {
    private final float mAlpha;
    private final ArrayList<MutableForegroundColorSpan> mSpans;
    private FireworksSpanGroup(float alpha) {
        mAlpha = alpha;
        mSpans = new ArrayList<MutableForegroundColorSpan>();
    }
    public void addSpan(MutableForegroundColorSpan span) {
        span.setAlpha((int) (mAlpha * 255));
        mSpans.add(span);
    }
    public void init() {
        Collections.shuffle(mSpans);
    }
    public void setAlpha(float alpha) {
        int size = mSpans.size();
        float total = 1.0f * size * alpha;
        for(int index = 0 ; index < size; index++) {
            MutableForegroundColorSpan span = mSpans.get(index);
            if(total >= 1.0f) {
                span.setAlpha(255);
                total -= 1.0f;
            } else {
                span.setAlpha((int) (total * 255));
                total = 0.0f;
            }
        }
    }
    public float getAlpha() { return mAlpha; }
}

我們創(chuàng)建一個自定義屬性動畫的屬性去更改FireworksSpanGroup的透明度

private static final Property<FireworksSpanGroup, Float> FIREWORKS_GROUP_PROGRESS_PROPERTY =
new Property<FireworksSpanGroup, Float>(Float.class, "FIREWORKS_GROUP_PROGRESS_PROPERTY") {
    @Override
    public void set(FireworksSpanGroup spanGroup, Float value) {
        spanGroup.setAlpha(value);
    }
    @Override
    public Float get(FireworksSpanGroup spanGroup) {
        return spanGroup.getAlpha();
    }
};

最后,我們創(chuàng)建span組并使用一個ObjectAnimator給其加上動畫缩歪。

final FireworksSpanGroup spanGroup = new FireworksSpanGroup();
//初始化包含多個spans的grop
//spanGroup.addSpan(span);
//給ActionBar的標(biāo)題設(shè)置spans
//mActionBarTitleSpannableString.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanGroup.init();
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(spanGroup, FIREWORKS_GROUP_PROGRESS_PROPERTY, 0.0f, 1.0f);
objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
    @Override
    public void onAnimationUpdate(ValueAnimator animation)
    {
        //更新標(biāo)題
        setTitle(mActionBarTitleSpannableString);
    }
});
objectAnimator.start();

8 TypeWriterSpan

TypeWriterSpan

有了上面的例子归薛,寫TypeWriterSpan就變得十分簡單了。
先創(chuàng)建TypeWriterSpanGroup

private static final class TypeWriterSpanGroup {

    private static final boolean DEBUG = false;
    private static final String TAG = "TypeWriterSpanGroup";

    private final float mAlpha;
    private final ArrayList<MutableForegroundColorSpan> mSpans;

    private TypeWriterSpanGroup(float alpha) {
        mAlpha = alpha;
        mSpans = new ArrayList<MutableForegroundColorSpan>();
    }

    public void addSpan(MutableForegroundColorSpan span) {
        span.setAlpha((int) (mAlpha * 255));
        mSpans.add(span);
    }

    public void setAlpha(float alpha) {
        int size = mSpans.size();
        float total = 1.0f * size * alpha;

        if(DEBUG) Log.d(TAG, "alpha " + alpha + " * 1.0f * size => " + total);

        for(int index = 0 ; index < size; index++) {
            MutableForegroundColorSpan span = mSpans.get(index);

            if(total >= 1.0f) {
                span.setAlpha(255);
                total -= 1.0f;
            } else {
                span.setAlpha((int) (total * 255));
                total = 0.0f;
            }

            if(DEBUG) Log.d(TAG, "alpha span(" + index + ") => " + alpha);
        }
    }

    public float getAlpha() {
        return mAlpha;
    }
}

添加Span

private TypeWriterSpanGroup buildTypeWriterSpanGroup(int start, int end) {
    final TypeWriterSpanGroup group = new TypeWriterSpanGroup(0);
    for(int index = start ; index <= end ; index++) {
        MutableForegroundColorSpan span = new MutableForegroundColorSpan(0, Color.BLACK);
        mSpans.add(span);
        group.addSpan(span);
        mBaconIpsumSpannableString.setSpan(span, index, index + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
    return group;
}

添加動畫

private void animateTypeWriter() {
    TypeWriterSpanGroup spanGroup = buildTypeWriterSpanGroup(0, mBaconIpsum.length() - 1);
    ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(spanGroup, TYPE_WRITER_GROUP_ALPHA_PROPERTY, 0.0f, 1.0f);
    objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //refresh
            mText.setText(mBaconIpsumSpannableString);
        }
    });
    objectAnimator.setInterpolator(mTypeWriterInterpolator);
    objectAnimator.setDuration(5000);
    objectAnimator.start();
}

添加動畫屬性變化器

 private static final Property<TypeWriterSpanGroup, Float> TYPE_WRITER_GROUP_ALPHA_PROPERTY =
        new Property<TypeWriterSpanGroup, Float>(Float.class, "TYPE_WRITER_GROUP_ALPHA_PROPERTY") {

            @Override
            public void set(TypeWriterSpanGroup spanGroup, Float value) {
                spanGroup.setAlpha(value);
            }

            @Override
            public Float get(TypeWriterSpanGroup spanGroup) {
                return spanGroup.getAlpha();
            }
        };

9 相關(guān)鏈接

Textview圖文基礎(chǔ)
段落級span
字符級span
自定義span

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末匪蝙,一起剝皮案震驚了整個濱河市主籍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逛球,老刑警劉巖千元,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異颤绕,居然都是意外死亡幸海,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門奥务,熙熙樓的掌柜王于貴愁眉苦臉地迎上來物独,“玉大人,你說我怎么就攤上這事氯葬〉猜ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵帚称,是天一觀的道長官研。 經(jīng)常有香客問我,道長闯睹,這世上最難降的妖魔是什么阀参? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮瞻坝,結(jié)果婚禮上蛛壳,老公的妹妹穿的比我還像新娘。我一直安慰自己所刀,他們只是感情好衙荐,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著浮创,像睡著了一般忧吟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上斩披,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天溜族,我揣著相機與錄音讹俊,去河邊找鬼。 笑死煌抒,一個胖子當(dāng)著我的面吹牛仍劈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播寡壮,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼贩疙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了况既?” 一聲冷哼從身側(cè)響起这溅,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棒仍,沒想到半個月后悲靴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡莫其,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年对竣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片榜配。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡否纬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蛋褥,到底是詐尸還是另有隱情临燃,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布烙心,位于F島的核電站膜廊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏淫茵。R本人自食惡果不足惜爪瓜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望匙瘪。 院中可真熱鬧铆铆,春花似錦、人聲如沸丹喻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碍论。三九已至谅猾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背税娜。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工坐搔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人敬矩。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓概行,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谤绳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容