Skip to content

Instantly share code, notes, and snippets.

@KrisYu
Created February 12, 2018 04:35
Show Gist options
  • Save KrisYu/3b0490081affce3fa49bc34c9b106100 to your computer and use it in GitHub Desktop.
Save KrisYu/3b0490081affce3fa49bc34c9b106100 to your computer and use it in GitHub Desktop.
//
// HorizontalScrollerView.swift
// RWBlueLibrary
//
// Created by Xue Yu on 2/4/18.
// Copyright © 2018 Razeware LLC. All rights reserved.
//
import UIKit
// 把 DataSource 和 Delegate 协议分开,这样更优雅,并且可以减少不必要的 @objc 的使用
// DataSource
protocol HorizontalScrollerViewDataSource: class {
// 在可以水平滚动的View里面有多少个view
func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int
// 既然是DataSource,滚到某个index的时候view
func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, viewAt index: Int) -> UIView
}
// Delegate
protocol HorizontalScrollerViewDelegate: class {
// 选择某个view的时候要做啥
func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, didSelectViewAt index: Int)
}
// 以上就可以看出来UITableView基本上是模板王者级别的,DataSource 和 Delegate 方法都跟它很像
// 定义
class HorizontalScrollerView: UIView {
//惯例 : weak var
weak var dataSource: HorizontalScrollerViewDataSource?
weak var delegate: HorizontalScrollerViewDelegate?
// 用 enum 来组织 Constants
private enum ViewConstants {
// Padding : 与边缘的距离
static let Padding: CGFloat = 10
// Dimensions : 大小
static let Dimensions: CGFloat = 100
// Offset :间距
static let Offset: CGFloat = 100
}
// 可滚动的灵魂 : scrollView
private let scroller = UIScrollView()
// 滚动内容的灵魂 : UIView们
private var contentViews = [UIView]()
// 这个init会在 programmatic init view时候被调用
override init(frame: CGRect) {
super.init(frame: frame)
initializeScrollView()
}
// 这个会在storyboard init是后被调用
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeScrollView()
}
// 某个意义上来说真正的init
func initializeScrollView(){
// HorizontalVie 是 scrollView 的delegate
// 因为我们需要滚 scrollView 然后调用一些方法
scroller.delegate = self
// 把 scroller 添加到HorizontalView
addSubview(scroller)
// 这个属性设置为false,我们才可以让scoller遵循以下的LayoutConstraint
scroller.translatesAutoresizingMaskIntoConstraints = false
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)
])
// 添加 tapRecognizer, 这样我们点击里面的某一个子view的时候,调用scrollerTapped 方法
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(scrollerTapped(gesture:)))
scroller.addGestureRecognizer(tapRecognizer)
}
// 这里做计算,centralView 是我们要把它移到中间去的View, 计算出offset,然后我们利用scroller把它移动到它该在的位置
// 给下面scrollerTapped用
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)
}
// 判定,如果 tap 的点在contentViews的某个,那么我们调用方法,告诉delegate 这个 view被选中了,同时我们把它移到中间去
@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)
}
// 便利方法,给上面的 scrollerTapped 用
func view(at index: Int) -> UIView {
return contentViews[index]
}
// 类似UITableView,如果 dataSource 改变,那么我们这个HorizontalView里面的东西也要改变
func reload() {
guard let dataSource = dataSource else {
return
}
// 移走原本的contentViews
contentViews.forEach { $0.removeFromSuperview() }
// 开始使用一系列常数计算, xValue是每个子View的水平位置
var xValue = ViewConstants.Offset
contentViews = (0..<dataSource.numberOfViews(in: self)).map {
index in
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)
// 计算出view的坐标, 添加到 scroller
scroller.addSubview(view)
xValue += ViewConstants.Dimensions + ViewConstants.Padding
return view
}
//这是极其重要的,如果忘了指定这个,scroller的默认大小就是 (0,0),不会显示
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, y: 0),
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, y:0), animated: true)
delegate?.horizontalScrollerView(self, didSelectViewAt: selectedIndex)
}
}
// 这个是用于当我们scroll 这个 scroller,会自动center CurrentView
// 实际上如果我们不使用 scroller.delegate = self
// 和如下方法,App某种程度上也是运行正常,但是水平滑动,并不会改变目前选中的view,并不是很科学
// 但是这个方法会“自动”设置滑动给应该中间的专辑到屏幕中央
extension HorizontalScrollerView: UIScrollViewDelegate {
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
centerCurrentView()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
centerCurrentView()
}
}
--------------------------------------------------------------------------------
Extension in ViewController
extension ViewController: HorizontalScrollerViewDelegate {
func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, didSelectViewAt index: Int) {
// 当我们选中一个view的时候,把之前选中的view highlight 给变成fasle,把我们选中的view highlight 变成 true,并且让对应的 UITableView 显示专辑数据
let previousAlbumView = horizontalScrollerView.view(at: currentAlbumIndex) as! AlbumView
previousAlbumView.highlightAlbum(false)
currentAlbumIndex = index
let albumView = horizontalScrollerView.view(at: currentAlbumIndex) as! AlbumView
albumView.highlightAlbum(true)
showDataForAlbum(at: index)
}
}
extension ViewController: HorizontalScrollerViewDataSource {
// DataSource 方法,同时也hightlight当前选中专辑index
func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int {
return allAlbums.count
}
func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, viewAt index: Int) -> UIView {
let album = allAlbums[index]
let albumView = AlbumView(frame: CGRect(x: 0, y: 0, width: 100, height:100), coverUrl: album.coverUrl)
if currentAlbumIndex == index {
albumView.highlightAlbum(true)
} else {
albumView.highlightAlbum(false)
}
return albumView
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment