Animación de Android equivalente para iOS "calculationMode"

Estoy intentando animar algunos drawables en Android , he establecido una ruta usando PathEvaluator que anima a lo largo de algunas curvas a lo largo de una ruta completa.

Cuando configuro una duration (por ejemplo, 6 segundos) divide la duración a la cantidad de curvas que he establecido, independientemente de su longitud, lo que hace que la animation sea más lenta en algunos segmentos y demasiado rápida en otros.

En iOS esto se puede arreglar usando

 animation.calculationMode = kCAAnimationCubicPaced; animation.timingFunction = ...; 

Lo que permite que iOS suavice todo su path en puntos medios y amplíe la duración de acuerdo con la longitud de cada segmento. ¿Hay alguna forma de get el mismo resultado en Android?

(además de romper el path en segmentos discretos y asignar manualmente cada segmento su propia duración, lo que es realmente feo e insostenible).

No creo que se pueda hacer nada con ObjectAnimator porque parece que no hay ninguna function que pueda llamarse para afirmar la duración relativa de un cierto fragment de la animation.

Desarrollé algo similar a lo que necesitas hace un time, pero funciona de manera ligeramente diferente: henetworkinga de Animación.

He modificado todo para que funcione con tus necesidades curvas y con la class PathPoint.

Aquí está una descripción:

  1. Suministro la list de puntos a la animation en el constructor.

  2. Calculo la longitud entre todos los puntos usando una calculadora de distancia simple. Luego sumo todo para get la longitud total de la ruta y almacenar las longitudes de segmento en un map para uso futuro (esto es para mejorar la eficiencia durante el time de ejecución).

  3. Al animar, utilizo el time de interpolación actual para descubrir qué 2 puntos estoy animando entre, teniendo en count la relación entre el time y la relación de distancia recorrida.

  4. Calculo el time que debería tomar para animar entre estos 2 puntos según la distancia relativa entre ellos, en comparación con la distancia total.

  5. Luego interpola por separado entre estos 2 puntos utilizando el cálculo en la class PathAnimator.

Aquí está el código:

