【问题标题】:Android animation equivalent for iOS "calculationMode"iOS“calculationMode”的Android动画等效
【发布时间】:2013-09-29 09:49:20
【问题描述】:

我正在尝试为Android 中的一些drawables 设置动画,我已经使用PathEvaluator 设置了一条路径,该路径沿着完整路径的一些曲线设置动画。

当我设置 duration(例如 6 秒)时,它会将持续时间拆分为我设置的曲线数量,而不管它们的长度如何,这会导致动画在某些片段上变慢而在其他片段上过快。

iOS 上可以使用

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

这让 iOS 可以将您的整个路径平滑到中间点,并根据每个段的长度跨越持续时间。 有没有办法在 Android 中获得相同的结果?

(除了将路径分成离散的段并手动为每个段分配自己的持续时间,这真的很难看且无法维护)。

【问题讨论】:

    标签: android ios animation android-animation bezier


    【解决方案1】:

    我不认为 ObjectAnimator 可以做任何事情,因为似乎没有可以调用的函数来断言动画某个片段的相对持续时间。

    我确实开发了一些类似于您不久前需要的东西,但它的工作方式略有不同 - 它继承自 Animation。

    我已根据您的弯曲需求和 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 等。

    虽然您在使用曲线时可能会注意到速度的一些轻微波动,但我相信这比简单地在所有路径之间平均分配时间要好得多。

    我希望这会有所帮助:)

    【讨论】:

    • 这是个好主意,我会研究贝塞尔曲线长度计算并尝试改进它。逻辑是我想要的,但代码不符合我的要求,因为我不是为视图设置动画,而是使用 ObjectAnimator.ofObject。我将奖励您并添加我编写的基于您的答案的代码作为其他用户使用的可接受答案。非常感谢!
    【解决方案2】:

    我尝试使用 Gil 的答案,但它不适合我制作动画的方式。 Gil 编写了一个 Animation 类,用于为 Views 设置动画。 我正在使用ObjectAnimator.ofObject() 为使用ValueProperties 的自定义类设置动画,这不能与自定义Animation 一起使用。

    这就是我所做的:

    1. 我扩展了PathEvaluator 并覆盖了它的评估方法。
    2. 我使用 Gil 的逻辑来计算路径总长度和分段长度
    3. 因为 PathEvaluator.evaluate 为每个带有 t 值的 PathPoint 调用 0..1,我需要标准化给我的插值时间,所以它将是增量的,并且不会为每个段归零。
    4. 我忽略了给我的开始/结束PathPoints 所以当前位置可以是 根据段的持续时间,在开始之前或结束之后沿着路径。
    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());
    

    【讨论】:

      【解决方案3】:

      更新:我刚刚意识到我可能重新发明了轮子,请查看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;
      }
      

      差不多就是这样。哦,为了更好的可读性,我在AnimatorPath 中添加了 Builder Pattern,因此所有 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;
      }
      

      注意:要处理可以给出小于 0 或大于 1 的分数的 Interpolators(例如 AnticipateOvershootInterpolator),请查看 com.nineoldandroids.animation.KeyframeSet.getValue(float fraction) 方法并实现 onAnimationUpdate(ValueAnimator animation) 中的逻辑。

      【讨论】:

      • 这是一个很棒的想法,但我更喜欢 Gil 的回答,因为我希望有一个尽可能通用的解决方案来添加到我的应用程序中的许多地方(每次修改路径时都不必猜测权重)。但这无论如何都是一种快速简便的方法。竖起大拇指。
      猜你喜欢
      • 2023-03-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-03
      • 2011-10-29
      • 2018-04-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多