【问题标题】:How to make UIBezierPath to fill from the center如何使 UIBezierPath 从中心填充
【发布时间】:2023-04-27 21:14:01
【问题描述】:

如何制作从中心开始绘制并向左右扩展的 UIBezierPath?

我当前的解决方案不是使用 UIBezierPath,而是使用另一个 UIView 中的 UIView。子视图位于其父视图的中心,当我需要扩展子视图时,我会调整子视图的宽度。然而,这个解决方案远非完美,因为视图位于 UIStackView 内部,而堆栈视图位于 UICollectionView 单元格中,有时无法正确获取宽度。

   override func layoutSubviews() {
    super.layoutSubviews()
    progressView.cornerRadius = cornerRadius
    clipsToBounds = true
    addSubview(progressView)
    NSLayoutConstraint.activate([
        progressView.topAnchor.constraint(equalTo: topAnchor),
        progressView.bottomAnchor.constraint(equalTo: bottomAnchor),
        progressView.centerXAnchor.constraint(equalTo: centerXAnchor)
    ])
    let width = frame.width
    let finalProgress = CGFloat(progress) * width
    let widthConstraint = progressView.widthAnchor.constraint(equalToConstant: finalProgress)
    widthConstraint.priority = UILayoutPriority(rawValue: 999)
    widthConstraint.isActive = true
}

进度是 double 类型的类属性,当它被设置时,我调用 layoutIfNeeded。

我也尝试调用 setNeedsLayout 和 layoutIfNeeded 但它不能解决问题。 SetNeedsDisplay 和 invalidateIntrinzicSize 也不起作用。

现在,我想使用贝塞尔路径,但问题是它从给定点开始并从左到右绘制。问题是:如何制作居中的 UIBezierPath 以及当我增加宽度时它会扩展(保持在中心)

这是我想要实现的图像。

【问题讨论】:

  • 尽管如此,我会这样做的方式正是您一开始所说的那样,一种以自己的宽度绘制水平形状的视图。
  • 问题是滚动几次后,它会停止更新,或者由于某种原因获取不正确,因为它不会每次都调用 layoutSubviews。我尝试强制 layoutSubviews 但它不起作用。
  • 那将是您未在问题中透露的其他原因。 “滚动几次后”表明您没有考虑单元重用。但是很难说,因为你没有展示相关代码。

标签: ios swift uibezierpath cashapelayer


【解决方案1】:

有很多选择。但这里有一些:

  1. strokeStartCABasicAnimationCAShapeLayerstrokeEnd,即:

    • 创建CAShapeLayer,其path 是最终的UIBezierPath(全宽),但其strokeStartstrokeEnd 均为0.5(即中途开始,中途结束,即什么都看不见但是)。

      func configure() {
          shapeLayer.strokeColor = strokeColor.cgColor
          shapeLayer.lineCap = .round
          setProgress(0, animated: false)
      }
      
      func updatePath() {
          let path = UIBezierPath()
          path.move(to: CGPoint(x: bounds.minX + lineWidth, y: bounds.midY))
          path.addLine(to: CGPoint(x: bounds.maxX - lineWidth, y: bounds.midY))
          shapeLayer.path = path.cgPath
          shapeLayer.lineWidth = lineWidth
      }
      
    • 然后,在CABasicAnimation 中,将strokeStart 更改为0strokeEnd 更改为1 以实现动画从中间向左和向右增长。例如:

      func setProgress(_ progress: CGFloat, animated: Bool = true) {
          let percent = max(0, min(1, progress))
          let strokeStart = (1 - percent) / 2
          let strokeEnd = 1 - (1 - percent) / 2
      
          if animated {
              let strokeStartAnimation = CABasicAnimation(keyPath: "strokeStart")
              strokeStartAnimation.fromValue = shapeLayer.strokeStart
              strokeStartAnimation.toValue = strokeStart
      
              let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
              strokeEndAnimation.fromValue = shapeLayer.strokeEnd
              strokeEndAnimation.toValue = strokeEnd
      
              let animation = CAAnimationGroup()
              animation.animations = [strokeStartAnimation, strokeEndAnimation]
      
              shapeLayer.strokeStart = strokeStart
              shapeLayer.strokeEnd = strokeEnd
      
              layer.add(animation, forKey: nil)
          } else {
              shapeLayer.strokeStart = strokeStart
              shapeLayer.strokeEnd = strokeEnd
          }
      }
      
  2. pathCAShapeLayer 的动画

    • 拥有创建UIBezierPath的方法:

      func setProgress(_ progress: CGFloat, animated: Bool = true) {
          self.progress = progress
      
          let cgPath = path(for: progress).cgPath
      
          if animated {
              let animation = CABasicAnimation(keyPath: "path")
              animation.fromValue = shapeLayer.path
              animation.toValue = cgPath
      
              shapeLayer.path = cgPath
      
              layer.add(animation, forKey: nil)
          } else {
              shapeLayer.path = cgPath
          }
      }
      

      在哪里

      func path(for progress: CGFloat) -> UIBezierPath {
          let path = UIBezierPath()
          let percent = max(0, min(1, progress))
          let width = bounds.width - lineWidth
          path.move(to: CGPoint(x: bounds.midX - width * percent / 2 , y: bounds.midY))
          path.addLine(to: CGPoint(x: bounds.midX + width * percent / 2, y: bounds.midY))
          return path
      }
      
    • 当您创建CAShapeLayer 调用setProgress 时,其值为0,并且没有动画;和

    • 当你想制作动画时,调用setProgress,值为1,但带有动画。

  3. 放弃UIBezierPath/CAShapeLayer 接近并使用UIViewUIView 基于块的动画:

    • 使用backgroundColor 创建短UIView,其层的cornerRadius 是视图高度的一半。

    • 定义约束,使视图的宽度为零。例如。您可以定义 centerXanchor 并将其放置在您想要的位置,并且 widthAnchor 为零。

    • 通过将现有的widthAnchorconstant 设置为您想要的任何宽度并将layoutIfNeeded 放置在UIView 动画块中来为约束设置动画。

【讨论】: