Giới thiệu Công cụ Bố cục Đơn giản

Nov 28 2022
Tôi yêu Bố cục tự động. Nó giúp ích rất nhiều khi thiết kế giao diện người dùng phức tạp.

Tôi thích Bố cục tự động . Nó giúp ích rất nhiều khi thiết kế giao diện người dùng phức tạp. Nhưng có những lúc giao diện người dùng rất đơn giản và Bố cục tự động có thể cảm thấy hơi quá mức cần thiết, trong khi những lúc khác, giao diện người dùng có thể hơi phức tạp và Bố cục tự động thực sự bắt đầu ảnh hưởng đến hiệu suất ứng dụng. Trước bố cục tự động, có một kỹ thuật khác để tạo giao diện người dùng, nó được gọi là Springs và Struts (còn được gọi là Bố cục thủ công trái ngược với Bố cục tự động). Tôi cũng rất thích Bố cục thủ công vì tính đơn giản của nó. Giống như mọi công cụ khác, có sự đánh đổi khi chọn công cụ tốt nhất cho công việc và nó cũng áp dụng khi chọn Bố cục tự động so với Bố cục thủ công.

Điều tốt là, Bố cục tự động không được thiết kế như một giải pháp thay thế cho Bố cục thủ công, mà giống như một phần bổ sung. Vì vậy, thay vì chúng ta phải tính toán framecho a UIView, chúng ta bắt đầu bằng a CGRect.zerovà để Bố cục Tự động tính toán framegiá trị sau. Hầu hết thời gian, điều đó thật tuyệt vời và không ảnh hưởng đến dòng chảy của chúng tôi. Những lần khác, chúng tôi có thể phải đợi quá trình chạy bố cục để đọc lại các framegiá trị được tính toán.

// let Auto layout calculate the frame values
DispatchQueue.main.async {
  // start using the frame values for something else.
}

Nguồn cảm hứng

Nguồn cảm hứng là từ cách làm UIBarButtonItemviệc với UIToolbarhoặc UINavigationBar. Nếu chúng tôi muốn xây dựng một giao diện người dùng như

Chúng tôi sẽ tạo một UIToolBarvà thêm một loạt cácUIBarButtonItem

let toolbar = UIToolbar(frame: toolbarFrame)
  let playButton = UIBarButtonItem(systemItem: .play)
  let pauseButton = UIBarButtonItem(systemItem: .pause)
  let rewindButton = UIBarButtonItem(systemItem: .rewind)
  let forwardButton = UIBarButtonItem(systemItem: .fastForward)
  let spaceButton = UIBarButtonItem(systemItem: .flexibleSpace)
  toolbar.items = [
    spaceButton,
    rewindButton, spaceButton,
    playButton, spaceButton,
    pauseButton, spaceButton,
    forwardButton, spaceButton,
  ]

Tôi nghĩ cách tiếp cận này có thể được sử dụng để xây dựng một công cụ bố cục rất đơn giản về mặt mô hình tinh thần nhưng có thể được sử dụng để xây dựng các bố cục phức tạp như chúng ta muốn.

Công cụ bố cục đơn giản

Với thiết kế đó, chúng ta có thể xây dựng công cụ bố trí. Nếu có một lớp Itemgiữ chỗ cho a UIViewvà một lớp khác Layoutnhận một hoặc nhiều trong số này Itemvà ngay lập tức tính toán giá frametrị của mọi Item. Sau đó, chúng ta có thể sử dụng framegiá trị được tính toán khi xây dựng các UIViewđối tượng của mình.

Vì vậy, để tạo một chế độ xem phụ toàn màn hình, chúng ta có thể tạo dưới dạng:

let layout = Layout(parentFrame: frame, direction: .column)
let mainItem = try layout.add(item: .flexible)
let redView = SLECreateView(try mainItem.frame(), .red)
addSubview(redView)
private func SLECreateView(_ frame: CGRect, _ color: UIColor) -> UIView {
  let view = UIView(frame: frame)
  view.backgroundColor = color
  return view
}

let layout = Layout(parentFrame: frame, direction: .column, alignment: .center)
try layout.add(item: .flexible)
try layout.add(item: .height(200))

let topFrame = try layout.frame(at: 0)
let bottomFrame = try layout.frame(at: 1)

addSubview(SLECreateView(topFrame, .red))
addSubview(SLECreateView(bottomFrame, .blue))

let mainLayout = Layout(parentFrame: frame, direction: .column)
try mainLayout.add(items: [.flexible, .height(44), .height(200)])

