這交互炸了!炫酷跳動的閃屏Logo標(biāo)題
BAT加入BATcoder技術(shù)群
作者:seagazer
https://www.jianshu.com/p/629d4056f6eb
1.分析
在日常開發(fā)中,經(jīng)常會遇到各種視覺效果,有的效果可能一眼看去會讓人覺得很復(fù)雜,但是我們必須明確一點(diǎn):
所有復(fù)雜動效都是可以分解成單一的基礎(chǔ)動作,比如縮放,平移,旋轉(zhuǎn)這些基礎(chǔ)單元,然后將所有基礎(chǔ)單元動作進(jìn)行組合,就會產(chǎn)生讓人眼前一亮的視覺動效。
首先看下下圖效果:

按照上面我們提到的思路進(jìn)行分解:
Logo的名稱LitePlayer被拆分為單個文字
所有文字隨機(jī)打散在屏幕各個位置
中間的Logo被隱藏
Logo文字從隨機(jī)位置平移到頁面固定位置
中間的Logo圖片逐漸顯示,并且附帶從下往上平移一小段位移
Logo被打散的文字組合成名稱
Logo組合成名稱后,有個漸變的光暈照射效果從左往右移動
動畫結(jié)束
當(dāng)我們把動畫拆解后,就可以針對每個拆解單元去構(gòu)造實現(xiàn)方案了。
2.實現(xiàn)
首先我們先對logo文字動畫進(jìn)行實現(xiàn):
1. 首先對于數(shù)據(jù)來源,我們期望傳入一個logo的字符串,內(nèi)部將字符串拆解為單個文字?jǐn)?shù)組:
// fill the text to array
private void fillLogoTextArray(String logoName) {
if (TextUtils.isEmpty(logoName)) {
return;
}
if (mLogoTexts.size() > 0) {
mLogoTexts.clear();
}
for (int i = 0; i < logoName.length(); i++) {
char c = logoName.charAt(i);
mLogoTexts.put(i, String.valueOf(c));
}
}
2. 所有文字需要隨機(jī)打散在屏幕各個位置,因為涉及到坐標(biāo),我們可以在onSizeChanged中進(jìn)行l(wèi)ogo文字隨機(jī)位置的初始化,同時我們構(gòu)建兩個集合存儲每個文字被打散和組合后的坐標(biāo)狀態(tài):
// 最終合成logo后的坐標(biāo)
private SparseArray<PointF> mQuietPoints = new SparseArray<>();
// logo被隨機(jī)打散的坐標(biāo)
private SparseArray<PointF> mRadonPoints = new SparseArray<>();
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
initLogoCoordinate();
}
private void initLogoCoordinate() {
float centerY = mHeight / 2f + mPaint.getTextSize() / 2 + mLogoOffset;
// calculate the final xy of the text
float totalLength = 0;
for (int i = 0; i < mLogoTexts.size(); i++) {
String str = mLogoTexts.get(i);
float currentLength = mPaint.measureText(str);
if (i != mLogoTexts.size() - 1) {
totalLength += currentLength + mTextPadding;
} else {
totalLength += currentLength;
}
}
// the draw width of the logo must small than the width of this AnimLogoView
if (totalLength > mWidth) {
throw new IllegalStateException("This view can not display all text of logoName, please change text size.");
}
float startX = (mWidth - totalLength) / 2;
if (mQuietPoints.size() > 0) {
mQuietPoints.clear();
}
for (int i = 0; i < mLogoTexts.size(); i++) {
String str = mLogoTexts.get(i);
float currentLength = mPaint.measureText(str);
mQuietPoints.put(i, new PointF(startX, centerY));
startX += currentLength + mTextPadding;
}
// generate random start xy of the text
if (mRadonPoints.size() > 0) {
mRadonPoints.clear();
}
// 構(gòu)建隨機(jī)初始坐標(biāo)
for (int i = 0; i < mLogoTexts.size(); i++) {
mRadonPoints.put(i, new PointF((float) Math.random() * mWidth, (float) Math.random() * mHeight));
}
}
3. 構(gòu)建動畫過程,定義一個屬性動畫從0-1計算進(jìn)度,在動畫過程通過重繪實現(xiàn)文字從凌亂打散的坐標(biāo)到最終組合坐標(biāo)進(jìn)行移動:
// init the translation animation
private void initOffsetAnimation() {
mOffsetAnimator = ValueAnimator.ofFloat(0, 1);
mOffsetAnimator.setDuration(mOffsetDuration);
mOffsetAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (mQuietPoints.size() <= 0 || mRadonPoints.size() <= 0) {
return;
}
mOffsetAnimProgress = (float) animation.getAnimatedValue();
invalidate();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
if (!isOffsetAnimEnd) {// offset animation
mPaint.setAlpha((int) Math.min(255, 255 * mOffsetAnimProgress + 100));
for (int i = 0; i < mQuietPoints.size(); i++) {
PointF quietP = mQuietPoints.get(i);
PointF radonP = mRadonPoints.get(i);
float x = radonP.x + (quietP.x - radonP.x) * mOffsetAnimProgress;
float y = radonP.y + (quietP.y - radonP.y) * mOffsetAnimProgress;
canvas.drawText(mLogoTexts.get(i), x, y, mPaint);
}
}
}
4. 此時我們已經(jīng)把logo文字動畫實現(xiàn)了,接下來看我們拆解的第7步,還有個光照效果。對于這種光照效果,首選方案是通過Gradient+Shader實現(xiàn)。
因為繪制漸變也涉及到坐標(biāo),所以動畫的初始化我們也放到了onSizeChanged中進(jìn)行:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
initLogoCoordinate();// 初始化坐標(biāo)動畫
initGradientAnimation(w);// 初始化漸變動畫
}
// init the gradient animation
private void initGradientAnimation(int width) {
mGradientAnimator = ValueAnimator.ofInt(0, 2 * width);
if (mGradientListener != null) {
mGradientAnimator.addListener(mGradientListener);
}
mGradientAnimator.setDuration(mGradientDuration);
mGradientAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mMatrixTranslate = (int) animation.getAnimatedValue();
invalidate();
}
});
mLinearGradient = new LinearGradient(-width, 0, 0, 0, new int[]{mTextColor, mGradientColor, mTextColor},
new float[]{0, 0.5f, 1}, Shader.TileMode.CLAMP);
mGradientMatrix = new Matrix();
}
5. 漸變動畫是在文字移動動畫結(jié)束后自動播放的,所以我們可以在初始化文字移動動畫時對動畫結(jié)束進(jìn)行監(jiān)聽處理,同時在繪制onDraw中對文字進(jìn)行繪制:
// init the translation animation
private void initOffsetAnimation() {
...
// 初始化移動動畫
...
mOffsetAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mGradientAnimator != null && isShowGradient) {
isOffsetAnimEnd = true;
mPaint.setShader(mLinearGradient);
mGradientAnimator.start();
}
}
});
}
@Override
protected void onDraw(Canvas canvas) {
if (!isOffsetAnimEnd) {// offset animation
...
// 文字移動動畫
...
} else {// gradient animation
for (int i = 0; i < mQuietPoints.size(); i++) {
PointF quietP = mQuietPoints.get(i);
canvas.drawText(mLogoTexts.get(i), quietP.x, quietP.y, mPaint);
}
mGradientMatrix.setTranslate(mMatrixTranslate, 0);
mLinearGradient.setLocalMatrix(mGradientMatrix);
}
}
6. 到此,文字動畫已經(jīng)實現(xiàn)了。剩下來就是一些自定義屬性的定義,對外提供一些屬性的setter和getter方法了,同時需要考慮在頁面生命周期過程中動畫的資源釋放。
好了,看下我們實現(xiàn)的效果:

