【问题标题】:Android - how to make RotateAnimation more smooth and "physical"?Android - 如何让 RotateAnimation 更流畅和“物理”?
【发布时间】:2014-03-29 00:52:25
【问题描述】:

我正在实现一种“指南针箭头”,它根据使用磁场传感器的设备的物理方向跟踪目的地。突然间我遇到了一个小问题。

获得方位角和方位角是可以的,但是执行逼真的动画变成了一项非常艰巨的任务。我尝试使用不同的插值器来使动画更加“物理”(即在真实的指南针中,箭头在发夹旋转后摆动,在运动过程中加速和减速等)。

现在我正在使用interpolator.accelerate_decelerate,一切都很好,直到更新开始迅速到来。这使得动画相互重叠,箭头变得抽搐和紧张。我想避免这种情况。我尝试实现一个队列,以使每个下一个动画都等到前一个结束,或者丢弃很快到来的更新。这使动画看起来很流畅,但箭头的行为变得完全不合逻辑。

所以我有 2 个问题:

1) 有没有办法在动画相互重叠的情况下使动画过渡更加平滑

2) 有没有办法停止当前正在处理的动画并获取对象的中间位置?

我的代码如下。 UpdateRotation() 方法处理方向和方位更新,并执行外部 viewArrow 视图的动画。

public class DirectionArrow {

// View that represents the arrow
final View viewArrow;

// speed of rotation of the arrow, degrees/sec
final double rotationSpeed;

// current values of bearing and azimuth
float bearingCurrent = 0;
float azimuthCurrent = 0;


/*******************************************************************************/

/**
 * Basic constructor
 * 
 * @param   view            View representing an arrow that should be rotated
 * @param   rotationSpeed   Speed of rotation in deg/sec. Recommended from 50 (slow) to 500 (fast)
 */
public DirectionArrow(View view, double rotationSpeed) {
    this.viewArrow = view;
    this.rotationSpeed = rotationSpeed;
}

/**
 * Extended constructor
 * 
 * @param   viewArrow       View representing an arrow that should be rotated
 * @param   rotationSpeed   Speed of rotation in deg/sec. Recommended from 50 (slow) to 500 (fast)
 * @param   bearing         Initial bearing 
 * @param   azimuth         Initial azimuth
 */
public DirectionArrow(View viewArrow, double rotationSpeed, float bearing, float azimuth){
    this.viewArrow = viewArrow;
    this.rotationSpeed = rotationSpeed;
    UpdateRotation(bearing, azimuth);
}

/**
 * Invoke this to update orientation and animate the arrow
 * 
 * @param   bearingNew  New bearing value, set >180 or <-180 if you don't need to update it 
 * @param   azimuthNew  New azimuth value, set >360 or <0 if you don't need to update it
 */
public void UpdateRotation(float bearingNew, float azimuthNew){

    // look if any parameter shouldn't be updated
    if (bearingNew < -180 || bearingNew > 180){
        bearingNew = bearingCurrent;
    }
    if (azimuthNew < 0 || azimuthNew > 360){
        azimuthNew = azimuthCurrent;
    }

    // log
    Log.println(Log.DEBUG, "compass", "Setting rotation: B=" + bearingNew + " A=" + azimuthNew);

    // calculate rotation value
    float rotationFrom = bearingCurrent - azimuthCurrent;
    float rotationTo = bearingNew - azimuthNew;

    // correct rotation angles
    if (rotationFrom < -180) {
        rotationFrom += 360;
    }
    while (rotationTo - rotationFrom < -180) {
        rotationTo += 360;
    }
    while (rotationTo - rotationFrom > 180) {
        rotationTo -= 360;
    }

    // log again
    Log.println(Log.DEBUG, "compass", "Start Rotation to " + rotationTo);

    // create an animation object
    RotateAnimation rotateAnimation = new RotateAnimation(rotationFrom, rotationTo, 
            Animation.RELATIVE_TO_SELF, (float) 0.5, Animation.RELATIVE_TO_SELF, (float) 0.5);

    // set up an interpolator
    rotateAnimation.setInterpolator(viewArrow.getContext(), interpolator.accelerate_decelerate);

    // force view to remember its position after animation
    rotateAnimation.setFillAfter(true);

    // set duration depending on speed
    rotateAnimation.setDuration((long) (Math.abs(rotationFrom - rotationTo) / rotationSpeed * 1000));

    // start animation
    viewArrow.startAnimation(rotateAnimation);

    // update cureent rotation
    bearingCurrent = bearingNew;
    azimuthCurrent = azimuthNew;
}
}

