Android實(shí)現(xiàn)炫酷跳動(dòng)的閃屏Logo標(biāo)題
分析
在日常開發(fā)中,經(jīng)常會(huì)遇到各種視覺效果,有的效果可能一眼看去會(huì)讓人覺得很復(fù)雜,但是我們必須明確一點(diǎn):
所有復(fù)雜動(dòng)效都是可以分解成單一的基礎(chǔ)動(dòng)作,比如縮放,平移,旋轉(zhuǎn)這些基礎(chǔ)單元,然后將所有基礎(chǔ)單元?jiǎng)幼鬟M(jìn)行組合,就會(huì)產(chǎn)生讓人眼前一亮的視覺動(dòng)效。
首先看下下圖效果:

按照上面我們提到的思路進(jìn)行分解:
Logo的名稱LitePlayer被拆分為單個(gè)文字
所有文字隨機(jī)打散在屏幕各個(gè)位置
中間的Logo被隱藏
Logo文字從隨機(jī)位置平移到頁面固定位置
中間的Logo圖片逐漸顯示,并且附帶從下往上平移一小段位移
Logo被打散的文字組合成名稱
Logo組合成名稱后,有個(gè)漸變的光暈照射效果從左往右移動(dòng)
動(dòng)畫結(jié)束
當(dāng)我們把動(dòng)畫拆解后,就可以針對每個(gè)拆解單元去構(gòu)造實(shí)現(xiàn)方案了。
實(shí)現(xiàn)
首先我們先對logo文字動(dòng)畫進(jìn)行實(shí)現(xiàn):
1、首先對于數(shù)據(jù)來源,我們期望傳入一個(gè)logo的字符串,內(nèi)部將字符串拆解為單個(gè)文字?jǐn)?shù)組:
// fill the text to arrayprivate 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ī)打散在屏幕各個(gè)位置,因?yàn)樯婕暗阶鴺?biāo),我們可以在onSizeChanged中進(jìn)行l(wèi)ogo文字隨機(jī)位置的初始化,同時(shí)我們構(gòu)建兩個(gè)集合存儲(chǔ)每個(gè)文字被打散和組合后的坐標(biāo)狀態(tài):
// 最終合成logo后的坐標(biāo)private SparseArray<PointF> mQuietPoints = new SparseArray<>();// logo被隨機(jī)打散的坐標(biāo)private SparseArray<PointF> mRadonPoints = new SparseArray<>();@Overrideprotected 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 textfloat 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 AnimLogoViewif (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 textif (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)建動(dòng)畫過程,定義一個(gè)屬性動(dòng)畫從0-1計(jì)算進(jìn)度,在動(dòng)畫過程通過重繪實(shí)現(xiàn)文字從凌亂打散的坐標(biāo)到最終組合坐標(biāo)進(jìn)行移動(dòng):
// init the translation animationprivate void initOffsetAnimation() {mOffsetAnimator = ValueAnimator.ofFloat(0, 1);mOffsetAnimator.setDuration(mOffsetDuration);mOffsetAnimator.setInterpolator(new AccelerateDecelerateInterpolator());mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {if (mQuietPoints.size() <= 0 || mRadonPoints.size() <= 0) {return;}mOffsetAnimProgress = (float) animation.getAnimatedValue();invalidate();}});}@Overrideprotected void onDraw(Canvas canvas) {if (!isOffsetAnimEnd) {// offset animationmPaint.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、此時(shí)我們已經(jīng)把logo文字動(dòng)畫實(shí)現(xiàn)了,接下來看我們拆解的第7步,還有個(gè)光照效果。對于這種光照效果,首選方案是通過Gradient+Shader實(shí)現(xiàn)。
因?yàn)槔L制漸變也涉及到坐標(biāo),所以動(dòng)畫的初始化我們也放到了onSizeChanged中進(jìn)行:
@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mWidth = w;mHeight = h;initLogoCoordinate();// 初始化坐標(biāo)動(dòng)畫initGradientAnimation(w);// 初始化漸變動(dòng)畫}// init the gradient animationprivate void initGradientAnimation(int width) {mGradientAnimator = ValueAnimator.ofInt(0, 2 * width);if (mGradientListener != null) {mGradientAnimator.addListener(mGradientListener);}mGradientAnimator.setDuration(mGradientDuration);mGradientAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic 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、漸變動(dòng)畫是在文字移動(dòng)動(dòng)畫結(jié)束后自動(dòng)播放的,所以我們可以在初始化文字移動(dòng)動(dòng)畫時(shí)對動(dòng)畫結(jié)束進(jìn)行監(jiān)聽處理,同時(shí)在繪制onDraw中對文字進(jìn)行繪制:
// init the translation animationprivate void initOffsetAnimation() {...// 初始化移動(dòng)動(dòng)畫...mOffsetAnimator.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {if (mGradientAnimator != null && isShowGradient) {isOffsetAnimEnd = true;mPaint.setShader(mLinearGradient);mGradientAnimator.start();}}});}@Overrideprotected void onDraw(Canvas canvas) {if (!isOffsetAnimEnd) {// offset animation...// 文字移動(dòng)動(dòng)畫...} else {// gradient animationfor (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、到此,文字動(dòng)畫已經(jīng)實(shí)現(xiàn)了。剩下來就是一些自定義屬性的定義,對外提供一些屬性的setter和getter方法了,同時(shí)需要考慮在頁面生命周期過程中動(dòng)畫的資源釋放。
好了,看下我們實(shí)現(xiàn)的效果:

7、對于上面Logo圖片的動(dòng)畫可以單獨(dú)對一個(gè)ImageView進(jìn)行平移+透明度動(dòng)畫實(shí)現(xiàn),這里就不花篇幅去描述了。
自定義View我相信大部分同學(xué)都已經(jīng)掌握熟練,但是對于復(fù)雜動(dòng)畫,是否能夠?qū)⑦@些熟練的能力用在刀刃上呢,也許會(huì)有部分同學(xué)看到一個(gè)華麗的效果就不知所措了。
本文沒有對動(dòng)畫進(jìn)行深入的分析,也沒涉及到復(fù)雜的數(shù)據(jù)運(yùn)算,只是通過一個(gè)簡單的例子,闡述了一種通用的動(dòng)效分析實(shí)現(xiàn)的方式,通過這種思維方式,你可以很清晰的了解自己每一步的實(shí)現(xiàn)以及目標(biāo)。
最后總結(jié)一下,對于自定義動(dòng)效而言,我們首先可以讓UI提供最終視覺效果,通過工具進(jìn)行單幀解析,觀察其中的每一幀之間的動(dòng)作關(guān)系,將其拆解為一個(gè)個(gè)基礎(chǔ)單元。
接著針對每個(gè)單元步驟進(jìn)行實(shí)現(xiàn),最后整合到一起,就能夠?qū)崿F(xiàn)一個(gè)連貫的效果了。這是一種思想,當(dāng)你熟練掌握這種思想后,還需要對一些數(shù)學(xué)知識(shí)有一定的了解,比如三角函數(shù),矩陣運(yùn)算等等。只要培養(yǎng)好這兩方面能力,日常開發(fā)中,任何復(fù)雜的動(dòng)效都不足以為懼。
源碼地址:
https://github.com/seagazer/animlogoview
到這里就結(jié)束啦。
