Android 吸入动画效果详解

1,背景

吸入(Inhale)效果,最初我是在iOS上面看到的,它是在Note程序中,用户可能添加了一页记录,在做删除时,它的删除效果是:这一页内容吸入到一个垃圾框的图标里面。请看下图所示:

===============================================================================

这里,我要介绍的是如何在Android上面实现一个类似的效果。先看看我实现的效果图。

上图演示了动画的某几帧,其中从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,其实只要我们沿着这两条Path来构造mesh顶点就可以了。

构建两条Path的代码如下:

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,从而算出每一个顶点的坐标。(请参考上图)

下面是计算顶点坐标的详细代码:

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;
        }
    }
}

4,如何绘制

绘制代码很简单,调用Canvas.drawBitmapMesh方法。最本质是要计算出一个顶点数组。

canvas.drawBitmapMesh(mBitmap,
        mInhaleMesh.getWidth(),
        mInhaleMesh.getHeight(),
        mInhaleMesh.getVertices(),
        0, null, 0, mPaint);

5,如何实现动画

protected void applyTransformation(float interpolatedTime, Transformation t)
        {
            int curIndex = 0;
            Interpolator interpolator = this.getInterpolator();
            if (null != interpolator)
            {
                float value = interpolator.getInterpolation(interpolatedTime);
                interpolatedTime = value;
            }
            if (mReverse)
            {
                interpolatedTime = 1.0f - interpolatedTime;
            }
            curIndex = (int)(mFromIndex + (mEndIndex - mFromIndex) * interpolatedTime);
            if (null != mListener)
            {
                mListener.onAnimUpdate(curIndex);
            }
        }

在动画里面,我们计算出要做动画的帧的index,假设我们把吸入动画分为20帧,在动画里面,计算出每一帧,最后通过onAnimUpdate(int index)方法回调,在这个方法实现里面,我们根据帧的index来重新计算一个新的mesh顶点数组,再用这个数组来绘制bitmap。这样,我们就可以看来一组连续变化的mesh,也就能看到吸扩效果的动画。

动画类里面,最核心就是扩展Animation类,重写applyTransformation方法。

6,总结

本文简单介绍了吸放效果的实现,根据这个原理,我们可以构造更加复杂的Path来做更多的效果。同时,也能实现向上,向左,向右的吸入效果。

最本质是我们要理解Mesh的概念,最核心的工作就是构造出Mesh的顶点坐标。

计算Mesh通常是一个很复杂的工作,作一些简单的变形还可以,对于太复杂的变形,可能还是不太方便。另外,像书籍翻页的效果,用mesh其实也是可以做到的。只是算法复杂一点。

这里不能给出完整的代码,原理可能不是说得太清楚,但愿给想实现的人一个思路指引吧。

7,实现代码

InhaleAnimationActivity.java

package com.nj1s.lib.test.anim;
import android.os.Bundle;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import com.nj1s.lib.mesh.InhaleMesh.InhaleDir;
import com.nj1s.lib.test.GABaseActivity;
import com.nj1s.lib.test.R;
import com.nj1s.lib.test.effect.BitmapMesh;
public class InhaleAnimationActivity extends GABaseActivity
{
    private static final boolean DEBUG_MODE = false;
    private BitmapMesh.SampleView mSampleView = null;
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        LinearLayout linearLayout = new LinearLayout(this);
        mSampleView = new BitmapMesh.SampleView(this);
        mSampleView.setIsDebug(DEBUG_MODE);
        mSampleView.setLayoutParams(new LinearLayout.LayoutParams(-1, -1));
        Button btn = new Button(this);
        btn.setText("Run");
        btn.setTextSize(20.0f);
        btn.setLayoutParams(new LinearLayout.LayoutParams(150, -2));
        btn.setOnClickListener(new View.OnClickListener()
        {
            boolean mReverse = false;
            @Override
            public void onClick(View v)
            {
                if (mSampleView.startAnimation(mReverse))
                {
                    mReverse = !mReverse;
                }
            }
        });
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        linearLayout.setGravity(Gravity.CENTER_VERTICAL);
        linearLayout.addView(btn);
        linearLayout.addView(mSampleView);
        setContentView(linearLayout);
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        getMenuInflater().inflate(R.menu.inhale_anim_menu, menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        switch(item.getItemId())
        {
        case R.id.menu_inhale_down:
            mSampleView.setInhaleDir(InhaleDir.DOWN);
            break;
        case R.id.menu_inhale_up:
            mSampleView.setInhaleDir(InhaleDir.UP);
            break;
        case R.id.menu_inhale_left:
            mSampleView.setInhaleDir(InhaleDir.LEFT);
            break;
        case R.id.menu_inhale_right:
            mSampleView.setInhaleDir(InhaleDir.RIGHT);
            break;
        }
        return super.onOptionsItemSelected(item);
    }
}

