2013-09-29 5 views
3

Я пытаюсь анимировать некоторые drawables в Android, я установил путь, используя PathEvaluator, который анимирует вдоль некоторых кривых вдоль полного пути.Анимационный эквивалент для Android для iOS «расчетMode»

Когда я устанавливаю duration (например, 6 секунд), он разбивает продолжительность на количество кривых, которые я установил, независимо от их длины, что заставляет анимацию замедляться на некоторых сегментах и ​​слишком быстро на других.

На iOS это может быть исправлено с помощью

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

, которая позволяет IOS сгладить весь свой путь в средние точки и охватывают продолжительность в зависимости от каждой длины сегмента. Есть ли способ получить тот же результат в Android?

(помимо того, что он разбивает путь на дискретные сегменты и назначает каждому сегменту свою собственную продолжительность вручную, что является действительно уродливым и неподъемным).

ответ

1

Я попытался использовать ответ Гила, но это не соответствовало тому, как я анимировал. Gil написал Animation класс, который используется для анимации View s. Я использовал ObjectAnimator.ofObject() для анимации пользовательских классов с использованием ValueProperties, которые не могут использоваться с пользовательскими Animation.

Так вот что я сделал:

  1. Выражаю PathEvaluator и переопределить его метод оценки.
  2. Я использую логику Гила рассчитать путь общей длину, и сегментированные длины
  3. Поскольку PathEvaluator.evaluate вызывается для каждого PathPoint с t значений 0..1, мне нужно, чтобы нормализовать интерполированное время, отпущенное мне, так это будет быть инкрементальным и не будет нулевым для каждого сегмента.
  4. Я игнорирую начальное/конечное значение PathPoint s, так что текущее положение может быть перед запуском или после конца по пути в зависимости от продолжительности сегмента.
  5. Пропускаю текущий ход, рассчитанный на мой super (PathEvaluator), чтобы рассчитать фактическое положение.

Это код:

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 ignoredStartPoint, 
      PathPoint ignoredEndPoint) { 

     float index = getStartIndexOfPoint(ignoredStartPoint); 
     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; 
    } 

} 

Это использование:

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

Я не думаю, что с ObjectAnimator можно что-либо сделать, потому что, как представляется, нет функции, которая может быть вызвана для утверждения относительной продолжительности определенного фрагмента анимации.

Я разработал нечто похожее на то, что вам нужно некоторое время назад, но оно работает несколько иначе - оно наследуется от анимации.

Я изменил все, чтобы работать с вашими изгибающими потребностями, и с классом PathPoint.

Вот обзор:

  1. Я поставляю список точек для анимации в конструкторе.

  2. Я вычислил длину между всеми точками, используя простой калькулятор расстояния. Затем я суммирую все, чтобы получить общую длину пути, и сохраните длины сегментов в карте для будущего использования (это для повышения эффективности во время выполнения).

  3. При анимации я использую текущее интерполяционное время, чтобы выяснить, какие 2 очка я оживляю между ними, учитывая отношение времени & отношение пройденного расстояния.

  4. Я подсчитываю время, необходимое для анимации между этими двумя точками в соответствии с относительным расстоянием между ними по сравнению с общим расстоянием.

  5. Затем я интерполирую отдельно между этими двумя точками, используя вычисления в классе PathAnimator.

Вот код:

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

} 

Вот пример, который показывает, как активировать анимацию:

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

Теперь, имейте в виду, что В настоящее время я вычисляю длину кривой в соответствии с приближением.Это, очевидно, вызовет некоторые неточности в скорости. Если вы считаете, что это недостаточно точно, не стесняйтесь изменять код. Кроме того, если вы хотите увеличить точность длины кривой, попробуйте уменьшить значение BEZIER_LENGTH_ACCURACY. Он должен быть делимым на 1, поэтому принятые значения могут быть 0,001, 0,000025 и т. Д.

Хотя вы можете заметить небольшие колебания скорости при использовании кривых, я уверен, что это намного лучше, чем просто делить время поровну между всеми пути.

Я надеюсь, что это помогает :)

+0

Это отличная идея, я буду смотреть в Безье длины кривой известково и попытаться и улучшить его , Логика была тем, что я хотел, но код не соответствовал моим требованиям, так как я не анимировал представление, но использовал ObjectAnimator.ofObject. Я вознагражу вас щедростью и добавлю код, который я написал, который основан на вашем ответе в качестве принятого ответа для других пользователей. Спасибо огромное! – marmor

+0

приветствует человека! рад, что я мог бы помочь :) –

1

UPDATE: Я просто понял, что я мог бы заново колесо, пожалуйста, посмотрите на Specifying Keyframes.


Отвратительно видеть, что ничего подобного не существует. В любом случае, если вы не хотите вычислять длину пути во время выполнения, тогда я смог добавить функциональность назначения весов для путей. Идея состоит в том, чтобы назначить вес вашему пути и запустить анимацию, если он чувствует себя хорошо, тогда хорошо и хорошо, иначе просто уменьшите или увеличьте вес, назначенный каждому пути.

После кода модифицированный код official Android sample, который вы указали в своем вопросе:

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

И это все !!!.

Конечно, я изменил и другие классы, но ничего не было добавлено. Например. в PathPoint я добавил это:

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

В AnimatorPath я изменил getPoints() метод, как это:

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

и то в значительной степени это. Да, и для удобства чтения я добавил Builder шаблон в AnimatorPath, так что все 3 метода были изменены следующим образом:

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

ПРИМЕЧАНИЕ: Для обработки Interpolator сек, что может дать фракции менее 0 или больше, чем 1 (например, AnticipateOvershootInterpolator) посмотрите на метод com.nineoldandroids.animation.KeyframeSet.getValue(float fraction) и реализуйте логику в onAnimationUpdate(ValueAnimator animation).

+0

Это потрясающая идея, но я предпочел ответ Гила, поскольку хочу иметь универсальное возможное решение для добавления ко многим местам в моем приложении (без угадывания весов всякий раз, когда я изменяю путь). Но это быстрый и легкий подход. Пальцы вверх. – marmor

Смежные вопросы