如何创建一个可以平稳调整大小的圆形UIView?

10
我正在尝试创建一个UIView,它在其边界内显示一个半透明圆形和不透明的边框。我想能够通过两种方式改变边界 - 在- [UIView animateWithDuration:animations:] 块中和在一次每秒触发多次的捏合手势识别器动作中。基于SO上的其他答案,我尝试了三种方法,但都不适合:
1. 在 layoutSubviews 中设置视图的层的角半径可使平移更加顺滑,但是在动画期间,该视图不会保持圆形;似乎cornerRadius不可动画化。 2. 在 drawRect:中绘制圆可获得始终为圆形的视图,但如果圆变得太大,则在捏合手势中调整大小会变得不流畅,因为设备花费太多时间重新绘制圆。 3. 添加CAShapeLayer并在layoutSublayersOfLayer中设置其路径属性,由于路径不是隐式可动画化的,因此在UIView动画中不会产生动画效果。
有没有方法可以创建一个始终为圆形且可平滑调整大小的视图?是否有其他类型的图层可用于利用硬件加速?
更新: 评论者要求我详细说明当我说我想在- [UIView animateWithDuration:animations:] 块中改变边界时我的意思。在我的代码中,我有一个包含圆形视图的视图。圆形视图(使用cornerRadius的版本)覆盖了 - [setBounds:] 以设置角半径。
-(void)setBounds:(CGRect)bounds
{
    self.layer.cornerRadius = fminf(bounds.size.width, bounds.size.height) / 2.0;
    [super setBounds:bounds];
}

圆形视图的边界在-[layoutSubviews]中设置:

-(void)layoutSubviews
{
    // some other layout is performed and circleRadius and circleCenter are
    // calculated based on the properties and current size of the view.

    self.circleView.bounds = CGRectMake(0, 0, circleRadius*2, circleRadius*2);
    self.circleView.center = circleCenter;
}

在动画中,视图有时会被调整大小,就像这样:

[UIView animateWithDuration:0.33 animations:^(void) {
    myView.frame = CGRectMake(x, y, w, h);
    [myView setNeedsLayout];
    [myView layoutIfNeeded];
}];

但是在这些动画期间,如果我使用具有cornerRadius的图层来绘制圆形视图,它会变成奇怪的形状。我无法将动画持续时间传递到layoutSubviews中,因此我需要在-[setBounds]中添加正确的动画。


@robmayoff 肯定是反过来吧?那个问题已经五个小时了,而这一个是从2012年的。 - Simon
是的,但另一个答案可以在当前的iOS下工作。 - rob mayoff
@robmayoff 那又怎样?重复的是问题,而不是答案。 - Simon
将那个问题作为这个问题的副本关闭并不会帮助人们找到可行的答案。将这个问题作为那个问题的副本关闭则会有所帮助。Stack Overflow 应该帮助人们找到可行的答案。 - rob mayoff
@robmayoff,请在此处发布更新后的答案,而不是在重复的问题中发布。 - Simon
显示剩余7条评论
4个回答

17
作为“iOS视图编程指南”中动画部分所述,UIKit和核心动画均提供了对动画的支持,但是每种技术提供的支持水平不同。在UIKit中,使用UIView对象执行动画。
您可以使用旧版或新版API来执行动画,以下是可以使用任一版本API进行动画的完整属性列表
[UIView beginAnimations:context:];
[UIView setAnimationDuration:];
// Change properties here...
[UIView commitAnimations];

或者更新的

[UIView animateWithDuration:animations:];

你正在使用的属性有:
  • frame
  • bounds
  • center
  • transform(CGAffineTransform,而不是CATransform3D)
  • alpha
  • backgroundColor
  • contentStretch
混淆人们的是,您还可以在UIView动画块中对图层上的相同属性进行动画处理,例如frame,bounds,position,opacity和backgroundColor。
同一部分继续说道:

在需要执行更复杂的动画或UIView类不支持的动画的地方,您可以使用核心动画和视图的基础层来创建动画。因为视图和图层对象紧密地链接在一起,所以对视图的图层进行的更改会影响视图本身。

几行下面,您可以阅读Core Animation可动画化属性列表,其中包括以下内容:
  • 图层的边框(包括图层的圆角)

至少有两种实现你想要的效果的好方法:

  • 动画化圆角
  • 使用CAShapeLayer并动画化路径

这两种方法都需要使用核心动画进行动画。如果你需要多个动画同时运行,你可以创建一个CAAnimationGroup并向其中添加一个动画数组。


更新:

尽可能少地更改代码来修复问题,可以通过在“同一时间”对图层进行角半径动画来完成。 我在“同一时间”周围加上引号,因为不能保证不在同一组的动画会完全同时完成。 根据您正在进行的其他动画,最好只使用基本动画和动画组。如果您正在在同一个视图动画块中对许多不同的视图应用更改,则可以查看CATransactions。

以下代码像您描述的那样动画化了框架和角半径。

UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(30, 30, 100, 100)];
[[circle layer] setCornerRadius:50];
[[circle layer] setBorderColor:[[UIColor orangeColor] CGColor]];
[[circle layer] setBorderWidth:2.0];
[[circle layer] setBackgroundColor:[[[UIColor orangeColor] colorWithAlphaComponent:0.5] CGColor]];
[[self view] addSubview:circle];

CGFloat animationDuration = 4.0; // Your duration
CGFloat animationDelay = 3.0; // Your delay (if any)

CABasicAnimation *cornerRadiusAnimation = [CABasicAnimation animationWithKeyPath:@"cornerRadius"];
[cornerRadiusAnimation setFromValue:[NSNumber numberWithFloat:50.0]]; // The current value
[cornerRadiusAnimation setToValue:[NSNumber numberWithFloat:10.0]]; // The new value
[cornerRadiusAnimation setDuration:animationDuration];
[cornerRadiusAnimation setBeginTime:CACurrentMediaTime() + animationDelay];

// If your UIView animation uses a timing funcition then your basic animation needs the same one
[cornerRadiusAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];

// This will keep make the animation look as the "from" and "to" values before and after the animation
[cornerRadiusAnimation setFillMode:kCAFillModeBoth];
[[circle layer] addAnimation:cornerRadiusAnimation forKey:@"keepAsCircle"];
[[circle layer] setCornerRadius:10.0]; // Core Animation doesn't change the real value so we have to.

[UIView animateWithDuration:animationDuration
                      delay:animationDelay
                    options:UIViewAnimationOptionCurveEaseInOut 
                 animations:^{
                     [[circle layer] setFrame:CGRectMake(50, 50, 20, 20)]; // Arbitrary frame ...
                     // You other UIView animations in here...
                 } completion:^(BOOL finished) {
                     // Maybe you have your completion in here...
                 }]; 

有没有一种方法可以确定我是否在现有的UIView动画块中工作,以便我可以将核心动画添加到setter中? - Simon
@Simon,我不确定你所说的“to the setter”是什么意思。我已经更新了我的答案,并提供了一些示例代码,其中使用了基本动画和animateWithDuration:块来展示如何完成它。 - David Rönnqvist
谢谢 - 这是一个不错的示例代码,尽管它不完全符合我的要求。 我所说的“到setter”是指我想以一种可以意识到它是否在UIView动画块内,并在调用时添加带有正确参数的动画的方式覆盖我的视图类上的setBounds:方法。 我会更新问题来解释。 - Simon
您的答案引导了我找到正确的答案,因此我点赞了您的其他答案 - 希望没关系。 - Simon

10

非常感谢David,这是我找到的解决方法。最终关键在于使用视图的-[actionForLayer:forKey:]方法,因为它在UIView块内部使用,而不是使用层的-[actionForKey]方法。

@implementation SGBRoundView

-(CGFloat)radiusForBounds:(CGRect)bounds
{
    return fminf(bounds.size.width, bounds.size.height) / 2;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor clearColor];
        self.opaque = NO;
        self.layer.backgroundColor = [[UIColor purpleColor] CGColor];
        self.layer.borderColor = [[UIColor greenColor] CGColor];
        self.layer.borderWidth = 3;
        self.layer.cornerRadius = [self radiusForBounds:self.bounds];
    }
    return self;
}

-(void)setBounds:(CGRect)bounds
{
    self.layer.cornerRadius = [self radiusForBounds:bounds];
    [super setBounds:bounds];
}

-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
    id<CAAction> action = [super actionForLayer:layer forKey:event];

    if ([event isEqualToString:@"cornerRadius"])
    {
        CABasicAnimation *boundsAction = (CABasicAnimation *)[self actionForLayer:layer forKey:@"bounds"];
            if ([boundsAction isKindOfClass:[CABasicAnimation class]] && [boundsAction.fromValue isKindOfClass:[NSValue class]])
        {            
            CABasicAnimation *cornerRadiusAction = [CABasicAnimation animationWithKeyPath:@"cornerRadius"];
            cornerRadiusAction.delegate = boundsAction.delegate;
            cornerRadiusAction.duration = boundsAction.duration;
            cornerRadiusAction.fillMode = boundsAction.fillMode;
            cornerRadiusAction.timingFunction = boundsAction.timingFunction;

            CGRect fromBounds = [(NSValue *)boundsAction.fromValue CGRectValue];
            CGFloat fromRadius = [self radiusForBounds:fromBounds];
            cornerRadiusAction.fromValue = [NSNumber numberWithFloat:fromRadius];

            return cornerRadiusAction;
        }
    }

    return action;
}

@end

通过使用视图提供的界限操作,我能够获得正确的持续时间、填充模式和时间函数,最重要的是委托 - 没有它,UIView动画的完成块不会运行。

半径动画几乎在所有情况下都遵循边界的动画方式 - 还有一些特殊情况需要解决,但基本上已经实现了。值得一提的是,捏合手势仍然有时会出现不流畅 - 我想即使加速绘制仍然是有代价的。


1
这真的很有帮助,几乎完美地运作。我只想指出,我认为你应该将cornerRadiusAction动画添加到图层中,而不是返回它(应返回原始动作),以便正确发送动画结束(在我的情况下,UIView animate块从未调用结束块,除非我返回原始动作)。此外,每当动画与标准不同时,您都会遇到问题(我为CASpringAnimation拼凑了一个方法,但它是私有API)。 - Rupert
1
你的解决方案很好,只是CASpringAnimation对我也有问题 - 我的解决方案是使用CABasicAnimation *cornerRadiusAction = [boundsAction copy];,然后分别设置.keypath.fromValue属性。所有其他.delegate等属性都被复制了。 - Loz
在Swift中,当将CAAction转换为CABasicAnimation时出现错误。有什么建议吗? - alcamla
这种方法可以修改以在新版本的iOS中再次工作,并适用于自动旋转动画。请参见此答案 - rob mayoff

1

iOS 11开始,如果在动画块内更改cornerRadius,UIKit会自动为其添加动画效果。


0

CAShapeLayer的路径属性并非隐式可动画化,但它是可动画化的。创建一个CABasicAnimation来改变圆形路径的大小应该很容易。只需确保路径具有相同数量的控制点(例如,更改完整圆弧的半径)。如果更改控制点的数量,事情会变得非常奇怪。根据文档,“结果未定义”。


我该如何使用 animateWithDuration:animations: 让它起作用? - Simon
1
你不能直接这样做。你需要创建一个CABasicAnimation对象并将其添加到视图的层中。你可以在视图动画的同时完成这个操作。 - Duncan C
啊,我需要它能够与UIView动画方法一起工作,因为我同时在对其他对象进行动画处理。 - Simon
这与隐式动画和显式动画没有太大关系,而更多地涉及视图属性动画与图层属性动画。 - David Rönnqvist
@Simon,您可以同时创建运行并发的UView动画和CABasicAnimations。 - Duncan C
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接