【问题讨论】:

    标签: android animation rotation android-sensors


    【解决方案1】:

    这是我的自定义 ImageDraw 类,我根据磁场中偶极子的圆周运动方程实现了指向箭头的物理行为。

    它不使用任何动画器或插值器——在每次迭代时,角度位置都会根据物理参数重新计算。这些参数可以通过setPhysical方法进行广泛的调整。例如,要使旋转更平稳和缓慢,增加alpha(阻尼系数),使箭头更灵敏,增加mB(磁场系数),使箭头在旋转时振荡,增加inertiaMoment

    动画和重绘是通过在每次迭代中调用invalidate() 隐式执行的。无需显式处理。

    要更新箭头应旋转的角度,只需调用rotationUpdate(由用户选择或使用设备方向传感器回调)。

    /**
     * Class CompassView extends Android ImageView to perform cool, real-life animation of objects
     * such compass needle in magnetic field. Rotation is performed relative to the center of image.
     * 
     * It uses angular motion equation of magnetic dipole in magnetic field to implement such animation.
     * To vary behaviour (damping, oscillation, responsiveness and so on) set various physical properties.
     * 
     * Use `setPhysical()` to vary physical properties.
     * Use `rotationUpdate()` to change angle of "magnetic field" at which image should rotate.
     *
     */
    
    public class CompassView extends ImageView {
    
    static final public float TIME_DELTA_THRESHOLD = 0.25f; // maximum time difference between iterations, s 
    static final public float ANGLE_DELTA_THRESHOLD = 0.1f; // minimum rotation change to be redrawn, deg
    
    static final public float INERTIA_MOMENT_DEFAULT = 0.1f;    // default physical properties
    static final public float ALPHA_DEFAULT = 10;
    static final public float MB_DEFAULT = 1000;
    
    long time1, time2;              // timestamps of previous iterations--used in numerical integration
    float angle1, angle2, angle0;   // angles of previous iterations
    float angleLastDrawn;           // last drawn anglular position
    boolean animationOn = false;    // if animation should be performed
    
    float inertiaMoment = INERTIA_MOMENT_DEFAULT;   // moment of inertia
    float alpha = ALPHA_DEFAULT;    // damping coefficient
    float mB = MB_DEFAULT;  // magnetic field coefficient
    
    /**
     * Constructor inherited from ImageView
     * 
     * @param context
     */
    public CompassView(Context context) {
        super(context);
    }
    
    /**
     * Constructor inherited from ImageView
     * 
     * @param context
     * @param attrs
     */
    public CompassView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    /**
     * Constructor inherited from ImageView
     * 
     * @param context
     * @param attrs
     * @param defStyle
     */
    public CompassView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    
    /**
     * onDraw override.
     * If animation is "on", view is invalidated after each redraw, 
     * to perform recalculation on every loop of UI redraw
     */
    @Override
    public void onDraw(Canvas canvas){
        if (animationOn){
            if (angleRecalculate(new Date().getTime())){
                this.setRotation(angle1);
            }
        } else {
            this.setRotation(angle1);
        }
        super.onDraw(canvas);
        if (animationOn){
            this.invalidate();
        }
    }
    
    /**
     * Use this to set physical properties. 
     * Negative values will be replaced by default values
     * 
     * @param inertiaMoment Moment of inertia (default 0.1)
     * @param alpha             Damping coefficient (default 10)
     * @param mB                Magnetic field coefficient (default 1000)
     */
    public void setPhysical(float inertiaMoment, float alpha, float mB){
        this.inertiaMoment = inertiaMoment >= 0 ? inertiaMoment : this.INERTIA_MOMENT_DEFAULT;
        this.alpha = alpha >= 0 ? alpha : ALPHA_DEFAULT;
        this.mB = mB >= 0 ? mB : MB_DEFAULT;
    }
    
    
    /**
     * Use this to set new "magnetic field" angle at which image should rotate
     * 
     * @param   angleNew    new magnetic field angle, deg., relative to vertical axis.
     * @param   animate     true, if image shoud rotate using animation, false to set new rotation instantly
     */
    public void rotationUpdate(final float angleNew, final boolean animate){
        if (animate){
            if (Math.abs(angle0 - angleNew) > ANGLE_DELTA_THRESHOLD){
                angle0 = angleNew;
                this.invalidate();
            }
            animationOn = true;
        } else {
            angle1 = angleNew;
            angle2 = angleNew;
            angle0 = angleNew;
            angleLastDrawn = angleNew;
            this.invalidate();
            animationOn = false;
        }
    }
    
    /**
     * Recalculate angles using equation of dipole circular motion
     * 
     * @param   timeNew     timestamp of method invoke
     * @return              if there is a need to redraw rotation
     */
    protected boolean angleRecalculate(final long timeNew){
    
        // recalculate angle using simple numerical integration of motion equation
        float deltaT1 = (timeNew - time1)/1000f;
        if (deltaT1 > TIME_DELTA_THRESHOLD){
            deltaT1 = TIME_DELTA_THRESHOLD;
            time1 = timeNew + Math.round(TIME_DELTA_THRESHOLD * 1000);
        }
        float deltaT2 = (time1 - time2)/1000f;
        if (deltaT2 > TIME_DELTA_THRESHOLD){
            deltaT2 = TIME_DELTA_THRESHOLD;
        }
    
        // circular acceleration coefficient
        float koefI = inertiaMoment / deltaT1 / deltaT2;
    
        // circular velocity coefficient
        float koefAlpha = alpha / deltaT1;
    
        // angular momentum coefficient
        float koefk = mB * (float)(Math.sin(Math.toRadians(angle0))*Math.cos(Math.toRadians(angle1)) - 
                                 (Math.sin(Math.toRadians(angle1))*Math.cos(Math.toRadians(angle0))));
    
        float angleNew = ( koefI*(angle1 * 2f - angle2) + koefAlpha*angle1 + koefk) / (koefI + koefAlpha);
    
        // reassign previous iteration variables
        angle2 = angle1;
        angle1 = angleNew;
        time2 = time1;
        time1 = timeNew;
    
        // if angles changed less then threshold, return false - no need to redraw the view
        if (Math.abs(angleLastDrawn - angle1) < ANGLE_DELTA_THRESHOLD){
            return false;
        } else {
            angleLastDrawn = angle1;
            return true;
        }
    }
    

    【讨论】:

    • 简直太棒了...非常感谢您分享这个!正是我需要而自己无法完成的。
    • 非常感谢!
    • 是的,这很好。
    【解决方案2】:

    您是否在过滤传感器数据?磁力计是一种痛苦的低通滤波,还不够。您可以使用 weighted-smoothing 或者舍入数据会有所帮助: Math.round(xyz * 10) / 10; ? 您还可以降低获取传感器更新的频率。这可能会有所帮助。

    mSensorManager.registerListener(this, mMagnetometer, 10000);
    

    【讨论】:

    • 谢谢,我会尝试使用平滑功能逐帧旋转来替换动画。这是一个很好的方法。
    • 运气好吗?我很想看看你的解决方案。 :D
    • 我想我会尝试将指数平滑作为第一种方法,然后将振荡器方程作为第二种方法,看看看起来更好更真实。我还没有找到任何方法来使用 Android 动画来实现它,而且我要注意,没有办法。
    • 我想我以不同的方式处理它。我尝试平滑数据并让动画完成它的工作。你不能只定义一个自定义视图并使用传感器数据移动它吗?
    • 我的应用程序中有 2 个角度,好吧,让我们考虑 azimuth 会根据传感器数据而变化,并且重复其物理运动会非常顺利。那么如何处理bearing 角度,由于用户的选择可能会间歇性地变化。这就是我无法避免重叠动画的原因。
    【解决方案3】:

    特别是对于 gilonm,固定大小队列的良好实现并获得其平均值:

    float queue[ARRAY_LENGTH] = {0};
    int queueFront = queue.length - 1 // position of front element
    float meanValue = 0; // calculated mean value
    
    float pushNewAndGetMean(float newValue){
       // recalculate mean value
       meanValue = meanValue + (newValue - queue[queueFront]) / queue.length;
       // overwrite value in front pointer position
       queue[queueFront] = newValue;
       // shift front pointer 1 step right or to '0' if end of array reached
       queueStart = (queueFront + 1) % array.length;
    
       return  meanValue
    };   
    

    在这里,不依赖于数组长度,您只需对变量进行 2 次重新分配(而不是 N),并且在均值计算中仅使用 3 个元素(而不是 N)。这使得算法复杂度为 O(1) 而不是 O(N)。

    【讨论】:

    • 谢谢!我将尝试实现这一点。感谢您的帮助!
    • 顺便说一句,如果我使用我提出的方法 - 数组中的元素越多 - 我得到的平滑度就越高(而且精度越低)。有没有办法使用你的方法来改变平滑精度的比率?
    • 更流畅并不意味着更不准确。由于中心极限定理,当测量次数增加时精度会增加。顺便说一句,它仅在指南针处于稳定位置的情况下起作用。移动时,每一种插值都会造成时间滞后——因为你无法预测下一刻它会如何移动
    【解决方案4】:

    您可以做的是从传感器获取数据的位置 - 您可以使用和排列来平均说最后 5 个读数 - 这应该会使事情顺利进行。

    类似这样的:

    声明一个数组private float azimArray[] = {0,0,0,0,0};

    现在您可以在哪里获取传感器数据,使用:

    azimArray[0] = azimArray[1]; azimArray[1] = azimArray[2]; azimArray[2] = azimArray[3]; azimArray[3] = azimArray[4]; azimArray[4] = event.values[0]; //get actual sensor data into last array cell currentAzimuth = Math.round(azimArray[0]+azimArray[1]+azimArray[2]+azimArray[3]+azimArray[4]/5);

    现在 currentAzimuth 保存最后 5 个读数的四舍五入平均值,这应该可以为您平滑。

    希望这有帮助!

    【讨论】:

    • thanx,但我的问题不是关于从传感器获取数据,而是关于当一个接一个并重叠动画时平滑动画的可能性。而且,抱歉,您的更新和数组方法及其平均值不正确且效率低下 - 您可以在 O(n) 时间内完成,而在 O(1) 时间内可以轻松完成
    • 您好,我知道您正在寻找的是平滑。我写给你的内容实际上对我来说非常有用,可以平滑罗盘动画。我使用了一个 Array[7],动画持续时间设置为 500 毫秒,并且传感器更新到 UI。 1)如果您能解释您评论中关于它效率低下的最后一部分,我将不胜感激 2)您找到任何解决方案了吗?谢谢!
    • 是的,我成功解决了我的问题。在这里查看我的答案,了解我的解决方案和计算固定大小队列平均值的正确算法。
    猜你喜欢
    • 1970-01-01
    • 2012-02-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-12
    • 2018-09-12
    • 2011-09-20
    相关资源
    最近更新 更多