7. 對于上面Logo圖片的動畫可以單獨(dú)對一個ImageView進(jìn)行平移+透明度動畫實現(xiàn),這里就不花篇幅去描述了。
自定義View我相信大部分同學(xué)都已經(jīng)掌握熟練,但是對于復(fù)雜動畫,是否能夠?qū)⑦@些熟練的能力用在刀刃上呢,也許會有部分同學(xué)看到一個華麗的效果就不知所措了。
本文沒有對動畫進(jìn)行深入的分析,也沒涉及到復(fù)雜的數(shù)據(jù)運(yùn)算,只是通過一個簡單的例子,闡述了一種通用的動效分析實現(xiàn)的方式,通過這種思維方式,你可以很清晰的了解自己每一步的實現(xiàn)以及目標(biāo)。
最后總結(jié)一下,對于自定義動效而言,我們首先可以讓UI提供最終視覺效果,通過工具進(jìn)行單幀解析,觀察其中的每一幀之間的動作關(guān)系,將其拆解為一個個基礎(chǔ)單元。
接著針對每個單元步驟進(jìn)行實現(xiàn),最后整合到一起,就能夠?qū)崿F(xiàn)一個連貫的效果了。這是一種思想,當(dāng)你熟練掌握這種思想后,還需要對一些數(shù)學(xué)知識有一定的了解,比如三角函數(shù),矩陣運(yùn)算等等。只要培養(yǎng)好這兩方面能力,日常開發(fā)中,任何復(fù)雜的動效都不足以為懼。
附項目源碼地址:
https://github.com/seagazer/animlogoview
推薦閱讀
? 耗時2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!
? 『BATcoder』做了多年安卓還沒編譯過源碼?一個視頻帶你玩轉(zhuǎn)!
BATcoder技術(shù)群,讓一部分人先進(jìn)大廠
大家好,我是劉望舒,騰訊云最具價值專家TVP,著有暢銷書《Android進(jìn)階之光》《Android進(jìn)階解密》《Android進(jìn)階指北》,蟬聯(lián)四屆電子工業(yè)出版社年度優(yōu)秀作者,谷歌開發(fā)者社區(qū)特邀講師。
前華為面試官,現(xiàn)大廠技術(shù)負(fù)責(zé)人。
想要加入 BATcoder技術(shù)群,公號回復(fù)BAT 即可。
為了防止失聯(lián),歡迎關(guān)注我的小號
微信改了推送機(jī)制,真愛請星標(biāo)本公號??
