2

我有一个自定义按钮,它是我自己的UIButton. 它看起来像一个带圆圈的箭头,从某个角度开始,在某个角度startAngle结束endAngle=startAngle+1.5*M_PIstartAngle是按钮的属性,然后在其drawRect:方法中使用。当按下这个按钮时,我想让这个箭头围绕它的中心连续旋转 2Pi。所以我认为我可以轻松使用[UIView beginAnimations: context:],但显然它不能使用,因为它不允许为自定义属性设置动画。CoreAnimation 也不适合,因为它只为CALayer属性设置动画。

那么UIView在iOS中实现子类的自定义属性动画的最简单方法是什么?或者也许我错过了一些东西,并且可以使用已经提到的技术?

谢谢你。

4

3 回答 3

2

感谢Jenox,我使用 CADisplayLink 更新了动画代码,这似乎是比 NSTimer 更正确的解决方案。所以我现在用 CADisplayLink 展示正确的实现。它与前一个非常接近,但更简单一些。

我们将 QuartzCore 框架添加到我们的项目中。然后我们将以下几行放在我们类的头文件中:

CADisplayLink* timer;
Float32 animationDuration;  // Duration of one animation loop
Float32 initAngle;  // Represents the initial angle 
Float32 angle;  // Angle used for drawing
CFTimeInterval startFrame;  // Timestamp of the animation start

-(void)startAnimation;  // Method to start the animation
-(void)animate;  // Method for updating the property or stopping the animation

现在在实现文件中,我们设置动画持续时间的值和其他初始值:

initAngle=0.75*M_PI;
angle=initAngle;
animationDuration=1.5f;  // Arrow makes full 360° in 1.5 seconds
startFrame=0;  // The animation hasn't been played yet

要启动动画,我们需要创建 CADisplayLink 实例,该实例将调用方法animate并将其添加到应用程序的主 RunLoop 中:

-(void)startAnimation
{
   timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(animate)];
   [timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

此计时器将animate在应用程序的每个 runLoop 中调用方法。

现在来实现我们在每次循环后更新属性的方法:

-(void)animate
{
   if(startFrame==0) {
      startFrame=timer.timestamp;  // Setting timestamp of start of animation to current moment
      return;  // Exiting till the next run loop
   } 
   CFTimeInterval elapsedTime = timer.timestamp-startFrame;  // Time that has elapsed from start of animation
   Float32 timeProgress = elapsedTime/animationDuration;  // Determine the fraction of full animation which should be shown
   Float32 animProgress = timingFunction(timeProgress); // The current progress of animation
   angle=initAngle+animProgress*2.f*M_PI;  // Setting angle to new value with added rotation corresponding to current animation progress
   if (timeProgress>=1.f) 
   {  // Stopping animation
      angle=initAngle; // Setting angle to initial value to exclude rounding effects
      [timer invalidate];  // Stopping the timer
      startFrame=0;  // Resetting time of start of animation
   }
   [self setNeedsDisplay];  // Redrawing with updated angle value
}

因此,与使用 NSTimer 的情况不同,我们现在不需要计算更新角度属性和重绘按钮的时间间隔。我们现在只需要计算从动画开始已经过去了多少时间,并将属性设置为与此进度相对应的值。

我必须承认,动画比 NSTimer 更流畅。默认情况下,CADisplayLink 在animate每个运行循环中调用该方法。当我计算帧速率时,它是 120 fps。我认为它的效率不是很高,所以我通过在将 CADisplayLink 的 frameInterval 属性添加到 mainRunLoop 之前将其降低到 22 fps:

timer.frameInterval=3;

这意味着它将animate在第一次运行循环时调用该方法,然后在接下来的 3 次循环中什么也不做,并在第 4 次调用,依此类推。这就是为什么 frameInterval 只能是整数。

于 2012-04-14T19:09:22.963 回答
1

再次感谢k06a建议使用计时器。我已经对使用 NSTimer 进行了一些研究,现在我想展示我的实现,因为我认为它对其他人有用。

所以在我的例子中,我有一个 UIButton 子类,它正在绘制一个从某个角度开始的弯曲箭头,Float32 angle;这是整个箭头绘制开始的主要属性。这意味着只需更改角度的值就会旋转整个箭头。所以为了制作这个旋转的动画,我在我的类的头文件中加入了以下几行:

NSTimer* timer;
Float32 animationDuration;  // Duration of one animation loop
Float32 animationFrameRate;  // Frames per second
Float32 initAngle;  // Represents the initial angle 
Float32 angle;  // Angle used for drawing
UInt8 nFrames;  // Number of played frames

-(void)startAnimation;  // Method to start the animation
-(void)animate:(NSTimer*) timer;  // Method for drawing one animation step and stopping the animation

现在在实现文件中,我设置动画的持续时间和帧速率以及绘制的初始角度:

initAngle=0.75*M_PI;
angle=initAngle;
animationDuration=1.5f;  // Arrow makes full 360° in 1.5 seconds
animationFrameRate=15.f;  // Frame rate will be 15 frames per second
nFrames=0;  // The animation hasn't been played yet

要开始动画,我们需要创建 NSTimer 实例,该实例将animate:(NSTimer*) timer每 1/15 秒调用一次方法:

-(void)startAnimation
{
   timer = [NSTimer scheduledTimerWithTimeInterval:1.f/animationFrameRate target:self selector:@selector(animate:) userInfo:nil repeats:YES];
}

此计时器将animate:立即调用方法,然后每 1/15 秒重复一次,直到手动停止。

所以现在来实现我们为单个步骤设置动画的方法:

-(void)animate:(NSTimer *)timer
{
   nFrames++;  // Incrementing number of played frames
   Float32 animProgress = nFrames/(animationDuration*animationFrameRate); // The current progress of animation
   angle=initAngle+animProgress*2.f*M_PI;  // Setting angle to new value with added rotation corresponding to current animation progress
   if (animProgress>=1.f) 
   {  // Stopping animation when progress is >= 1
      angle=initAngle; // Setting angle to initial value to exclude rounding effects
      [timer invalidate];  // Stopping the timer
      nFrames=0;  // Resetting counter of played frames for being able to play animation again
   }
   [self setNeedsDisplay];  // Redrawing with updated angle value
}

我要提到的第一件事是,对我来说,angle==initAngle由于舍入效应,比较线不起作用。完全旋转后它们并不完全相同。这就是为什么我检查它们是否足够接近,然后将角度值设置为初始值,以在多次重复动画循环后阻止角度值的小漂移。为了完全正确,这段代码还必须管理角度的转换,使其始终介于 0 和 2*M_PI 之间,如下所示:

angle=normalizedAngle(initAngle+animProgress*2.f*M_PI);

在哪里

Float32 normalizedAngle(Float32 angle)
{
   while(angle>2.f*M_PI) angle-=2.f*M_PI;
   while(angle<0.f) angle+=2.f*M_PI;
   return angle
}

另一个重要的事情是,不幸的是,我不知道任何简单的方法可以将easeIn、easeOut 或其他默认动画曲线应用于这种手动动画。我认为它不存在。但是,当然,可以手动完成。代表该计时功能的线是

Float32 animProgress = nFrames/(animationDuration*animationFrameRate);

它可以被视为Float32 y = x;,即线性行为,恒定速度,与时间速度相同。但是您可以将其修改为y = cos(x)or y = sqrt(x)or y = pow(x,3.f),这将产生一些非线性行为。考虑到 x 将从 0(动画开始)变为 1(动画结束),您可以自己考虑。

为了更好看的代码,最好做一些独立的计时功能:

Float32 animationCurve(Float32 x)
{
  return sin(x*0.5*M_PI);
}

但是现在,由于动画进度和时间之间的依赖关系不是线性的,所以使用时间作为停止动画的指标会更安全。(例如,您可能希望使箭头旋转 1.5 圈,然后旋转回初始角度,这意味着您的 animProgress 将从 0 变为 1.5,而不是返回 1,而 timeProgress 将从 0 变为 1。)所以为了安全起见,我们现在将时间进度和动画进度分开:

Float32 timeProgress = nFrames/(animationDuration*animationFrameRate);
Float32 animProgress = animationCurve(timeProgress);

然后检查时间进度以确定动画是否应该停止:

if(timeProgress>=1.f)
{
   // Stop the animation
}

顺便说一句,如果有人知道一些具有有用的动画计时功能列表的资源,如果您分享它们,我将不胜感激。

内置 MacOS X 实用程序 Grapher 在可视化功能方面有很大帮助,因此您可以看到动画进度将如何取决于时间进度。

希望它可以帮助某人...

于 2012-04-13T16:26:17.123 回答
0
- (void)onImageAction:(id)sender
{
    UIButton *iconButton = (UIButton *)sender;
    if(isExpand)
{
    isExpand = FALSE;

    // With Concurrent Block Programming:
    [UIView animateWithDuration:0.4 animations:^{
        [iconButton setFrame:[[btnFrameList objectAtIndex:iconButton.tag] CGRectValue]];
    } completion: ^(BOOL finished) {
        [self animationDidStop:@"Expand" finished:YES context:nil];
    }];
}
else
{
    isExpand = TRUE;

    [UIView animateWithDuration:0.4 animations:^{
        [iconButton setFrame:CGRectMake(30,02, 225, 205)];
    }];

    for(UIButton *button in [viewThumb subviews])
    {
        [button setUserInteractionEnabled:FALSE];
        //[button setHidden:TRUE];
    }
    [viewThumb bringSubviewToFront:iconButton];

    [iconButton setUserInteractionEnabled:TRUE];
   // [iconButton setHidden:FALSE];
    }
}
于 2013-06-13T13:18:35.927 回答