【问题标题】:How to get 1 pixel width borders in UICollectionView cells? (code provided)如何在 UICollectionView 单元格中获得 1 像素宽度的边框? (提供代码)
【发布时间】:2016-01-08 23:12:40
【问题描述】:

如何在 UICollectionView 中获得一个像素完美的一 (1) 像素和边框线(例如制作月历)。问题是细胞相交的地方,它们的边界相交,所以它实际上是 2 个像素(不是 1 个像素)。

因此,这是一个与 CollectionView 布局相关的问题/问题。随着每个单元格放置自己的 1 像素边框的方式上升,当所有单元格以零间距相遇时,整体看起来像 2 像素边框(collectionView 的外边缘除外,它将是 1 像素)

寻求使用什么方法/代码来解决这个问题。

这是我使用的自定义 UICollectionViewCell 类,我在这里使用 drawrect 在单元格上设置了边框。

import UIKit

class GCCalendarCell: UICollectionViewCell {

    @IBOutlet weak var title : UITextField!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    override func drawRect(rect: CGRect) {
        self.layer.borderWidth = 1
        self.layer.borderColor = UIColor.redColor().CGColor
    }

}

【问题讨论】:

  • 您是否尝试将borderWidth 设置为0.5?
  • 0.5 高度依赖于屏幕密度。这应该适用于 Retina 屏幕 (@2x),但不适用于 Retina HD 屏幕 (@3x)。
  • 你可以像这里描述的那样创建装饰视图:stackoverflow.com/a/15796502/2035054
  • 侧节点:我建议在 init 方法中而不是在-drawRect: 中分配图层的边框颜色和宽度。如果没有进行自定义绘图,则不覆盖-drawRect: 会更有效,并且会导致每次绘制视图时都会冗余地分配这些属性。
  • @IgorPalaguta 谢谢 - 这是我一直在寻找的潜在客户类型 - 理想情况下,希望有一种方法可以让你开箱即用地更容易做到这一点,但如果不仅仅是为了学习这个跨度>

标签: ios uitableview uiview uicollectionview uicollectionviewlayout


【解决方案1】:

您可以采取一些策略来解决此问题。哪个最好取决于您的布局的一些细节。

选项 1:半边框

在每个项目周围绘制 1/2 所需宽度的边框。这已被建议。它实现了项目之间边框的正确宽度,但集合边缘周围的边框也将是所需宽度的一半。要解决此问题,您可以在整个集合或部分周围绘制边框。

如果您的月份布局仅呈现给定月份中的天数,这意味着每个月都有一个独特的非矩形形状,那么修复边缘边框比它的价值要复杂得多。

选项 2:重叠单元格

在每个项目周围绘制完整边框,但调整框架,使它们重叠一半边框宽度,隐藏冗余。

这是最简单的策略并且效果很好,但请注意以下几点:

  • 请注意,布局的外边缘也略有变化。如有必要,对此进行补偿。
  • 边框会重叠,因此不能使用半透明边框。
  • 如果不是所有边框的颜色都相同,则会丢失一些边缘。如果只有一个单元格具有不同的边框,您可以调整项目的 z-index 以使其位于其他单元格的顶部。

选项 3:手动绘图

完全不用在单元格上使用边框,您可以在容器周围添加边框,然后在超级视图中手动绘制边框。这可以通过覆盖drawRect(_:) 并使用UIBezierPaths 在单元格的间隔处绘制水平和垂直线来完成。这是最手动的选项。它为您提供更多控制权,但工作量更大。

选项 4:在每个单元格中仅绘制一些边框

与其指定图层的边框宽度和边框颜色(在整个视图周围绘制边框),不如选择性地绘制单元格的边缘。例如:每个单元格只能绘制它的底部和右边缘,除非它的上方或右侧没有单元格。

如果您的网格不规则,这可能会再次导致并发症。它还要求每个单元格了解它在网格中的上下文,这需要更多的结构。如果你走这条路,我建议子类化UICollectionViewLayoutAttributes 并添加关于应该绘制哪些边的附加信息。可以通过在drawRect(_:) 中为每个边缘创建UIBezierPath 来再次完成边缘绘制。

【讨论】:

  • 非常好 - 谢谢 Anthony - re 选项 4 我想那么会有办法告诉(在布局代码中)你当前在哪个单元格的位置?即,如果您无法获得单元格的动态当前位置,那么这将不起作用,因为您必须知道您是否位于视图边缘之一的单元格处
  • 是的。不管布局的细节如何,布局负责确定单元格的位置,所以它应该有必要的信息。如果您有自定义 UICollectionViewLayout,这将是最容易使用的。考虑日历:布局可以检查一天是一周的第一天还是一个月的第一周,并添加应该绘制左边框和上边框的信息。也可以使用子类UICollectionViewFlowLayout 来实现,但如何确定位置可能并不明显。
  • 感谢 Anthony - 只是为了理解,与使用 UICollectionViewLayout 的完整自定义类相比,使用 UICollectionViewFlowLayout 可能更难确定位置?
  • 使用UICollectionViewFlowLayout 是绝对可行的,但您有时需要对其提供的帧进行逆向工程,以确定单元格在网格中的位置。您可以通过子类化流布局或检查UICollectionViewController 中的布局来做到这一点。流布局实际上没有“列”的概念。相反,如果根据它们的大小在行中流动项目。如果所有项目的大小都相同,您将获得所需的行为,但请注意,您使用的工具比您需要的强大得多,这可能会导致未来的复杂化。
【解决方案2】:

我一般用边框宽度除以屏幕比例:

width = 1.0f / [UIScreen mainScreen].scale

【讨论】:

  • 虽然这种方法解决了单元格之间 1 点边界的问题,但边缘上的单元格将只有 1/2 点边界。
【解决方案3】:

使用 nativeScale!

CGFloat width = 1.0f / [UIScreen mainScreen].nativeScale;

【讨论】:

  • 我不确定这将如何解决我遇到的 UICollectiveView 布局问题?即问题是每个单元格绘制一个边框,给出 2 x 1 像素宽度
【解决方案4】:

关于非常彻底的接受答案,我认为这些方法中的任何一个都不是很干净。为了使它在所有场景中看起来都很好,您将要“手动”绘制网格,但在 drawRect 中这样做不是一个好主意 a) 因为这会在您的单元格 under 下绘制并且b) drawRect 每秒绘制屏幕 60 次,因此您会遇到大型滚动视图的性能问题。

更好的 IMO 是使用 CALayers。使用 CALayer,您可以将网格线拖放到视图上一次,然后它就保留在 GPU 上,因此即使是巨大的网格也可以以 60fps 的速度绘制。我还发现实施更容易、更灵活。这是一个工作示例:

class GridLineCollectionView: UICollectionView {

    private var gridLineContainer: CALayer!
    private var verticalLines = [CALayer]()
    private var horizontalLiines = [CALayer]()

    private var cols: Int {
        return Int(self.contentSize.width / self.itemSize.width)
    }
    private var rows: Int {
        return Int(self.contentSize.height / self.itemSize.height)
    }
    private var colWidth: CGFloat {
        return self.itemSize.width + self.sectionInsets.left + self.sectionInsets.right
    }
    private var rowHeight: CGFloat {
        return self.itemSize.height + self.sectionInsets.top + self.sectionInsets.bottom
    }
    private var itemSize: CGSize {
        return (self.collectionViewLayout as! UICollectionViewFlowLayout).itemSize
    }
    private var sectionInsets: UIEdgeInsets {
        return (self.collectionViewLayout as! UICollectionViewFlowLayout).sectionInset
    }


    override func layoutSubviews() {
        super.layoutSubviews()

        if self.gridLineContainer == nil {
            self.gridLineContainer = CALayer()
            self.gridLineContainer.frame = self.bounds
            self.layer.addSublayer(self.gridLineContainer)
        }

        self.addNecessaryGridLayers()
        self.updateVerticalLines()
        self.updateHorizontalLines()
    }

    private func addNecessaryGridLayers() {
        while self.verticalLines.count < cols - 1 {
            let newLayer = CALayer()
            newLayer.backgroundColor = UIColor.darkGray.cgColor
            self.verticalLines.append(newLayer)
            self.gridLineContainer.addSublayer(newLayer)
        }
        while self.horizontalLiines.count < rows + 1 {
            let newLayer = CALayer()
            newLayer.backgroundColor = UIColor.darkGray.cgColor
            self.horizontalLiines.append(newLayer)
            self.gridLineContainer.addSublayer(newLayer)
        }
    }

    private func updateVerticalLines() {
        for (idx,layer) in self.verticalLines.enumerated() {
            let x = CGFloat(idx + 1) * self.colWidth
            layer.frame = CGRect(x: x, y: 0, width: 1, height: CGFloat(self.rows) * self.rowHeight)
        }
    }

    private func updateHorizontalLines() {
        for (idx,layer) in self.horizontalLiines.enumerated() {
            let y = CGFloat(idx) * self.rowHeight
            layer.frame = CGRect(x: 0, y: y, width: CGFloat(self.cols) * self.colWidth, height: 1)
        }
    }
}

