4

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

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

iOS这可以使用固定

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

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

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

4

3 回答 3

2

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

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

我已经修改了所有内容以满足您的弯曲需求和 PathPoint 类。

这是一个概述:

  1. 我在构造函数中为动画提供点列表。

  2. 我使用一个简单的距离计算器计算所有点之间的长度。然后我将其全部汇总以获得路径的总长度,并将段长度存储在地图中以供将来使用(这是为了提高运行时的效率)。

  3. 制作动画时,我使用当前插值时间来确定我在哪两个点之间制作动画,同时考虑时间的比率和行进距离的比率。

  4. 我根据它们之间的相对距离计算在这两个点之间制作动画所需的时间,与总距离相比。

  5. 然后,我使用 PathAnimator 类中的计算在这两个点之间分别插值。

这是代码:

曲线动画.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 等。

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

我希望这有帮助 :)

于 2013-10-03T10:59:57.130 回答
2

我尝试使用 Gil 的答案,但它不适合我制作动画的方式。Gil 编写了一个Animation用于动画Views 的类。我曾经使用不能与 custom 一起使用的自ObjectAnimator.ofObject()定义类动画。ValuePropertiesAnimation

所以这就是我所做的:

  1. 我扩展PathEvaluator并覆盖了它的评估方法。
  2. 我使用 Gil 的逻辑来计算路径总长度和分段长度
  3. 由于PathEvaluator.evaluate每个值为 0..1 调用PathPointt我需要标准化给我的插值时间,所以它将是增量的并且不会为每个段归零。
  4. 我忽略了PathPoint给我的 start/end s,因此当前位置可以在路径的 start 之前或 end 之后,具体取决于段的持续时间。
  5. 我将计算出的当前进度传递给 my 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());
于 2013-10-08T14:19:53.667 回答
1

更新:我刚刚意识到我可能重新发明了轮子,请查看指定关键帧


令人震惊的是,没有任何此类可用的东西。无论如何,如果您不想在运行时计算路径长度,那么我可以添加为路径分配权重的功能。想法是为您的路径分配一个权重并运行动画,如果它感觉不错,那么很好,否则只需减少或增加分配给每个路径的权重。

以下代码是您在问题中指出的官方 Android 示例的修改代码:

 // 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 Pattern 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 的分数的 s(例如AnticipateOvershootInterpolator),请查看com.nineoldandroids.animation.KeyframeSet.getValue(float fraction)方法并实现onAnimationUpdate(ValueAnimator animation).

于 2013-10-08T09:23:48.337 回答