【问题标题】:UIBezierPath outline around multiple subviewsUIBezierPath 围绕多个子视图的轮廓
【发布时间】:2016-02-29 07:12:39
【问题描述】:

我有一个包含多个子视图的视图。我需要围绕用户选择的所有子视图绘制轮廓,并忽略未选择的子视图。我尝试创建一个凸包,但它不能正确解决我的目的。是否有 iOS 内置的东西可以用来在选定视图周围绘制边界?

我发现了这个,但它仅用于相交视图:link

这是我正在尝试做的草图。 S 表示选择的视图,NS 表示未选择的视图。红色标记的线是轮廓。

为了澄清,如果示例中的左上、右上和左下之间存在视图视图,则无法创建路径,因此不应绘制。

【问题讨论】:

  • 目前还不清楚您到底想要发生什么。如果用户在您的示例中选择左上角、左下角和右下角视图怎么办?
  • 然后会绘制一条避开右上角的路径。如果示例中左上、右上和左下之间存在视图视图,则无法创建路径,因此不应绘制。

标签: ios objective-c xcode swift uibezierpath


【解决方案1】:

这是一个计算路径的函数(在 Playground 中)。我还没有时间添加排除逻辑。我相信可以通过将顶线和底线转换为可以测试交叉点的矩形列表来完成。 (如果我有时间,我会编辑我的帖子以添加)。

 import Foundation
 import UIKit
 import XCPlayground

 // compute enclosing Path for list of views
 // ----------------------------------------
 // - path is composed of a top line that hugs the topmost views
 //   and of a bottom line that hugs the bottom most views
 // - The two lines span the minimum and maximum x coordinates of
 //   the views in the list
 // NOTE: to do this cleanly, all four sides should be considered
 //       (I merely showed top and bottom to give an idea of the method)
 //
 func enclosingPathForViews(views:[UIView], margin:CGFloat = 3) -> UIBezierPath
 { 
   let frames = views.map({$0.frame.insetBy(dx: -margin, dy: -margin)})
   var path = UIBezierPath()

   // top left and right corners of each view
   // sorted from left to right, top to bottom
   var topPoints:[CGPoint] = frames.reduce(  Array<CGPoint>(),
                           combine: { $0 + [ CGPoint(x:$1.minX,y:$1.minY),
                                             CGPoint(x:$1.maxX,y:$1.minY) ] })
   topPoints = topPoints.sort({ $0.x == $1.x ? $0.y < $1.y : $0.x < $1.x })

   // trace top line from left to right
   // moving up or down when appropriate                                          
   var previousPoint = topPoints.first!
   path.moveToPoint(previousPoint) 
   for point in topPoints
   {
      guard point.y == previousPoint.y
         || point.y < previousPoint.y
            && frames.contains({$0.minX == point.x && $0.minY < previousPoint.y })
         || point.y > previousPoint.y
            && !frames.contains({ $0.maxX > point.x && $0.minY < point.y })
      else  { continue }

      if point.y < previousPoint.y
      { path.addLineToPoint(CGPoint(x:point.x, y:previousPoint.y)) }
      if point.y > previousPoint.y
      { path.addLineToPoint(CGPoint(x:previousPoint.x, y:point.y)) }
      path.addLineToPoint(point)
      previousPoint = point
   }

   // botom left and right corners of each view
   // sorted from right to left, bottom to top
   var bottomPoints:[CGPoint] = frames.reduce(  Array<CGPoint>(),
                                combine: { $0 + [ CGPoint(x:$1.minX,y:$1.maxY),
                                                  CGPoint(x:$1.maxX,y:$1.maxY) ] })
   bottomPoints = bottomPoints.sort({ $0.x == $1.x ? $0.y > $1.y : $0.x > $1.x })

   // trace bottom line from right to left
   // starting where top line left off (rightmost top corner)
   // moving up or down when appropriate                                          
   for point in bottomPoints
   {
      guard point.y == previousPoint.y
         || point.y > previousPoint.y
            && frames.contains({$0.maxX == point.x && $0.maxY > previousPoint.y })
         || point.y < previousPoint.y
            && !frames.contains({ $0.minX < point.x && $0.maxY > point.y })
      else  { continue }

      if point.y > previousPoint.y
      { path.addLineToPoint(CGPoint(x:point.x, y:previousPoint.y)) }
      if point.y < previousPoint.y
      { path.addLineToPoint(CGPoint(x:previousPoint.x, y:point.y)) }
      path.addLineToPoint(point)
      previousPoint = point
   }

   // close back to leftmost point of top line
   path.closePath()

   return path
 }

 // TESTS:
 // ======

 // UIView (container)
 // ------------------
 let viewSize    = CGSize(width: 300, height: 300)
 let view:UIView = UIView(frame: CGRect(origin: CGPointZero, size: viewSize))
 view.backgroundColor = UIColor.whiteColor()

 XCPlaygroundPage.currentPage.liveView = view


 // Selected Views
 // --------------
 var selectedViews:[UIView] = 
 [
    UIView(frame:CGRect(x: 130, y: 50, width: 50, height: 50)),
    UIView(frame:CGRect(x: 60, y: 30, width: 50, height: 50)),
    UIView(frame:CGRect(x: 20, y: 110, width: 50, height: 50))
 //   , UIView(frame:CGRect(x: 150, y: 150, width: 50, height: 50))
 ]

 for subView in selectedViews 
 { 
    subView.backgroundColor = UIColor.greenColor()
    view.addSubview(subView)
 }

 // Excluded views (non-selected)
 // --------------
 var excludedViews:[UIView] = 
 [
    UIView(frame:CGRect(x: 150, y: 110, width: 50, height: 50)),
 ]
 for subView in excludedViews 
 { 
    subView.backgroundColor = UIColor.redColor()
    view.addSubview(subView)
 }


 // CoreGraphics drawing
 // --------------------
 UIGraphicsBeginImageContextWithOptions(viewSize, false, 0)

 UIColor.blackColor().setStroke()
 let path = enclosingPathForViews(selectedViews)
 path.stroke()

 // set image to view layer 
 view.layer.contents = UIGraphicsGetImageFromCurrentImageContext().CGImage
 UIGraphicsEndImageContext()

【讨论】:

    【解决方案2】:

    如果你只需要设置边界,你应该继承或扩展对象,例如UIButton,要覆盖控件事件调用或不进行子类化,在 IBAction 或手势回调中,在按钮上设置 button.layer.borderWidth = 1.0。当你想隐藏它时,将它设置回 0.0。

    您还可以设置borderColor 和cornerRadius。

    根据按钮内容,您可能需要设置 clipsSubviews = true

    【讨论】:

    • 感谢您的回复。我添加了一个链接,以便用图表更好地解释我的问题。
    猜你喜欢
    • 2019-11-24
    • 1970-01-01
    • 2016-02-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多