let headerFrame = try mainLayout.frame(at: 0)
let toolbarFrame = try mainLayout.frame(at: 1)
let footerFrame = try mainLayout.frame(at: 2)

addSubview(SLECreateView(headerFrame, .red))
addSubview(SLECreateView(toolbarFrame, .blue))
addSubview(SLECreateView(footerFrame, .yellow))

let contentLayout = Layout(parentFrame: footerFrame, direction: .row)
try contentLayout.add(items: [.flexible, .flexible])
let content1Frame = try contentLayout.frame(at: 0)
let content2Frame = try contentLayout.frame(at: 1)

addSubview(SLECreateView(content1Frame, .cyan))
addSubview(SLECreateView(content2Frame, .magenta))

Việc triển khai công cụ bố trí này hóa ra không phức tạp bằng. Nếu chúng tôi cung cấp Itemcó thể có một số thuộc tính cố định và những thuộc tính khác linh hoạt.

public class Item {
    // no values fixed
    public static var flexible: Item { get }
    // partially fixed
    public static func width(_ value: CGFloat) -> Item
    public static func height(_ value: CGFloat) -> Item
    // all fixed
    public static func size(_ value: CGSize) -> Item
    // ...
}

public class Item {
  // ...

  public func frame() throws -> CGRect {
    return try rect.frame()
  }

  internal let originalWidth: CGFloat?
  internal let originalHeight: CGFloat?
  private let rect = Rect()

  private init(width: CGFloat?, height: CGFloat?) {
    originalWidth = width
    originalHeight = height
  }

  // called by layout engine
  func updateSize(value: CGFloat, 
                  in direction: Direction, 
                  parentSize: CGSize) { /* update rect */ }

  func updateOrigin(itemOrigin: CGPoint,
                    in direction: Direction, 
                    alignment: Alignment, 
                    parentFrame: CGRect) -> CGPoint { /* update rect */ }
}

private class Rect {
  internal private(set) var width: CGFloat?
  internal private(set) var height: CGFloat?
  private var x: CGFloat?
  private var y: CGFloat?

  // read back by Item
  func frame() throws -> CGRect {
    guard let originX = x, let originY = y, let width = width, let height = height else {
      throw LayoutError.itemIncomplete
    }
    return CGRect(x: originX, y: originY, width: width, height: height)
  }

  // set by layout engine
  func set(origin: CGPoint) {
    x = origin.x
    y = origin.y
  }

  // set by layout engine
  func set(size: CGSize) {
    width = size.width
    height = size.height
 }
}

extension Layout {
  public func add(item: Item) throws {
    items.append(item)
    try updateFrames()
  }
}

private extension Layout {
  func updateFrames() throws {

    // calculate total flex height
    var totalFlexSpace = parentFrame.height
    var flexItems = 0
    for item in items {
      if let space = item.originalHeight {
        totalFlexSpace -= space
      } else {
        flexItems += 1
      }
    }

    // calculate height per flex item
    let itemSpace = totalFlexSpace/CGFloat(max(flexItems, 1))
    guard itemSpace >= 0 else {
      throw LayoutError.outOfSpace
    }

    // update final frames per item
    var itemOrigin = parentFrame.origin
    for item in items {
      item.updateSize(value: itemSpace,
                      in: .column,
                      parentSize: parentFrame.size)
      itemOrigin = item.updateOrigin(itemOrigin: itemOrigin,
                                     in: .column,
                                     alignment: alignment,
                                     parentFrame: parentFrame)
    }
  }
}

Và giờ đây, dường như không khó để tưởng tượng việc hỗ trợ căn chỉnh cho các chế độ xem phụ (hiện tại tất cả chúng đều được đặt 0.0hoặc tất cả được căn chỉnh thành start) với nội dung như:

public enum Alignment {
  case leading
  case center
  case trailing
}

private extension Alignment {
  func align(parent: CGFloat, item: CGFloat) -> CGFloat {
    switch self {
    case .leading: return 0
    case .trailing: return (parent - item)
    case .center: return (parent - item) / 2.0
    }
  }
}

let offset = alignment.align(parent: parentFrame.height, item: rect.height)
y = parentFrame.origin.y + offset

Toàn bộ mã cho Simple Layout Enginecó sẵn tại:

Ngoài ra còn có một triển khai Objective-C mà tôi nghĩ có cách triển khai đơn giản hơn nhiều

Và cuối cùng là bài viết gốc: