效果圖

知識(shí)點(diǎn)
- 學(xué)習(xí) UIScrollView 和 UIPageControl 的基本用法
- 學(xué)習(xí) 三個(gè) UIImageView 實(shí)現(xiàn)無限循環(huán)滾動(dòng)思想
- 學(xué)習(xí) 我的 UIView 常用擴(kuò)展
- 學(xué)習(xí) 給我們的視圖設(shè)置代理事件
主要設(shè)計(jì)思想
概要介紹我在設(shè)計(jì)此控件的一些具體步驟與思想
- 創(chuàng)建 UIScrollView 作為滾動(dòng)視圖的容器;
- 創(chuàng)建 UIPageControl 作為顯示頁數(shù)以及總頁數(shù)的組件
- 在 UIScrollView 中創(chuàng)建三個(gè) UIImageView 分別作為 上一頁 當(dāng)前頁 下一頁
- 通過綁定數(shù)據(jù) data 的 didSet 來動(dòng)態(tài)部署 UIScrollView 中的 UIImageView 視圖。(實(shí)現(xiàn)無限循環(huán))
- 通過 Timer 實(shí)現(xiàn)定時(shí)跳轉(zhuǎn)到下一個(gè)視圖
- 調(diào)用 UIScrollView 擴(kuò)張 extension 實(shí)現(xiàn)具體操作細(xì)節(jié)
教程開始
- UIScrollView 使用
博主習(xí)慣創(chuàng)建組件的方式如下:
// fileprivate 是 Swift 3.0 新增加的訪問控制權(quán)限:文件內(nèi)訪問
// 封裝控件時(shí),我們要主動(dòng)的隱藏掉具體的實(shí)現(xiàn),只開放使用接口即可
fileprivate var scrollView: UIScrollView = {
let object = UIScrollView()
// 是否可以拉伸滑動(dòng)
object.bounces = false
// 隱藏 水平和垂直 滾動(dòng)條
object.showsVerticalScrollIndicator = false
object.showsHorizontalScrollIndicator = false
// 分頁
object.isPagingEnabled = true
return object
}()
- UIPageControl 使用
創(chuàng)建 UIPageControl 組件,構(gòu)建如下:
fileprivate var pageControl: UIPageControl = {
let object = UIPageControl()
// 單頁面下 隱藏
object.hidesForSinglePage = true
// 當(dāng)前頁背景色
object.currentPageIndicatorTintColor = UIColor.red
// 其他也背景色
object.pageIndicatorTintColor = UIColor.gray
return object
}()
- 主視圖配置
主要包括:主視圖的屬性配置、添加子視圖
private func prepareUI() {
self.backgroundColor = UIColor.white
self.scrollView.delegate = self
// 添加 滑動(dòng)試圖
self.addSubview(scrollView)
createScrollView()
// 添加 頁面控制
self.addSubview(pageControl)
}
private func layoutUI() {
scrollView.frame = self.bounds
pageControl.frame = CGRect(x: self.frame.width - 85, y: self.frame.height - 25, width: 80, height: 20)
scrollView.contentSize = CGSize(width: CGFloat(imageCount) * viewSize.width, height: viewSize.height)
}
- 構(gòu)建 UIScrollView 子視圖
重點(diǎn):通常情況下,開發(fā)者可能根據(jù)圖片數(shù)組的數(shù)量在 UIScrollView 中創(chuàng)建對(duì)應(yīng)數(shù)量的 UIImageView 擴(kuò)充 UIScrollView 的 contentSize 實(shí)現(xiàn)所有圖片的滑動(dòng);這種設(shè)計(jì)思路比較常規(guī),但有兩個(gè)問題:1.如果圖片數(shù)據(jù)過大,此控件將占用過大的內(nèi)存去創(chuàng)建視圖;2.很難實(shí)現(xiàn)無限循環(huán)滑動(dòng)。
我的思想:在 UIScrollView 中只創(chuàng)建三個(gè) UIImageView 作為 上一頁圖片 當(dāng)前頁圖片 下一頁圖片 的容器。通過判斷當(dāng)前頁所在 圖片數(shù)據(jù)中的位置,動(dòng)態(tài)為三個(gè) UIImageView 填充指定圖片。
// 創(chuàng)建 滑動(dòng)視圖子視圖
func createScrollView() {
for i in 0 ..< 3 {
let imageView: UIImageView = {
let object = UIImageView()
object.isUserInteractionEnabled = true
object.contentMode = UIViewContentMode.scaleToFill
return object
}()
imageView.frame = CGRect(x: CGFloat(i) * viewSize.width, y: 0, width: viewSize.width, height: viewSize.height)
let tap = UITapGestureRecognizer(target: self, action: #selector(touchImage))
imageView.addGestureRecognizer(tap)
scrollView.addSubview(imageView)
}
}
- 重點(diǎn):動(dòng)態(tài)更新 UIImageView 視圖實(shí)現(xiàn)無限滾動(dòng)
核心知識(shí):如果通過三個(gè) UIImageView 實(shí)現(xiàn) 無限圖片的 無限循環(huán)滑動(dòng):
func updateScrollView() {
// 遍歷三次
for i in 0 ..< 3 {
獲取 UIScrollView 中 UIImageView
let imageView = scrollView.subviews[i] as! UIImageView
// 獲取 當(dāng)前展示的圖片是 序號(hào)
var index = pageControl.currentPage
// 如果 UIImageView UIScrollView 中的第二個(gè)視圖,即 上一個(gè)圖片
if i == 0 {
// 則 index 等于 當(dāng)前圖片序號(hào) - 1
index -= 1
}
// 如果 UIImageView 是 UIScrollView 中的第三個(gè)視圖,即下一個(gè)圖片
if i == 2 {
// 則 index 等于 當(dāng)前圖片序號(hào) + 1
index += 1
}
// 越界操作 操作
if index < 0 {
index = pageControl.numberOfPages - 1
}
if index >= pageControl.numberOfPages {
index = 0
}
imageView.tag = index
if let currentData = data {
imageView.image = UIImage(named: currentData[index].imageUrl!)
}
}
// 調(diào)整 UIScrollView 偏移量 使其永遠(yuǎn)只顯示中間的 UIImageView
scrollView.contentOffset = CGPoint(x: viewSize.width, y: 0)
}
糊涂的童鞋,奉上我畫的示意圖
假如我們的圖片數(shù)據(jù)只有 6 張圖片;
紅色代表原始數(shù)組,藍(lán)色代表我們的 UIScrollView 兩邊越界是 填充收尾圖片

最后部分
設(shè)置定時(shí)器
// MARK: Timer
fileprivate func startTimer() {
let selector = #selector(nextImage)
timer = Timer(timeInterval: 3.0, target: self, selector: selector, userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode: RunLoopMode.commonModes)
}
fileprivate func stopTimer() {
timer?.invalidate()
timer = nil
}
擴(kuò)展完善交互時(shí)的事件
extension ImageScrollView: UIScrollViewDelegate {
// 當(dāng) scrollView 有偏移量時(shí)出發(fā)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var page: Int = 0
var minDistance: CGFloat = CGFloat(MAXFLOAT)
for i in 0 ..< 3 {
let imageView = scrollView.subviews[i] as! UIImageView
// 由于當(dāng)前頁有用都是中間頁,所以當(dāng) 偏移量==圖片寬度時(shí),就是當(dāng)前頁。
let distance:CGFloat = abs(imageView.x - scrollView.contentOffset.x)
if distance < minDistance {
minDistance = distance
page = imageView.tag
}
}
pageControl.currentPage = page
}
// 開始拖動(dòng) UIScrollView 事件
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
stopTimer()
}
// 結(jié)束拖動(dòng) UIScrollView 事件
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
startTimer()
}
//
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
updateScrollView()
}
// UIScrollView 結(jié)束滾動(dòng)
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
updateScrollView()
}
}
學(xué)會(huì)設(shè)置代理
具體步驟:
- 定義代理 delegate
- 配置代理事件
- 實(shí)例對(duì)象,調(diào)用對(duì)象代理
// 1
@objc protocol ImageScrollViewDelegate {
@objc optional func touchAt(index: Int)
}
// 2
class ImageScrollView: UIView {
var delegate: ImageScrollViewDelegate?
// 觸發(fā)代理
func touchImage(tap: UITapGestureRecognizer) {
if let index = tap.view?.tag {
delegate?.touchAt!(index: index)
}
}
...... }
// 3
// 實(shí)現(xiàn)代理:在調(diào)用 ImageScrollView 控件的 VC 中擴(kuò)展,具體看源碼
extension ImageScrollViewController: ImageScrollViewDelegate {
func touchAt(index: Int) {
print(index)
}
}
源碼
組件源碼
import UIKit
@objc protocol ImageScrollViewDelegate {
@objc optional func touchAt(index: Int)
}
class ImageScrollView: UIView {
var delegate: ImageScrollViewDelegate?
fileprivate var timer: Timer?
var viewSize: CGSize!
var data: [ImageScrollData]? {
didSet {
if timer != nil {
timer!.invalidate()
timer = nil
}
if let scrollData = data {
pageControl.numberOfPages = scrollData.count
pageControl.currentPage = 0
updateScrollView()
startTimer()
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.frame = frame
viewSize = frame.size
prepareUI()
layoutUI()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func prepareUI() {
self.backgroundColor = UIColor.white
self.scrollView.delegate = self
// 添加 滑動(dòng)試圖
self.addSubview(scrollView)
createScrollView()
// 添加 頁面控制
self.addSubview(pageControl)
}
private func layoutUI() {
scrollView.frame = self.bounds
pageControl.frame = CGRect(x: self.frame.width - 85, y: self.frame.height - 25, width: 80, height: 20)
scrollView.contentSize = CGSize(width: CGFloat(3) * viewSize.width, height: viewSize.height)
}
// 創(chuàng)建 滑動(dòng)視圖子視圖
func createScrollView() {
for i in 0 ..< 3 {
let imageView: UIImageView = {
let object = UIImageView()
object.isUserInteractionEnabled = true
object.contentMode = UIViewContentMode.scaleToFill
return object
}()
imageView.frame = CGRect(x: CGFloat(i) * viewSize.width, y: 0, width: viewSize.width, height: viewSize.height)
let tap = UITapGestureRecognizer(target: self, action: #selector(touchImage))
imageView.addGestureRecognizer(tap)
scrollView.addSubview(imageView)
}
}
func updateScrollView() {
for i in 0 ..< 3 {
let imageView = scrollView.subviews[i] as! UIImageView
var index = pageControl.currentPage
if i == 0 {
index -= 1
}
if i == 2 {
index += 1
}
if index < 0 {
index = pageControl.numberOfPages - 1
}
if index >= pageControl.numberOfPages {
index = 0
}
imageView.tag = index
if let currentData = data {
imageView.image = UIImage(named: currentData[index].imageUrl!)
}
}
scrollView.contentOffset = CGPoint(x: viewSize.width, y: 0)
}
// MARK: Timer
fileprivate func startTimer() {
let selector = #selector(nextImage)
timer = Timer(timeInterval: 3.0, target: self, selector: selector, userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode: RunLoopMode.commonModes)
}
fileprivate func stopTimer() {
timer?.invalidate()
timer = nil
}
func nextImage() {
scrollView.setContentOffset(CGPoint(x: 2.0 * viewSize.width, y: 0), animated: true)
}
func touchImage(tap: UITapGestureRecognizer) {
if let index = tap.view?.tag {
delegate?.touchAt!(index: index)
}
}
// 初始化 滑動(dòng)視圖
fileprivate var scrollView: UIScrollView = {
let object = UIScrollView()
object.bounces = false
object.showsVerticalScrollIndicator = false
object.showsHorizontalScrollIndicator = false
object.isPagingEnabled = true
return object
}()
fileprivate var pageControl: UIPageControl = {
let object = UIPageControl()
object.hidesForSinglePage = true
object.currentPageIndicatorTintColor = UIColor.red
object.pageIndicatorTintColor = UIColor.gray
return object
}()
}
extension ImageScrollView: UIScrollViewDelegate {
// 當(dāng) scrollView 有偏移量時(shí)出發(fā)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var page: Int = 0
var minDistance: CGFloat = CGFloat(MAXFLOAT)
for i in 0 ..< 3 {
let imageView = scrollView.subviews[i] as! UIImageView
// 由于當(dāng)前頁有用都是中間頁,所以當(dāng) 偏移量==圖片寬度時(shí),就是當(dāng)前頁。
let distance:CGFloat = abs(imageView.x - scrollView.contentOffset.x)
if distance < minDistance {
minDistance = distance
page = imageView.tag
}
}
pageControl.currentPage = page
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
stopTimer()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
startTimer()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
updateScrollView()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
updateScrollView()
}
}
extension UIView {
/// X值
open var x: CGFloat {
return self.frame.origin.x
}
/// Y值
open var y: CGFloat {
return self.frame.origin.y
}
/// 寬度
open var width: CGFloat {
return self.frame.size.width
}
///高度
open var height: CGFloat {
return self.frame.size.height
}
open var size: CGSize {
return self.frame.size
}
open var origin: CGPoint {
return self.frame.origin
}
}
調(diào)用的 ViewController
import UIKit
class ImageScrollViewController: UIViewController {
var imageScrollView = ImageScrollView(frame: CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: 150))
var data = [ImageScrollData]()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
edgesForExtendedLayout = .init(rawValue: 0)
self.title = "圖片無限滾動(dòng)"
self.view.addSubview(imageScrollView)
for i in 1 ... 6 {
let item = ImageScrollData(imageUrl: "image_scroll_0\(i).jpg", imageDescribe: nil)
data.append(item)
}
imageScrollView.data = data
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension ImageScrollViewController: ImageScrollViewDelegate {
func touchAt(index: Int) {
print(index)
}
}