【问题标题】:Draw dynamic shapes iOS at very high rate以非常高的速率绘制动态形状 iOS
【发布时间】:2023-03-28 08:41:01
【问题描述】:

我正在尝试创建一种看起来像这样的“雷达”:

这个想法是蓝色部分将像指南针一样指示用户与物品的接近程度。当用户远离目标时,形状会变窄、变长和蓝色,当用户靠近时,形状会变短、变粗和变红,如下图:

为了实现这一点,我画了几个圈子(实际上我尝试了 IBDesignable 和 IBInspectable 但我一直收到“构建失败”但这是另一个主题)

我的问题是:这是否可以绘制“锥形”部分,该部分应该以高速率改变其尺寸而不会有滞后的风险? 我可以使用 UIBezierPath / AddArc 或 Addline 方法来实现这一点,还是我正走向死胡同?

【问题讨论】:

    标签: ios swift uibezierpath


    【解决方案1】:

    有两种方法:

    1. 定义复杂的贝塞尔曲线形状和动画使用CABasicAnimation

      - (UIBezierPath *)pathWithCenter:(CGPoint)center angle:(CGFloat)angle radius:(CGFloat)radius {
          UIBezierPath *path = [UIBezierPath bezierPath];
      
          CGPoint point1 = CGPointMake(center.x - radius * sinf(angle), center.y - radius * cosf(angle));
      
          [path moveToPoint:center];
          [path addLineToPoint:point1];
      
          NSInteger curves = 8;
          CGFloat currentAngle = M_PI_2 * 3.0 - angle;
      
          for (NSInteger i = 0; i < curves; i++) {
              CGFloat nextAngle = currentAngle + angle * 2.0 / curves;
              CGPoint point2 = CGPointMake(center.x + radius * cosf(nextAngle), center.y + radius * sinf(nextAngle));
              CGFloat controlPointRadius = radius / cosf(angle / curves);
              CGPoint controlPoint = CGPointMake(center.x + controlPointRadius * cosf(currentAngle + angle / curves), center.y + controlPointRadius * sinf(currentAngle + angle / curves));
              [path addQuadCurveToPoint:point2 controlPoint:controlPoint];
              currentAngle = nextAngle;
              point1 = point2;
          }
          [path closePath];
          return path;
      }
      

      我只是将圆弧分割成一系列四边形曲线。我发现三次曲线可以更接近,但只需几条四边形曲线,我们就可以得到非常好的近似值。

      然后使用 CABasicAnimation 对其进行动画处理:

      - (void)viewDidAppear:(BOOL)animated {
          [super viewDidAppear:animated];
      
          CAShapeLayer *layer = [CAShapeLayer layer];
          layer.fillColor = self.startColor.CGColor;
          layer.path = self.startPath.CGPath;
          [self.view.layer addSublayer:layer];
          self.radarLayer = layer;
      }
      
      - (IBAction)didTapButton:(id)sender {
          self.radarLayer.path = self.finalPath.CGPath;
          CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
          pathAnimation.fromValue = (id)self.startPath.CGPath;
          pathAnimation.toValue = (id)self.finalPath.CGPath;
          pathAnimation.removedOnCompletion = false;
      
          self.radarLayer.fillColor = self.finalColor.CGColor;
          CABasicAnimation *fillAnimation = [CABasicAnimation animationWithKeyPath:@"fillColor"];
          fillAnimation.fromValue = (id)self.startColor.CGColor;
          fillAnimation.toValue = (id)self.finalColor.CGColor;
          fillAnimation.removedOnCompletion = false;
      
          CAAnimationGroup *group = [CAAnimationGroup animation];
          group.animations = @[pathAnimation, fillAnimation];
          group.duration = 2.0;
          [self.radarLayer addAnimation:group forKey:@"bothOfMyAnimations"];
      }
      

      这会产生:

    2. 另一种方法是使用简单的贝塞尔曲线,但带有显示链接的动画:

      如果您想使用 CAShapeLayerCADisplayLink(类似于针对屏幕更新优化的计时器),您可以执行以下操作:

      @interface ViewController ()
      
      @property (nonatomic, strong) CADisplayLink *displayLink;
      @property (nonatomic) CFAbsoluteTime startTime;
      @property (nonatomic) CFAbsoluteTime duration;
      @property (nonatomic, weak) CAShapeLayer *radarLayer;
      
      @end
      
      @implementation ViewController
      
      - (void)viewDidAppear:(BOOL)animated {
          [super viewDidAppear:animated];
      
          CAShapeLayer *layer = [CAShapeLayer layer];
          [self.view.layer addSublayer:layer];
          self.radarLayer = layer;
          [self updateRadarLayer:layer percent:0.0];
      }
      
      - (IBAction)didTapButton:(id)sender {
          [self startDisplayLink];
      }
      
      - (void)updateRadarLayer:(CAShapeLayer *)radarLayer percent:(CGFloat)percent {
          CGFloat angle = M_PI_4 * (1.0 - percent / 2.0);
          CGFloat distance = 200 * (0.5 + percent / 2.0);
      
          UIBezierPath *path = [UIBezierPath bezierPath];
          [path moveToPoint:self.view.center];
          [path addArcWithCenter:self.view.center radius:distance startAngle:M_PI_2 * 3.0 - angle endAngle:M_PI_2 * 3.0 + angle clockwise:true];
          [path closePath];
      
          radarLayer.path = path.CGPath;
          radarLayer.fillColor = [self colorForPercent:percent].CGColor;
      }
      
      - (UIColor *)colorForPercent:(CGFloat)percent {
          return [UIColor colorWithRed:percent green:0 blue:1.0 - percent alpha:1];
      }
      
      - (void)startDisplayLink {
          self.startTime = CFAbsoluteTimeGetCurrent();
          self.duration = 2.0;
          self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
          [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
      }
      
      - (void)handleDisplayLink:(CADisplayLink *)link {
          CGFloat percent = (CFAbsoluteTimeGetCurrent() - self.startTime) / self.duration;
          if (percent < 1.0) {
              [self updateRadarLayer:self.radarLayer percent:percent];
          } else {
              [self stopDisplayLink];
              [self updateRadarLayer:self.radarLayer percent:1.0];
          }
      }
      
      - (void)stopDisplayLink {
          [self.displayLink invalidate];
          self.displayLink = nil;
      }
      

      这会产生:

    【讨论】:

    • 感谢@Rob 提供清晰详细的答案。我在 Swift 中得到了这个工作,并将代码粘贴到一个单独的答案中。现在我必须将我的项目距离映射到此以完成此操作,如果我成功将在 github 上发布代码:)
    • 杰出答案(已投票)我确实注意到 OP 示例图像的形状的弯曲部分是半径小得多的圆弧,您的图像是纯“饼图”。在我看来,OP 图像是一个小半径圆(由弦切割的圆)与三角形相结合的子部分,但您的回答是一个好的开始。
    • 您使用 8 个二次贝塞尔曲线的方法既好又简单。你知道这与一个真正的圆圈有多接近吗?我看过一些文章说 4 个三次贝塞尔曲线导致的形状与真正的圆的差异仅远小于 1%,但您的结果看起来确实完全可以接受。
    • 重新计算贝塞尔曲线的数量,我刚刚尝试了 8 并用传统的弧线覆盖了它,它们似乎比我的眼睛更近,所以我宣布成功。但是贝塞尔曲线的数量是一个变量(我的代码在这方面保持灵活),所以他可以试验他想要的任意数量的贝塞尔曲线。我所知道的是两个太小了,八个看起来很棒,对性能的影响可以忽略不计。我发现如果使用二次贝塞尔曲线,则需要更少的贝塞尔曲线,但控制点的计算更加神秘,所以我在这里保持简单。
    • Rob 是对的,我的模型很快就画好了,而且相当丑陋。馅饼片正是我需要的,我认为它看起来更好
    【解决方案2】:

    您可能应该使用一个或多个 CAShapeLayer 对象和核心动画。

    您可以将UIBezierPath 中的CGPath 安装到形状层中,并且可以为形状层的更改设置动画,但要使动画正常工作,您需要具有相同数量和类型的控件动画开始和结束时形状中的点。

    【讨论】:

    • 不幸的是,因为这个饼楔的角度在变化,所以这不起作用。我认为他必须使用CADisplayLink 制作动画。如果角度没有改变,那么简单的CABasicAnimation 就可以做到,但在这种情况下不会。
    • @Rob:感谢您指出这一点。你认为 SpriteKit 可以在这里帮助我吗?因为我从未使用过 CADisplay Link。我当然可以尝试以固定角度作为开始
    • @Rob,应该仍然可以使用 CABasicAnimation。您可以使用三次贝塞尔曲线而不是使用 arc 命令手动构建弧。 (在所有情况下,您只需要使用相同数量的贝塞尔线段构建弧线。)
    • (弧线不适用于开始/结束角度的任意变化,因为在内部它根据弧线的角度跨度使用不同数量的三次贝塞尔子路径,因此控制点的数量可以改变。)如果你手工建立你的弧,那么你可以保持控制点的数量和类型不变。
    • @DuncanC - 我建议您尝试使用一个工作示例更新您的答案,其中弧的角度正在变化。我想你会发现它比你想象的要复杂。我认为显示链接会容易得多,但每个人都有自己的。我将发布显示链接示例。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-05-27
    • 2017-08-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-30
    • 2021-02-18
    相关资源
    最近更新 更多