如何创建可平滑调整大小的圆形 UIView?

Posted

技术标签:

【中文标题】如何创建可平滑调整大小的圆形 UIView?【英文标题】:How do I create a smoothly resizable circular UIView? 【发布时间】:2012-05-19 21:42:05 【问题描述】:

我正在尝试创建一个 UIView,它显示一个半透明的圆圈,其边界内有一个不透明的边框。我希望能够以两种方式更改边界 - 在 -[UIView animateWithDuration:animations:] 块内和在每秒触发几次的捏手势识别器操作中。我已经根据 SO 上其他地方的答案尝试了三种方法,但没有一种是合适的。

    layoutSubviews 中设置视图层的圆角半径可以提供平滑的平移,但在动画期间视图不会保持圆形;似乎cornerRadius 是不可动画的。

    drawRect: 中绘制圆圈会提供始终如一的圆形视图,但如果圆圈变得太大,则在捏合手势中调整大小会变得不稳定,因为设备花费太多时间重绘圆圈。

    添加一个 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] 中添加正确的动画。

【问题讨论】:

1 应该保持圆形,只要您将圆角半径和大小一起设置动画(可能在 CAAnimationGroup 中)。在任何时候,cornerRadius 都需要是宽度的一半,宽度需要与高度相同。 我需要它在 animateWithDuration:animations: 中工作 - 可以与 CAAnimationGroup 一起使用吗? animateWithDuration:分组本身,因此不需要显式动画组。只要圆角半径保持在矩形边的一半,就可以将圆角半径与大小一起设置动画。 嗯......好吧,它没有。发生的情况是新的圆角半径立即显示,然后动画从那里开始。 另请参阅this question,其中用户遇到与cornerRadius 相同的问题。 【参考方案1】:

正如“查看 ios 编程指南”中的 section on Animations 所说

UIKit 和 Core Animation 都提供对动画的支持,但每种技术提供的支持级别各不相同。在 UIKit 中,动画是使用 UIView 对象执行的

属性的完整列表,您可以使用旧版本进行动画处理

[UIView beginAnimations:context:];
[UIView setAnimationDuration:];
// Change properties here...
[UIView commitAnimations];

或更新的

[UIView animateWithDuration:animations:];

(您正在使用的)是:

框架 界限 居中 变换(CGAffineTransform,不是 CATransform3D) 阿尔法 背景颜色 内容拉伸

让人困惑的是,你还可以在 UIView 动画块内的图层上为相同的属性设置动画,即框架、边界、位置、不透明度、背景颜色。

同一节继续说:

在您想要执行更复杂的动画或 UIView 类不支持的动画的地方,您可以使用 Core Animation 和视图的底层来创建动画。由于视图和图层对象错综复杂地链接在一起,因此对视图图层的更改会影响视图本身。

往下几行,您可以阅读 Core Animation 动画属性列表,您可以在其中看到这个:

图层的边框(包括图层的角是否圆角)

至少有两个不错的选择可以达到您所追求的效果:

动画圆角半径 使用 CAShapeLayer 并为路径设置动画

这两个都要求您使用 Core Animation 制作动画。如果您需要多个动画作为一个运行,您可以创建一个 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 动画块中,以便我可以将核心动画添加到设置器? @Simon 我不确定你对“设置者”的意思我已经用一些示例代码更新了我的答案,这些示例代码同时使用了基本动画和 animateWithDuration: 块来向你展示它是如何做到的可以的。 谢谢 - 这是很好的示例代码,即使它不是我想要的。我所说的“设置器”的意思是我想在我的视图类上覆盖 setBounds: 以一种知道它何时位于 UIView 动画块内并在调用时添加具有正确参数的动画的方式。我会更新问题来解释。 你的回答让我得到了正确的答案,所以我赞成你的其他一些答案 - 希望没关系。【参考方案2】:

非常感谢大卫,这是我找到的解决方案。最后,关键在于使用视图的 -[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 动画的完成块不会运行。

半径动画几乎在所有情况下都遵循边界的动画 - 我正在尝试消除一些边缘情况,但它基本上就在那里。还值得一提的是,捏合手势有时仍然很生涩——我想即使是加速绘图也很昂贵。

【讨论】:

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

从 iOS 11 开始,如果您在动画块内更改 UIKit,则 UIKit 会为 cornerRadius 设置动画。

【讨论】:

【参考方案4】:

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

【讨论】:

如何使用 animateWithDuration:animations: 来实现? 你没有。您必须创建一个 CABasicAnimation 对象并将其添加到您的视图层。您可以与视图动画同时执行此操作。 啊。我需要它与 UIView 动画方法一起使用,因为我正在同时为其他对象设置动画。 它与隐式动画与显式动画没有任何关系,但更多的是与视图属性动画与图层属性动画。 @David,术语隐式动画是特定于当您更改图层的动画属性时自动发生的动画。所以两者都是。形状层的路径属性是一种奇怪的情况。它是动画的,但不是隐式的。如果您直接更改形状图层的路径,则更改不会动画,但如果您从 CAAnimation 进行更改,它将为更改设置动画。

以上是关于如何创建可平滑调整大小的圆形 UIView?的主要内容,如果未能解决你的问题,请参考以下文章

使用UIToolbar调整UIView的大小不会同时调整大小

创建后调整uiview子类的大小

UIView 根据边缘调整大小

如何调整 UIView 的大小并强制其子视图调整大小

如何在加载了 nib 文件的 UIView 中调整子视图的大小

调整单元格大小时如何使表格视图动画平滑