Android - 如何让RotateAnimation更加流畅和"物理"?

Ale*_*you 1 animation android rotation android-sensors

我正在使用磁场传感器实现一种跟随目的地的"罗盘箭头",具体取决于设备的物理方向.突然间我遇到了一个小问题.

获得方位和方位角是可以的,但是执行逼真的动画变成了一项非常艰巨的任务.我尝试使用不同的内插器使动画更"物理"(即在真实的罗盘中,箭头在发夹旋转后振荡,在运动期间加速和减速等).

现在我正在使用interpolator.accelerate_decelerate,一切都很好,直到更新快速到达.这使得动画彼此重叠,箭头变得抽搐和紧张.我想避免这种情况.我试图实现一个队列,使每个下一个动画等到上一个结束,或者删除非常快的更新.这使动画看起来很流畅,但箭头的行为变成了绝对不合逻辑的.

所以我有两个问题:

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;
}
}
Run Code Online (Sandbox Code Playgroud)

Ale*_*you 9

这是我的自定义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;
    }
}
Run Code Online (Sandbox Code Playgroud)