SwiftUI:View clipped

masking遮罩是一種強大的技術(shù),我們可以使用它將應用程序的設(shè)計推向下一個層次。SwiftUI提供了多種方式來做這件事:讓我們從SwiftUI的剪輯開始。

Clipping遮罩

每個視圖都有一個受到綁定的frame。這個frame用于組成整個視圖層次結(jié)構(gòu)布局。當進入繪圖階段時,視圖內(nèi)容可能會超出它的frame。
例如,以下視圖:

base.png
Text("Five stars")
  .background(Color.yellow)
  .font(.system(size: 90))
  .border(Color.red)

紅色邊框顯示內(nèi)容frame,在本例中,內(nèi)容frame也與綁定的frame一致。

讓我們來看另一個例子:

base-borders.png
Text("Five stars")
  .background(Color.yellow)
  .font(.system(size: 90))
  .fixedSize()
  .border(Color.blue)
  .frame(width: 200, height: 100)
  .border(Color.red)

由于fixedSize()視圖修飾符,Text需要多少空間就占用多少空間。
然而,我們也應用另一個.frame(width: 200, height: 50)視圖修改器在上面。

對于視圖層次結(jié)構(gòu)的其余部分,這個視圖限制由紅色邊框表示,而藍色邊框表示視圖內(nèi)容占用的空間。

在布局階段:

  • 只有綁定的框架/紅色邊框?qū)⒈豢紤]
  • 內(nèi)容frame/藍色邊框完全被忽略

由于SwiftUI默認允許內(nèi)容溢出,即使內(nèi)容超出了視圖的邊緣也會被繪制。為了避免這種情況,我們可以使用剪輯:

clipped.png
Text("Five stars")
  .background(Color.yellow)
  .font(.system(size: 90))
  .fixedSize()
  .border(Color.blue)
  .frame(width: 200, height: 100)
  .border(Color.red)
  .clipped() // ????

clipped()視圖修改器將視圖的繪制限制在其綁定frame內(nèi),其他所有內(nèi)容將被隱藏。

換句話說,clipped()應用一個等效于綁定幀“矩形”的遮罩,從而隱藏超出該矩形的任何內(nèi)容。

SwiftUI提供了兩個clipped()替代方法:cornerRadius(_:)clipShape(_:style)。

Corner Radius

cornerRadius(_:)的行為與clipped()完全相同,但它并沒有與綁定frame 1:1匹配,而是讓我們指定一個用于最終蒙版的角半徑:

corner-radius.png
Text("Five stars")
  .background(Color.yellow)
  .font(.system(size: 90))
  .fixedSize()
  .frame(width: 200, height: 100)
  .cornerRadius(50) // ????

使用與之前相同的思維過程,cornerRadius(_:)應用一個等效于視圖綁定框架矩形的蒙版,這次是圓角。

.clipped().cornerradius(0)有更好的性能。

Clip Shape

到目前為止,我們一直在使用矩形,clipShape(_:style:)消除了這個限制,讓我們使用任何形狀作為剪輯蒙版:

circle.png
Text("Five stars")
  ...
  .clipShape(Circle())

形狀盡可能適合包含它們的視圖(即視圖綁定frame)的自然大小。

我們并不局限于SwiftUI提供的形狀。我們也可以聲明:

star.png
Text("Five stars")
  ...
  .clipShape(Star())
struct Star: Shape {
  @Clamping(0...Int.max) var points: Int = 5
  var innerRatio = 0.4

  func path(in rect : CGRect) -> Path {
    let center = CGPoint(x: rect.midX, y: rect.midY)
    let angle: Double = .pi / Double(points)
    var path = Path()
    var startPoint: CGPoint = rect.origin

    let outerRadius = min(rect.width / 2, rect.height / 2)
    let innerRadius = outerRadius * innerRatio

    let maxCorners = 2 * points
    for corner in 0 ..< maxCorners {
      let radius = (corner % 2) == 0 ? outerRadius : innerRadius

      let x = center.x + cos(Double(corner) * angle) * radius
      let y = center.y + sin(Double(corner) * angle) * radius
      let point = CGPoint(x: x, y: y)

      if corner == 0 {
        startPoint = point
        path.move(to: point)
      } else {
        path.addLine(to: point)
      }
      if corner == (maxCorners - 1) {
        path.addLine(to: startPoint)
      }
    }
    return path
  }
}

類似于.clipped()可以被看作是.cornerRadius(0)上的便利,.cornerRadius(x)可以被看作是.clipshape(RoundedRectangle(cornerRadius: x))上的便利。

@Clamping為自定義修飾器:

@propertyWrapper
public struct Clamping<Value: Comparable> {
  var value: Value
  let range: ClosedRange<Value>

  public init(wrappedValue: Value, _ range: ClosedRange<Value>) {
    precondition(range.contains(wrappedValue))
    self.range = range
    self.value = wrappedValue
  }

  public var wrappedValue: Value {
    get { value }
    set { value = min(max(range.lowerBound, newValue), range.upperBound) }
  }
}

奇偶規(guī)則

當定義一個Shape形狀時,它的一些部分可以多次繪制。我們可以將這些部分視為“重疊區(qū)域”。例如,以這個DoubleEllipse Shape定義為例,它由兩個任意數(shù)量重疊的橢圓組成:

double-eclipses.gif
struct FSView: View {
  @State var overlapping: Double = 0.1

  var body: some View {
    VStack {
      DoubleEllipse(overlapping: overlapping)
        .frame(width: 300, height: 100)
      HStack {
        Text("Overlapping")
        Slider(value: $overlapping, in: 0.0...1.0)
      }
    }
  }
}

struct DoubleEllipse: Shape {
  /// 1 = complete overlap
  /// 0 = no overlap
  @Clamping(0.0...1.0) var overlapping: Double = 0

  func path(in rect: CGRect) -> Path {
    let rectSize = CGSize(width: (rect.width / 2) * (1 + overlapping), height: rect.height)

    var path = Path()
    path.addEllipse(in: CGRect(origin: .zero, size: rectSize))
    let secondEllipseOrigin = CGPoint(x: (rect.width / 2) * (1 - overlapping), y: rect.origin.y)
    path.addEllipse(in: CGRect(origin: secondEllipseOrigin, size: rectSize))

    return path
  }
}

默認情況下,SwiftUI按照定義繪制所有內(nèi)容。然而,我們也可以應用一個fill(style:) Shape修飾符,以不同的方式填充那些重疊的區(qū)域:

double-eclipses-eo.gif
struct FSView: View {
  @State var overlapping: Double = 0.1

  var body: some View {
    VStack {
      DoubleEllipse(overlapping: overlapping)
        .fill(style: FillStyle(eoFill: true, antialiased: true)) // ????
        .frame(width: 300, height: 100)
      HStack {
        Text("Overlapping")
        Slider(value: $overlapping, in: 0.0...1.0)
      }
    }
  }
}

神奇之處在于oeFill參數(shù),其中eo代表奇偶(規(guī)則),描述如下:《形狀中的一個“內(nèi)部”點是通過在任意方向繪制一條從該點到無窮遠的射線,并計算射線穿過給定形狀的路徑段的數(shù)量來確定的。如果這個數(shù)是奇數(shù),點在里面;否則的話,那就是在外面》

定義不僅僅是重疊,但這很可能是它在SwiftUI掩蔽時的用法。

fill(style:) Shape修飾符返回some View,這意味著我們不能在clipShape(_:style:)中使用它,因為后者需要一個Shape實例。也就是說,.clipShape(_:style:)第二個參數(shù)解決了這個問題,讓我們傳遞一個FillStyle:

clipshape-even-odd.gif
VStack {
  Text("Five stars")
    .background(Color.yellow)
    .font(.system(size: 90))
    .clipShape(
      OverlappingEllipses(ellipsesNumber: ellipsesNumber, overlapping: overlapping),
      style: FillStyle(eoFill: true, antialiased: false) // ????
    )
  Stepper("Ellipses number:", value: $ellipsesNumber, in: 2...16)
  HStack {
    Text("Overlapping")
    Slider(value: $overlapping, in: 0.0...1.0)
  }
}

動畫剪輯遮罩

Shapes形狀既符合View視圖又符合Animatable動畫,我們可以在形狀中聲明var animatableData: CGFloat,以利用這一點:

struct OverlappingEllipses: Shape {
  @Clamping(1...Int.max) var ellipsesNumber: Int = 2
  @Clamping(0.0...1.0) var overlapping: Double = 0

  var animatableData: CGFloat { // ????
    get { overlapping }
    set { overlapping = newValue }
  }

  func path(in rect: CGRect) -> Path {
    let rectWidth = (rect.width / Double(ellipsesNumber)) * (1 + Double(ellipsesNumber - 1) * overlapping)
    let rectSize = CGSize(width: rectWidth, height: rect.height)

    var path = Path()
    for index in 0..<ellipsesNumber {
      let ellipseOrigin = CGPoint(x: (rect.width - rectWidth) * Double(index) / Double(ellipsesNumber - 1), y: rect.origin.y)
      path.addEllipse(in: CGRect(origin: ellipseOrigin, size: rectSize))
    }

    return path
  }
}

有了這個,我們就可以把我們目前所介紹的所有內(nèi)容都放到一起,輕松地獲得一些迷幻效果:

clipshape-animation.gif
struct FSView: View {
  @State var overlapping: Double = 0

  var body: some View {
    VStack(spacing: 16) {
        Text("Five stars")
          ...
          .clipShape(
            OverlappingEllipses(ellipsesNumber: 8, overlapping: overlapping),
            style: FillStyle(eoFill: true, antialiased: false)
          )

      Text("Five stars")
        ...
        .clipShape(
          OverlappingRectangles(rectanglesNumber: 8, overlapping: overlapping),
          style: FillStyle(eoFill: true, antialiased: false)
        )

      Button("Show/Hide") {
        withAnimation(.easeInOut(duration: 2)) {
          overlapping = overlapping == 1 ? 0 : 1
        }
      }
    }
  }
}

OverlappingRectangles定義如下:

struct OverlappingRectangles: Shape {
  @Clamping(1...Int.max) var rectanglesNumber: Int = 2
  @Clamping(0.0...1.0) var overlapping: Double = 0

  var animatableData: CGFloat {
    get { overlapping }
    set { overlapping = newValue }
  }

  func path(in rect: CGRect) -> Path {
    let rectWidth = (rect.width / Double(rectanglesNumber)) * (1 + Double(rectanglesNumber - 1) * overlapping)
    let rectSize = CGSize(width: rectWidth, height: rect.height)

    var path = Path()
    for index in 0..<rectanglesNumber {
      let ellipseOrigin = CGPoint(x: (rect.width - rectWidth) * Double(index) / Double(rectanglesNumber - 1), y: rect.origin.y)
      path.addRect(CGRect(origin: ellipseOrigin, size: rectSize))
    }

    return path
  }
}struct OverlappingRectangles: Shape {
  @Clamping(1...Int.max) var rectanglesNumber: Int = 2
  @Clamping(0.0...1.0) var overlapping: Double = 0

  var animatableData: CGFloat {
    get { overlapping }
    set { overlapping = newValue }
  }

  func path(in rect: CGRect) -> Path {
    let rectWidth = (rect.width / Double(rectanglesNumber)) * (1 + Double(rectanglesNumber - 1) * overlapping)
    let rectSize = CGSize(width: rectWidth, height: rect.height)

    var path = Path()
    for index in 0..<rectanglesNumber {
      let ellipseOrigin = CGPoint(x: (rect.width - rectWidth) * Double(index) / Double(rectanglesNumber - 1), y: rect.origin.y)
      path.addRect(CGRect(origin: ellipseOrigin, size: rectSize))
    }

    return path
  }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容