简体   繁体   中英

Animating rotation changes of UIImageView

I'm making an app that (among other things) displays a simplified compass image that rotates according to the device's rotation. The problem is that simply doing this:

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

inside CLLocationManager's didUpdateHeading method makes the animation ugly and choppy. I have already used Instruments to find out whether its simply my app not being able to render at more than 30-48 fps, but that's not the case.

How can I smooth out the image view's rotation so that it's more like Apple's own Compass app?

Instead of using the current instant value, try using the average of the last N values for the true heading. The value may be jumping around a lot in a single instant but settle down "in the average".

Assuming you have a member variable storedReadings which is an NSMutableArray:

-(void)addReading(float):newReading
{
    [storedReadings addObject:[NSNumber numberWithFloat:newReading]];
    while([storedReadings count] > MAX_READINGS)
    {
        [storedReadings removeObjectAtIndex:0];
    }
}

then when you need the average value (timer update?)

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

You get to pick MAX_READINGS a priori.

NEXT LEVEL(S) UP

If the readings are not jumping around so much but the animation is still choppy, you probably need to do something like a "smooth" rotation. At any given time, you have the current angle you are displaying, theta (store this in your class, start it out at 0). You also have your target angle, call it target . This is the value you get from the smoothed calcReading function. The error is defined as the difference between the two:

error = target-theta;

Set up a timer callback with a period of something like 0.05 seconds (20x per second). What you want to do is adjust theta so that the error is driven towards 0. You can do this in a couple of ways:

  1. thetaNext += kProp * (target - theta); //This is proportional feedback.
  2. thetaNext += kStep * sign(target-theta); // This moves theta a fixed amount each update. sign(x) = +1 if x >= 0 and -1 if x < 0.

The first solution will cause the rotation to change sharply the further it is from the target. It will also probably oscillate a little bit as it swings past the "zero" point. Bigger values of kProp will yield faster response but also more oscillation. Some tuning will be required.

The second solution will be much easier to control...it just "ticks" the compass needle around each time. You can set kStep to something like 1/4 degree, which gives you a "speed" of rotation of about (1/4 deg/update) * (20 updates/seconds) = 5 degrees per second. This is a bit slow, but you can see the math and change kStep to suit your needs. Note that you may to "band" the "error" value so that no action is taken if the error < kStep (or something like that). This prevents your compass from shifting when the angle is really close to the target. You can change kStep when the error is small so that it "slides" into the ending position (ie kStep is smaller when the error is small).

For dealing with Angle Issues (wrap around), I "normalize" the angle so it is always within -Pi/Pi. I don't guarantee this is the perfect way to do it, but it seems to get the job done:

   // 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;
   }

By doing it this way, -pi is the angle you reach from going in either direction as you continue to rotate left/right. That is to say, there is not a discontinuity in the number going from say 0 to 359 degrees.

SO PUTTING THIS ALL TOGETHER

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
}

I put this code into a demo program I had written for Cocos2d. It shows a character (big box) being chased by some monsters (smaller boxes) and has an arrow in the center that always points towards the character. The updateArrow call is made on a timer tick (the update(dt) function) regularly. The player's position on the screen is set by the user tapping on the screen and the angle is based on the vector from the arrow to the player. In the function, I show all three options for setting the angle of the arrow:

Option 1

Just set it based on where the player is (ie just set it).

Option 2

Use proportional feedback to adjust the arrow's angle each time step.

Option 3

Step the angle of the arrow each timestep a little bit if the error angle is more than the step size.

Here is a picture showing roughly what it looks like:

在此输入图像描述

And, all the code is available here on github . Just look in the HelloWorldLayer.m file.

Was this helpful?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM