尝试通过使用 UICollectionViewCompositionalLayout 实现固定高度、动态宽度(包装内容)水平 UICollectionView 的单元格
Posted
技术标签:
【中文标题】尝试通过使用 UICollectionViewCompositionalLayout 实现固定高度、动态宽度(包装内容)水平 UICollectionView 的单元格【英文标题】:Attempt to achieve fixed height, dynamic width (wrap content) horizontal UICollectionView's cell by using UICollectionViewCompositionalLayout 【发布时间】:2020-08-31 11:09:04 【问题描述】:在https://***.com/a/51231881/72437 中,它展示了如何使用UICollectionViewCompositionalLayout
在垂直UICollectionView
的单元格中实现全宽动态高度
我们希望在水平的UICollectionView
上实现相同的要求
-
固定高度 44
最小宽度 44
随着内容的增长,宽度应该动态放大
我们的解决方案如下所示
class MenuTabsView: UIView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
lazy var collView: UICollectionView =
let itemSize = NSCollectionLayoutSize(
widthDimension: NSCollectionLayoutDimension.estimated(44),
heightDimension: NSCollectionLayoutDimension.fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(
layoutSize: itemSize
)
item.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 0,
bottom: 0,
trailing: 1
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: itemSize,
subitem: item,
count: 1
)
let section = NSCollectionLayoutSection(group: group)
let configuration = UICollectionViewCompositionalLayoutConfiguration()
configuration.scrollDirection = .horizontal
let layout = UICollectionViewCompositionalLayout(section: section, configuration: configuration)
let cv = UICollectionView.init(frame: CGRect.zero, collectionViewLayout: layout)
cv.showsHorizontalScrollIndicator = false
cv.backgroundColor = .white
cv.delegate = self
cv.dataSource = self
return cv
()
我们期望通过使用widthDimension: NSCollectionLayoutDimension.estimated(44)
,这是使单元格宽度动态增长的关键。但是,这并没有按预期工作。好像
请问,我们如何使用UICollectionViewCompositionalLayout
来解决这个问题?完整的可行项目位于https://github.com/yccheok/PageViewControllerWithTabs/tree/UICollectionViewCompositionalLayout
p/s
我们希望避免使用UICollectionViewDelegateFlowLayout method collectionView(_:layout:sizeForItemAt:)
。因为,我们的单元格可以变得复杂,它会包含除UILabel
之外的其他视图。必须手动计算内容大小,会使解决方案不灵活且容易出错。
【问题讨论】:
【参考方案1】:我已经创造了你需要的东西。请按照以下步骤操作:
创建一个内部带有标签的单元格。 为标签添加尾随和前导约束(我的示例有 10 和 10)(重要) 将标签设置为垂直对齐。 将标签的宽度设置为 >= 44。您可以通过检查约束并从等于 >= 更改来做到这一点。完成此操作后,您需要创建布局。我有以下功能(在你的情况下,我的身高 50 只是改为 44):
func layoutConfig() -> UICollectionViewCompositionalLayout
return UICollectionViewCompositionalLayout (sectionNumber, env) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(44), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(44), heightDimension: .absolute(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
return section
然后在调用委托或数据源方法之前,在 viewDidLoad() 函数中,您需要调用以下内容(假设您保持相同的函数名称):
collectionView.collectionViewLayout = layoutConfig()
您应该得到以下结果。它水平滚动:
这里是示例的源代码:
https://github.com/williamfinn/HorizontalCollectionExample
【讨论】:
天哪!非常感谢!我想我注意到我的问题是调用NSCollectionLayoutGroup.horizontal( layoutSize:, subitem:, count: )
而不是你的NSCollectionLayoutGroup.horizontal(layoutSize:, subitems: )
。今晚我可以睡得更香,明天继续。非常感谢!【参考方案2】:
你似乎一遍又一遍地问同样的问题?
如果您先提出完整的问题,可能会有所帮助。
您说“我们的单元格内容可能会变得复杂”,但您没有提供任何有关“复杂”可能是什么的信息。
这里有一个例子可能会让你朝着正确的方向前进。
首先,输出...每个“选项卡”都有一个图像视图、一个标签和一个按钮,或者如下元素的组合:
// 1st tab is Image + Label + Button
// 2nd tab is Label + Button
// 3rd tab is Image + Label
// 4th tab is Image + Button
// 5th tab is Label Only
// 6th tab is Button Only
// 7th tab is Image Only
使用不同颜色的标签:
使用相同颜色的标签,除了“活动”标签:
在标签元素上使用背景颜色来查看框架:
我用它作为标签信息的结构:
struct TabInfo
var name: String? = ""
var color: Int = 0
var imageName: String? = ""
var buttonTitle: String? = ""
我在你的 GitHub 存储库中使用了这个:
class Utils
static func intToUIColor(argbValue: Int) -> UIColor
// & binary AND operator to zero out other color values
// >> bitwise right shift operator
// Divide by 0xFF because UIColor takes CGFloats between 0.0 and 1.0
let red = CGFloat((argbValue & 0xFF0000) >> 16) / 0xFF
let green = CGFloat((argbValue & 0x00FF00) >> 8) / 0xFF
let blue = CGFloat(argbValue & 0x0000FF) / 0xFF
let alpha = CGFloat((argbValue & 0xFF000000) >> 24) / 0xFF
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
视图控制器的示例代码:
class TabsTestViewController: UIViewController
// 1st tab is Image + Label + Button
// 2nd tab is Label + Button
// 3rd tab is Image + Label
// 4th tab is Image + Button
// 5th tab is Label Only
// 6th tab is Button Only
// 7th tab is Image Only
var tabs = [
TabInfo(name: "All", color: 0xff5481e6, imageName: "swiftBlue64x64", buttonTitle: "One"),
TabInfo(name: "Calendar", color: 0xff7cb342, imageName: nil, buttonTitle: "Two"),
TabInfo(name: "Home", color: 0xffe53935, imageName: "swiftBlue64x64", buttonTitle: nil),
TabInfo(name: nil, color: 0xfffb8c00, imageName: "swiftBlue64x64", buttonTitle: "Work"),
TabInfo(name: "Label Only", color: 0xffe00000, imageName: nil, buttonTitle: nil),
TabInfo(name: nil, color: 0xff008000, imageName: nil, buttonTitle: "Button Only"),
TabInfo(name: nil, color: 0xff000080, imageName: "swiftBlue64x64", buttonTitle: nil),
]
let menuTabsView: MenuTabsView =
let v = MenuTabsView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
()
let otherView: UIView =
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
return v
()
override func viewDidLoad()
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(menuTabsView)
view.addSubview(otherView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain menuTabsView Top / Leading / Trailing
menuTabsView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
menuTabsView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
menuTabsView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
// constrain otherView Leading / Trailing / Bottom
otherView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
otherView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
otherView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// constrain otherView Top to menuTabsView Bottom
otherView.topAnchor.constraint(equalTo: menuTabsView.bottomAnchor, constant: 0.0),
])
// un-comment to set all tab colors to green - 0xff7cb342
// except first tab
//for i in 1..<tabs.count
// tabs[i].color = 0xff7cb342
//
menuTabsView.dataArray = tabs
// set background color of "bottom bar" to first tab's background color
guard let tab = tabs.first else
return
menuTabsView.bottomBar.backgroundColor = Utils.intToUIColor(argbValue: tab.color)
MenuTabsView
的示例代码:
class MenuTabsView: UIView
var tabsHeight: CGFloat = 44
var dataArray: [TabInfo] = []
didSet
self.collView.reloadData()
lazy var collView: UICollectionView =
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 1
// needed to prevent last cell being clipped
layout.minimumInteritemSpacing = 1
layout.estimatedItemSize = CGSize(width: 100, height: self.tabsHeight)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
return cv
()
let bottomBar: UIView =
let v = UIView()
return v
()
override init(frame: CGRect)
super.init(frame: frame)
commonInit()
required init?(coder: NSCoder)
super.init(coder: coder)
commonInit()
func commonInit() -> Void
collView.translatesAutoresizingMaskIntoConstraints = false
bottomBar.translatesAutoresizingMaskIntoConstraints = false
addSubview(collView)
addSubview(bottomBar)
NSLayoutConstraint.activate([
// collection view constrained Top / Leading / Trailing
collView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
collView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
collView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// collection view Height constrained to 44
collView.heightAnchor.constraint(equalToConstant: tabsHeight),
// "bottom bar" constrained Leading / Trailing / Bottom
bottomBar.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
bottomBar.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
bottomBar.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
// "bottom bar" Height constrained to 4-pts
bottomBar.heightAnchor.constraint(equalToConstant: 4.0),
// collection view Bottom constrained to "bottom bar" Top
collView.bottomAnchor.constraint(equalTo: bottomBar.topAnchor),
])
collView.register(MyStackCell.self, forCellWithReuseIdentifier: "cell")
collView.dataSource = self
collView.delegate = self
collView.backgroundColor = .clear
backgroundColor = .white
extension MenuTabsView: UICollectionViewDelegate, UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int
return 1
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
return dataArray.count
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyStackCell
let t = dataArray[indexPath.item]
cell.configure(with: t.name, imageName: t.imageName, buttonTitle: t.buttonTitle, bkgColor: t.color)
//cell.configure(with: t.name, or: nil, or: "My Button")
return cell
最后是集合视图单元:
class MyStackCell: UICollectionViewCell
// can be set by caller to change default cell height
public var stackHeight: CGFloat = 36.0
private var stackHeightConstraint: NSLayoutConstraint!
private let label: UILabel =
let v = UILabel()
v.textAlignment = .center
return v
()
private let imageView: UIImageView =
let v = UIImageView()
return v
()
private let button: UIButton =
let v = UIButton()
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
return v
()
private let stack: UIStackView =
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.alignment = .center
v.spacing = 8
return v
()
override init(frame: CGRect)
super.init(frame: frame)
commonInit()
required init?(coder: NSCoder)
super.init(coder: coder)
commonInit()
func commonInit() -> Void
contentView.addSubview(stack)
// stack views in cells get cranky
// so we set priorities on desired element constraints to 999 (1 less than required)
// to avoid console warnings
var c = imageView.heightAnchor.constraint(equalToConstant: 32.0)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// image view has 1:1 ratio
c = imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// minimum width for label if desired
c = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// minimum width for button if desired
c = button.widthAnchor.constraint(greaterThanOrEqualToConstant: 44.0)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// height for stack view
stackHeightConstraint = stack.heightAnchor.constraint(equalToConstant: stackHeight)
stackHeightConstraint.priority = UILayoutPriority(rawValue: 999)
stackHeightConstraint.isActive = true
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4.0),
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4.0),
])
stack.addArrangedSubview(imageView)
stack.addArrangedSubview(label)
stack.addArrangedSubview(button)
// during development, so we can see the frames
// delete or comment-out when satisfied with layout
//imageView.backgroundColor = .yellow
//label.backgroundColor = .green
//button.backgroundColor = .blue
func customTabHeight(_ h: CGFloat) -> Void
stackHeightConstraint.constant = h
func configure(with name: String?, imageName: String?, buttonTitle: String?, bkgColor: Int?) -> Void
// set and show elements
// or hide if nil
if let s = imageName, s.count > 0
if let img = UIImage(named: s)
imageView.image = img
imageView.isHidden = false
else
imageView.isHidden = true
if let s = name, s.count > 0
label.text = s
label.isHidden = false
else
label.isHidden = true
if let s = buttonTitle, s.count > 0
button.setTitle(s, for: [])
button.isHidden = false
else
button.isHidden = true
if let c = bkgColor
backgroundColor = Utils.intToUIColor(argbValue: c)
override func layoutSubviews()
super.layoutSubviews()
// set the mask in layoutSubviews
let maskPath = UIBezierPath(roundedRect: bounds,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: 12.0, height: 12.0))
let shape = CAShapeLayer()
shape.path = maskPath.cgPath
layer.mask = shape
请注意,这只是示例代码!!!
它的目的不是为生产做好准备——它只是为了帮助你。
【讨论】:
感谢您的指点!我对您的解决方案非常感兴趣。我肯定想在明天之前花一天的时间进行彻底的尝试,因为你知道如何设法使用UICollectionViewDelegateFlowLayout
而不必实现collectionView(_:layout:sizeForItemAt:)
。现在,我不知道它是如何通过浏览代码来工作的。我以某种方式接受@williamfinn 的回答,因为他在UICollectionViewCompositionalLayout
上指出了我的错误。还是谢谢。【参考方案3】:
实现UICollectionViewDelegateFlowLayout
方法collectionView(_:layout:sizeForItemAt:)
方法根据你的文本返回单元格大小。
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
let text = "This is your text"
let size = text.size(withAttributes:[.font: UIFont.systemFont(ofSize:18.0)])
return CGSize(width: size.width + 10.0, height: 44.0)
【讨论】:
嗨,我们宁愿不使用这种方式。原因是,我们的单元格内容会变得复杂。以上是一个简单的例子。一旦单元格内容变得复杂,就没有简单的方法来衡量内容大小。 @CheokYanCheng 即使内容变得复杂,单元格宽度也必须有最大限制吧? 对不起。这不是我想要传达的信息。简而言之,我们只想告诉系统“根据当前内容包装此单元格宽度”。我们希望避免执行手动计算来测量内容大小。我们已经经历过,我们知道这样做很痛苦,而且容易出错。我们需要一个可靠的解决方案,而 UICollectionViewCompositionalLayout 似乎是正确的方向。以上是关于尝试通过使用 UICollectionViewCompositionalLayout 实现固定高度、动态宽度(包装内容)水平 UICollectionView 的单元格的主要内容,如果未能解决你的问题,请参考以下文章
尝试通过 Spring Boot Rest 使用 Jackson 验证 JSON
尝试使用特定用户通过 ssh 登录时出现“写入失败:管道损坏”