提问人:Aniket Prakash 提问时间:7/27/2023 更新时间:8/2/2023 访问量:107
以编程方式使用 UIScrollView 的 Teaser 轮播视图
Teaser Carousel View using UIScrollView programatically
问:
我想使用 UIScrollView 实现一个预告片轮播视图(即视口中显示的部分左右视图以及居中的主视图)。
视图层次结构:
ViewController -> ScrollView -> Horizontal Stack View -> 和嵌入其中的 3 个视图
class ViewController: UIViewController {
override func viewDidLoad() {
let scroll = CarouselView(views: [])
view.addSubview(scroll)
scroll.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scroll.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scroll.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scroll.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
])
}
}
class CarouselView: UIView, UIScrollViewDelegate {
var views: [UIView] = []
init(views: [UIView]) {
super.init(frame: .zero)
self.views = views
scrollView.delegate = self
setupSubviews()
setupLayoutConstraints()
setupDummyViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupSubviews() {
addSubview(scrollView)
scrollView.addSubview(stackView)
}
private func setupLayoutConstraints() {
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: topAnchor),
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
scrollView.heightAnchor.constraint(equalToConstant: 150.0),
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
])
}
private func setupDummyViews() {
let view1 = UIView()
view1.backgroundColor = .red
let view2 = UIView()
view2.backgroundColor = .yellow
let view3 = UIView()
view3.backgroundColor = .green
let views = [view1, view2, view3]
for view in views {
stackView.addArrangedSubview(view)
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
])
}
}
// MARK: - UI Components
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = 0.0
stackView.distribution = .fillEqually
stackView.alignment = .fill
return stackView
}()
}
问题:
- 视图不可滚动。
- 左右部分视图未显示。请指导我!!包括仅处理 1、2 和 3 个视图的案例
答:
对于当前代码,无法滚动的原因是 的实例没有高度。scroll
CarouselView
默认情况下,a 具有 -- 因此您可以看到在视图框架之外延伸的任何子视图。UIView
.clipsToBounds = false
如果按原样设置并运行代码:scroll.clipsToBounds = true
let scroll = CarouselView(views: [])
scroll.clipsToBounds = true
你不会看到任何东西。
超出其超视图框架的视图无法接收触摸。因此,即使你可以看到它们,你也无法与它们互动。
您在约束设置中遗漏了重要的一行:
scrollView.topAnchor.constraint(equalTo: topAnchor),
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
scrollView.heightAnchor.constraint(equalToConstant: 150.0),
// you need this line to give a height to "self"
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
现在,您可以滚动(并且仍然使用 )。scroll.clipsToBounds = true
下一步是使轮播界面子视图小于整个视图宽度。但是,将页面设置为滚动视图的整个宽度。scroll
scrollView.isPagingEnabled = true
许多不同的方法可以解决这个问题,包括禁用和自行处理视图定位、减速等;使用带有计算内容插图的插图;等。.isPagingEnabled
UICollectionView
或者,我们可以变得有点棘手......
让我们将 scrollView 的 Width 设置为所需的“部分”宽度,如下所示 - 浅灰色是 Carousel 视图背景,滚动视图周围有一个黑色轮廓):
现在,当我们滚动时,我们看到这个:
启用分页后,它将“捕捉”到下一个子视图:
听起来我们是其中的一部分......但是,我们也希望看到部分上一个/下一个子视图。
因此,让我们设置:scrollView.clipsToBounds = false
但是,在尝试时,我们很快就会发现问题。
由于子视图在滚动视图的“可见但在框架之外”,因此我们只能通过在滚动视图本身(黑色轮廓内)拖动来滚动。
为了允许从视图的任何部分拖动,我们可以像这样实现(在 Carouself 视图类中):hitTest(...)
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
return scrollView
}
return super.hitTest(point, with: event)
}
现在,如果触摸在视图内部,我们告诉 scrollView 使用该触摸。如果它位于视图之外,我们将返回以允许触摸对任何其他 UI 元素执行操作。super...
这是一个完整的例子......
- 我正在控制器中设置“虚拟”视图,以便更轻松地测试 1、2、3 等视图。
- 我对您的约束进行了一些更改,以使用滚动视图的和
.contentLayoutGuide
.frameLayoutGuide
- 为了避免混淆,我还重命名了您的视图。
scroll
myCarouselView
View Controller 类
class CarouselViewController: UIViewController {
override func viewDidLoad() {
let colors: [UIColor] = [
.red, .green, .blue,
.cyan, .magenta, .yellow,
]
let numViews: Int = 3
var views: [UIView] = []
for i in 0..<numViews {
let v = UIView()
v.backgroundColor = colors[i % colors.count]
views.append(v)
}
let myCarouselView = CarouselView(views: views)
view.addSubview(myCarouselView)
myCarouselView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
myCarouselView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
myCarouselView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
myCarouselView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
])
// so we can see the view framing
myCarouselView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
}
}
CarouselView 类
class CarouselView: UIView, UIScrollViewDelegate {
var views: [UIView] = []
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
return scrollView
}
return super.hitTest(point, with: event)
}
init(views: [UIView]) {
super.init(frame: .zero)
self.views = views
scrollView.delegate = self
setupSubviews()
setupLayoutConstraints()
for view in views {
stackView.addArrangedSubview(view)
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
])
}
// so we can see the views that are outside the frame of the scroll view
scrollView.clipsToBounds = false
// let's give the scroll view a border to make it clear
scrollView.layer.borderWidth = 2
scrollView.layer.borderColor = UIColor.black.cgColor
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupSubviews() {
addSubview(scrollView)
scrollView.addSubview(stackView)
}
private func setupLayoutConstraints() {
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: topAnchor),
scrollView.heightAnchor.constraint(equalToConstant: 150.0),
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
scrollView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.60),
scrollView.centerXAnchor.constraint(equalTo: centerXAnchor),
stackView.topAnchor.constraint(equalTo: cg.topAnchor),
stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
stackView.heightAnchor.constraint(equalTo: fg.heightAnchor),
])
}
// MARK: - UI Components
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = 0.0
stackView.distribution = .fillEqually
stackView.alignment = .fill
return stackView
}()
}
编辑
如果您希望在轮播视图之间保持间距,请不要更改堆栈视图间距。
相反,请将子视图设计为包含间距。
例如,我们可以添加一个白色作为“可见框架”,并添加一个居中的标签作为子框。我们将用顶部和底部的 8 个点来约束白色视图,在前导和后跟上限制 4 个点。UIView
因此,单个视图将如下所示:
其中两个在 Zero-spacing 堆栈视图中并排显示,如下所示:
现在,子视图之间有 8 个点间距的视觉效果。
我们也可以稍微“设置”子视图的样式,如下所示:
因此,上面发布的类没有变化。CarouselView
我们将创建一个类的开头:CarouselCardView
class CarouselCardView: UIView {
let label: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.font = .systemFont(ofSize: 48.0, weight: .regular)
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// add a view with rounded corners be the "visible frame"
let rv = UIView()
rv.backgroundColor = .white
rv.layer.cornerRadius = 12
// let's give it a very light shadow
rv.layer.shadowOffset = .init(width: 0.0, height: 1.0)
rv.layer.shadowColor = UIColor.black.cgColor
rv.layer.shadowRadius = 2.0
rv.layer.shadowOpacity = 0.5
rv.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(rv)
addSubview(label)
NSLayoutConstraint.activate([
rv.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
rv.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4.0),
rv.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4.0),
rv.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
然后在示例视图控制器中,我们将创建以下实例,而不是具有不同颜色背景的“虚拟”视图:CarouselCardView
class CarouselViewController: UIViewController {
override func viewDidLoad() {
let numViews: Int = 5
var views: [UIView] = []
for i in 0..<numViews {
let v = CarouselCardView()
v.label.text = "\(i)"
views.append(v)
}
let myCarouselView = CarouselView(views: views)
view.addSubview(myCarouselView)
myCarouselView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
myCarouselView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
myCarouselView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
myCarouselView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
])
// so we can see the view framing
myCarouselView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
}
}
评论