/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Контур может быть полезен для создания интересной анимации - перемещения картинки вдоль произвольной кривой. Для начала познакомимся со статьёй на эту тему.
Возникла задача сделать анимацию — двигать картинку вдоль заданной кривой. Итак, у нас есть некоторая кривая, например, построенная из набора точек и для красивости сглаженная.
//набор точек
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);
Получилось в точности то, что и требовалось:
Полный код приведен ниже.
/**
* 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;
}
}
Вернёмся к нашим примерам. Помните, мы проектировали лестницу для котов?
Доработаем лестницу, чтобы она стала замкнутой, и добавим изображение кота-андроида из набора счастливых котят.
Оформим в виде нового класса AnimationPathView.
package ru.alexanderklimov.path;
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.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;
public class AnimationPathView extends View {
private Paint mPaint;
private Path mPath;
private Bitmap mBitmap;
private PathMeasure mPathMeasure;
private Matrix mMatrix;
private int mOffsetX, mOffsetY;
private float mPathLength;
private float mStep; // distance each step
private float mDistance; // distance moved
private float[] mPosition;
private float[] mTan;
public AnimationPathView(Context context) {
super(context);
// TODO Auto-generated constructor stub
init();
}
public AnimationPathView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public AnimationPathView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(3);
mPaint.setStyle(Paint.Style.STROKE);
mBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.ic_android_cat);
mOffsetX = mBitmap.getWidth() / 2;
mOffsetY = mBitmap.getHeight() / 2;
mPath = new Path();
mPath.moveTo(50, 50);
mPath.lineTo(150, 50);
mPath.lineTo(150, 100);
mPath.lineTo(250, 100);
mPath.lineTo(250, 150);
mPath.lineTo(350, 150);
mPath.lineTo(350, 200);
mPath.lineTo(450, 200);
mPath.lineTo(450, 250);
mPath.lineTo(50, 250);
mPath.close();
// mPath.lineTo(50, 50);
mPathMeasure = new PathMeasure(mPath, false);
mPathLength = mPathMeasure.getLength();
Toast.makeText(getContext(), "Path Length: " + mPathLength,
Toast.LENGTH_LONG).show();
mStep = 1;
mDistance = 0;
mPosition = new float[2];
mTan = new float[2];
mMatrix = new Matrix();
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawPath(mPath, mPaint);
if (mDistance < mPathLength) {
mPathMeasure.getPosTan(mDistance, mPosition, mTan);
mMatrix.reset();
float degrees = (float) (Math.atan2(mTan[1], mTan[0]) * 180.0 / Math.PI);
mMatrix.postRotate(degrees, mOffsetX, mOffsetY);
mMatrix.postTranslate(mPosition[0] - mOffsetX, mPosition[1] - mOffsetY);
canvas.drawBitmap(mBitmap, mMatrix, null);
mDistance += mStep;
} else {
mDistance = 0;
}
invalidate();
}
}
Подключим компонент программно в активности:
setContentView(new AnimationPathView(this));
Готово.
При повороте на углах лестницы фигура резко меняет своё направление. Сделаем это движение плавным. Для примера я также изменил смещение по высоте, чтобы фигура скользила вдоль лестницы, "перебирая лапами".
package ru.alexanderklimov.path;
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.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;
public class AnimationPathView extends View {
private Paint mPaint;
private Path mPath;
private Bitmap mBitmap;
private PathMeasure mPathMeasure;
private Matrix mMatrix;
private int mOffsetX, mOffsetY;
private float mPathLength;
private float mStep; // distance each step
private float mDistance; // distance moved
private float[] mPosition;
private float[] mTan;
private float mCurX, mCurY;
private float mCurAngle; // current angle
private float mTargetAngle; // target angle
private float mStepAngle; // angle each step
public AnimationPathView(Context context) {
super(context);
// TODO Auto-generated constructor stub
init();
}
public AnimationPathView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public AnimationPathView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(3);
mPaint.setStyle(Paint.Style.STROKE);
mBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.ic_android_cat);
mOffsetX = mBitmap.getWidth() / 2;
mOffsetY = mBitmap.getHeight();
mPath = new Path();
mPath.moveTo(150, 150);
mPath.lineTo(250, 150);
mPath.lineTo(250, 200);
mPath.lineTo(350, 200);
mPath.lineTo(350, 250);
mPath.lineTo(450, 250);
mPath.lineTo(450, 300);
mPath.lineTo(550, 300);
mPath.lineTo(550, 350);
mPath.lineTo(150, 350);
mPath.close();
mPathMeasure = new PathMeasure(mPath, false);
mPathLength = mPathMeasure.getLength();
Toast.makeText(getContext(), "Path Length: " + mPathLength,
Toast.LENGTH_LONG).show();
mStep = 1;
mDistance = 0;
mCurX = 0;
mCurY = 0;
mStepAngle = 1;
mCurAngle = 0;
mTargetAngle = 0;
mPosition = new float[2];
mTan = new float[2];
mMatrix = new Matrix();
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawPath(mPath, mPaint);
mMatrix.reset();
if ((mTargetAngle - mCurAngle) > mStepAngle) {
mCurAngle += mStepAngle;
mMatrix.postRotate(mCurAngle, mOffsetX, mOffsetY);
mMatrix.postTranslate(mCurX, mCurY);
canvas.drawBitmap(mBitmap, mMatrix, null);
} else if ((mCurAngle - mTargetAngle) > mStepAngle) {
mCurAngle -= mStepAngle;
mMatrix.postRotate(mCurAngle, mOffsetX, mOffsetY);
mMatrix.postTranslate(mCurX, mCurY);
canvas.drawBitmap(mBitmap, mMatrix, null);
} else {
mCurAngle = mTargetAngle;
if (mDistance < mPathLength) {
mPathMeasure.getPosTan(mDistance, mPosition, mTan);
mTargetAngle = (float) (Math.atan2(mTan[1], mTan[0]) * 180.0 / Math.PI);
mMatrix.postRotate(mCurAngle, mOffsetX, mOffsetY);
mCurX = mPosition[0] - mOffsetX;
mCurY = mPosition[1] - mOffsetY;
mMatrix.postTranslate(mCurX, mCurY);
canvas.drawBitmap(mBitmap, mMatrix, null);
mDistance += mStep;
} else {
mDistance = 0;
}
}
invalidate();
}
}
Результат.
Мы создавали контур программно. Но мы можем создать контур динамически движением пальца на экране, который можно скопировать в наш первый контур. И пустить по созданному контуру фигурку кота.
package ru.alexanderklimov.path;
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.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
public class AnimationPathView extends View {
private Paint mPaint;
private Path mPath;
private Path mTouchPath;
private Bitmap mBitmap;
private PathMeasure mPathMeasure;
private Matrix mMatrix;
private int mOffsetX, mOffsetY;
private float mPathLength;
private float mStep; // distance each step
private float mDistance; // distance moved
private float[] mPosition;
private float[] mTan;
private float mCurX, mCurY;
private float mCurAngle; // current angle
private float mTargetAngle; // target angle
private float mStepAngle; // angle each step
public AnimationPathView(Context context) {
super(context);
// TODO Auto-generated constructor stub
init();
}
public AnimationPathView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public AnimationPathView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(3);
mPaint.setStyle(Paint.Style.STROKE);
mBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.ic_android_cat);
mOffsetX = mBitmap.getWidth() / 2;
mOffsetY = mBitmap.getHeight();
mTouchPath = new Path();
mPath = new Path();
mPath.moveTo(150, 150);
mPath.lineTo(250, 150);
mPath.lineTo(250, 200);
mPath.lineTo(350, 200);
mPath.lineTo(350, 250);
mPath.lineTo(450, 250);
mPath.lineTo(450, 300);
mPath.lineTo(550, 300);
mPath.lineTo(550, 350);
mPath.lineTo(150, 350);
mPath.close();
mPathMeasure = new PathMeasure(mPath, false);
mPathLength = mPathMeasure.getLength();
Toast.makeText(getContext(), "Path Length: " + mPathLength,
Toast.LENGTH_LONG).show();
mStep = 1;
mDistance = 0;
mCurX = 0;
mCurY = 0;
mStepAngle = 1;
mCurAngle = 0;
mTargetAngle = 0;
mPosition = new float[2];
mTan = new float[2];
mMatrix = new Matrix();
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
if (mPath.isEmpty()) {
return;
}
canvas.drawPath(mPath, mPaint);
mMatrix.reset();
if ((mTargetAngle - mCurAngle) > mStepAngle) {
mCurAngle += mStepAngle;
mMatrix.postRotate(mCurAngle, mOffsetX, mOffsetY);
mMatrix.postTranslate(mCurX, mCurY);
canvas.drawBitmap(mBitmap, mMatrix, null);
invalidate();
} else if ((mCurAngle - mTargetAngle) > mStepAngle) {
mCurAngle -= mStepAngle;
mMatrix.postRotate(mCurAngle, mOffsetX, mOffsetY);
mMatrix.postTranslate(mCurX, mCurY);
canvas.drawBitmap(mBitmap, mMatrix, null);
invalidate();
} else {
mCurAngle = mTargetAngle;
if (mDistance < mPathLength) {
mPathMeasure.getPosTan(mDistance, mPosition, mTan);
mTargetAngle = (float) (Math.atan2(mTan[1], mTan[0]) * 180.0 / Math.PI);
mMatrix.postRotate(mCurAngle, mOffsetX, mOffsetY);
mCurX = mPosition[0] - mOffsetX;
mCurY = mPosition[1] - mOffsetY;
mMatrix.postTranslate(mCurX, mCurY);
canvas.drawBitmap(mBitmap, mMatrix, null);
mDistance += mStep;
invalidate();
} else {
// mDistance = 0;
mMatrix.postRotate(mCurAngle, mOffsetX, mOffsetY);
mMatrix.postTranslate(mCurX, mCurY);
canvas.drawBitmap(mBitmap, mMatrix, null);
}
}
// invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mTouchPath.reset();
mTouchPath.moveTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
mTouchPath.lineTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_UP:
mTouchPath.lineTo(event.getX(), event.getY());
mPath = new Path(mTouchPath);
mPathMeasure = new PathMeasure(mPath, false);
mPathLength = mPathMeasure.getLength();
mStep = 1;
mDistance = 0;
mCurX = 0;
mCurY = 0;
mStepAngle = 1;
mCurAngle = 0;
mTargetAngle = 0;
invalidate();
break;
}
return true;
}
}
Разные устройства имеют свои технические характеристики - размеры экрана, разрешения и т.д. И для каждого устройства скорость движения объекта по траектории будет своя. Чтобы замерить производительность кода, добавим дополнительные строчки в класс AnimationPathView.
private Paint mTextPaint;
private long mLastTime;
private void init() {
...
mTextPaint = new Paint();
mTextPaint.setColor(Color.BLUE);
mTextPaint.setStrokeWidth(1);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setTextSize(26);
...
mLastTime = System.currentTimeMillis();
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
if (mPath.isEmpty()) {
return;
}
long startNanos = System.nanoTime();
long startMillis = System.currentTimeMillis();
...
long endNanos = System.nanoTime();
long betweenFrame = startMillis - mLastTime;
int fps = (int) (1000 / betweenFrame);
String strProcessingTime = "Processing Time (ns = 0.000001ms) = "
+ (endNanos - startNanos);
String strBetweenFrame = "Between Frame (ms) = " + betweenFrame;
String strFPS = "Frame Per Second (approximate) = " + fps;
mLastTime = startMillis;
canvas.drawText(strProcessingTime, 10, 30, mTextPaint);
canvas.drawText(strBetweenFrame, 10, 60, mTextPaint);
canvas.drawText(strFPS, 10, 90, mTextPaint);
canvas.drawText(String.valueOf(mPathLength), 10, 120, mTextPaint);
}
Для управления скоростью движения объекта вдоль контура добавьте код из статьи Change speed of Animation follow touch path (ссылка внизу). Для изменения направления движения объекта смотрите статью Animation follow touch path forward and backward.
Android-er: Animation of moving bitmap along path
Android-er: Smooth turning along path
Android-er: Animation follow touch path
Android-er: Know the performance, timing and speed of animation
Android-er: Change speed of Animation follow touch path
Android-er: Animation follow touch path forward and backward