本文鏈接http://immortalz.me/559.html 轉(zhuǎn)載請注明
好久沒有寫博客了,一直在彌補基礎(chǔ)堤框, 今天帶來的是一個博主有史以來見過最最精美的動畫效果了域滥。所以我才迫不及待的拆輪子纵柿。今天拆的部分是如下的效果。
無圖無真相呀
前人種樹启绰,后人乘涼昂儒。很早的時候大神CJJ關(guān)于這個庫的一些分析,大家可以去看看委可。
http://www.reibang.com/p/a4dabb3554c1
不過由于該文章中對于動畫的具體實現(xiàn)沒有提及渊跋,所以才有這篇文章的存在了=.=
因為本人水平不咋地,先奉上
這個庫的下載地址
https://github.com/danielzeller/Depth-LIB-Android-
大家如果之前接觸過類似的效果着倾,其實也就沒有必要看下去啦=.=
好了拾酝,廢話說了這么多,如果有興趣往下看屈呕,就繼續(xù)吧微宝!
一.說明
因為代碼已經(jīng)存在,所以我認為就沒有必要照著代碼一點點講虎眨,感覺這樣的方式更容易把讀者弄得云里霧里的,所以我這里決定從零開始一步步來實現(xiàn)這個效果(當(dāng)然因為我是先仔細看了一遍代碼的镶摘,所以你懂的)
這個地址是本文的代碼(基本都是從原來的庫取出來的,只不過精簡了些)凄敢,大家閱讀前也可以去下載看看扑庞。
本文demo 下載地址:https://github.com/ImmortalZ/Learn_Depth
二.分析
通過上面的效果圖罐氨,我們看到我們要實現(xiàn)的這樣的效果需要攻破幾個難關(guān)
1.水的背景波浪是浪起來的?
2.水面上的波紋是如何看起來隨機產(chǎn)生并且粗細不同的租悄?
答1:這里的背景波浪不同平常于我們經(jīng)常看到別人發(fā)的用正弦函數(shù)+三次貝塞爾曲線形成的這種效果
我們可以注意到整個背景有一種被扭曲的效果纪吮,背景的色彩呈現(xiàn)擠壓然后釋放然后擠壓這種效果碾盟。
想要實現(xiàn)我們的這種效果,我們這里需要一個我們較少使用的方法canvas.drawBitmapMesh
答2:對于第二個問題熙尉,我想留在后面適當(dāng)?shù)奈恢迷訇U述。
知識點 drawBitmapMesh 補充
網(wǎng)上關(guān)于drawBitmapMesh這個知識點铅歼,有兩位大神已經(jīng)很好的用牛逼的例子說明了。
地址在這:
初學(xué)Android,圖形圖像之使用drawBitmapMesh扭曲圖像(三十二)
我就提煉出一些精要的東西好了
好了,假定你已經(jīng)知道drawBitmapMesh的大致用法了瓤的。
三.實現(xiàn)##
有了上面的分析圈膏,我們就開始一步步來實現(xiàn)吧
整個項目的工程如下
3.1先來介紹Renderable
可以看到Renderable很簡單医增。我們的Water類繼承自Renderable
3.2 Water類
然后我們創(chuàng)建Water類茫多,用來承載整個海浪的效果(背景大波浪+波紋)
3.2.1 實現(xiàn)背景大波浪的效果
為了避免混淆天揖,我把實現(xiàn)波紋的效果給注釋掉了些阅,
public class Water extends Renderable {
private float mWidth;
private float mHeight;
private PathBitmapMesh mWaterMesh;
private float mWaveHeight;
private Path mWaterPath = new Path();
private int mNumWaves;
/*private Foam[] foams = new Foam[1];
long lastEmit;
private int emitInterWall = 1000;*/
/**
*
* @param water water圖像
* @param foam 海浪圖像
* @param y 海浪起始左上角坐標的y值
* @param width 海浪顯示的寬度
* @param height 海浪顯示的高度
* @param numWaves 海浪整個寬度被分成多少份
*/
public Water(Bitmap water, Bitmap foam, float y, float width, float height, int numWaves) {
super(water, 0, y);
mWidth = width;
mHeight = height;
mWaterMesh = new PathBitmapMesh(water, 1500);
mWaveHeight = height / 20;
mNumWaves = numWaves;
/*foams[0] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, 0, height / 12, 1500);
foams[1] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, -height / 5, height / 5, 1500);
foams[1].setAlpha(100);
foams[2] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, -height / 12, height / 12, 1450);
foams[2].setVerticalOffset(height / 7);
foams[3] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, -height / 12, height / 12, 1400);
foams[3].setVerticalOffset(height / 4);
lastEmit = System.currentTimeMillis();*/
createPath();
}
private void createPath() {
mWaterPath.reset();
mWaterPath.moveTo(0, y);
int step = (int) (mWidth / mNumWaves);
boolean changeDirection = true;
for (int i = 0; i < mNumWaves; i++) {
if (changeDirection) {
mWaterPath.cubicTo(x + step * i, y, x + step * i + step / 2f, y + mWaveHeight, x + step * i + step, y);
} else {
mWaterPath.cubicTo(x + step * i, y, x + step * i + step / 2f, y - mWaveHeight, x + step * i + step, y);
}
changeDirection = !changeDirection;
}
}
@Override
public void draw(Canvas canvas) {
mWaterMesh.draw(canvas);
/*for (Foam foam : foams) {
foam.draw(canvas);
}*/
}
@Override
public void update(float deltaTime) {
mWaterMesh.matchVertsToPath(mWaterPath, mHeight, ((bitmap.getWidth() / mNumWaves) * 4f));
/*for (Foam foam : foams) {
foam.update(deltaTime);
}
for (Foam foam : foams) {
foam.matchVertsToPath(mWaterPath, ((foam.getBitmap().getWidth() / mNumWaves) * 4f));
}
if (lastEmit + emitInterWall < System.currentTimeMillis()) {
for (Foam foam : foams) {
foam.calcWave();
}
lastEmit = System.currentTimeMillis();
}*/
}
}
先介紹createPath這個方法恕刘,這里面利用cubicTo創(chuàng)建了一個三次貝塞爾曲線褐着,目的就是:我們知道圖像扭曲時的寬度是大于二維圖像平面的寬度的频敛。
所以我們這里一條多個三次貝塞爾曲線形成的曲線來模擬圖像扭曲時的寬度
這樣說比較抽象,大致想表達的就是紅色來模擬圖像扭曲時的長度
createPath這個方法中有個有趣的參數(shù)mNumWaves鹊碍,這個用來表示海浪整個寬度被分成多少份(這個例子中我們把海浪分成了6份)侈咕。
那我們就來分析我們的背景大波浪的形成辦法楼眷。
按照上面drawBitmapMesh的講解,我們要想形成大波浪张吉,就需要計算出Mesh格子各個頂點的坐標(事實上這也是最最關(guān)鍵的一步勺择,知道了這一步后面的都是小菜J『恕)
這里我們把海浪寬度分成了6份,高度分成了1份來處理笔刹。所以我們需要處理的頂點個數(shù)就是
(6+1)*(1+1) = 14(圖中14個黑點)
然后我們接下來的工作就是計算著14個點的坐標!
這里就是PathBitmapMesh類需要完成的工作了日月。
(注釋部分先忽略,后面再說)
public class PathBitmapMesh {
protected static int HORIZONTAL_COUNT = 6;//水平方向分片數(shù)
protected static int VERTICAL_COUNT = 1;//垂直方向分片
private int mTotalCount;//總共需要計算的網(wǎng)格頂點個數(shù)
protected Bitmap bitmap;
protected float[] drawingVerts;//需要繪制的Verts網(wǎng)格坐標
protected float[] staticVerts;//最初始的Verts網(wǎng)格坐標
private Paint mPaint = new Paint();
//private ValueAnimator mValueAnimator;
//protected float pathOffsetPercent;
protected float[] coordsX = new float[2];
protected float[] coordsY = new float[2];
public PathBitmapMesh(Bitmap bitmap, long duration) {
mTotalCount = (HORIZONTAL_COUNT + 1) * (VERTICAL_COUNT + 1);
drawingVerts = new float[mTotalCount * 2];
staticVerts = new float[mTotalCount * 2];
this.bitmap = bitmap;
initVert();
//startValuesAnim(duration);
}
/*private void startValuesAnim(long duration) {
mValueAnimator = ValueAnimator.ofFloat(0, 0.3334f);
mValueAnimator.setDuration(duration);
mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
mValueAnimator.setRepeatMode(ValueAnimator.RESTART);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
pathOffsetPercent = (float) animation.getAnimatedValue();
}
});
mValueAnimator.start();
}*/
private void initVert() {
float bitmapWidth = (float) bitmap.getWidth();
float bitmapHeight = (float) bitmap.getHeight();
int index = 0;
for (int y = 0; y <= VERTICAL_COUNT; y++) {
float fy = bitmapHeight / VERTICAL_COUNT * y;
for (int x = 0; x <= HORIZONTAL_COUNT; x++) {
float fx = bitmapWidth / HORIZONTAL_COUNT * x;
setXY(drawingVerts, index, fx, fy);
setXY(staticVerts, index, fx, fy);
index++;
}
}
}
protected void setXY(float[] arrys, int index, float x, float y) {
arrys[2 * index] = x;
arrys[2 * index + 1] = y;
}
public void matchVertsToPath(Path path, float bottomY, float extraOffset) {
PathMeasure pm = new PathMeasure(path, false);
for (int i = 0; i < staticVerts.length / 2; i++) {
float orignX = staticVerts[2 * i];
float orignY = staticVerts[2 * i + 1];
float percentOffsetX = orignX / bitmap.getWidth();
float percentOffsetY = orignX / (bitmap.getWidth() + extraOffset);
//percentOffsetY += pathOffsetPercent;
pm.getPosTan(pm.getLength() * (percentOffsetX), coordsX, null);
pm.getPosTan(pm.getLength() * (percentOffsetY), coordsY, null);
if (orignY == 0) {
setXY(drawingVerts, i, coordsX[0], coordsY[1]);
} else {
setXY(drawingVerts, i, coordsX[0], bottomY);
}
}
}
public void draw(Canvas canvas) {
canvas.drawBitmapMesh(bitmap, HORIZONTAL_COUNT, VERTICAL_COUNT, drawingVerts, 0, null, 0, mPaint);
}
public Bitmap getBitmap() {
return bitmap;
}
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
}
可以看到initVert()進行了drawingVerts蜂绎,staticVerts兩個記錄頂點坐標數(shù)組初始化工作。
draw方法中直接調(diào)用drawBitmapMesh完成扭曲圖像的繪制。
本文鏈接http://immortalz.me/559.html 轉(zhuǎn)載請注明
重點在于matchVertsToPath方法找岖!
上面說過了,我們用一條多個三次貝塞爾曲線形成的曲線來模擬圖像扭曲時的寬度帖旨。那我們怎么來取得這條曲線上任一點的坐標呢箕昭?答案就是PathMeasure。我們可以通過該函數(shù)的getPosTan方法解阅,通過給定曲線的某一長度落竹,得到該點的坐標。
例如:pm.getPosTan(pm.getLength() * (percentOffsetX), coordsX, null);
pm.getLength()得到整條曲線長度货抄,percentOffsetX得到曲線上比例值述召,最后把得到的坐標返回給coordsX數(shù)組。
假設(shè)我們的bitmap寬度為60蟹地,這里我們把整個圖像分成了6份遍愿,以A點舉例。
percentOffsetX = 1/6曲管;
因為我們在繪制path時是以整個圖像顯示的寬度作為標準的,而不是bitmap的寬度胰默。
所以我們的path長度不等于bitmap的寬度挺据,這也就是為什么bitmap的寬度很小白嘁,卻能鋪滿整個屏幕的寬度
如果path的長度為600策肝,那么A的y值也就是coordsX[0] = 600*1/6=100
通過這樣的方法我們就可以得到14個頂點的坐標
細心的朋友會發(fā)現(xiàn),那這樣我們有一個percentOffsetX也好了呀蛀醉,為啥需要percentOffsetY奶躯?
(percentOffsetX字面理解為X方向偏移占據(jù)比例)
如果我們試著把extraOffset給注釋掉
那么運行的效果就是
你也許會想不會呀开呐,明明我們的path是三次貝塞爾曲線丛塌,不是彎的嗎?怎么直了!
原因就是:path只是用來模擬圖像扭曲時寬度播揪,并不是真正的形狀!
按照我們之前的計算結(jié)果A,B點的坐標y值都一樣谓形,所以整個圖像看起來效果是直的寒跳。
而如果我們加上extraOffset誓琼,用藍色點的y值來表示A,B 坐標的y值巧颈,那么就會出現(xiàn)這樣的效果了勾栗!
那我們怎么讓我們的波浪動起來呢?
很簡單盏筐!讓A,B第一行的七個坐標y值不斷變化即可围俘!
創(chuàng)建一個ValueAnimator,讓其循環(huán)周期的變化即可,具體大家可以看之前代碼注釋部分
為什么是0-1/3f呢?
很簡單琢融,還記得我們設(shè)置的extraOffset=((bitmap.getWidth() / mNumWaves) * 4f)界牡;
也就是extraOffset = w/62 = 2w/3; (w表示bitmap.getWidth())
那么代入計算得到A點的percentOffsetY = 1/(w+2w/3)=3w/5(也就是A'的y位置)
我們要想讓波浪形成一個周期,很明顯吏奸,讓A'運動至A''即可
得到A''-A'=w/3
所以ValueAnimator.ofFloat(0, 1 / 3f)
進行到欢揖,大波浪終于算是完成了!
3.2.1 實現(xiàn)水紋效果
有了前面大波浪的經(jīng)驗奋蔚,我們實現(xiàn)水紋應(yīng)該更簡單了她混!因為原理類似呀!
首先創(chuàng)建一個類Foam 繼承自 PathBitmapMesh (意味著我們可以復(fù)用PathBitmapMesh里面的東西)
public class Foam extends PathBitmapMesh {
private float[] foamCoords;
private float[] easedFoamCoords;
private int mHorizontalSlices;//水紋水平方向分片
private float minHeight;//水紋最小高度
private float maxHeight;//水紋最大高度
private float verticalOffset;
public Foam(int horizontalSlices, Bitmap bitmap, float minHeight, float maxHeight, long duration) {
super(bitmap, duration);
mHorizontalSlices = horizontalSlices;
this.minHeight = minHeight;
this.maxHeight = maxHeight;
init();
}
private void init() {
foamCoords = new float[mHorizontalSlices];
easedFoamCoords = new float[mHorizontalSlices];
for (int i = 0; i < mHorizontalSlices; i++) {
foamCoords[i] = 0;
easedFoamCoords[i] = 0;
}
}
/**
* 隨著時間的流逝不斷更改
* @param deltaTime
*/
public void update(float deltaTime) {
for (int i = 0; i < foamCoords.length; i++) {
easedFoamCoords[i] += ((foamCoords[i] - easedFoamCoords[i])) * deltaTime;
}
}
/**
* 根據(jù)傳入的最低泊碑,最高高度得到一個適合的高度
*/
public void calcWave() {
for (int i = 0; i < foamCoords.length; i++) {
foamCoords[i] = MathHelper.randomRange(minHeight, maxHeight);
}
}
/**
* 計算水紋的各個頂點坐標
* @param path
* @param extraOffset
*/
public void matchVertsToPath(Path path, float extraOffset) {
PathMeasure pm = new PathMeasure(path, false);
int index = 0;
for (int i = 0; i < staticVerts.length / 2; i++) {
float orignX = staticVerts[2 * i];
float orignY = staticVerts[2 * i + 1];
float percentOffsetX = orignX / bitmap.getWidth();
float percentOffsetY = orignX / (bitmap.getWidth() + extraOffset);
percentOffsetY += pathOffsetPercent;
pm.getPosTan(pm.getLength() * percentOffsetX, coordsX, null);
pm.getPosTan(pm.getLength() * percentOffsetY, coordsY, null);
if (orignY == 0) {
setXY(drawingVerts, i, coordsX[0], coordsY[1]+verticalOffset);
} else {
float desiredYCoord = Math.max(coordsY[1], coordsY[1] + easedFoamCoords[Math.min(easedFoamCoords.length - 1, index)]);
setXY(drawingVerts, i, coordsX[0], desiredYCoord+verticalOffset);
index += 1;
}
}
}
public void setVerticalOffset(float verticalOffset) {
this.verticalOffset = verticalOffset;
}
}
還是重點來看matchVertsToPath方法
這里我畫了一個水紋的簡圖(藍色的線所圍成)
可以看到對于第一排坐標就是跟隨者大波浪的坐標即可
我們需要改變的是第二排的坐標y值坤按,這里就根據(jù)我們傳入的minHeight,maxHeight計算所得(具體計算方式也好理解,可以看代碼馒过,我們實際上的計算方式可以不按照原代碼的臭脓,只需要y值不一樣即可)
參量verticalOffset我們可以設(shè)定水紋的起始高度,這樣通過修改verticalOffset值腹忽,我們可以讓多個水紋在垂直高度不同位置顯示来累。
最后實現(xiàn)的效果就是(把Water代碼注釋部分去掉)因為圖像就是扭曲的,水紋給我們的感覺就像是隨機產(chǎn)生窘奏,又水紋每一份頂點的y值不同嘹锁,所以就粗細不同了。
注意着裹,因為我們的大波浪頂點的高度是需要時刻變化的领猾,而水紋的高度并不需要時刻變化,只需要每隔一段時間變化即可骇扇,所以我們可以設(shè)置讓水紋的高度每隔1秒計算變化一次
四.合成##
通過上面的分析摔竿,我們已經(jīng)成功掌握了大波浪+水紋實現(xiàn)的原理,最后再創(chuàng)建WaterScreenView類少孝,把背景山和太陽的光輝加上继低,就完美實現(xiàn)了!
public class WaterScreenView extends View {
private Water mWater;
private Renderable[] mRenderables;
public WaterScreenView(Context context) {
super(context);
}
public WaterScreenView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public WaterScreenView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mRenderables == null && getWidth() != 0) {
init();
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mRenderables == null) {
init();
}
}
private void init() {
mRenderables = new Renderable[1];
Bitmap waterBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.water);
Bitmap foam = BitmapFactory.decodeResource(getResources(), R.drawable.foam);
setLayerType(View.LAYER_TYPE_HARDWARE, null);
mWater = new Water(waterBitmap, foam, getHeight() * 0.65f, getWidth(), getHeight(), 6);
mRenderables[0] = mWater;
Bitmap aura = BitmapFactory.decodeResource(getResources(), R.drawable.sun_aura);
mRenderables[1] = new Renderable(aura, getWidth() * 0.5f, getHeight() * 0.35f);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float deltaTime = FrameRateCounter.timeStep();
for (Renderable renderable : mRenderables) {
renderable.draw(canvas);
renderable.update(deltaTime);
}
if (!isPause) {
invalidate();
}
}
private boolean isPause = false;
}
最終效果:
五.結(jié)尾##
嘩啦啦的寫了這么多稍走,畫圖好累呀郁季。原效果中的噪聲效果還沒有來得及分享冷溃,一篇文章內(nèi)容太多就太亂了。
總而言之梦裂,就是好好利用drawBitmapMesh這個牛逼的東東吧
有機會分享給大家怎么利用drawBitmapMesh讓這個圖像的屁股扭動起來!
嗯盖淡,我就是ImmortalZ,一個Android小菜鳥年柠,歡迎大家一起學(xué)習(xí)。
六.下載##
原裝效果代碼下載地址:
https://github.com/danielzeller/Depth-LIB-Android-
本次拆輪子精簡版代碼下載地址:https://github.com/ImmortalZ/Learn_Depth
歡迎star,如果對我感興趣褪迟,可以follow哦冗恨!
本文來自個人博客 http://immortalz.me/559.html
聯(lián)系方式:
我的微信