UIImageView 的动画旋转变化

Posted

技术标签:

【中文标题】UIImageView 的动画旋转变化【英文标题】:Animating rotation changes of UIImageView 【发布时间】:2014-01-13 23:19:38 【问题描述】:

我正在制作一个应用程序,该应用程序(除其他外)显示一个简化的指南针图像,该图像根据设备的旋转而旋转。问题是简单地这样做:

float heading = -1.0f * M_PI * trueHeading / 180.0f; //trueHeading is always between 0 and 359, never 360
self.compassNeedle.transform = CGAffineTransformMakeRotation(heading);

在 CLLocationManager 的 didUpdateHeading 方法中,动画变得丑陋且不稳定。 我已经使用 Instruments 来确定它是否只是我的应用程序无法以超过 30-48 fps 的速度渲染,但事实并非如此。

如何平滑图像视图的旋转,使其更像 Apple 自己的 Compass 应用程序?

【问题讨论】:

【参考方案1】:

不要使用当前的即时值,而是尝试使用最后 N 个值的平均值作为真实航向。该值可能会在一瞬间大幅上涨,但“平均而言”会稳定下来。

假设你有一个成员变量storedReadings,它是一个NSMutableArray:

-(void)addReading(float):newReading

    [storedReadings addObject:[NSNumber numberWithFloat:newReading]];
    while([storedReadings count] > MAX_READINGS)
    
        [storedReadings removeObjectAtIndex:0];
    

那么当你需要平均值时(定时器更新?)

-(float)calcReading

    float result = 0.0f;
    if([storedReadings count] > 0)
    
       foreach(NSNumber* reading in storedReadings)
       
           result += [reading floatValue];
         
       result /= [storedReadings count];
    
    return result;

您可以先验地选择 MAX_READINGS 个。

下一关

如果读数没有跳动太多,但动画仍然不稳定,您可能需要执行“平滑”旋转之类的操作。在任何给定时间,您都有当前显示的角度,theta(将其存储在您的类中,从 0 开始)。你也有你的目标角度,称之为target。这是您从平滑的 calcReading 函数中获得的值。 错误定义为两者的区别:

error = target-theta;

设置一个时间为 0.05 秒(每秒 20 次)的计时器回调。您要做的是调整theta,使error 趋向0。您可以通过以下几种方式做到这一点:

    thetaNext += kProp * (target - theta); //这是比例反馈。 thetaNext += kStep * sign(target-theta); // 每次更新都会使 theta 移动一个固定的量。如果 x >= 0,则符号(x) = +1,如果 x

第一种解决方案将导致旋转距离目标越远,旋转就会急剧变化。当它摆动超过“零”点时,它也可能会有点振荡。 较大的 kProp 值会产生更快的响应,但也会产生更多的振荡。需要进行一些调整。

第二种解决方案将更容易控制......它每次只是“滴答”罗盘指针。您可以将 kStep 设置为 1/4 度,这为您提供大约(1/4 度/更新)*(20 次更新/秒)= 5 度/秒的旋转“速度”。这有点慢,但您可以查看数学并更改 kStep 以满足您的需要。 注意,您可以“绑定”“错误”值,以便在错误

为了处理角度问题(环绕),我将角度“标准化”,使其始终在 -Pi/Pi 范围内。我不保证这是做到这一点的完美方式,但它似乎可以完成工作:

   // Takes an angle greater than +/- M_PI and converts it back
   // to +/- M_PI.  Useful in Box2D where angles continuously
   // increase/decrease.
   static inline float32 AdjustAngle(float32 angleRads)
   
      if(angleRads > M_PI)
      
         while(angleRads > M_PI)
         
            angleRads -= 2*M_PI;
         
      
      else if(angleRads < -M_PI)
      
         while(angleRads < -M_PI)
         
            angleRads += 2*M_PI;
         
      
      return angleRads;
   

通过这种方式,-pi 是您在继续向左/向右旋转时从任一方向到达的角度。也就是说,从 0 度到 359 度的数字没有不连续性。

所以把这一切放在一起

static inline float Sign(float value)

   if(value >= 0)
      return 1.0f;
   return -1.0f;


//#define ROTATION_OPTION_1
//#define ROTATION_OPTION_2
#define ROTATION_OPTION_3

-(void)updateArrow

   // Calculate the angle to the player
   CGPoint toPlayer = ccpSub(self.player.position,self.arrow.position);
   // Calculate the angle of this...Note there are some inversions
   // and the actual image is rotated 90 degrees so I had to offset it
   // a bit.
   float angleToPlayerRads = -atan2f(toPlayer.y, toPlayer.x);
   angleToPlayerRads = AdjustAngle(angleToPlayerRads);

   // This is the angle we "wish" the arrow would be pointing.
   float targetAngle = CC_RADIANS_TO_DEGREES(angleToPlayerRads)+90;
   float errorAngle = targetAngle-self.arrow.rotation;

   CCLOG(@"Error Angle = %f",errorAngle);


#ifdef ROTATION_OPTION_1
   // In this option, we just set the angle of the rotated sprite directly.
   self.arrow.rotation = CC_RADIANS_TO_DEGREES(angleToPlayerRads)+90;
#endif


#ifdef ROTATION_OPTION_2
   // In this option, we apply proportional feedback to the angle
   // difference.
   const float kProp = 0.05f;
   self.arrow.rotation += kProp * (errorAngle);
#endif

#ifdef ROTATION_OPTION_3
   // The step to take each update in degrees.
   const float kStep = 4.0f;
   //  NOTE:  Without the "if(fabs(...)) check, the angle
   // can "dither" around the zero point when it is very close.
   if(fabs(errorAngle) > kStep)
   
      self.arrow.rotation += Sign(errorAngle)*kStep;
   
#endif

我将此代码放入我为 Cocos2d 编写的演示程序中。它显示了一个角色(大盒子)被一些怪物(小盒子)追赶,并且中心有一个箭头,它始终指向角色。 updateArrow 调用定期在计时器滴答声(update(dt) 函数)上进行。玩家在屏幕上的位置由用户点击屏幕设置,角度基于从箭头到玩家的矢量。在函数中,我展示了设置箭头角度的所有三个选项:

选项 1

只需根据玩家所在的位置进行设置(即设置即可)。

选项 2

使用比例反馈来调整每个时间步的箭头角度。

选项 3

如果误差角度大于步长,则每个时间步长一点箭头的角度。

这是一张大致显示其外观的图片:

还有,all the code is available here on github。只需查看 HelloWorldLayer.m 文件即可。

这有帮助吗?

【讨论】:

确认,我使用了相同的方法,它产生了可接受的结果 @MichałZygar 总是很高兴知道我不是唯一一个做这种事情的人 ;) 我已经考虑过这一点,但由于感知到实现它的困难而回避了它。但我想没有办法绕过它,所以我不妨这样做。感谢你的回答。 :) 希望对您有所帮助。您也可以随时收集数据(异步调用 addReading),但只在定期(每秒 10 次等)的计时器上调用 calcReading。为了使运动看起来真正平滑,考虑逐步完成点之间的旋转,而不是仅仅设置角度(即,在 S 时间步内逐步通过 N 度朝向新角度,而不是仅仅设置它)。 如果你想真的很邪恶,在角度周围放置一个 PID 控制回路,并给它一个物理模型作为旋转体,这样你就可以对其施加扭矩。但我认为这有点矫枉过正......但至少你知道它就在那里:)。

以上是关于UIImageView 的动画旋转变化的主要内容,如果未能解决你的问题,请参考以下文章

如何旋转 UIImage 或 UIImageView? [复制]

如何在 UIImageView 中居中缩放(旋转)UIImage

旋转 UIImageView 数据 -

UIImage 从一个 UIImageView 移动到另一个 UIImageView 的动画

当动画 UIImage 被分配两次时,UITableViewCell 中的 UIImageView 没有动画

UIImageview 旋转动画