BitmapMesh.java

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.nj1s.lib.test.effect;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import com.nj1s.lib.mesh.InhaleMesh;
import com.nj1s.lib.mesh.InhaleMesh.InhaleDir;
import com.nj1s.lib.test.R;
public class BitmapMesh {
    public static class SampleView extends View {
        private static final int WIDTH = 40;
        private static final int HEIGHT = 40;
        private final Bitmap mBitmap;
        private final Matrix mMatrix = new Matrix();
        private final Matrix mInverse = new Matrix();
        private boolean mIsDebug = false;
        private Paint mPaint = new Paint();
        private float\[\] mInhalePt = new float\[\] {0, 0};
        private InhaleMesh mInhaleMesh = null;
        public SampleView(Context context) {
            super(context);
            setFocusable(true);
            mBitmap = BitmapFactory.decodeResource(getResources(),
                                                     R.drawable.beach);
            mInhaleMesh = new InhaleMesh(WIDTH, HEIGHT);
            mInhaleMesh.setBitmapSize(mBitmap.getWidth(), mBitmap.getHeight());
            mInhaleMesh.setInhaleDir(InhaleDir.DOWN);
        }
        public void setIsDebug(boolean isDebug)
        {
            mIsDebug = isDebug;
        }
        public void setInhaleDir(InhaleMesh.InhaleDir dir)
        {
            mInhaleMesh.setInhaleDir(dir);
            float w = mBitmap.getWidth();
            float h = mBitmap.getHeight();
            float endX = 0;
            float endY = 0;
            float dx = 10;
            float dy = 10;
            mMatrix.reset();
            switch (dir)
            {
            case DOWN:
                endX = w / 2;
                endY = getHeight() - 20;
                break;
            case UP:
                dy = getHeight() - h - 20;
                endX = w / 2;
                endY = -dy + 10;
                break;
            case LEFT:
                dx = getWidth() - w - 20;
                endX = -dx + 10;
                endY = h / 2;
                break;
            case RIGHT:
                endX = getWidth() - 20;
                endY = h / 2;
                break;
            }
            mMatrix.setTranslate(dx, dy);
            mMatrix.invert(mInverse);
            buildPaths(endX, endY);
            buildMesh(w, h);
            invalidate();
        }
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh)
        {
            super.onSizeChanged(w, h, oldw, oldh);
            float bmpW = mBitmap.getWidth();
            float bmpH = mBitmap.getHeight();
            mMatrix.setTranslate(10, 10);
            //mMatrix.setTranslate(10, 10);
            mMatrix.invert(mInverse);
            mPaint.setColor(Color.RED);
            mPaint.setStrokeWidth(2);
            mPaint.setAntiAlias(true);
            buildPaths(bmpW / 2, h - 20);
            buildMesh(bmpW, bmpH);
        }
        public boolean startAnimation(boolean reverse)
        {
            Animation anim = this.getAnimation();
            if (null != anim && !anim.hasEnded())
            {
                return false;
            }
            PathAnimation animation = new PathAnimation(0, HEIGHT + 1, reverse,
                    new PathAnimation.IAnimationUpdateListener()
            {
                @Override
                public void onAnimUpdate(int index)
                {
                    mInhaleMesh.buildMeshes(index);
                    invalidate();
                }
            });
            if (null != animation)
            {
                animation.setDuration(1000);
                this.startAnimation(animation);
            }
            return true;
        }
        @Override
        protected void onDraw(Canvas canvas)
        {
            Log.i("leehong2", "onDraw  =========== ");
            canvas.drawColor(0xFFCCCCCC);
            canvas.concat(mMatrix);
            canvas.drawBitmapMesh(mBitmap,
                    mInhaleMesh.getWidth(),
                    mInhaleMesh.getHeight(),
                    mInhaleMesh.getVertices(),
                    0, null, 0, mPaint);
            // ===========================================
            // Draw the target point.
            mPaint.setColor(Color.RED);
            mPaint.setStyle(Style.FILL);
            canvas.drawCircle(mInhalePt\[0\], mInhalePt\[1\], 5, mPaint);
            if (mIsDebug)
            {
                // ===========================================
                // Draw the mesh vertices.
                canvas.drawPoints(mInhaleMesh.getVertices(), mPaint);
                // ===========================================
                // Draw the paths
                mPaint.setColor(Color.BLUE);
                mPaint.setStyle(Style.STROKE);
                Path\[\] paths = mInhaleMesh.getPaths();
                for (Path path : paths)
                {
                    canvas.drawPath(path, mPaint);
                }
            }
        }
        private void buildMesh(float w, float h)
        {
            mInhaleMesh.buildMeshes(w, h);
        }
        private void buildPaths(float endX, float endY)
        {
            mInhalePt\[0\] = endX;
            mInhalePt\[1\] = endY;
            mInhaleMesh.buildPaths(endX, endY);
        }
        int mLastWarpX = 0;
        int mLastWarpY = 0;
        @Override
        public boolean onTouchEvent(MotionEvent event)
        {
            float\[\] pt = { event.getX(), event.getY() };
            mInverse.mapPoints(pt);
            if (event.getAction() == MotionEvent.ACTION_UP)
            {
                int x = (int)pt\[0\];
                int y = (int)pt\[1\];
                if (mLastWarpX != x || mLastWarpY != y) {
                    mLastWarpX = x;
                    mLastWarpY = y;
                    buildPaths(pt\[0\], pt\[1\]);
                    invalidate();
                }
            }
            return true;
        }
    }
    private static class PathAnimation extends Animation
    {
        public interface IAnimationUpdateListener
        {
            public void onAnimUpdate(int index);
        }
        private int mFromIndex = 0;
        private int mEndIndex = 0;
        private boolean mReverse = false;
        private IAnimationUpdateListener mListener = null;
        public PathAnimation(int fromIndex, int endIndex, boolean reverse, IAnimationUpdateListener listener)
        {
            mFromIndex = fromIndex;
            mEndIndex = endIndex;
            mReverse = reverse;
            mListener = listener;
        }
        public boolean getTransformation(long currentTime, Transformation outTransformation) {
            boolean more = super.getTransformation(currentTime, outTransformation);
            Log.d("leehong2", "getTransformation    more = " + more);
            return more;
        }
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t)
        {
            int curIndex = 0;
            Interpolator interpolator = this.getInterpolator();
            if (null != interpolator)
            {
                float value = interpolator.getInterpolation(interpolatedTime);
                interpolatedTime = value;
            }
            if (mReverse)
            {
                interpolatedTime = 1.0f - interpolatedTime;
            }
            curIndex = (int)(mFromIndex + (mEndIndex - mFromIndex) * interpolatedTime);
            if (null != mListener)
            {
                Log.i("leehong2", "onAnimUpdate  =========== curIndex = " + curIndex);
                mListener.onAnimUpdate(curIndex);
            }
        }
    }
}

