1. 程式人生 > Android開發 >?通過擼一個滾動圖來學習介面卡模式

?通過擼一個滾動圖來學習介面卡模式

什麼是介面卡模式

介面卡模式屬於結構型模式中的一種,使用的非常多,極為常見,本文通過擼一個滾動圖來學習介面卡模式,程式碼Swifty。

介面卡模式,重點適配二字,Apple的做法和原本有些不同,體現在UITableViewDelegate、UITableViewDataSource中。

需求

需求如下:

  • 點選頂部圖片時,下方的tableview展示對應的資訊
  • 傳入的圖片數量不固定
  • 需要有reload操作

使用介面卡模式實現

定義協議 DataSource、Delegate

資料來源

protocol HorizontalScrollerViewDataSource: class {
  // Ask the data source how many views it wants to present inside the horizontal scroller
  func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int
  // Ask the data source to return the view that should appear at <index>
  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView,viewAt index: Int) -> UIView
}
複製程式碼

互動

protocol HorizontalScrollerViewDelegate: class {
  // inform the delegate that the view at <index> has been selected
  func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView,didSelectViewAt index: Int)
}
複製程式碼

完整的 HorizontalScrollerView

class HorizontalScrollerView: UIView {
  weak var dataSource: HorizontalScrollerViewDataSource?
  weak var delegate: HorizontalScrollerViewDelegate?
  // 1
  private enum ViewConstants {
    static let Padding: CGFloat = 10
    static let Dimensions: CGFloat = 100
    static let Offset: CGFloat = 100
  }
  // 2
  private let scroller = UIScrollView()
  // 3
  private var contentViews = [UIView]()
  override init(frame: CGRect) {
    super.init(frame: frame)
    initializeScrollView()
  }
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    initializeScrollView()
  }
  func initializeScrollView() {
    scroller.delegate = self
    //1
    addSubview(scroller)
    //2
    scroller.translatesAutoresizingMaskIntoConstraints = false
    //3
    NSLayoutConstraint.activate([
      scroller.leadingAnchor.constraint(equalTo: self.leadingAnchor),scroller.trailingAnchor.constraint(equalTo: self.trailingAnchor),scroller.topAnchor.constraint(equalTo: self.topAnchor),scroller.bottomAnchor.constraint(equalTo: self.bottomAnchor)
      ])
    //4
    let tapRecognizer = UITapGestureRecognizer(target: self,action: #selector(scrollerTapped(gesture:)))
    scroller.addGestureRecognizer(tapRecognizer)
  }
  func scrollToView(at index: Int,animated: Bool = true) {
    let centralView = contentViews[index]
    let targetCenter = centralView.center
    let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2)
    scroller.setContentOffset(CGPoint(x: targetOffsetX,y: 0),animated: animated)
  }
  @objc func scrollerTapped(gesture: UITapGestureRecognizer) {
    let location = gesture.location(in: scroller)
    guard
      let index = contentViews.index(where: { $0.frame.contains(location)})
      else { return }
    delegate?.horizontalScrollerView(self,didSelectViewAt: index)
    scrollToView(at: index)
  }
  func view(at index :Int) -> UIView {
    return contentViews[index]
  }
  func reload() {
    // 1 - Check if there is a data source,if not there is nothing to load.
    guard let dataSource = dataSource else {
      return
    }
    //2 - Remove the old content views
    contentViews.forEach { $0.removeFromSuperview() }
    // 3 - xValue is the starting point of each view inside the scroller
    var xValue = ViewConstants.Offset
    // 4 - Fetch and add the new views
    contentViews = (0..<dataSource.numberOfViews(in: self)).map {
      index in
      // 5 - add a view at the right position
      xValue += ViewConstants.Padding
      let view = dataSource.horizontalScrollerView(self,viewAt: index)
      view.frame = CGRect(x: CGFloat(xValue),y: ViewConstants.Padding,width: ViewConstants.Dimensions,height: ViewConstants.Dimensions)
      scroller.addSubview(view)
      xValue += ViewConstants.Dimensions + ViewConstants.Padding
      // 6 - Store the view so we can reference it later
      return view
    }
    // 7
    scroller.contentSize = CGSize(width: CGFloat(xValue + ViewConstants.Offset),height: frame.size.height)
  }
  private func centerCurrentView() {
    let centerRect = CGRect(
      origin: CGPoint(x: scroller.bounds.midX - ViewConstants.Padding,size: CGSize(width: ViewConstants.Padding,height: bounds.height)
    )
    guard let selectedIndex = contentViews.index(where: { $0.frame.intersects(centerRect) })
      else { return }
    let centralView = contentViews[selectedIndex]
    let targetCenter = centralView.center
    let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2)
    scroller.setContentOffset(CGPoint(x: targetOffsetX,animated: true)
    delegate?.horizontalScrollerView(self,didSelectViewAt: selectedIndex)
  }
}
extension HorizontalScrollerView: UIScrollViewDelegate {
  func scrollViewDidEndDragging(_ scrollView: UIScrollView,willDecelerate decelerate: Bool) {
    if !decelerate {
      centerCurrentView()
    }
  }
  func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    centerCurrentView()
  }
}
複製程式碼

外部使用

設定代理 與資料重新整理

horizontalScrollerView.dataSource = self
horizontalScrollerView.delegate = self
horizontalScrollerView.reload()
複製程式碼

滾動到某一行

horizontalScrollerView.scrollToView(at: currentAlbumIndex,animated: false)
複製程式碼

總結

適配是一種對映關係,邏輯如下:

A --->  介面卡 ---> A'
複製程式碼

傳統做法:通常情況下介面卡模式是:A提供的介面不滿足B,使用介面卡C,來轉換A的介面,使得B能夠滿足。

apple的做法:通過協議向介面卡中傳入引數,從而得到想要的結果,省略了C,B直接操作A的介面,就能達到適配的目的。

如果讓你做這個需求的話,你怎麼設計?會使用介面卡模式嗎?如有不同見解入群solo。

其他

  • 參考文章
  • 程式碼下載

本文使用 mdnice 排版