本篇文章已授權(quán)微信公眾號(鴻洋)獨(dú)家發(fā)布
做開發(fā)已經(jīng)3年有余了,一事無成昭齐,一直靜不下心來安心做好一件事情尿招,很多時(shí)候內(nèi)心很浮躁,迷失了前行的方向阱驾,我該當(dāng)何去何從就谜?一個(gè)人閑下來的時(shí)候,總想放縱自己里覆,二個(gè)人在一起總會(huì)吵吵鬧鬧丧荐,在物質(zhì)驅(qū)使的年代里,活得一點(diǎn)不像自己喧枷,像我這樣的人虹统,還有多少人?
心里的話一直想找人說隧甚,讓大家見笑了车荔。本篇是講技術(shù)的,而不是來聽我的感慨戚扳。先來看看文本跳動(dòng)
控件的最終效果圖:
一款優(yōu)秀的 app
往往會(huì)有一些新穎的控件忧便,讓用戶在視覺與體驗(yàn)都會(huì)覺得很棒,第一時(shí)間留住用戶的心帽借。那么怎樣打造一款炫酷的自定義控件
(只考慮編程層面)珠增?
群里有很大一部分童鞋反應(yīng),覺得寫自定義控件
好難砍艾,害怕去寫蒂教,有的還沒開始就結(jié)束了,還有的開始沒多久就放棄了辐董,剩下的都在放棄的路上悴品。其實(shí)寫自定義控件
并沒有那么難禀综,那我談?wù)勛约旱男牡眉蚝妫袃牲c(diǎn):
第一,拆分
第二定枷,模仿
控件的難點(diǎn)在于動(dòng)畫孤澎,動(dòng)畫往往有規(guī)律可循,那么我們需要把動(dòng)畫變慢去尋找規(guī)律欠窒,一是ui
設(shè)計(jì)師給出相應(yīng)的參數(shù)覆旭,有時(shí)需要模仿競品的控件效果退子,可以在開發(fā)者選擇
中設(shè)置動(dòng)畫縮放時(shí)長:
還可以通過adb
命令進(jìn)行錄屏與截圖:
// 截屏
adb shell /system/bin/screencap -p /sdcard/a.png
// 錄屏
adb shell screenrecord /sdcard/a.mp4
// 拉到電腦上
adb pull /sdcard/a.mp4
動(dòng)畫變慢了,我們就可以進(jìn)行拆分型将,在平面(2D)動(dòng)畫中我們一般只考慮橫縱坐標(biāo)寂祥,那么就可以拆分為 x
和 y
方向的動(dòng)畫,動(dòng)畫無非就是平移七兜,旋轉(zhuǎn)丸凭,縮放以及透明度變換中的一種或者幾種,化繁為簡腕铸,本篇會(huì)以文本跳動(dòng)
控件具體講解惜犀。
第二點(diǎn)心得是模仿,騰訊狠裹,小米一直在模仿虽界,一直很成功,那么寫控件一樣涛菠,也需要模仿莉御,尤其對于初學(xué)者,更需要模仿俗冻。先不論你寫的控件是不是最優(yōu)方案颈将,性能是否有過渡損耗,動(dòng)手模仿寫一寫言疗,可以找些簡單例子練練手晴圾,強(qiáng)力推薦 啟艦大牛,寫的很細(xì)很全面噪奄,有時(shí)候需要知道競品使用了什么控件死姚,可以讓你少走許多彎路?推薦一款sdk
自帶的工具:
看到這里勤篮,你有信心寫好上方跳動(dòng)文本
控件嗎都毒?如果是你拿到這個(gè)控件需求,你會(huì)怎么分析碰缔?歡迎留言账劲,很多時(shí)候?qū)崿F(xiàn)方案往往不會(huì)是一種,你的答案金抡,可能對他人有所幫助瀑焦,接下來具體講解跳動(dòng)文本
控件的實(shí)現(xiàn)。
跳動(dòng)文本案例
需求分析:
1梗肝、篩選出新舊文本的相同字符并記錄索引值
2榛瓮、計(jì)算篩選出每個(gè)相同字符在動(dòng)畫周期內(nèi)的偏移量
3、舊文本剩余字符的平移與透明度動(dòng)畫
4巫击、新文本剩余字符的平移與透明度動(dòng)畫
為了方便理解禀晓,簡化字符精续,延遲動(dòng)畫時(shí)長,效果如下:
從以上效果圖可以知道粹懒,紅色字體的 wen
為已經(jīng)繪制的文本(舊文本)重付,白色的 tianxia
為即將繪制的文本(新文本),新舊文本相同的字符為 n
凫乖,針對新舊文本拆分 x堪夭,y
軸方向的動(dòng)畫:
舊文本
- x 軸方向字符
n
平移 - y 軸方向除
n
外的字符平移,透明度(向上平移拣凹,透明度0~1)
新文本
- x 軸方向無動(dòng)畫
- y 軸方向除
n
外的字符平移森爽,透明度(向上平移,透明度1~0)
拆分后你會(huì)發(fā)現(xiàn)就只有簡單的平移與透明度動(dòng)畫嚣镜,是不是一下就覺得簡單了許多爬迟。
篩選相同字符
直接看代碼:
public static List<CharacterDiffResult> diff(CharSequence oldText, CharSequence newText) {
List<CharacterDiffResult> differentList = new ArrayList<>();
Set<Integer> skip = new HashSet<>();
for (int i = 0; i < oldText.length(); i++) {
char c = oldText.charAt(i);
for (int j = 0; j < newText.length(); j++) {
if (!skip.contains(j) && c == newText.charAt(j)) {
skip.add(j);
CharacterDiffResult different = new CharacterDiffResult();
different.c = c;
// 在舊文本中的位置
different.fromIndex = i;
// 在新文本中的位置
different.moveIndex = j;
differentList.add(different);
break;
}
}
}
return differentList;
}
在CharacterDiffResult
類中存取了新舊文本的相同字符以及在新舊文本中的位置,這個(gè)方法應(yīng)該不難理解菊匿,接下來分析相同字符在動(dòng)畫周期內(nèi)的偏移量付呕。
相同字符偏移量
首先需要弄清一個(gè)概念,那就是繪制字符的x
跌捆,y
坐標(biāo):
/**
* text:要繪制的文字
* x:繪制原點(diǎn)x坐標(biāo)
* y:繪制原點(diǎn)y坐標(biāo)
* paint:用來做畫的畫筆
*/
public void drawText(String text, float x, float y, Paint paint)
這里的x
徽职,y
表示的是基線
的x
,y
坐標(biāo)佩厚,具體請參考自定義控件之繪圖篇( 五):drawText()詳解
通過以下方法可以獲取新舊文本的基線
的x
坐標(biāo):
int layoutDirection = ViewCompat.getLayoutDirection(EvaporateTextView.this);
// 獲取x坐標(biāo)
oldStartX = layoutDirection == LAYOUT_DIRECTION_LTR ? getLayout().getLineLeft(0) : getLayout().getLineRight(0);
為了計(jì)算偏移量姆钉,需要存取新舊字符串中每個(gè)字符的寬度:
mOldPaint.setTextSize(mTextSize);
mOldPaint.setColor(getCurrentTextColor());
mOldPaint.setTypeface(getTypeface());
oldGapList.clear();
for (int i = 0; i < mOldText.length(); i++) {
// measureText 測量文本寬度
oldGapList.add(mOldPaint.measureText(String.valueOf(mOldText.charAt(i))));
}
}
通過以上參數(shù)就可以獲取到動(dòng)畫周期內(nèi)相同字符的偏移量(基線的x坐標(biāo)):
/**
* 獲取舊文本字符n的x坐標(biāo)
*
* @param from 舊文本字符n的索引位置
* @param move 新文本字符n的索引位置
* @param progress 當(dāng)前進(jìn)度
* @param startX 新文本baseline的x坐標(biāo)
* @param oldStartX 舊文本baseline的y坐標(biāo)
* @param gaps 新文本每個(gè)字符對應(yīng)的x坐標(biāo)集合
* @param oldGaps 舊文本每個(gè)字符對應(yīng)的x坐標(biāo)集合
* @return
*/
public static float getOffset(int from, int move, float progress, float startX, float oldStartX,
List<Float> gaps, List<Float> oldGaps) {
float dist = startX;
for (int i = 0; i < move; i++) {
dist += gaps.get(i);
}
float cur = oldStartX;
for (int i = 0; i < from; i++) {
cur += oldGaps.get(i);
}
return cur + (dist - cur) * progress;
}
根據(jù)數(shù)學(xué)公式(n 字符x坐標(biāo) = 起點(diǎn) + (終點(diǎn) - 起點(diǎn))x 進(jìn)度)
,起點(diǎn)與終點(diǎn)分別對應(yīng)舊新文本字符 n
所在的 x
軸坐標(biāo)抄瓦,進(jìn)度的取值范圍為[0~1]
潮瓶。
舊文本平移透明動(dòng)畫
// 透明度動(dòng)畫
mOldPaint.setAlpha((int) ((1 - pp) * 255));
// pp 表示的是進(jìn)度
float y = startY - pp * mTextHeight;
// (oldGapList.get(i) - width) / 2 值為0 oldOffset + (oldGapList.get(i) - width) / 2
// oldOffset 基線x坐標(biāo) 平移動(dòng)畫
canvas.drawText(mOldText.charAt(i) + "", 0, 1, oldOffset, y, mOldPaint);
oldOffset += oldGapList.get(i);
請參考注釋,或者下載demo
對應(yīng)代碼理解钙姊,文末會(huì)給出demo
地址毯辅,還有干貨喲。
新文本平移透明動(dòng)畫
動(dòng)畫比較簡單煞额,直接貼代碼:
if (i < mText.length()) {
if (!CharacterUtils.stayHere(i, differentList)) {
// 漸顯效果 延遲 alpha 的計(jì)算稍微費(fèi)腦一點(diǎn)
int alpha = (int) (255f / charTime * (progress * duration - charTime * i / mostCount));
alpha = alpha > 255 ? 255 : alpha;
alpha = alpha < 0 ? 0 : alpha;
mPaint.setAlpha(alpha);
mPaint.setTextSize(mTextSize);
// float pp = progress * duration / (charTime + charTime / mostCount * (mText.length() - 1));
float pp = progress;
float y = mTextHeight + startY - pp * mTextHeight;
float width = mPaint.measureText(mText.charAt(i) + "");
canvas.drawText(mText.charAt(i) + "", 0, 1, offset + (gapList.get(i) - width) / 2, y, mPaint);
}
offset += gapList.get(i);
}
跳動(dòng)文本
講到這里就差不多了思恐,有什么不懂的地方,請留言討論膊毁。
總結(jié)
寫好控件在于分析與拆分胀莹,再復(fù)雜的動(dòng)畫也是由簡單的動(dòng)畫組合而成,先化繁為簡媚媒,后以簡組繁嗜逻,多練多寫,多借鑒更優(yōu)的實(shí)現(xiàn)方案缭召。本篇的文本跳動(dòng)
控件栈顷,是否為你打開了一扇大門?
小編正在維護(hù)MeiWidgetView庫嵌巷,有炫酷的控件可以推薦萄凤,希望有人能夠和小編一起維護(hù),萬分感謝搪哪,給小編一顆 star靡努,才是最好最美的回報(bào)。
參考的相關(guān)文章地址:
https://github.com/hanks-zyh/HTextView
https://blog.csdn.net/harvic880925/article/details/50995268