我每周會(huì)寫(xiě)一篇源代碼分析的文章,以后也可能會(huì)有其他主題.
如果你喜歡我寫(xiě)的文章的話,歡迎關(guān)注我的新浪微博@達(dá)達(dá)達(dá)達(dá)sky
地址: http://weibo.com/u/2030683111
每周我會(huì)第一時(shí)間在微博分享我寫(xiě)的文章,也會(huì)積極轉(zhuǎn)發(fā)更多有用的知識(shí)給大家.謝謝關(guān)注_,說(shuō)不定什么時(shí)候會(huì)有福利哈.
HTextView是一個(gè)用來(lái)給TextView里的文字做各種轉(zhuǎn)換動(dòng)畫(huà)的開(kāi)源庫(kù),第一次看到這個(gè)庫(kù)的時(shí)候就被這些動(dòng)畫(huà)吸引了,不僅提供了多種動(dòng)畫(huà)選擇,而且還有重復(fù)字符的位移動(dòng)畫(huà),的確別出心裁,雖然實(shí)現(xiàn)起來(lái)并不是多么復(fù)雜,但是從1700+的star數(shù)上還是可以看出它的受歡迎程度,所以今天我們就來(lái)分析看看它到底是如何實(shí)現(xiàn)的.有哪些值得我們借鑒的地方,又有哪些不完善的地方静秆。
使用方法
HTextView的使用方法還是比較簡(jiǎn)單的,只需要調(diào)用hTextView.setAnimateType();
來(lái)設(shè)定一種動(dòng)畫(huà)的類(lèi)型,再調(diào)用hTextView.animateText();
將字符串傳入就可以執(zhí)行切換動(dòng)畫(huà)了,此外還提供了hTextView.reset();
方法來(lái)重置動(dòng)畫(huà),具體代碼如下:
hTextView.setAnimateType(HTextViewType.SCALE);
hTextView.animateText(sentences[mCounter]);
類(lèi)關(guān)系圖
當(dāng)我們?nèi)シ治鲆粋€(gè)項(xiàng)目的時(shí)候,首先看這個(gè)類(lèi)庫(kù)的UML類(lèi)圖往往是最直觀的,能很清晰的將各個(gè)類(lèi)的關(guān)系用圖的形式展示的很清楚,這里我是使用的Android Studio的插件simpleUMLCE來(lái)自動(dòng)生成的類(lèi)圖,非常方便推薦給大家,另外如果看不懂UML圖可以參照深入淺出UML類(lèi)圖系列,已經(jīng)講得很詳細(xì)我就不再補(bǔ)充了。
從類(lèi)圖上看我們可以很清晰的看出,首先是定義了一個(gè)IHText
的接口,然后HText
實(shí)現(xiàn)了IHText
的接口,然后左邊的那么多類(lèi),可以從名字上大致猜出是各種動(dòng)畫(huà)的具體實(shí)現(xiàn)他們都是繼承了HText類(lèi),值得一提的是PixelateText
和BurnText
是直接實(shí)現(xiàn)IHText
的,我猜測(cè)應(yīng)該是作者后期重構(gòu)代碼的時(shí)候忘記這兩個(gè)類(lèi)了,實(shí)際上這兩個(gè)類(lèi)也是可以繼承自HText
來(lái)實(shí)現(xiàn).最后可以看出IHText
是和HTextView
相互耦合的.好的,類(lèi)圖就講到這里,下面我們來(lái)看具體實(shí)現(xiàn).
源碼分析
在開(kāi)始分析一個(gè)開(kāi)源項(xiàng)目的時(shí)候,我們往往先從其定義的接口來(lái)看,所以我們先看IHText
接口是如何定義的:
public interface IHText {
void init(HTextView hTextView, AttributeSet attrs, int defStyle);
void animateText(CharSequence text);
void onDraw(Canvas canvas);
void reset(CharSequence text);
}
首先init()
方法,顧名思義應(yīng)該是進(jìn)行一些初始化的操作,annimateText
應(yīng)該就是讓文字開(kāi)始做動(dòng)畫(huà)的方法,onDraw
這個(gè)大家應(yīng)該都很熟悉了,因?yàn)樽鰟?dòng)畫(huà),實(shí)際上就是一幀一幀的繪制然后來(lái)組成動(dòng)畫(huà),所以onDraw
方法也是必須的.最后一個(gè)reset
應(yīng)該就是重置文字以及一些狀態(tài)等等.
看完了接口定義,不用著急,接下來(lái)我們?nèi)タ?code>HTextView,精簡(jiǎn)后的代碼如下:
public class HTextView extends TextView {
private IHText mIHText = new ScaleText();
private AttributeSet attrs;
private int defStyle;
public HTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
private void init(AttributeSet attrs, int defStyle) {
this.attrs = attrs;
this.defStyle = defStyle;
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.HTextView);
int animateType = typedArray.getInt(R.styleable.HTextView_animateType, 0);
switch (animateType) {
case 0:
mIHText = new ScaleText();
break;
case 1:
mIHText = new EvaporateText();
break;
case 2:
mIHText = new FallText();
break;
}
typedArray.recycle();
initHText(attrs, defStyle);
}
private void initHText(AttributeSet attrs, int defStyle) {
mIHText.init(this, attrs, defStyle);
}
public void animateText(CharSequence text) {
mIHText.animateText(text);
}
@Override
protected void onDraw(Canvas canvas) {
mIHText.onDraw(canvas);
}
public void reset(CharSequence text) {
mIHText.reset(text);
}
public void setAnimateType(HTextViewType type) {
switch (type) {
case SCALE:
mIHText = new ScaleText();
break;
case EVAPORATE:
mIHText = new EvaporateText();
break;
case FALL:
mIHText = new FallText();
break;
}
initHText(attrs, defStyle);
}
}
從代碼中可以看出HTextView
繼承自TextView,并在初始化的時(shí)候根據(jù)animateType
來(lái)實(shí)例化對(duì)應(yīng)的IHText
,然后再對(duì)外暴露的animateText()
,onDraw()
,reset()
這幾個(gè)方法里都是直接調(diào)用IHText
來(lái)進(jìn)行處理.值得一提的是HTextView
重寫(xiě)了onDraw
方法,這樣也就意味著一些TextView
的特性就沒(méi)法使用了,比如添加drawable,換行等等..
看到這里我們知道了原來(lái)就是通過(guò)type來(lái)實(shí)例化對(duì)應(yīng)的動(dòng)畫(huà)執(zhí)行類(lèi),然后再做具體的處理.其實(shí)這里就是設(shè)計(jì)模式中的策略模式,我們先引出來(lái),文章后面我們?cè)俳榻B策略模式.這樣我們只需要去找一個(gè)實(shí)例去具體分析它的實(shí)現(xiàn)就能明白整個(gè)庫(kù)的原理了,這里我們就拿ScaleText
類(lèi)來(lái)分析具體動(dòng)畫(huà)的實(shí)現(xiàn)方式.去到ScaleText
里發(fā)現(xiàn)它繼承自HText
類(lèi)的,所以我們先來(lái)看看HText
類(lèi)的代碼:
public abstract class HText implements IHText {
protected Paint mPaint, mOldPaint;
protected float[] gaps = new float[100];
protected float[] oldGaps = new float[100];
protected float mTextSize;
protected CharSequence mText;
protected CharSequence mOldText;
protected List<CharacterDiffResult> differentList = new ArrayList<>();
protected float oldStartX = 0; // 原來(lái)的字符串開(kāi)始畫(huà)的x位置
protected float startX = 0; // 新的字符串開(kāi)始畫(huà)的x位置
protected float startY = 0; // 字符串開(kāi)始畫(huà)的y, baseline
protected HTextView mHTextView;
@Override
public void init(HTextView hTextView, AttributeSet attrs, int defStyle){
mHTextView = hTextView;
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(mHTextView.getCurrentTextColor());
mPaint.setStyle(Paint.Style.FILL);
mOldPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mOldPaint.setColor(mHTextView.getCurrentTextColor());
mOldPaint.setStyle(Paint.Style.FILL);
mText = mHTextView.getText();
mOldText = mHTextView.getText();
mTextSize = mHTextView.getTextSize();
initVariables();
mHTextView.postDelayed(new Runnable() {
@Override
public void run() {
prepareAnimate();
}
}, 50);
}
@Override
public void animateText(CharSequence text) {
mHTextView.setText(text);
mOldText = mText;
mText = text;
prepareAnimate();
animatePrepare(text);
animateStart(text);
}
@Override
public void onDraw(Canvas canvas) {
drawFrame(canvas);
}
private void prepareAnimate() {
mTextSize = mHTextView.getTextSize();
mPaint.setTextSize(mTextSize);
for (int i = 0; i < mText.length(); i++) {
gaps[i] = mPaint.measureText(mText.charAt(i) + "");
}
mOldPaint.setTextSize(mTextSize);
for (int i = 0; i < mOldText.length(); i++) {
oldGaps[i] = mOldPaint.measureText(mOldText.charAt(i) + "");
}
oldStartX = (mHTextView.getMeasuredWidth() - mHTextView.getCompoundPaddingLeft() - mHTextView.getPaddingLeft() - mOldPaint
.measureText(mOldText.toString())) / 2f;
startX = (mHTextView.getMeasuredWidth() - mHTextView.getCompoundPaddingLeft() - mHTextView.getPaddingLeft() - mPaint
.measureText(mText.toString())) / 2f;
startY = mHTextView.getBaseline();
differentList.clear();
differentList.addAll(CharacterUtils.diff(mOldText, mText));
}
public void reset(CharSequence text) {
animatePrepare(text);
mHTextView.invalidate();
}
/**
* 類(lèi)被實(shí)例化時(shí)初始化
*/
protected abstract void initVariables();
/**
* 具體實(shí)現(xiàn)動(dòng)畫(huà)
*
* @param text
*/
protected abstract void animateStart(CharSequence text);
/**
* 每次動(dòng)畫(huà)前初始化調(diào)用
*
* @param text
*/
protected abstract void animatePrepare(CharSequence text);
/**
* 動(dòng)畫(huà)每次刷新界面時(shí)調(diào)用
*
* @param canvas
*/
protected abstract void drawFrame(Canvas canvas);
}
首先HText
是一個(gè)抽象類(lèi),并且在初始化的時(shí)候,分別初始化了需要畫(huà)舊的文字和新的文字的畫(huà)筆,以及對(duì)新舊文字的賦值,最后調(diào)用了prepareAnimate();
方法.在這個(gè)方法里,首先設(shè)置了兩種Paint
的TextSize
,然后計(jì)算了每一個(gè)文字的寬度并保存在了gaps
和oldGaps
兩個(gè)數(shù)組里,最后計(jì)算了mOldText
和mText
里相同字符的位置信息.這里主要是為了做到當(dāng)兩組Text
中有相同字符時(shí)就不執(zhí)行默認(rèn)動(dòng)畫(huà),而進(jìn)行字符的平移動(dòng)畫(huà),使動(dòng)畫(huà)更靈動(dòng).
看完初始化方法以及prepareAnimate();
之后,我們留意到類(lèi)的最后的四個(gè)抽象方法.這里作者注釋的比較清楚了,就不再過(guò)多解釋,這里我們就能明白不論是ScaleText
類(lèi)還是其他動(dòng)畫(huà)類(lèi)型的類(lèi),實(shí)際上都是實(shí)現(xiàn)了這些方法,然后HText
中對(duì)這些方法進(jìn)行了調(diào)用,從而會(huì)執(zhí)行子類(lèi)中的相應(yīng)實(shí)現(xiàn)然后來(lái)實(shí)現(xiàn)具體的某個(gè)動(dòng)畫(huà),實(shí)際上這里就是設(shè)計(jì)模式中的模板方法.這里同樣先不多說(shuō),文章最后會(huì)做總結(jié).
然后我們?cè)倬唧w看這四個(gè)抽象方法分別是在哪里被調(diào)用的,我們就能理解實(shí)現(xiàn)HText
的子類(lèi)實(shí)現(xiàn)的這四個(gè)抽象方法會(huì)在什么時(shí)候調(diào)用.從上面的代碼可以看出來(lái)initVariables();
方法是在init();
方法里調(diào)用用來(lái)初始化,animatePrepare(CharSequence text);
和animateStart(CharSequence text);
是在prepareAnimate();
方法里調(diào)用用來(lái)準(zhǔn)備動(dòng)畫(huà)和開(kāi)始動(dòng)畫(huà),最后drawFrame(Canvas canvas);
會(huì)在onDraw(Canvas canvas);
方法里不斷調(diào)用察郁。所以從文章開(kāi)始的使用方法中我們知道是調(diào)用hTextView.animateText(text);
就可以執(zhí)行動(dòng)畫(huà)了,所以最終都是調(diào)用繼承自HText
的子類(lèi)的animatePrepare(CharSequence text);
和animateStart(CharSequence text);
方法。然后一定是在這里開(kāi)始動(dòng)畫(huà),不斷的觸發(fā)onDraw()
方法來(lái)完成動(dòng)畫(huà)菜谣。
好的,下面我們就來(lái)看看ScaleText
的具體實(shí)現(xiàn),由于篇幅原因,我們只具體分析一個(gè)ScaleText
的實(shí)現(xiàn),其余效果的實(shí)現(xiàn)只是繪制方法的不同,可以試著自己去閱讀研究桅打。ScaleText
具體代碼如下:
public class ScaleText extends HText {
float mostCount = 20;
float charTime = 400;
private long duration;
private float progress;
@Override
protected void initVariables() {
}
@Override
protected void animateStart(CharSequence text) {
int n = mText.length();
n = n <= 0 ? 1 : n;
// 計(jì)算動(dòng)畫(huà)總時(shí)間
duration = (long) (charTime + charTime / mostCount * (n - 1));
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, duration).setDuration(duration);
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
progress = (float) animation.getAnimatedValue();
mHTextView.invalidate();
}
});
valueAnimator.start();
}
@Override
protected void animatePrepare(CharSequence text) {
}
@Override
public void drawFrame(Canvas canvas) {
float offset = startX;
float oldOffset = oldStartX;
int maxLength = Math.max(mText.length(), mOldText.length());
for (int i = 0; i < maxLength; i++) {
// draw old text
if (i < mOldText.length()) {
float percent = progress / duration;
int move = CharacterUtils.needMove(i, differentList);
if (move != -1) {
mOldPaint.setTextSize(mTextSize);
mOldPaint.setAlpha(255);
float p = percent * 2f;
p = p > 1 ? 1 : p;
float distX = CharacterUtils.getOffset(i, move, p, startX, oldStartX, gaps, oldGaps);
canvas.drawText(mOldText.charAt(i) + "", 0, 1, distX, startY, mOldPaint);
} else {
mOldPaint.setAlpha((int) ((1 - percent) * 255));
mOldPaint.setTextSize(mTextSize * (1 - percent));
float width = mOldPaint.measureText(mOldText.charAt(i) + "");
canvas.drawText(mOldText.charAt(i) + "", 0, 1, oldOffset + (oldGaps[i] - width) / 2, startY, mOldPaint);
}
oldOffset += oldGaps[i];
}
// draw new text
if (i < mText.length()) {
if (!CharacterUtils.stayHere(i, differentList)) {
int alpha = (int) (255f / charTime * (progress - charTime * i / mostCount));
if (alpha > 255) alpha = 255;
if (alpha < 0) alpha = 0;
float size = mTextSize * 1f / charTime * (progress - charTime * i / mostCount);
if (size > mTextSize) size = mTextSize;
if (size < 0) size = 0;
mPaint.setAlpha(alpha);
mPaint.setTextSize(size);
float width = mPaint.measureText(mText.charAt(i) + "");
canvas.drawText(mText.charAt(i) + "", 0, 1, offset + (gaps[i] - width) / 2, startY, mPaint);
}
offset += gaps[i];
}
}
}
}
先來(lái)看ScaleText
中定義的幾個(gè)變量mostCount
是表示最多同時(shí)執(zhí)行動(dòng)畫(huà)的字符個(gè)數(shù),為了實(shí)現(xiàn)順序的動(dòng)畫(huà)執(zhí)行,charTime
表示mostCount
個(gè)字符的動(dòng)畫(huà)時(shí)間,根據(jù)字符個(gè)數(shù)的不同動(dòng)畫(huà)時(shí)間不同,duration
表示動(dòng)畫(huà)總時(shí)間,progress
顯然是表示進(jìn)度.所以在animateStart(CharSequence text)页徐;
方法中,是根據(jù)字符個(gè)數(shù)的不同來(lái)計(jì)算總時(shí)間,代碼如下:
int n = mText.length();
n = n <= 0 ? 1 : n;
// 計(jì)算動(dòng)畫(huà)總時(shí)間
duration = (long) (charTime + charTime / mostCount * (n - 1));
然后再通過(guò)ValueAnimator
設(shè)置好progress
的區(qū)間以及動(dòng)畫(huà)的duration
,最后在onAnimationUpdate(ValueAnimator animation)
的回調(diào)接口里,不斷的拿到當(dāng)前的progress
然后調(diào)用mHTextView.invalidate();
來(lái)不斷更新,我們都知道最終會(huì)不斷的調(diào)用onDraw();
方法所以流轉(zhuǎn)到最后還是調(diào)用ScaleText
的drawFrame(Canvas canvas);
方法.所以最終的動(dòng)畫(huà)都是在這里實(shí)現(xiàn)的。
從drawFrame(Canvas canvas);
來(lái)看,首先是拿到了新舊字符串各自X方向的偏移量,因?yàn)榭葱Ч覀兛梢园l(fā)現(xiàn)ScaleText
切換的過(guò)程中總共有三種動(dòng)畫(huà):
1.oldText中不重復(fù)字符的縮小動(dòng)畫(huà).
2.oldText中與newText中重復(fù)字符的位移動(dòng)畫(huà).
3.newText中不重復(fù)字符的放大動(dòng)畫(huà).
這三種動(dòng)畫(huà)都是在drawFrame(Canvas canvas);
方法里處理的,首先是循環(huán)繪制每一個(gè)字符,然后先繪制oldText
并在oldText
首先判斷這個(gè)字符是不是需要平移動(dòng)畫(huà)通過(guò)CharacterUtils.needMove(i, differentList);
來(lái)判斷,當(dāng)不返回-1
時(shí)表示需要進(jìn)行平移動(dòng)畫(huà),當(dāng)返回-1
時(shí)就進(jìn)行縮小和透明動(dòng)畫(huà),然后緊接著繪制newText
,通過(guò)!CharacterUtils.stayHere(i, differentList);
方法來(lái)跳過(guò)重復(fù)字符的繪制,然后再通過(guò)progress
的值來(lái)計(jì)算出當(dāng)前繪制的字符的大小和透明度存谎。所以通過(guò)不斷增加的progress
和onDraw();
方法的調(diào)用再配合這一系列算法,最終實(shí)現(xiàn)了我們要的動(dòng)畫(huà)拔疚。講到這里整個(gè)庫(kù)我們應(yīng)該整體的理解了。
我想有人會(huì)說(shuō),為什么這么一個(gè)簡(jiǎn)單的動(dòng)畫(huà)寫(xiě)這么類(lèi),我用一個(gè)類(lèi)就能寫(xiě)出來(lái),又是定義接口,又是抽象類(lèi)又是繼承煩不煩?可是我們不要忘了,他還有另外9種動(dòng)畫(huà)實(shí)現(xiàn),將來(lái)還可能有幾十種拓展出來(lái)的動(dòng)畫(huà).如果都寫(xiě)在一個(gè)類(lèi)里實(shí)現(xiàn),那就毫無(wú)拓展性可言了,所以這里我們要聊聊設(shè)計(jì)模式的好處了既荚。
設(shè)計(jì)模式
此項(xiàng)目中用到兩種常用的設(shè)計(jì)模式分別是策略模式和模板方法設(shè)計(jì)模式.
策略模式
策略模式(點(diǎn)擊關(guān)于策略模式的詳解) 定義了一系列的算法稚失,并將每一個(gè)算法封裝起來(lái),而且使它們還可以相互替換恰聘。策略模式讓算法獨(dú)立于使用它的客戶(hù)而獨(dú)立變化句各。所以在這個(gè)庫(kù)中,每一種動(dòng)畫(huà)都相當(dāng)于一種獨(dú)立算法,又可以相互替換,所以每一個(gè)實(shí)現(xiàn)了HText
的子類(lèi)相當(dāng)于組成了策略模式,這樣做使得類(lèi)庫(kù)的結(jié)構(gòu)清晰明了,拓展方便,耦合度低吸占。缺點(diǎn)就是策略越多實(shí)現(xiàn)的子類(lèi)就會(huì)增加。不過(guò)相對(duì)于策略模式的好處這點(diǎn)也不算什么了凿宾。
模板方法
模板方法(點(diǎn)擊關(guān)于模板方法的詳解)定義一個(gè)操作中的算法的框架矾屯,而將一些步驟延遲到子類(lèi)中。使得子類(lèi)可以不改變一個(gè)算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟初厚。模板方法在此庫(kù)中的體現(xiàn)是HText
這個(gè)抽象類(lèi)定義的那四個(gè)抽象方法,分別在HText
中進(jìn)行調(diào)用,將這些步驟延遲到子類(lèi)中執(zhí)行,所以子類(lèi)可以實(shí)現(xiàn)各種各樣的動(dòng)畫(huà)效果,這是很典型的模板方法設(shè)計(jì)模式件蚕。
個(gè)人評(píng)價(jià)
至此,我們就算是徹底了解了HTextView,雖然并沒(méi)有多么復(fù)雜,但是它使用的這些典型的設(shè)計(jì)模式以及各種動(dòng)畫(huà)的實(shí)現(xiàn)確實(shí)可以從中讓我們學(xué)到不少知識(shí)。尤其是各種動(dòng)畫(huà)的具體實(shí)現(xiàn),能為我們自己在做相關(guān)動(dòng)畫(huà)時(shí)提供不少思路!