最核心的类

InhaleMesh

package com.nj1s.lib.mesh;
import android.graphics.Path;
import android.graphics.PathMeasure;
public class InhaleMesh extends Mesh
{
    public enum InhaleDir
    {
        UP,
        DOWN,
        LEFT,
        RIGHT,
    }
    private Path mFirstPath  = new Path();
    private Path mSecondPath = new Path();
    private PathMeasure mFirstPathMeasure  = new PathMeasure();
    private PathMeasure mSecondPathMeasure = new PathMeasure();
    private InhaleDir mInhaleDir = InhaleDir.DOWN;
    public InhaleMesh(int width, int height)
    {
        super(width, height);
    }
    public void setInhaleDir(InhaleDir inhaleDir)
    {
        mInhaleDir = inhaleDir;
    }
    public InhaleDir getInhaleDir()
    {
        return mInhaleDir;
    }
    @Override
    public void buildPaths(float endX, float endY)
    {
        if (mBmpWidth <= 0 || mBmpHeight <= 0)
        {
            throw new IllegalArgumentException(
                    "Bitmap size must be > 0, do you call setBitmapSize(int, int) method?");
        }
        switch (mInhaleDir)
        {
        case UP:
            buildPathsUp(endX, endY);
            break;
        case DOWN:
            buildPathsDown(endX, endY);
            break;
        case RIGHT:
            buildPathsRight(endX, endY);
            break;
        case LEFT:
            buildPathsLeft(endX, endY);
            break;
        }
    }
    @Override
    public void buildMeshes(int index)
    {
        if (mBmpWidth <= 0 || mBmpHeight <= 0)
        {
            throw new IllegalArgumentException(
                    "Bitmap size must be > 0, do you call setBitmapSize(int, int) method?");
        }
        switch (mInhaleDir)
        {
        case UP:
        case DOWN:
            buildMeshByPathOnVertical(index);
            break;
        case RIGHT:
        case LEFT:
            buildMeshByPathOnHorizontal(index);
            break;
        }
    }
    public Path\[\] getPaths()
    {
        return new Path\[\] { mFirstPath, mSecondPath };
    }
    private void buildPathsDown(float endX, float 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);
    }
    private void buildPathsUp(float endX, float endY)
    {
        mFirstPathMeasure.setPath(mFirstPath, false);
        mSecondPathMeasure.setPath(mSecondPath, false);
        float w = mBmpWidth;
        float h = mBmpHeight;
        mFirstPath.reset();
        mSecondPath.reset();
        mFirstPath.moveTo(0, h);
        mSecondPath.moveTo(w, h);
        mFirstPath.lineTo(0, 0);
        mSecondPath.lineTo(w, 0);
        mFirstPath.quadTo(0, (endY - h) / 2, endX, endY);
        mSecondPath.quadTo(w, (endY - h) / 2, endX, endY);
    }
    private void buildPathsRight(float endX, float 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(0, h);
        mFirstPath.lineTo(w, 0);
        mSecondPath.lineTo(w, h);
        mFirstPath.quadTo((endX + w) / 2, 0, endX, endY);
        mSecondPath.quadTo((endX + w) / 2, h, endX, endY);
    }
    private void buildPathsLeft(float endX, float endY)
    {
        mFirstPathMeasure.setPath(mFirstPath, false);
        mSecondPathMeasure.setPath(mSecondPath, false);
        float w = mBmpWidth;
        float h = mBmpHeight;
        mFirstPath.reset();
        mSecondPath.reset();
        mFirstPath.moveTo(w, 0);
        mSecondPath.moveTo(w, h);
        mFirstPath.lineTo(0, 0);
        mSecondPath.lineTo(0, h);
        mFirstPath.quadTo((endX - w) / 2, 0, endX, endY);
        mSecondPath.quadTo((endX - w) / 2, h, endX, endY);
    }
    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;
        if (mInhaleDir == InhaleDir.DOWN)
        {
            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;
                }
            }
        }
        else if (mInhaleDir == InhaleDir.UP)
        {
            for (int y = HEIGHT; y >= 0; --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 buildMeshByPathOnHorizontal(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 / WIDTH;
        float len2 = secondLen / WIDTH;
        float firstPointDist  = timeIndex * len1;
        float secondPointDist = timeIndex * len2;
        float width = mBmpWidth;
        mFirstPathMeasure.getPosTan(firstPointDist, pos1, null);
        mFirstPathMeasure.getPosTan(firstPointDist + width, 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_X = FIRST_DIST / WIDTH;
        mSecondPathMeasure.getPosTan(secondPointDist, pos1, null);
        mSecondPathMeasure.getPosTan(secondPointDist + width, 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_X = SECOND_DIST / WIDTH;
        if (mInhaleDir == InhaleDir.RIGHT)
        {
            for (int x = 0; x <= WIDTH; ++x)
            {
                mFirstPathMeasure.getPosTan(x * FIRST_X + firstPointDist, pos1, null);
                mSecondPathMeasure.getPosTan(x * SECOND_X + secondPointDist, pos2, null);
                float h = pos2\[1\] - pos1\[1\];
                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 y = 0; y <= HEIGHT; ++y)
                {
                    // x = y * dx / dy
                    float fy = y * h / HEIGHT;
                    float fx = fy * dx / dy;
                    index = y * (WIDTH + 1) + x;
                    mVerts\[index * 2 + 0\] = fx + fx1;
                    mVerts\[index * 2 + 1\] = fy + fy1;
                }
            }
        }
        else if (mInhaleDir == InhaleDir.LEFT)
        {
            for (int x = WIDTH; x >= 0; --x)
            //for (int x = 0; x <= WIDTH; ++x)
            {
                mFirstPathMeasure.getPosTan(x * FIRST_X + firstPointDist, pos1, null);
                mSecondPathMeasure.getPosTan(x * SECOND_X + secondPointDist, pos2, null);
                float h = pos2\[1\] - pos1\[1\];
                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 y = 0; y <= HEIGHT; ++y)
                {
                    // x = y * dx / dy
                    float fy = y * h / HEIGHT;
                    float fx = fy * dx / dy;
                    index = y * (WIDTH + 1) + WIDTH - x;
                    mVerts\[index * 2 + 0\] = fx + fx1;
                    mVerts\[index * 2 + 1\] = fy + fy1;
                }
            }
        }
    }
}

Mesh类的实现

/*
 * System: CoreLib
 * @version     1.00
 *
 * Copyright (C) 2010, LZT Corporation.
 *
 */
package com.nj1s.lib.mesh;
public abstract class Mesh
{
    protected int WIDTH      = 40;
    protected int HEIGHT     = 40;
    protected int mBmpWidth   = -1;
    protected int mBmpHeight  = -1;
    protected final float\[\] mVerts;
    public Mesh(int width, int height)
    {
        WIDTH  = width;
        HEIGHT = height;
        mVerts  = new float\[(WIDTH + 1) * (HEIGHT + 1) * 2\];
    }
    public float\[\] getVertices()
    {
        return mVerts;
    }
    public int getWidth()
    {
        return WIDTH;
    }
    public int getHeight()
    {
        return HEIGHT;
    }
    public static void setXY(float\[\] array, int index, float x, float y)
    {
        array\[index*2 + 0\] = x;
        array\[index*2 + 1\] = y;
    }
    public void setBitmapSize(int w, int h)
    {
        mBmpWidth  = w;
        mBmpHeight = h;
    }
    public abstract void buildPaths(float endX, float endY);
    public abstract void buildMeshes(int index);
    public void buildMeshes(float w, float h)
    {
        int index = 0;
        for (int y = 0; y <= HEIGHT; ++y)
        {
            float fy = y * h / HEIGHT;
            for (int x = 0; x <= WIDTH; ++x)
            {
                float fx = x * w / WIDTH;
                setXY(mVerts, index, fx, fy);
                index += 1;
            }
        }
    }
}

转自csdn:http://blog.csdn.net/leehong2005/article/details/9127095