您好,登錄后才能下訂單哦!
自定義View是android高級(jí)UI知識(shí)體系的重要一環(huán)。也是區(qū)分中高級(jí)開發(fā)者的分水嶺。高級(jí)開發(fā)者,知識(shí)體系完善,但凡能夠語言描述出來的特效,他們總能給出解決方案。而中級(jí)開發(fā)者由于眼界受限,往往遇到復(fù)雜需求就無從下手。
一些看似復(fù)雜的特效,其實(shí)android已經(jīng)為我們提供了一套解決方案,這是中級(jí)進(jìn)階高級(jí)的必學(xué)知識(shí)。
本文給出完整攻略,保證一篇入魂。= =!
(順手留下GitHub鏈接,需要獲取相關(guān)面試等內(nèi)容的可以自己去找)
https://github.com/xiangjiana/Android-MS
下圖中可以看到,首先我們看到了一個(gè)心形,然后有波浪在跳動(dòng),最后綠色填滿了整個(gè)心形
誒?心形是怎么繪制的?誒?波浪是怎么畫出來的,又是如何動(dòng)起來的?誒? 文字是怎么呈現(xiàn)出同一時(shí)刻的兩種顏色的?
不知道是不是有人有這樣的疑惑````請繼續(xù)往下看.
拿到一個(gè)復(fù)雜特效,第一件事不要慌,先仔細(xì)分析一下,這個(gè)特效里面具體有哪些細(xì)節(jié)可以拆分出來。復(fù)雜的東西都是由簡單的細(xì)節(jié) 組合而成。
開始拆解
1、繪制區(qū)域是一個(gè)心形
2、波浪從最下面開始, 逐漸用綠色填充了整個(gè)心形
3、中間有文字內(nèi)容“ 一條大灰狼”,并且在波浪增長的過程中,文字存在一段時(shí)間的上下兩部分 顏色不同的狀態(tài).
本案例用到的知識(shí)點(diǎn):
1、 canvas.clipPath 畫布裁剪
2、 canvas.save 畫布狀態(tài)保存
3、 canvas.restore 恢復(fù)
4、 canvas.translate 畫布平移
5、 path.rCubicTo 構(gòu)建三階貝塞爾曲線(相當(dāng)于上一個(gè)點(diǎn)位置)
6、屬性動(dòng)畫 ValueAnimator / AnimatorSet
開始擼碼
第 1步:構(gòu)建一個(gè)心形區(qū)域
當(dāng)一個(gè)復(fù)雜圖形擺在我們面前,而且還是不規(guī)則圖形,我們首先應(yīng)該想到的,就是 android.graphics.Path 類,它可以記錄復(fù)雜圖形的全部點(diǎn)組成的路徑。關(guān)鍵代碼:
/**
* 構(gòu)建心形
* <p>
* 注意,它這個(gè)是以 矩形區(qū)域中心點(diǎn)為基準(zhǔn)的圖形,所以繪制的時(shí)候,必須先把坐標(biāo)軸移動(dòng)到 區(qū)域中心
*/
private void initHeartPath(Path path) {
List<PointF> pointList = new ArrayList<>();
pointList.add(new PointF(0,Utils.dp2px(-38)));
pointList.add(new PointF(Utils.dp2px(50),Utils.dp2px(-103)));
pointList.add(new PointF(Utils.dp2px(112),Utils.dp2px(-61)));
pointList.add(new PointF(Utils.dp2px(112),Utils.dp2px(-12)));
pointList.add(new PointF(Utils.dp2px(112),Utils.dp2px(37)));
pointList.add(new PointF(Utils.dp2px(51),Utils.dp2px(90)));
pointList.add(new PointF(0,Utils.dp2px(129)));
pointList.add(new PointF(Utils.dp2px(-51),Utils.dp2px(90)));
pointList.add(new PointF(Utils.dp2px(-112),Utils.dp2px(37)));
pointList.add(new PointF(Utils.dp2px(-112), Utils.dp2px(-12)));
pointList.add(new PointF(Utils.dp2px(-112),Utils.dp2px(-61)));
pointList.add(new PointF(Utils.dp2px(-50),Utils.dp2px(-103)));
path.reset();
for(int i =0; i <4; i++) {
if (i ==0) {
path.moveTo(pointList.get(i *3).x, pointList.get(i *3).y);
} else {
path.lineTo(pointList.get(i * 3).x, pointList.get(i *3).y);
}
int endPointIndex;
if (i ==3) {
endPointIndex = 0;
} else {
endPointIndex = i *3+3;
}
path.cubicTo(pointList.get(i *3+1).x, pointList.get(i *3+1).y,
pointList.get(i *3+2).x, pointList.get(i *3+2).y,
pointList.get(endPointIndex).x, pointList.get(endPointIndex).y);
//你的心形就是用貝塞爾曲線來畫的嗎
}
path.close();
path.computeBounds(mHeartRect,false);
//把path所占據(jù)的最小矩形區(qū)域,返回出去
}
傳入一個(gè) Path引用,然后在方法內(nèi)部對(duì) path進(jìn)行各種 api調(diào)用改變其屬性. 這里需要提及一個(gè)重點(diǎn):最后一行代碼
path.computeBounds(mHeartRect,false);
意思是,無論什么樣的 path,它都會(huì)占據(jù)一個(gè)最小矩形區(qū)域,computeBounds
方法可以獲取這個(gè)矩形區(qū)域,設(shè)置給入?yún)?mHeartRect
.
第 2步:將心形區(qū)域裁剪出來, 裁剪之后,后續(xù)的繪制都只會(huì)顯示在這個(gè)區(qū)域之內(nèi)
(為了作圖方便,我們通常先把坐標(biāo)軸原點(diǎn)移動(dòng)到 繪制區(qū)域的正中央)
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth();
int height = getHeight();
canvas.translate(width / 2, height /2);
//為了作圖方便,我們通常先把坐標(biāo)軸原點(diǎn)移動(dòng)到 繪制區(qū)域的正中央
...省略無關(guān)代碼
canvas.clipPath(mMainPath);
//裁剪心形區(qū)域
canvas.save();
//保存畫布狀態(tài)
...省略無關(guān)代碼
}
第 3步:繪制波浪區(qū)域
這里有兩點(diǎn)細(xì)節(jié)
1)波浪區(qū)域分為兩塊, top和 bottom 上下兩塊
2) 整個(gè)波浪區(qū)域的長度為 心形矩形范圍寬度的 2倍 ( ?為什么是2倍?因?yàn)樯厦娴牟ɡ藙?dòng)畫,其實(shí)是整個(gè)波浪區(qū)域平移造成的視覺效果,為了讓這個(gè)動(dòng)畫可以無限執(zhí)行,設(shè)計(jì)兩倍寬度,當(dāng)一半的寬度向右移動(dòng)剛好觸及心形矩形區(qū)域的右邊框的時(shí)候,讓它還原到原始位置,這樣就能無縫銜接。)
關(guān)鍵代碼1 - 波浪path的構(gòu)建
/**
* @param ifTop 是否是上部分; 上下部分的封口位置不一樣
* @param r 心形的矩形區(qū)域
* @param process 當(dāng)前進(jìn)度值
*/
private void resetWavePath(boolean ifTop,RectF r,float process,Pathpath) {
final float width = r.width();
final float height = r.width();
path.reset();
if( ifTop) {
path.moveTo(r.left - width, r.top);
} else {
path.moveTo(r.left - width, r.bottom);
//下部,初始位置點(diǎn)在 下
}
float waveHeight = height /8f;//波動(dòng)的最大幅度
//找到矩形區(qū)域的左邊線中點(diǎn)
path.lineTo(r.left - width,r.bottom - height * process);
//做兩個(gè)周期的貝塞爾曲線
for (int i =0; i < 2; i++) {
float px1, py1, px2, py2, px3, py3;
px1 = width /4;
py1 = -waveHeight;
px2 = width /4*3;
py2 = waveHeight;
px3 = width;
py3 = 0;
path.rCubicTo(px1, py1, px2, py2, px3, py3);
}
if (ifTop) {
path.lineTo(r.right, r.top);
} else {
path.lineTo(r.right, r.bottom);
}
path.close();
}
關(guān)鍵代碼2- 屬性動(dòng)畫改變兩個(gè)全局變量波浪的向上增長系數(shù)以及橫向波浪動(dòng)畫系數(shù):
AnimatorSet animatorSet;
// 動(dòng)起來
public void startAnimator() {
if(animatorSet == null) {
animatorSet = new AnimatorSet();
ValueAnimator growAnimator = ValueAnimator.ofFloat(0f, 1f);
growAnimator.addUpdateListener(animation -> growProcess =(float) animation.getAnimatedValue());
growAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animatorSet.cancel();
}
});
growAnimator.setInterpolator(new DecelerateInterpolator());
growAnimator.setDuration((long)(4000/ animatorSpeedCoefficient));
ValueAnimator waveAnimator = ValueAnimator.ofFloat(0f,1f);
waveAnimator.setRepeatCount(ValueAnimator.INFINITE);
waveAnimator.setRepeatMode(ValueAnimator.RESTART);
waveAnimator.addUpdateListener(animation -> {
waveProcess = (float) animation.getAnimatedValue();
invalidate();
});
waveAnimator.setInterpolator(new LinearInterpolator());
waveAnimator.setDuration((long)(1000/ animatorSpeedCoefficient));
animatorSet.playTogether(growAnimator, waveAnimator);
animatorSet.start();
} else {
animatorSet.cancel();
animatorSet.start();
}
}
關(guān)鍵代碼3- 利用屬性動(dòng)畫改變的全局變量,構(gòu)建動(dòng)態(tài)效果
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth();
int height = getHeight();
canvas.translate(width /2, height /2);
//為了作圖方便,我們通常先把坐標(biāo)軸原點(diǎn)移動(dòng)到 繪制區(qū)域的正中央
curXOffset = waveProcess * mHeartRect.width();
//當(dāng)前X軸方向上 波浪偏移量
canvas.clipPath(mMainPath);
canvas.save();
mainRect = new Rect();
...省略無關(guān)代碼
// 上波浪區(qū)域
resetWavePath(true, mHeartRect, growProcess, topWavePath);
canvas.translate(curXOffset,0);
canvas.clipPath(topWavePath);
canvas.drawPath(topWavePath, mTopPaint);
...省略無關(guān)代碼
//下波浪區(qū)域
resetWavePath(false, mHeartRect, growProcess, bottomWavePath);
canvas.restore();
canvas.translate(curXOffset,0);
canvas.clipPath(bottomWavePath);
canvas.drawPath(bottomWavePath, mBottomPaint);
...省略無關(guān)代碼
}
第 4步:繪制“一條大灰狼” 到心形中央,并且達(dá)成雙色效果
這里有兩個(gè)細(xì)節(jié):
canvas.drawText
, 就算你把paint 設(shè)置了 .setTextAlign(Paint.Align.CENTER);
它也未必會(huì)在你給的 x,y為中心 繪制。原因就不解釋了,谷歌大佬就是這么設(shè)計(jì)的。解決方法:利用paint.getTextBounds
,獲得文字的矩形區(qū)域。然后在真正canvas.drawText
,計(jì)算y的時(shí)候考慮這個(gè)矩形區(qū)域,就像下面這樣如下mainRect = new Rect(); textBottomPaint.getTextBounds(text,0, text.length(), mainRect);
- 由于之前波浪的橫向移動(dòng),坐標(biāo)軸產(chǎn)生了平移,所以我繪制文字,要將平移的距離減去,再繪制,保證居中,且文字位置不隨著波浪的橫向移動(dòng)而變化。
完整代碼如下(此步驟的關(guān)鍵代碼已經(jīng)標(biāo)紅):
來解答 乍一看里面提出的3個(gè)問題:
誒?心形是怎么繪制的?答:構(gòu)建Path,然后
canvas.clipPath
裁剪畫布,裁剪之后,所有的作圖效果就只在這個(gè)心形區(qū)域內(nèi)可見誒?波浪是怎么畫出來的,又是如何動(dòng)起來的?答:波浪,或者說波浪區(qū)域,也是 Path構(gòu)建,主要由一根波浪線以及三根直線組成,是一個(gè)封閉區(qū)域. 讓波浪動(dòng)起來,其實(shí)就是 canvas平移操作,利用屬性動(dòng)畫+雙倍寬度的波浪區(qū)域,形成無縫無限循環(huán)動(dòng)畫.
誒? 文字是怎么呈現(xiàn)出同一時(shí)刻的兩種顏色的?答:在兩個(gè)相鄰的波浪區(qū)域,使用不一樣的顏色繪制兩次文字。視覺效果上還是一串文字,但是實(shí)際上是兩次繪制的組合效果。神奇嗎?神奇?zhèn)€屁,其實(shí)就是 同一位置繪制兩次文字,后面的覆蓋前面的......話粗理不粗- -!
要想隨心所欲地掌控自定義View,需要有完整的知識(shí)體系。
view的樹形結(jié)構(gòu)概念
測量,布局,繪制流程
事件分發(fā)/滑動(dòng)沖突核心原理
CanvasPaintPath繪制常用api
Bitmap位圖
屬性動(dòng)畫
如果與 系統(tǒng)的某些View發(fā)生交互,還有可能需要你了解 系統(tǒng)源碼
但是要想隨心所欲地使用 自定義View,僅僅如此還不夠,還需要:良好的數(shù)學(xué)基礎(chǔ)
因?yàn)榇蟛糠值牟灰?guī)則圖形,可能都需要數(shù)學(xué)公式思想的輔助,像是:
心形path的構(gòu)建
無限波浪的設(shè)計(jì)思路
后續(xù)文章將會(huì) 提到的 貝塞爾曲線的使用
都離不開多年前數(shù)學(xué)課上的時(shí)候養(yǎng)成的數(shù)學(xué)思維,如果數(shù)學(xué)基礎(chǔ)比較糟糕,做起這些特效,往往會(huì)比較困難.
(順手留下GitHub鏈接,需要獲取相關(guān)面試等內(nèi)容的可以自己去找)
https://github.com/xiangjiana/Android-MS
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。