CurveAnimation.java:

 public class CurveAnimation extends Animation { private static final float BEZIER_LENGTH_ACCURACY = 0.001f; // Must be divisible by one. Make smaller to improve accuracy, but will increase runtime at start of animation. private List<PathPoint> mPathPoints; private float mOverallLength; private Map<PathPoint, Double> mSegmentLengths = new HashMap<PathPoint, Double>(); // map between the end point and the length of the path to it. public CurveAnimation(List<PathPoint> pathPoints) { mPathPoints = pathPoints; if (mPathPoints == null || mPathPoints.size() < 2) { Log.e("CurveAnimation", "There must be at least 2 points on the path. There will be an exception soon!"); } calculateOverallLength(); } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { PathPoint[] startEndPart = getStartEndForTime(interpolatedTime); PathPoint startPoint = startEndPart[0]; PathPoint endPoint = startEndPart[1]; float startTime = getStartTimeOfPoint(startPoint); float endTime = getStartTimeOfPoint(endPoint); float progress = (interpolatedTime - startTime) / (endTime - startTime); float x, y; float[] xy; if (endPoint.mOperation == PathPoint.CURVE) { xy = getBezierXY(startPoint, endPoint, progress); x = xy[0]; y = xy[1]; } else if (endPoint.mOperation == PathPoint.LINE) { x = startPoint.mX + progress * (endPoint.mX - startPoint.mX); y = startPoint.mY + progress * (endPoint.mY - startPoint.mY); } else { x = endPoint.mX; y = endPoint.mY; } t.getMatrix().setTranslate(x, y); super.applyTransformation(interpolatedTime, t); } private PathPoint[] getStartEndForTime(float time) { double length = 0; if (time == 1) { return new PathPoint[] { mPathPoints.get(mPathPoints.size() - 2), mPathPoints.get(mPathPoints.size() - 1) }; } PathPoint[] result = new PathPoint[2]; for (int i = 0; i < mPathPoints.size() - 1; i++) { length += calculateLengthFromIndex(i); if (length / mOverallLength >= time) { result[0] = mPathPoints.get(i); result[1] = mPathPoints.get(i + 1); break; } } return result; } private float getStartTimeOfPoint(PathPoint point) { float result = 0; int index = 0; while (mPathPoints.get(index) != point && index < mPathPoints.size() - 1) { result += (calculateLengthFromIndex(index) / mOverallLength); index++; } return result; } private void calculateOverallLength() { mOverallLength = 0; mSegmentLengths.clear(); double segmentLength; for (int i = 0; i < mPathPoints.size() - 1; i++) { segmentLength = calculateLengthFromIndex(i); mSegmentLengths.put(mPathPoints.get(i + 1), segmentLength); mOverallLength += segmentLength; } } private double calculateLengthFromIndex(int index) { PathPoint start = mPathPoints.get(index); PathPoint end = mPathPoints.get(index + 1); return calculateLength(start, end); } private double calculateLength(PathPoint start, PathPoint end) { if (mSegmentLengths.containsKey(end)) { return mSegmentLengths.get(end); } else if (end.mOperation == PathPoint.LINE) { return calculateLength(start.mX, end.mX, start.mY, end.mY); } else if (end.mOperation == PathPoint.CURVE) { return calculateBezeirLength(start, end); } else { return 0; } } private double calculateLength(float x0, float x1, float y0, float y1) { return Math.sqrt(((x0 - x1) * (x0 - x1)) + ((y0 - y1) * (y0 - y1))); } private double calculateBezeirLength(PathPoint start, PathPoint end) { double result = 0; float x, y, x0, y0; float[] xy; x0 = start.mX; y0 = start.mY; for (float progress = BEZIER_LENGTH_ACCURACY; progress <= 1; progress += BEZIER_LENGTH_ACCURACY) { xy = getBezierXY(start, end, progress); x = xy[0]; y = xy[1]; result += calculateLength(x, x0, y, y0); x0 = x; y0 = y; } return result; } private float[] getBezierXY(PathPoint start, PathPoint end, float progress) { float[] result = new float[2]; float oneMinusT, x, y; oneMinusT = 1 - progress; x = oneMinusT * oneMinusT * oneMinusT * start.mX + 3 * oneMinusT * oneMinusT * progress * end.mControl0X + 3 * oneMinusT * progress * progress * end.mControl1X + progress * progress * progress * end.mX; y = oneMinusT * oneMinusT * oneMinusT * start.mY + 3 * oneMinusT * oneMinusT * progress * end.mControl0Y + 3 * oneMinusT * progress * progress * end.mControl1Y + progress * progress * progress * end.mY; result[0] = x; result[1] = y; return result; } } 

Aquí hay una muestra que muestra cómo activar la animation:

 private void animate() { AnimatorPath path = new AnimatorPath(); path.moveTo(0, 0); path.lineTo(0, 300); path.curveTo(100, 0, 300, 900, 400, 500); CurveAnimation animation = new CurveAnimation(path.mPoints); animation.setDuration(5000); animation.setInterpolator(new LinearInterpolator()); btn.startAnimation(animation); } 

Ahora, tenga en count que actualmente estoy calculando la longitud de la curva según una aproximación. Esto obviamente causará algunas inexactitudes leves en la velocidad. Si cree que no es lo suficientemente preciso, no dude en modificar el código. Además, si desea boost la precisión de la longitud de la curva, intente disminuir el valor de BEZIER_LENGTH_ACCURACY. Debe ser divisible en 1, por lo que los valores aceptados pueden ser 0.001, 0.000025, etc.

Si bien puede notar algunas fluctuaciones leves en la velocidad al usar curvas, estoy seguro de que es mucho mejor que simplemente dividir el time de manera equitativa entre todas las routes.

Espero que esto ayude 🙂

Traté de usar la respuesta de Gil, pero no encajaba con la forma en que estaba animando. Gil escribió una class de Animation que se utiliza para animar a la View s. Estaba utilizando ObjectAnimator.ofObject() para animar classs personalizadas utilizando ValueProperties que no se pueden usar con Animation personalizada.

Entonces esto es lo que hice:

  1. PathEvaluator y PathEvaluator su método de evaluación.
  2. Utilizo la lógica de Gil para calcular la longitud total de la ruta y las longitudes segmentadas
  3. Como se llama PathEvaluator.evaluate para cada PathPoint con t valores 0..1, necesitaba normalizar el time interpolado que se me dio, por lo que será incremental y no se networkingucirá a cero para cada segmento.
  4. PathPoint el inicio / final PathPoint s que se me ha dado para que la position actual pueda ser antes o después del inicio a lo largo de la ruta, dependiendo de la duración del segmento.
  5. Paso el progreso actual calculado a mi super ( PathEvaluator ) para PathEvaluator la position real.

Este es el código:

 public class NormalizedEvaluator extends PathEvaluator { private static final float BEZIER_LENGTH_ACCURACY = 0.001f; private List<PathPoint> mPathPoints; private float mOverallLength; private Map<PathPoint, Double> mSegmentLengths = new HashMap<PathPoint, Double>(); public NormalizedEvaluator(List<PathPoint> pathPoints) { mPathPoints = pathPoints; if (mPathPoints == null || mPathPoints.size() < 2) { Log.e("CurveAnimation", "There must be at least 2 points on the path. There will be an exception soon!"); } calculateOverallLength(); } @Override public PathPoint evaluate(float interpolatedTime, PathPoint ignonetworkingStartPoint, PathPoint ignonetworkingEndPoint) { float index = getStartIndexOfPoint(ignonetworkingStartPoint); float normalizedInterpolatedTime = (interpolatedTime + index) / (mPathPoints.size() - 1); PathPoint[] startEndPart = getStartEndForTime(normalizedInterpolatedTime); PathPoint startPoint = startEndPart[0]; PathPoint endPoint = startEndPart[1]; float startTime = getStartTimeOfPoint(startPoint); float endTime = getStartTimeOfPoint(endPoint); float progress = (normalizedInterpolatedTime - startTime) / (endTime - startTime); return super.evaluate(progress, startPoint, endPoint); } private PathPoint[] getStartEndForTime(float time) { double length = 0; if (time == 1) { return new PathPoint[] { mPathPoints.get(mPathPoints.size() - 2), mPathPoints.get(mPathPoints.size() - 1) }; } PathPoint[] result = new PathPoint[2]; for (int i = 0; i < mPathPoints.size() - 1; i++) { length += calculateLengthFromIndex(i); if (length / mOverallLength >= time) { result[0] = mPathPoints.get(i); result[1] = mPathPoints.get(i + 1); break; } } return result; } private float getStartIndexOfPoint(PathPoint point) { for (int ii = 0; ii < mPathPoints.size(); ii++) { PathPoint current = mPathPoints.get(ii); if (current == point) { return ii; } } return -1; } private float getStartTimeOfPoint(PathPoint point) { float result = 0; int index = 0; while (mPathPoints.get(index) != point && index < mPathPoints.size() - 1) { result += (calculateLengthFromIndex(index) / mOverallLength); index++; } return result; } private void calculateOverallLength() { mOverallLength = 0; mSegmentLengths.clear(); double segmentLength; for (int i = 0; i < mPathPoints.size() - 1; i++) { segmentLength = calculateLengthFromIndex(i); mSegmentLengths.put(mPathPoints.get(i + 1), segmentLength); mOverallLength += segmentLength; } } private double calculateLengthFromIndex(int index) { PathPoint start = mPathPoints.get(index); PathPoint end = mPathPoints.get(index + 1); return calculateLength(start, end); } private double calculateLength(PathPoint start, PathPoint end) { if (mSegmentLengths.containsKey(end)) { return mSegmentLengths.get(end); } else if (end.mOperation == PathPoint.LINE) { return calculateLength(start.mX, end.mX, start.mY, end.mY); } else if (end.mOperation == PathPoint.CURVE) { return calculateBezeirLength(start, end); } else { return 0; } } private double calculateLength(float x0, float x1, float y0, float y1) { return Math.sqrt(((x0 - x1) * (x0 - x1)) + ((y0 - y1) * (y0 - y1))); } private double calculateBezeirLength(PathPoint start, PathPoint end) { double result = 0; float x, y, x0, y0; float[] xy; x0 = start.mX; y0 = start.mY; for (float progress = BEZIER_LENGTH_ACCURACY; progress <= 1; progress += BEZIER_LENGTH_ACCURACY) { xy = getBezierXY(start, end, progress); x = xy[0]; y = xy[1]; result += calculateLength(x, x0, y, y0); x0 = x; y0 = y; } return result; } private float[] getBezierXY(PathPoint start, PathPoint end, float progress) { float[] result = new float[2]; float oneMinusT, x, y; oneMinusT = 1 - progress; x = oneMinusT * oneMinusT * oneMinusT * start.mX + 3 * oneMinusT * oneMinusT * progress * end.mControl0X + 3 * oneMinusT * progress * progress * end.mControl1X + progress * progress * progress * end.mX; y = oneMinusT * oneMinusT * oneMinusT * start.mY + 3 * oneMinusT * oneMinusT * progress * end.mControl0Y + 3 * oneMinusT * progress * progress * end.mControl1Y + progress * progress * progress * end.mY; result[0] = x; result[1] = y; return result; } } 

Este es el uso:

 NormalizedEvaluator evaluator = new NormalizedEvaluator((List<PathPoint>) path.getPoints()); ObjectAnimator anim = ObjectAnimator.ofObject(object, "position", evaluator, path.getPoints().toArray()); 

ACTUALIZACIÓN: me di count de que podría haber reinventado la rueda, mira Especificación de fotogtwigs key .


Es impactante ver que nada está disponible de este tipo. De todos modos, si no desea calcular la longitud de la ruta en time de ejecución, pude agregar la funcionalidad de asignar pesos a las routes. Idea es asignar un peso a su ruta y ejecutar la animation si se siente bien, bueno y bueno, solo disminuya o aumente el peso asignado a cada ruta.

El código siguiente es el código modificado de la muestra oficial de Android que señaló en su pregunta:

  // Set up the path we're animating along AnimatorPath path = new AnimatorPath(); path.moveTo(0, 0).setWeight(0); path.lineTo(0, 300).setWeight(30);// assign arbitrary weight path.curveTo(100, 0, 300, 900, 400, 500).setWeight(70);// assign arbitrary weight final PathPoint[] points = path.getPoints().toArray(new PathPoint[] {}); mFirstKeyframe = points[0]; final int numFrames = points.length; final PathEvaluator pathEvaluator = new PathEvaluator(); final ValueAnimator anim = ValueAnimator.ofInt(0, 1);// dummy values anim.setDuration(1000); anim.setInterpolator(new LinearInterpolator()); anim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = animation.getAnimatedFraction(); // Special-case optimization for the common case of only two // keyframes if (numFrames == 2) { PathPoint nextPoint = pathEvaluator.evaluate(fraction, points[0], points[1]); setButtonLoc(nextPoint); } else { PathPoint prevKeyframe = mFirstKeyframe; for (int i = 1; i < numFrames; ++i) { PathPoint nextKeyframe = points[i]; if (fraction < nextKeyframe.getFraction()) { final float prevFraction = prevKeyframe .getFraction(); float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); PathPoint nextPoint = pathEvaluator.evaluate( intervalFraction, prevKeyframe, nextKeyframe); setButtonLoc(nextPoint); break; } prevKeyframe = nextKeyframe; } } } }); 

Y eso es !!!.

Por supuesto que modifiqué otras classs también, pero no se agregó nada grande. Por ejemplo, en PathPoint , agregué esto:

 float mWeight; float mFraction; public void setWeight(float weight) { mWeight = weight; } public float getWeight() { return mWeight; } public void setFraction(float fraction) { mFraction = fraction; } public float getFraction() { return mFraction; } 

En AnimatorPath he modificado el método getPoints() como este:

 public Collection<PathPoint> getPoints() { // calculate fractions float totalWeight = 0.0F; for (PathPoint p : mPoints) { totalWeight += p.getWeight(); } float lastWeight = 0F; for (PathPoint p : mPoints) { p.setFraction(lastWeight = lastWeight + p.getWeight() / totalWeight); } return mPoints; } 

Y eso es casi todo. Ah, y para una mejor legibilidad, agregué Patrón de AnimatorPath en AnimatorPath , así que todos los 3 methods se cambiaron así:

 public PathPoint moveTo(float x, float y) {// same for lineTo and curveTo method PathPoint p = PathPoint.moveTo(x, y); mPoints.add(p); return p; } 

NOTA: para manejar Interpolator s que pueden dar una fracción menor que 0 o mayor que 1 (por ejemplo, AnticipateOvershootInterpolator ), observe el com.nineoldandroids.animation.KeyframeSet.getValue(float fraction) e implemente la lógica en onAnimationUpdate(ValueAnimator animation) .