【讨论】:

    【解决方案5】:

    我通过继承UICollectionViewFlowLayout 并利用layoutAttributesForElementsInRect 方法解决了这个问题。有了这个,您可以设置自定义框架属性并允许重叠。这是最基本的例子:

    @interface EqualBorderCellFlowLayout : UICollectionViewFlowLayout <UICollectionViewDelegateFlowLayout>
    
    /*! @brief Border width. */
    @property (strong, nonatomic) NSNumber *borderWidth;
    
    @end
    
    #import "EqualBorderCellFlowLayout.h"
    
    @interface EqualBorderCellFlowLayout() { }
    @end
    
    @implementation EqualBorderCellFlowLayout
    
    - (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
        
        NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
        
        for (UICollectionViewLayoutAttributes *attribute in attributes) {
            
            if (self.borderWidth) {
                
                CGFloat origin_x = attribute.frame.origin.x;
                CGFloat origin_y = attribute.frame.origin.y;
                CGFloat width = attribute.frame.size.width;
                CGFloat height = attribute.frame.size.height;
                
                if (attribute.frame.origin.x == 0) {
                    width = width + self.borderWidth.floatValue;
                }
                
                height = height + self.borderWidth.floatValue;
                attribute.frame = CGRectMake(origin_x, origin_y, width, height);
                
            }
            
        }
        
        return attributes;
        
    }
    
    - (CGFloat)minimumLineSpacing {
        return 0;
    }
    
    - (CGFloat)minimumInteritemSpacing {
        return 0;
    }
    
    - (UIEdgeInsets)sectionInset {
        return UIEdgeInsetsMake(0, 0, 0, 0);
    }
    
    @end
    

    【讨论】:

    • 显然,就我在 layoutAttributesForElementsInRect 中使用的值而言,这可能不是适合您的正确解决方案,但可以根据需要进行更改。
    【解决方案6】:

    这里的主要挑战是,由于屏幕WIDTH限制,很难提供像素完美的边框/单元格测量值。即,几乎总是存在无法分割的剩余像素。例如,如果你想要一个内边框为 1px 的 3 列网格,屏幕宽度为 375px,则意味着每列宽度必须为 124.333 px,这是不可能的,所以如果向下取整并使用 124px 作为宽度,你将有 1 px 剩余。 (额外的微小像素对设计的影响有多大,这不是很疯狂吗?)

    一个选项是选择一个可以为您提供完美数字的边框宽度,但这并不理想,因为您不应该让机会决定您的设计。此外,它可能不会成为面向未来的解决方案。

    我研究了在 Instagram 上表现最好的应用程序,正如预期的那样,它们的边框看起来像素完美。我检查了每一列的宽度,结果发现最后一列的宽度不一样,这证实了我的怀疑。对于人眼来说,不可能分辨出网格列是窄还是宽了一个像素,但是当边框宽度不同时,它绝对可见。所以这种方法似乎是最好的

    起初我尝试使用单个可重复使用的单元格视图并根据是否是最后一列来更新宽度。这种方法有点工作,但单元格看起来有问题,因为它不适合调整单元格的大小。所以我只是创建了一个额外的可重复使用的单元格,它将放在最后一列以避免调整大小。

    首先,设置集合的宽度和间距,并为集合视图单元格注册 2 个可重复使用的标识符

    let borderWidth : CGFloat = 1
    let columnsCount : Int = 3
    let cellWidth = (UIScreen.main.bounds.width - borderWidth*CGFloat(columnsCount-1))/CGFloat(columnsCount)
    
    let layout = UICollectionViewFlowLayout()
    layout.estimatedItemSize = CGSize(width: cellWidth, height: cellWidth)
    layout.minimumLineSpacing = borderWidth
    layout.minimumInteritemSpacing = borderWidth
    
    collectionView = UICollectionView(frame: self.frame, collectionViewLayout: layout)
    collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: lastReuseIdentifier)
    collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
    

    然后创建一个辅助方法来查看某个索引路径的单元格是否在最后一列

    func isLastColumn(indexPath: IndexPath) -> Bool {
        return (indexPath.row % columnsCount) == columnsCount-1
    }
    

    为您的集合视图更新cellForItemAt 委托方法,以便使用最后一列单元格

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let identifier = isLastColumn(indexPath: indexPath) ? lastReuseIdentifier : reuseIdentifier
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
        return cell
    }
    

    最后,以编程方式更新单元格的大小,因此最后一列单元格会占用所有剩余的可用空间

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = isLastColumn(indexPath: indexPath) ? UIScreen.main.bounds.width-(cellWidth+borderWidth)*CGFloat(columnsCount -1) : cellWidth
        return CGSize(width: width, height: cellWidth)
    }
    

    最终结果应该是这样的(附赠食物图片):

    【讨论】:

    • 尽管你在这里写了正确的东西,OP 的问题是关于单元格的边框,而不是关于项目间的间距(想象一下单元格边框应该是黑色,而集合视图背景颜色是白色)
    • @sixl 这是问题的简单部分,哈哈。挑战是当 2 个单元格彼此相邻时避免“双边框”问题
    猜你喜欢
    • 2011-02-14
    • 1970-01-01
    • 1970-01-01
    • 2018-07-20
    • 2011-04-28
    • 1970-01-01
    • 2016-12-10
    • 2012-04-03
    • 1970-01-01
    相关资源
    最近更新 更多