酷炫动画DepthLIBAndroid源码学习

项目GitHub地址:https://github.com/danielzeller/Depth-LIB-Android-

#Fragment转场动画
TransitionHelper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static ObjectAnimator introAnimate(final DepthLayout target, final float moveY, final float customElevation, long delay, int subtractDelay) {
//设置动画中心点x,y坐标
target.setPivotY(getDistanceToCenter(target));
target.setPivotX(getDistanceToCenterX(target));
//设置摄像机距离(z轴),由于view全屏,摄像机需拉高,
target.setCameraDistance(10000 * target.getResources().getDisplayMetrics().density);
//沿Y轴移动View,从底部进入;-moveY * target.getResources().getDisplayMetrics().density而不是0,可以达到分层效果
ObjectAnimator translationY2 = ObjectAnimator.ofFloat(target, View.TRANSLATION_Y, target.getResources().getDisplayMetrics().heightPixels, -moveY * target.getResources().getDisplayMetrics().density).setDuration(800);
translationY2.setInterpolator(new ExpoOut());
translationY2.setStartDelay(700 + subtractDelay);
translationY2.start();
target.setTranslationY(target.getResources().getDisplayMetrics().heightPixels);
//沿X轴移动View,从左进入
ObjectAnimator translationX2 = ObjectAnimator.ofFloat(target, View.TRANSLATION_X, -target.getResources().getDisplayMetrics().widthPixels, 0).setDuration(800);
translationX2.setInterpolator(new ExpoOut());
translationX2.setStartDelay(700 + subtractDelay);
translationX2.start();
target.setTranslationX(-target.getResources().getDisplayMetrics().widthPixels);
//矫正-moveY * target.getResources().getDisplayMetrics().density距离
ObjectAnimator translationY = ObjectAnimator.ofFloat(target, View.TRANSLATION_Y, 0).setDuration(700);
translationY.setInterpolator(new BackOut());
translationY.setStartDelay(700 + 800);
translationY.start();
//绕X轴旋转,方向:顺时针(与x轴正向方向相对,即Y轴朝Z轴旋转)。X轴 Y轴旋转都为3D层变换
ObjectAnimator rotationX = ObjectAnimator.ofFloat(target, View.ROTATION_X, TARGET_ROTATION_X, 0).setDuration(1000);
rotationX.setInterpolator(new QuintInOut());
rotationX.setStartDelay(700 + FISRTDELAY + subtractDelay);
rotationX.start();
target.setRotationX(TARGET_ROTATION_X);
//自定义View实现shadow
ObjectAnimator elevation = ObjectAnimator.ofFloat(target, "CustomShadowElevation", customElevation * target.getResources().getDisplayMetrics().density, target.getCustomShadowElevation()).setDuration(1000);
elevation.setInterpolator(new QuintInOut());
elevation.setStartDelay(700 + FISRTDELAY + subtractDelay * 2);
elevation.start();
target.setCustomShadowElevation(customElevation * target.getResources().getDisplayMetrics().density);
//缩放动画,0.5f放大到原来1f倍大小。
ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, TARGET_SCALE, target.getScaleX()).setDuration(1000);
scaleX.setInterpolator(new CircInOut());
scaleX.setStartDelay(700 + FISRTDELAY + subtractDelay);
scaleX.start();
target.setScaleX(TARGET_SCALE);

ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, TARGET_SCALE, target.getScaleY()).setDuration(1000);
scaleY.setInterpolator(new CircInOut());
scaleY.setStartDelay(700 + FISRTDELAY + subtractDelay);
scaleY.start();
target.setScaleY(TARGET_SCALE);
//Z轴不变,X轴与Y轴组成的2D面内旋转
ObjectAnimator rotation = ObjectAnimator.ofFloat(target, View.ROTATION, TARGET_ROTATION, 0).setDuration(1400);
rotation.setInterpolator(new QuadInOut());
rotation.setStartDelay(FISRTDELAY + subtractDelay);
rotation.start();
target.setRotation(TARGET_ROTATION);
rotation.addListener(getShowStatusBarListener(target));
return scaleY;
}

#WaterFragment水流动画
WaterFragment中是WaterSceneView呈现了水流的View,WaterSceneView继承View,通过重写onDraw方法,将水流画上去的。

1
2
3
4
5
6
7
8
9
10
11
WaterSceneView.java
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
for (Renderable renderable : renderables) {
renderable.draw(canvas);
renderable.update(deltaTime, 0);
}
...
}

Renderable.java内的方法很简单,是渲染的基类,抽象了通用坐标等属性和draw方法。我们看到renderables是一个数组,它在init()时初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WaterSceneView.java
private void init() {
renderables = new Renderable[4];
...
water = new Water(waterBitmap, foam, getYCoordByPercent(0.65f), getYCoordByPercent(1f), getXCoordByPercent(1f), 6);
renderables[0] = water;
...
renderables[1] = new Renderable(aura, getXCoordByPercent(0.5f), getYCoordByPercent(0.35f));
...
noiseScratchEffect = new NoiseEffect(noiseScratch, 100, 2f);
renderables[2] = noiseScratchEffect;
noise = new NoiseEffect(noiseReg, 30, 1.5f);
renderables[3] = noise;
...
}

Water对象即水流对象,继承了渲染基类:Renderable,实现了draw方法:

1
2
3
4
5
6
Water.java
@Override
public void draw(Canvas canvas) {
water.draw(canvas);
...
}

waterPathBitmapMesh类型,PathBitmapMesh实现了draw方法:

1
2
3
4
PathbitmapMesh.java
public void draw(Canvas canvas) {
canvas.drawBitmapMesh(bitmap, HORIZONTAL_SLICES, VERTICAL_SLICES, drawingVerts, 0, null, 0, paint);
}

终于看到了实际绘制的方法。至此基本的绘制流程就走通了:View(WaterSceneView.java) -> onDraw() -> Water.java ->draw() -> PathBitmapMesh.java -> draw() -> canvas.drawBitmapMesh(…)

##核心绘制类:PathBitmapMesh.java。
水流位图(49*201):

那waterSceneView是怎么铺满X轴呢?来看看Canvas提供的drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)

  • 工作原理
    Draw the bitmap through the mesh, where mesh vertices are evenly distributed across the bitmap. There are meshWidth+1 vertices across, and meshHeight+1 vertices down. The verts array is accessed in row-major order, so that the first meshWidth+1 vertices are distributed across the top of the bitmap from left to right
    通过将图片平方成若干网格的顶点坐标画到屏幕上(注意,顶点坐标不是顶点在位图上的坐标)。所以这里有(meshWidth+1)*(meshHeight+1)个顶点。verts数组是一个有序的一维数组,从左上角开始所有的顶点坐标都被放入verts数组中,偶数为x坐标,奇数为y坐标。

  • 具体实现
    图片网格对应顶点坐标,canvas会扭曲图片到对应坐标,自动计算周围的扭曲曲线(该demo看不出扭曲的效果,替换纯色water图片可以看出为横向拉伸效果)。这样就达到了填铺的效果。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    PathbitmapMesh.java

    private static final int HORIZONTAL_SLICES = 6;
    private static final int VERTICAL_SLICES = 1;

    public void matchVertsToPath(Path path, float bottomCoord, float extraOffset) {
    PathMeasure pm = new PathMeasure(path, false);
    for (int i = 0; i < staticVerts.length / 2; i++) {
    float yIndexValue = staticVerts[i * 2 + 1];
    float xIndexValue = staticVerts[i * 2];
    float percentOffsetX = (0.000001f + xIndexValue) / bitmap.getWidth();
    float percentOffsetX2 = (0.000001f + xIndexValue) / (bitmap.getWidth() + extraOffset);
    percentOffsetX2 += pathOffsetPercent;
    pm.getPosTan(pm.getLength() * (1f - percentOffsetX), coords, null);
    pm.getPosTan(pm.getLength() * (1f - percentOffsetX2), coords2, null);
    if (yIndexValue == 0) {
    setXY(drawingVerts, i, coords[0], coords2[1]);
    } else {
    float desiredYCoord = bottomCoord;
    setXY(drawingVerts, i, coords[0], desiredYCoord);
    }
    }
    }

    public void draw(Canvas canvas) {
    canvas.drawBitmapMesh(bitmap, HORIZONTAL_SLICES, VERTICAL_SLICES, drawingVerts, 0, null, 0, paint);
    }

drawingVerts顶点坐标数组,matchVertsToPath(Path path, float bottomCoord, float extraOffset)为计算坐标的方法。
matchVertsToPath()通过指定path和PathMeasure来计算对应点坐标,PathMeasure几个方法:

  • float getLength() 返回path的长度
  • boolean getPosTan(float distance, float[] pos, float[] tan) 传入一个距离distance,计算该距离的点坐标并填入pos,切线数据放入tan

这个path就相当于变化的海浪线,沿这这条线,传入一个距离就能知道对应的坐标,非常方便的就能绘画出对应的海浪。staticVerts在位图中顶点坐标数组(0-6 0-1) = 7 2 = 14个顶点。得到各顶点百分比float percentOffsetX = (0.000001f + xIndexValue) / bitmap.getWidth(),通过这个百分比pm.getPosTan(pm.getLength() * (1f - percentOffsetX), coords, null);计算得到各顶点对应的坐标,这里用(1-percentOffseX)使得图片反转(注:纯色无法看出)。当yIndexValue == 0的时候为网格上部分高顶点(Y轴方向只有一格),随时间变化取不同高度,形成海面波动的效果,同时引入第二个percentOffsetX2取X轴错位的Y坐标(这里取的是-0.06个百分比,因为这段时间是呈线性递减变化直到24%左右,变换平滑),让波动更生动不生硬。而yIndexValue !=0的时候底部直接设置view底部的坐标即可。

##Foam泡沫效果
Foam.java也是继承PathBitmapMesh,能够实现海平面被光照射的反光效果,使水面更有层次感,看下具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
Foam.java
public void matchVertsToPath(Path path, float extraOffset) {
...
if (yIndexValue == 0) {
setXY(drawingVerts, i, coords[0], coords2[1] + verticalOffset);
} else {
float desiredYCoord = Math.max(coords2[1], coords2[1] + easedFoamCoords[Math.min(easedFoamCoords.length - 1, index)]);
setXY(drawingVerts, i, coords[0], desiredYCoord + verticalOffset);
index += 1;
}
}
}

Water不同的地方在于每条白光的底部坐标是变化的,Water中第二行所有顶点的Y坐标都是固定的,这里需要设计一个随机的变化来填充白光第二行的Y坐标,这里不理解原作者的意图,做法如下:

1
2
3
4
5
6
Foam.java
void update(float deltaTime) {
for (int i = 0; i < foamCoords.length; i++) {
easedFoamCoords[i] += ((foamCoords[i] - easedFoamCoords[i])) * deltaTime;
}
}

#WindFragment树林-熊-风动画

##树林动画
和WaterFragment一样,WindFragment里也有个主要View:BearSceneView。BearSceneView.java中包含了RenderableThreeParticleSystem flamesParticleSystem sparksSmoke,AuraDrawable
RenderableThree.java(作者失误,树应为tree)与water实现基本一致,不同之处在于树木是随风左右弯曲,所以他的path包含leftPath和rightPath,然后根据leftPath计算每棵树的第一列XY坐标,rightPath计算第二列XY坐标。这里不做具体解释
AuraDrawable.java光晕实现类,没什么内容,交替两帧图片画到View上即可。
ParticleSystem.java粒子系统类,实现了flames火焰动画,sparks火星动画。里面有两个方法:

  • draw(Canvas canvas)负责绘制矩形颗粒
  • update(float deltaTime,float wind)更新下一次绘制的颗粒XY坐标的计算,值根据增量时间,和风力大小来生成,增量时间越大Y值越大,风力越大X值越大,反之亦然。