我有一个自定义的 UIImageView,它的作用就像一个“轮播”,用户可以滑动它来查看图像(顺便说一下,我改编自 Medium 上的这篇优秀帖子。
我希望将角圆化为 20,但我找不到 imageView 内容模式的正确值。
我想要发生的是缩放图像以填充视图并保留其外观,我通常会使用 .scaleAspectFill 来实现。但由于这个自定义视图的设置方式,它变成了这种奇怪的混乱,正如你所看到的。
我已经粘贴了下面的自定义类 - 有人有什么想法吗?
class ImageCarouselView: UIView {
private var images: [UIImage?] = []
private var index = 0
private let screenWidth = UIScreen.main.bounds.width
var delegate: ImageCarouselViewDelegate?
lazy var previousImageView = imageView(image: nil, contentMode: .scaleAspectFit)
lazy var currentImageView = imageView(image: nil, contentMode: .scaleAspectFit)
lazy var nextImageView = imageView(image: nil, contentMode: .scaleAspectFit)
var topView = UIView()
lazy var previousImageLeadingConstraint: NSLayoutConstraint = {
return previousImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -screenWidth)
}()
lazy var currentImageLeadingConstraint: NSLayoutConstraint = {
return currentImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
}()
lazy var nextImageLeadingConstraint: NSLayoutConstraint = {
return nextImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: screenWidth)
}()
convenience init(_ images: [UIImage?]) {
self.init()
self.images = images
self.setUpActions()
}
init() {
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.heightAnchor.constraint(greaterThanOrEqualToConstant: 300).isActive = true
self.layer.cornerRadius = 20
self.clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func load(images: [UIImage?]) {
print("ImageCarouselView - Laod Images")
self.images = images
self.setUpActions()
}
private func setUpActions() {
setupLayout()
setupSwipeRecognizer()
setupImages()
}
private func setupLayout() {
self.subviews.forEach({ $0.removeFromSuperview() })
addSubview(previousImageView)
addSubview(currentImageView)
addSubview(nextImageView)
previousImageLeadingConstraint = previousImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -screenWidth)
currentImageLeadingConstraint = currentImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
nextImageLeadingConstraint = nextImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: screenWidth)
NSLayoutConstraint.activate([
previousImageLeadingConstraint,
previousImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
previousImageView.widthAnchor.constraint(equalToConstant: screenWidth),
currentImageLeadingConstraint,
currentImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
currentImageView.widthAnchor.constraint(equalToConstant: screenWidth),
nextImageLeadingConstraint,
nextImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
nextImageView.widthAnchor.constraint(equalToConstant: screenWidth),
])
}
private func setupImages() {
print(images.count)
guard images.count > 0 else { return }
currentImageView.image = images[self.index]
guard images.count > 1 else { return }
if (index == 0) {
previousImageView.image = images[images.count - 1]
nextImageView.image = images[index + 1]
}
if (index == (images.count - 1)) {
previousImageView.image = images[index - 1]
nextImageView.image = images[0]
}
}
private func setupSwipeRecognizer() {
guard images.count > 1 else { return }
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes))
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes))
leftSwipe.direction = .left
rightSwipe.direction = .right
self.addGestureRecognizer(leftSwipe)
self.addGestureRecognizer(rightSwipe)
}
@objc private func handleSwipes(_ sender: UISwipeGestureRecognizer) {
if (sender.direction == .left) {
showNextImage()
}
if (sender.direction == .right) {
showPreviousImage()
}
}
private func showPreviousImage() {
previousImageLeadingConstraint.constant = 0
currentImageLeadingConstraint.constant = screenWidth
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: { _ in
self.nextImageView = self.currentImageView
self.currentImageView = self.previousImageView
self.previousImageView = self.imageView(image: nil, contentMode: .scaleAspectFit)
self.index = self.index == 0 ? self.images.count - 1 : self.index - 1
self.delegate?.imageCarouselView(self, didShowImageAt: self.index)
self.previousImageView.image = self.index == 0 ? self.images[self.images.count - 1] : self.images[self.index - 1]
self.setupLayout()
})
}
private func showNextImage() {
nextImageLeadingConstraint.constant = 0
currentImageLeadingConstraint.constant = -screenWidth
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: { _ in
self.previousImageView = self.currentImageView
self.currentImageView = self.nextImageView
self.nextImageView = self.imageView(image: nil, contentMode: .scaleAspectFit)
self.index = self.index == (self.images.count - 1) ? 0 : self.index + 1
self.delegate?.imageCarouselView(self, didShowImageAt: self.index)
self.nextImageView.image = self.index == (self.images.count - 1) ? self.images[0] : self.images[self.index + 1]
self.setupLayout()
})
}
func imageView(image: UIImage? = nil, contentMode: UIImageView.ContentMode) -> UIImageView {
let view = UIImageView()
view.image = image
view.contentMode = .scaleAspectFit
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.init(white: 0.3, alpha: 1)
return view
}
}
创建“轮播”视图的方法有很多……这是一个有趣的方法。它不允许“左右拖动”——只能滑动——但如果这是期望的目标,那就没问题了。
有几件事它做错了......
第一:
private let screenWidth = UIScreen.main.bounds.width
是一个非常糟糕的主意。除非视图实际上是屏幕的整个宽度,否则该类将无法工作。它也不会适应框架变化(例如设备旋转)。而且,例如,如果应用程序在 iPad 上以多任务模式运行,它将会严重失败。
所以,让我们使用 view 宽度来代替,并在
layoutSubviews()
中更新它:
private var myWidth: CGFloat = 0.0
override func layoutSubviews() {
super.layoutSubviews()
// if the width has changed...
// this will be true on first layout
// and on frame change (such as device rotation)
if myWidth != bounds.width {
myWidth = bounds.width
// update image view positions
previousImageLeadingConstraint.constant = -myWidth
currentImageLeadingConstraint.constant = 0
nextImageLeadingConstraint.constant = myWidth
}
}
接下来,代码创建一个新的图像视图,并在每次滑动时完全重建视图层次结构...这比所需的处理量要多得多。
相反,我们可以重新定位现有图像视图并在滑动动画完成时更新其
.image
属性。
所以,如果我们假设我们以此开始(红色虚线是“幻灯片放映视图”的框架):
问题是——什么应该有圆角?
在动画制作过程中考虑这些图像:
您希望在动画过程中角点的外观将决定我们是否对图像视图、视图本身、两者都进行圆角处理,或者都不进行圆角处理。
这是您的
ImageCarouselView
类的修改版本:
class ImageCarouselView: UIView {
// public properties
public var cornerRadius: CGFloat = 32.0 { didSet { updateCorners() } }
public var animDuration: Double = 0.3
public var shouldRoundFrame: Bool = true { didSet { updateCorners() } }
public var shouldRoundImages: Bool = false { didSet { updateCorners() } }
public var delegate: ImageCarouselViewDelegate?
// private properties
private var images: [UIImage?] = []
private var index = 0
private var myWidth: CGFloat = 0.0
private lazy var previousImageView = imageView(image: nil, contentMode: .scaleAspectFill)
private lazy var currentImageView = imageView(image: nil, contentMode: .scaleAspectFill)
private lazy var nextImageView = imageView(image: nil, contentMode: .scaleAspectFill)
private var previousImageLeadingConstraint: NSLayoutConstraint!
private var currentImageLeadingConstraint: NSLayoutConstraint!
private var nextImageLeadingConstraint: NSLayoutConstraint!
convenience init(_ images: [UIImage?]) {
self.init()
self.images = images
self.setUpActions()
}
init() {
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.heightAnchor.constraint(greaterThanOrEqualToConstant: 300).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func load(images: [UIImage?]) {
print("ImageCarouselView - Laod Images")
self.images = images
self.setUpActions()
}
private func setUpActions() {
self.clipsToBounds = true
setupLayout()
setupSwipeRecognizer()
setupImages()
updateCorners()
}
private func setupLayout() {
// this should only get called once, on init
// so we shouldn't have any subviews
// but in case it gets called again...
self.subviews.forEach({ $0.removeFromSuperview() })
addSubview(previousImageView)
addSubview(currentImageView)
addSubview(nextImageView)
previousImageLeadingConstraint = previousImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -myWidth)
currentImageLeadingConstraint = currentImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)
nextImageLeadingConstraint = nextImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: myWidth)
NSLayoutConstraint.activate([
previousImageLeadingConstraint,
currentImageLeadingConstraint,
nextImageLeadingConstraint,
// all image views centered vertically
previousImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
currentImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
nextImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
// all image views have same width as self
previousImageView.widthAnchor.constraint(equalTo: self.widthAnchor),
currentImageView.widthAnchor.constraint(equalTo: self.widthAnchor),
nextImageView.widthAnchor.constraint(equalTo: self.widthAnchor),
// all image views have same height as self
previousImageView.heightAnchor.constraint(equalTo: self.heightAnchor),
currentImageView.heightAnchor.constraint(equalTo: self.heightAnchor),
nextImageView.heightAnchor.constraint(equalTo: self.heightAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
// if the width has changed...
// this will be true on first layout
// and on frame change (such as device rotation)
if myWidth != bounds.width {
myWidth = bounds.width
// update image view positions
previousImageLeadingConstraint.constant = -myWidth
currentImageLeadingConstraint.constant = 0
nextImageLeadingConstraint.constant = myWidth
}
}
private func setupImages() {
guard images.count > 0 else { return }
currentImageView.image = images[self.index]
guard images.count > 1 else { return }
if (index == 0) {
previousImageView.image = images[images.count - 1]
nextImageView.image = images[index + 1]
}
if (index == (images.count - 1)) {
previousImageView.image = images[index - 1]
nextImageView.image = images[0]
}
}
private func setupSwipeRecognizer() {
guard images.count > 1 else { return }
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes))
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes))
leftSwipe.direction = .left
rightSwipe.direction = .right
self.addGestureRecognizer(leftSwipe)
self.addGestureRecognizer(rightSwipe)
}
@objc private func handleSwipes(_ sender: UISwipeGestureRecognizer) {
if (sender.direction == .left) {
showNextImage()
}
if (sender.direction == .right) {
showPreviousImage()
}
}
private func showPreviousImage() {
// we're sliding previousImageView - currently "out-of-view on-the-left"
// from left-to-right to x: 0
previousImageLeadingConstraint.constant = 0
// we're sliding currentImageView - currently visible
// from left-to-right to x: width of self
currentImageLeadingConstraint.constant = myWidth
UIView.animate(withDuration: animDuration, delay: 0.0, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: { _ in
// move previousImageView back to "out-of-view on-the-left"
self.previousImageLeadingConstraint.constant = -self.myWidth
// move currentImageView back to x: 0
self.currentImageLeadingConstraint.constant = 0.0
// set nextImageView's image to current image
self.nextImageView.image = self.currentImageView.image
// set currentImageView's image to previous image
self.currentImageView.image = self.previousImageView.image
// update previousImageView's image based on indexing
self.index = self.index == 0 ? self.images.count - 1 : self.index - 1
self.delegate?.imageCarouselView(self, didShowImageAt: self.index)
self.previousImageView.image = self.index == 0 ? self.images[self.images.count - 1] : self.images[self.index - 1]
})
}
private func showNextImage() {
// we're sliding nextImageView - currently "out-of-view on-the-right"
// from right-to-left to x: 0
nextImageLeadingConstraint.constant = 0
// we're sliding currentImageView - currently visible
// from right-to-left to x: -width of self
currentImageLeadingConstraint.constant = -myWidth
UIView.animate(withDuration: animDuration, delay: 0.0, options: .curveEaseIn, animations: {
self.layoutIfNeeded()
}, completion: { _ in
// move nextImageView back to "out-of-view on-the-right"
self.nextImageLeadingConstraint.constant = self.myWidth
// move currentImageView back to x: 0
self.currentImageLeadingConstraint.constant = 0.0
// set previousImageView's image to current image
self.previousImageView.image = self.currentImageView.image
// set currentImageView's image to next image
self.currentImageView.image = self.nextImageView.image
// update nextImageView's image based on indexing
self.index = self.index == (self.images.count - 1) ? 0 : self.index + 1
self.delegate?.imageCarouselView(self, didShowImageAt: self.index)
self.nextImageView.image = self.index == (self.images.count - 1) ? self.images[0] : self.images[self.index + 1]
})
}
func imageView(image: UIImage? = nil, contentMode: UIImageView.ContentMode) -> UIImageView {
let view = UIImageView()
view.clipsToBounds = true
view.image = image
view.contentMode = .scaleAspectFill
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.init(white: 0.3, alpha: 1)
return view
}
private func updateCorners() {
// round the corners of self and the image views as specified
var r: CGFloat
r = self.shouldRoundFrame ? self.cornerRadius : 0.0
self.layer.cornerRadius = r
r = self.shouldRoundImages ? self.cornerRadius : 0.0
[previousImageView, currentImageView, nextImageView].forEach { v in
v.layer.cornerRadius = r
}
}
}
protocol ImageCarouselViewDelegate: NSObjectProtocol {
func imageCarouselView(_ imageCarouselView: ImageCarouselView, didShowImageAt index: Int)
}
和一个示例视图控制器:
class SlideShowViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var images: [UIImage] = []
["ss01", "ss02", "ss03","ss04",].forEach { sName in
guard let img = UIImage(named: sName) else {
fatalError("Could not load image: \(sName)")
}
images.append(img)
}
let slideshowView = ImageCarouselView(images)
slideshowView.delegate = self
slideshowView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(slideshowView)
let g = view.safeAreaLayoutGuide
// let's make the slideshowView frame
// 90% of the view width, with max of 600-points
// 300-points height
// centered horizontally and vertically
let maxWidth: CGFloat = 600.0
let targetW: NSLayoutConstraint = slideshowView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.9)
targetW.priority = .required - 1
NSLayoutConstraint.activate([
targetW,
slideshowView.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth),
slideshowView.heightAnchor.constraint(equalToConstant: 300.0),
slideshowView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
slideshowView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// change these to see the different "corner rounding"
slideshowView.shouldRoundFrame = true
slideshowView.shouldRoundImages = true
// if you want to adjust the animation speed
//slideshowView.animDuration = 1.0
}
}
extension SlideShowViewController: ImageCarouselViewDelegate {
func imageCarouselView(_ imageCarouselView: ImageCarouselView, didShowImageAt index: Int) {
// do something with index
print("didShow:", index)
}
}