One, foreword
In the previous part, we learned how to use UICollectionView to create a normal BannerView. In general products, in addition to displaying images, BannerView also needs to have the following small functions:
- Support left and right infinite cycle round seeding;
- Support for PageIndicator (a very small View component that is usually used with BannerView)
- Support timing switch (including animation);
- Support user manual touch, stop timing, and after the finger release, restart timing;
Let’s cut the crap and get to work.
Two, left and right infinite cycle round seeding
If you were reading the previous article, you might have noticed that the second argument in the initialization (convenience constructor) is loop: Bool. I wrote the last share, just left a “hole”, and did not realize the specific logic, however, the last article gives the source already, if there is a small partner has seen.
2.1. Add member variable loop
public class BannerPageView: UICollectionView UICollectionViewDelegate, UICollectionViewDataSource {/ / about whether to support an infinite loop, the default is true fileprivate var loop: Bool = true }Copy the code
2.2. Facilitate constructor assignment
// Extension BannerPageView {// Extension BannerPageView {// Extension BannerPageView public Convenience init(frame: CGRect, loop: Bool = true) { ...... // Call self.init. Designated keyword, convenience, the required "/ / https://juejin.cn/post/6932885089546141709. Self init (frame: Frame, collectionViewLayout: layout) // Whether infinite loop, default = true self.loop = loop...... }}Copy the code
2.3. Adjust the data source during input
Here’s a little more on how to make data loop indefinitely:
- Incoming source start data N;
- Modify the source data, insert the source data [n-1] in position 0, insert the source data [0] in position 0;
- Use the adjusted data as the dataSource of UICollectionView.
- When the data scrolls to the 0th position, adjust its subscript to the second-to-last position (no animation switch);
- When the data scrolls to the last position, adjust its subscript to the second positive-number position (no animation switch);
In this way, we can browse back and forth between [1 and n-2] to achieve an infinite loop; The code is as follows:
public class BannerPageView: UICollectionView.UICollectionViewDelegate.UICollectionViewDataSource {
.
public func setUrls(_ urls: [String]) {
// Original data: [a, b, c]
self.urls = urls
reData()
}
public func setLoop(_ loop: Bool) {
self.loop = loop
}
func reData(a) {
// If infinite loop is supported, the data becomes: [c, a, b, c, a]
if loop {
urls!.insert(urls!.last!, at: 0)
urls!.append(urls![1])
}
reloadData()
layoutIfNeeded()
if loop {
// If the loop is infinite, the index 0 is now 1 because two additional items are added before and after the data
scrollToItem(at: IndexPath(row: loop ? 1 : 0, section: 0),
at: UICollectionView.ScrollPosition(rawValue: 0),
// When repositioning subscripts, do not animate, otherwise the user will feel strange
animated: false)}}.
}
Copy the code
The above code is the adjustment made when the data is initially passed in; Or, if the data is later updated; At the same time, as I said above, every time we scroll the data, we need to determine whether we reach position 0 or position N-1, and if we do, we need to adjust; UICollectionView: UICollectionView delegate: UICollectionViewDelegate: UICollectionViewDelegate: UICollectionViewDelegate
public class BannerPageView: UICollectionView.UICollectionViewDelegate.UICollectionViewDataSource {
.
// MARK: UICollectionViewDelegate
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Calculate page subscript = horizontal scroll offset/width
var idx = Int(contentOffset.x / frame.size.width)
// If infinite loop is enabled, you need to determine whether to reposition after each scroll
if loop {
// Take [c, a, b, c, a] for example
if idx = = 0 {
// If idx == 0, it indicates that we have slipped to the leftmost c, and we need to scroll it to the second from the bottom.
scrollToItem(at: IndexPath(row: urls!.count - 2, section: 0),
at: UICollectionView.ScrollPosition(rawValue: 0),
animated: false)}else if idx = = urls!.count - 1 {
// If idx == last, indicating that it has slipped to the far right of a, we need to scroll it to the first bit.
scrollToItem(at: IndexPath(row: 1, section: 0),
at: UICollectionView.ScrollPosition(rawValue: 0),
animated: false)}}}.
}
Copy the code
PageIndicator
PageIndicator is well understood, which is to tell the user the scroll number of the current scroll chart, as shown below:
The dots in the red box:
- The number represents the number of pictures in the rotation graph;
- Pure white solid dot represents the current subscript;
- Translucent dots represent unselected states;
PageIndicator is also a custom widget (we learned how to draw circles and color in the AD page earlier), so here’s the code:
import UIKit
fileprivate let kGap: CGFloat = 5.0
// Translucent white background
fileprivate let kBgColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.5).cgColor
// Solid white background
fileprivate let kFgColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1).cgColor
class BannerPageIndicator: UIView {
var indicators: [CAShapeLayer] = []
var curIdx: Int = 0
// Add dots
public func addCircleLayer(_ nums: Int) {
if nums > 0 {
for _ in 0..<nums {
let circle = CAShapeLayer()
circle.fillColor = kBgColor
indicators.append(circle)
layer.addSublayer(circle)
}
}
}
// Count and center
public override func layoutSubviews(a) {
super.layoutSubviews()
let count = indicators.count
let d = bounds.height
let totalWidth = d * CGFloat(count) + kGap * CGFloat(count)
let startX = (bounds.width - totalWidth) / 2
for i in 0..<count {
let x = (d + kGap) * CGFloat(i) + startX
let circle = indicators[i]
circle.path = UIBezierPath(roundedRect: CGRect(x: x, y: 0, width: d, height: d), cornerRadius: d / 2).cgPath
}
setCurIdx(0)}// Sets the subscript of the currently displayed graph
public func setCurIdx(_ idx: Int) {
// Modify the current dot background (translucent)
indicators[curIdx].fillColor = kBgColor
// Modify the subscript
curIdx = idx
// Then modify the actual corresponding image subscript dot background (pure white)
indicators[curIdx].fillColor = kFgColor
}
}
Copy the code
4. BannerPageView associates with BannerPageIndicator
We already have two widgets, and their relationship is shown below:
When our BannerPageView switches, we need to call back to notify the BannerView, and the BannerView sets the indicator dot; In iOS, both OC and Swift are implemented via a Delegate (Protocol). Here, we define a BannerDelegate:
import Foundation
public protocol BannerDelegate: NSObjectProtocol {
func didPageChange(idx: Int)
}
Copy the code
4.1 BannerView implements delegation
import UIKit
public class BannerView: UIView.BannerDelegate {
fileprivate var banner: BannerPageView?
fileprivate var indicators: BannerPageIndicator?
public override init(frame: CGRect) {
super.init(frame: frame)
banner = BannerPageView(frame: frame, loop: true)
// Set the delegate to itself
banner?.bannerDelegate = self
addSubview(banner!)
indicators = BannerPageIndicator(frame: CGRect.zero)
indicators?.translatesAutoresizingMaskIntoConstraints = false
addSubview(indicators!)}required init?(coder: NSCoder) {
super.init(coder: coder)
}
public func setData(_ urls: [String]._ loop: Bool) {
banner?.setLoop(loop)
banner?.setUrls(urls)
adjustIndicator(urls.count)
}
// MARK: BannerDelegate
public func didPageChange(idx: Int) {
indicators?.setCurIdx(idx)
}
func adjustIndicator(_ count: Int) {
indicators?.addCircleLayer(count)
NSLayoutConstraint.activate([
indicators!.widthAnchor.constraint(equalToConstant: frame.width),
indicators!.heightAnchor.constraint(equalToConstant: 8),
indicators!.centerXAnchor.constraint(equalTo: centerXAnchor),
indicators!.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10)]}}Copy the code
4.2, Modify BannerPageView (delegate callback)
public class BannerPageView: UICollectionView.UICollectionViewDelegate.UICollectionViewDataSource {
var bannerDelegate: BannerDelegate?
.
// If the scrolling is cyclic, calculate whether to reposition after the scrolling ends
func redirectPosition(a) {
// Calculate page subscript = horizontal scroll offset/width
var idx = Int(contentOffset.x / frame.size.width)
// If infinite loop is enabled, you need to determine whether to reposition after each scroll
if loop {
// Take [c, a, b, c, a] for example
if idx = = 0 {
// If idx == 0, it indicates that we have slipped to the leftmost c, and we need to scroll it to the second from the bottom.
scrollToItem(at: IndexPath(row: urls!.count - 2, section: 0), at: UICollectionView.ScrollPosition(rawValue: 0), animated: false)
idx = urls!.count - 3
} else if idx = = urls!.count - 1 {
// If idx == last, indicating that it has slipped to the far right of a, we need to scroll it to the first bit.
scrollToItem(at: IndexPath(row: 1, section: 0), at: UICollectionView.ScrollPosition(rawValue: 0), animated: false)
idx = 0
} else {
idx - = 1
}
}
bannerDelegate?.didPageChange(idx: idx)
}
// MARK: UICollectionViewDelegate
// This method is called only when the user's finger touches the scroll
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
redirectPosition()
}
.
}
Copy the code
V. Timing switch (including animation)
For the AD page, we used the GCD Timer. Today, we will use another kind of Timer: Timer (Swift)/NSTimer (OC); Adding a timer to the Banner is easy (here’s a quick thumbs-up on Swift extension for code splitting) :
class BannerPageView: UICollectionView.UICollectionViewDelegate.UICollectionViewDataSource {
fileprivate var timer: Timer?
.
public func setUrls(_ urls: [String]) {
.
startTimer()
}
// MARK: UICollectionViewDelegate
// This method is executed when setContentOffset or scrollRectVisible is complete and animated = true
// Note: This method will not be called if animated = false
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
redirectPosition()
}
.
}
// Extension: handle timer
extension BannerPageView {
func startTimer(a) {
endTimer()
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [weak self] _ in
self?.next()
})
}
// End the timer
func endTimer(a) {
timer?.invalidate()
timer = nil
}
func next(a) {
let idx = Int(contentOffset.x / frame.size.width)
scrollToItem(at: IndexPath(row: idx + 1, section: 0),
at: UICollectionView.ScrollPosition(rawValue: 0),
animated: true)}}Copy the code
We already have the timer, however, there is a user experience problem: when the user fingers touch, because the timer is constantly triggered, it will still trigger the page turning, so we need to deal with:
- When the user touches, the timer stops;
- When the user releases, restart the timer;
The implementation is simple, we only need to deal with two methods in the UIScrollViewDelegate, as follows:
// Extension: handle timer
extension BannerPageView {
// User finger touch stop timer
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
endTimer()
}
// Restart the timer after release
func scrollViewDidEndDragging(_ scrollView: UIScrollView.willDecelerate decelerate: Bool) {
startTimer()
}
}
Copy the code
Handle click events
Banner clicking on this is easy, we just need to add Tap to the BannerView:
public class BannerView: UIView.BannerDelegate {
.
public override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap)))
.
}
@objc func handleTap(a) {
print("handleTap ==== \(String(describing: indicators?.curIdx))")}}Copy the code
Seven,
So much for Banner, to summarize:
- In this article, we inherit from UICollectionView. In actual development, we can also directly use UICollectionView as a BannerView.
- Because we are using a double window, our BannerView has already finished a page turn by the time the countdown (5s) ends. This is not a problem if you use a single window; (In real development, network requests are also involved, so single/dual Windows have their own advantages);
We learned UICollectionView through Banner, this is just the most basic usage, we will use more complex scenes in the later “floors”.
All source code so far: Passgate
If you have any questions, please contact us. Thank you!