Android 吸入动画效果详解
1,背景
上图演示了动画的某几帧,其中从1 - 4,演示了图片从原始图形吸入到一个点(红色标识)。
实现这样的效果,我们利用了Canvas.drawBitmapMesh()方法,这里涉及到了一个Mesh的概念。
2,Mesh的概念
Mesh表示网格,说得通俗一点,可以将画板想像成一张格子布,在这个张布上绘制图片。对于一个网格端点均匀分布的网格来说,横向有meshWidth + 1个顶点,纵向有meshHeight + 1个端点。顶点数据verts是以行优先的数组(二维数组以一维数组表示,先行后列)。网格可以不均匀分布。
上图中显示了把图片分成很多格子,上图中的每个格子是均匀的,它的顶点数是:(meshWidth + 1) * (meshHeight + 1)个,那么放这些顶点的一维数据的大小应该是:(meshWidth + 1) * (meshHeight + 1) * 2 (一个点包含x, y坐标)
float[] vertices = new float[:(meshWidth + 1) * (meshHeight + 1) * 2];
试想,我们让这个格子(mesh)不均匀分布,那么绘制出来的图片就会变形,
3,如何构建Mesh
吸入动画的核心是吸入到一个点,那么我们就是要在不同的时刻构造出不同的mesh的顶点坐标,我们是怎么做的呢?
3.1,创建两条路径(Path)
假如我们的吸入效果是从上到下吸入,
上图中蓝色的线表示我们构造的Path,其实只要我们沿着这两条Path来构造mesh顶点就可以了。
构建两条Path的代码如下:
[java]
mFirstPathMeasure.setPath(mFirstPath, false);
mSecondPathMeasure.setPath(mSecondPath, false);
float w = mBmpWidth;
float h = mBmpHeight;
mFirstPath.reset();
mSecondPath.reset();
mFirstPath.moveTo(0, 0);
mSecondPath.moveTo(w, 0);
mFirstPath.lineTo(0, h);
mSecondPath.lineTo(w, h);
mFirstPath.quadTo(0, (endY + h) / 2, endX, endY);
mSecondPath.quadTo(w, (endY + h) / 2, endX, endY);
mFirstPathMeasure.setPath(mFirstPath, false);
mSecondPathMeasure.setPath(mSecondPath, false);
float w = mBmpWidth;
float h = mBmpHeight;
mFirstPath.reset();
mSecondPath.reset();
mFirstPath.moveTo(0, 0);
mSecondPath.moveTo(w, 0);
mFirstPath.lineTo(0, h);
mSecondPath.lineTo(w, h);
mFirstPath.quadTo(0, (endY + h) / 2, endX, endY);
mSecondPath.quadTo(w, (endY + h) / 2, endX, endY);
3.2,根据Path来计算顶点坐标
算法:
1,假如我们把格子分为WIDTH, HEIGHT份,把Path的长度分的20份,[0, length],表示20个时刻。
2,第0时间,我们要的形状是一个矩形,第1时刻可能是梯形,第n时间可能是一个三角形。下图说明了动画过程中图片的变化。
3,第一条(左)Path的长度为len1,第二条(右)Path的长度为len2,对于任意时刻 t [0 - 20],我们可以知道梯形的四个顶点距Path最顶端的length。
左上角:t * (len1 / 20)
左下角:t * (len1 / 20) + bitmapHeight
右上角:t * (len2 / 20)
右下角:t * (len2 / 20) + bitmapHeight
我们可以通过PathMeasure类根据length算出在Path上面点的坐标,也就是说,根据两条Path,我们可以分别算了四个顶点的坐标,我这里分别叫做A, B, C, D(顺时针方向),有了点的坐标,我们可以算出AD,BC的长度,并且将基进行HEIGHT等分(因为我们把mesh分成宽WIDTH,高HEIGHT等分),将AD,BC上面每等分的点连接起来形成一条直接,将再这条直接水平WIDTH等分,根据直线方程,依据x算出y,从而算出每一个顶点的坐标。(请参考上图)
下面是计算顶点坐标的详细代码:
[java]
private void buildMeshByPathOnVertical(int timeIndex)
{
mFirstPathMeasure.setPath(mFirstPath, false);
mSecondPathMeasure.setPath(mSecondPath, false);
int index = 0;
float[] pos1 = {0.0f, 0.0f};
float[] pos2 = {0.0f, 0.0f};
float firstLen = mFirstPathMeasure.getLength();
float secondLen = mSecondPathMeasure.getLength();
float len1 = firstLen / HEIGHT;
float len2 = secondLen / HEIGHT;
float firstPointDist = timeIndex * len1;
float secondPointDist = timeIndex * len2;
float height = mBmpHeight;
mFirstPathMeasure.getPosTan(firstPointDist, pos1, null);
mFirstPathMeasure.getPosTan(firstPointDist + height, pos2, null);
float x1 = pos1[0];
float x2 = pos2[0];
float y1 = pos1[1];
float y2 = pos2[1];
float FIRST_DIST = (float)Math.sqrt( (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) );
float FIRST_H = FIRST_DIST / HEIGHT;
mSecondPathMeasure.getPosTan(secondPointDist, pos1, null);
mSecondPathMeasure.getPosTan(secondPointDist + height, pos2, null);
x1 = pos1[0];
x2 = pos2[0];
y1 = pos1[1];
y2 = pos2[1];
float SECOND_DIST = (float)Math.sqrt( (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) );
float SECOND_H = SECOND_DIST / HEIGHT;
for (int y = 0; y <= HEIGHT; ++y)
{
mFirstPathMeasure.getPosTan(y * FIRST_H + firstPointDist, pos1, null);
mSecondPathMeasure.getPosTan(y * SECOND_H + secondPointDist, pos2, null);
float w = pos2[0] - pos1[0];
float fx1 = pos1[0];
float fx2 = pos2[0];
float fy1 = pos1[1];
float fy2 = pos2[1];
float dy = fy2 - fy1;
float dx = fx2 - fx1;
for (int x = 0; x <= WIDTH; ++x)
{
// y = x * dy / dx
float fx = x * w / WIDTH;
float fy = fx * dy / dx;
mVerts[index * 2 + 0] = fx + fx1;
mVerts[index * 2 + 1] = fy + fy1;
index += 1;
}
}
}
private void buildMeshByPathOnVertical(int timeIndex)
{
mFirstPathMeasure.setPath(mFirstPath, false);
mSecondPathMeasure.setPath(mSecondPath, false);
int index = 0;
&n
补充:移动开发 , Android ,