Pull to refresh

Перемещение картинки вдоль произвольной кривой

Reading time 4 min
Views 14K

Возникла задача сделать анимацию — двигать картинку вдоль заданной кривой. Погуглил и с удивлением нашел, что несколько вопросов с подобной задачей на stackoverflow.com остаются без ответа уже не один год. Пришлось засучить рукава, покопаться в документации и найти подходящее решение.
Итак у нас есть некоторая кривая. Например построенная из набора точек и для красивости сглаженная.
        //набор точек
        List<PointF> aPoints = new ArrayList<PointF>();

        aPoints.add(new PointF(10f, 160f));
        aPoints.add(new PointF(100f, 100f));
        aPoints.add(new PointF(300f, 220f));
        aPoints.add(new PointF(640f, 180f));

        //строим сглаженную кривую
        Path ptCurve = new Path();

        PointF point = aPoints.get(0);
        ptCurve.moveTo(point.x, point.y);
        for(int i = 0; i < aPoints.size() - 1; i++){
            point = aPoints.get(i);
            PointF next = aPoints.get(i+1);
            ptCurve.quadTo(
                point.x, point.y, 
                (next.x + point.x) / 2, (point.y + next.y) / 2
            );
        }



Задача — получать координаты точек на нашей кривой, чтобы там рисовать нашу картинку. Для этого воспользуемся классом PathMeasure. При помощи этого класса «замерим» длину кривой. А чтобы найти нужную точку, можно передать объекту этого класса длину, на которую точка удалена от начала.

Вот так, например, можно получить координаты точки посередине кривой:
        PathMeasure pm = new PathMeasure(ptCurve, false);
        float afP[] = {0f, 0f}; //здесь будут координаты

        pm.getPosTan(pm.getLength() * 0.5f, afP, null);


Последним параметром (я передал там null) можно, аналогично координатам, получить параметры касательной в этой точке.

Более того есть метод getMatrix, который дает готовую матрицу трансформации — смещение и нужный поворот. Его мы и будем использовать для вывода спрайта.
        Matrix mxTransform = new Matrix();

        pm.getMatrix(
                pm.getLength() * 0.5f,
                mxTransform,
                PathMeasure.POSITION_MATRIX_FLAG + 
                PathMeasure.TANGENT_MATRIX_FLAG
        );
        mxTransform.preTranslate(-bmSprite.getWidth(), -bmSprite.getHeight());

        canvas.drawBitmap(bmSprite, mxTransform, null);


Получилось в точности то, что и требовалось:


Полный код приведен ниже или можно скачать проект из репозитория — SpriteAlongPath или воспользоваться меркуриалом — hg clone bitbucket.org/TedBeer/spritealongpath.

/**
 * User: TedBeer
 * Date: 30/01/12
 * Time: 12:32
 */
package net.tedbeer;

import android.app.Activity;
import android.os.Bundle;
import android.content.Context;
import android.graphics.*;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
import java.util.*;

public class moveSprite extends Activity
{
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(new SceneView(this));
    }
}

public class SceneView extends View {
    private static Bitmap bmSprite;
    private static Bitmap bmBackground;
    private static Rect rSrc, rDest;
    
    //animation step
    private static int iMaxAnimationStep = 20;
    private int iCurStep = 0;

    //points defining our curve
    private List<PointF> aPoints = new ArrayList<PointF>();
    private Paint paint;
    private Path ptCurve = new Path(); //curve
    private PathMeasure pm;            //curve measure
    private float fSegmentLen;         //curve segment length

    public SceneView(Context context) {
        super(context);
        //destination rectangle
        Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        rDest = new Rect(0, 0, display.getWidth(), display.getHeight());
        
        //load background
        if (bmBackground == null) {
            bmBackground = BitmapFactory.decodeResource(getResources(), R.drawable.winter_mountains);
            rSrc = new Rect(0, 0, bmBackground.getWidth(), bmBackground.getHeight());
        }

        //load sprite
        if (bmSprite == null)
            bmSprite = BitmapFactory.decodeResource(getResources(), R.drawable.sledge3);

        //init random set of points
        aPoints.add(new PointF(10f, 160f));
        aPoints.add(new PointF(100f, 100f));
        aPoints.add(new PointF(300f, 220f));
        aPoints.add(new PointF(640f, 180f));
        //init smooth curve
        PointF point = aPoints.get(0);
        ptCurve.moveTo(point.x, point.y);
        for(int i = 0; i < aPoints.size() - 1; i++){
            point = aPoints.get(i);
            PointF next = aPoints.get(i+1);
            ptCurve.quadTo(point.x, point.y, (next.x + point.x) / 2, (point.y + next.y) / 2);
        }
        pm = new PathMeasure(ptCurve, false);
        fSegmentLen = pm.getLength() / iMaxAnimationStep;//20 animation steps

        //init paint object
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(3);
        paint.setColor(Color.rgb(0, 148, 255));
        
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(bmBackground, rSrc, rDest, null);
        canvas.drawPath(ptCurve, paint);
        //animate the sprite
        Matrix mxTransform = new Matrix();
        if (iCurStep <= iMaxAnimationStep) {
            pm.getMatrix(fSegmentLen * iCurStep, mxTransform,
                    PathMeasure.POSITION_MATRIX_FLAG + PathMeasure.TANGENT_MATRIX_FLAG);
            mxTransform.preTranslate(-bmSprite.getWidth(), -bmSprite.getHeight());
            canvas.drawBitmap(bmSprite, mxTransform, null);

            iCurStep++; //advance to the next step
            invalidate();
        } else {
            iCurStep = 0;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN ) { //run animation
            invalidate();
            return true;
        }
        return false;
    }

}


Update: заменил видео на другое, где явно видно, что спрайт еще и поворачивается.
В репозитории добавлена ветка tween-eng, где для анимации используется указанный в комментарии java-universal-tween-engine
Tags:
Hubs:
+67
Comments 25
Comments Comments 25

Articles