Swift - UIView 绘制 1 像素宽度的线

Posted

技术标签:

【中文标题】Swift - UIView 绘制 1 像素宽度的线【英文标题】:Swift - UIView draw 1 pixel-width line 【发布时间】:2014-12-09 18:14:52 【问题描述】:

我正在尝试像这样使用 Swift 在 UIView 上画一条线:

CGContextSetLineWidth(context, 1.0)
CGContextBeginPath(context)
    CGContextMoveToPoint(context, x1, y1)
    CGContextAddLineToPoint(context, x2, y2)
CGContextStrokePath(context)

但绘制的线是 2 像素宽。我尝试了另一种方式:

CGContextSetLineWidth(context, 0.5)
CGContextBeginPath(context)
    CGContextMoveToPoint(context, x1, y1)
    CGContextAddLineToPoint(context, x2, y2)
CGContextStrokePath(context)

但结果是一样的。 我在做什么错以及如何使用 Swift 在 UIView 上绘制 1 像素宽的线?

【问题讨论】:

只是好奇;你怎么知道它是 2 像素? CGContextSetLineWidth(context, 1) - the width is almost alwayas at least 2 pixels instead 1的可能重复 不错的重复问题候选,但我很惊讶没有完整且正确的标记答案。不过,我可能找不到另一个重复项。 @Unheilig,很简单!我做了一个截图并放大了它。 How to Draw a single point line in ios的可能重复 【参考方案1】:

这里有两个问题:

    CoreGraphics 绘图函数以点(屏幕布局的单位,在所有设备的近似物理尺寸上保持不变)而不是以像素为单位工作。不同屏幕比例的设备上每点的像素数不同:iOS 7 及更高版本仅支持 iPad 2 和 iPad mini 的 1x 设备,iPhone 4、iPad 3、iPad mini 2 及更高版本为 2x,iPhone 6 除外/6s/7 Plus 是 3 倍。因此,如果您想要一个设备像素的细线,则在大多数当前设备上需要 0.5 磅的线宽(在 iPhone 6 Plus 上需要 0.33 磅的线宽)。

    一条线的宽度以一个点的正方形区域为中心。因此,如果您有一条从 (10.0, 10.0) 到 (10.0, 20.0) 的线,它实际上会位于 x 坐标为 10.0 和 9.0 的像素之间——渲染时,抗锯齿将对 10.0 和 9.0 列像素进行着色在线条颜色的 50% 处,而不是在 100% 处对一列进行着色。要解决这个问题,您需要定位您的线条,使其完全在一个像素内。 (设备像素,而不是布局点。)

因此,要获得 1 像素的细线,您既需要减小线宽,又需要将绘制的点偏移一定量,该量取决于屏幕的比例因子。这是您的测试用例的扩展:

// pass in the scale of your UIScreen
func drawHairline(in context: CGContext, scale: CGFloat, color: CGColor) 

    // pick which row/column of pixels to treat as the "center" of a point
    // through which to draw lines -- favor true center for odd scales, or
    // offset to the side for even scales so we fall on pixel boundaries
    let center: CGFloat
    if Int(scale) % 2 == 0 
        center = 1 / (scale * 2)
     else 
        center = 0
    

    let offset = 0.5 - center // use the "center" choice to create an offset
    let p1 = CGPoint(x: 50 + offset, y: 50 + offset)
    let p2 = CGPoint(x: 50 + offset, y: 75 + offset)

    // draw line of minimal stroke width
    let width = 1 / scale
    context.setLineWidth(width)
    context.setStrokeColor(color)
    context.beginPath()
    context.move(to: p1)
    context.addLine(to: p2)
    context.strokePath()

centerChoice 计算概括了必须选择在哪个子点像素上画线的问题。您必须绘制通过点的中心(偏移量 0.5)才能在 1x 显示器上遮蔽整个像素,或者在 3x 显示器上仅遮蔽点的中间像素,但在 2x 显示器上,偏移量 0.5 在两个像素之间这弥补了一点。因此,对于 2x,您必须选择偏移 0.25 或偏移 0.75 — center 行通常用于偶数或奇数比例因子。

注意 #1:我将您的测试用例更改为画一条垂直线,因为这样更容易看到抗锯齿的效果。无论如何,对角线都会得到一些抗锯齿,但如果垂直或水平线的宽度和位置正确,则不会得到抗锯齿。

注意 #2:iPhone 6/6s/7 Plus 的逻辑比例为 3.0,物理显示比例约为 2.61——你可能想玩一下screen.scalescreen.nativeScale,看看哪个让你看起来更好看结果。

【讨论】:

我所做的一切都和你写的完全一样。 Here is my drawRect function,但结果是一样的,you can look at it here。如您所见,两条线的宽度均为 2 像素。我的设备是 iPhone 4S,所以宽度 0.5 应该可以工作,但事实并非如此。 我的错——我用原始数字测试了 offsetwidth 的代码错误。 (通用代码仅适用于 3x 显示器。)在 2x 显示器上,您必须选择两个像素中的哪一个来绘制一个点(它不能居中,因为它在像素之间),所以 offset 有为 0.25 或 0.75。 @Yuri:更新答案以在广义计算中包含 2x。你也可以用幻数来做到这一点,但我的 CS 教授总是告诉我这些很糟糕。 :) offset 的这种计算适用于@1-3x,但它不能一概而论。如果有@4x,我会感到惊讶,但为了缓解我的强迫症:let offset = 0.5 - (Int(scale) % 2 == 0 ? 1 / (scale * 2) : 0)【参考方案2】:

我使用此代码查看底线为 1px 的视图。其他示例在 x3 显示器(例如 6 Plus)上效果不佳

import UIKit

class ViewWithBottomLine: UIView 
    @IBInspectable var separatorColor: UIColor = Your default color

    override func draw(_ rect: CGRect) 
        super.draw(rect)

        guard let context = UIGraphicsGetCurrentContext() else 
            return
        

        let scale = UIScreen.main.scale

        let width = 1 / scale
        let offset = width / 2

        context.setLineWidth(width)
        context.setStrokeColor(separatorColor.cgColor)
        context.beginPath()
        context.move(to: CGPoint(x: 0, y: rect.maxY - offset))
        context.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - offset))
        context.strokePath()
    

【讨论】:

【参考方案3】:

作为 rickster 答案的补充。

你可以在 storeboard 中使用这个类:

/** View that have line with 1px on the top */
class lineView :UIView 

    // pass in the scale of your UIScreen
    func drawHairline(in context: CGContext, scale: CGFloat, color: CGColor) 

        // pick which row/column of pixels to treat as the "center" of a point
        // through which to draw lines -- favor true center for odd scales, or
        // offset to the side for even scales so we fall on pixel boundaries
        let center: CGFloat
        if Int(scale) % 2 == 0 
            center = 1 / (scale * 2)
         else 
            center = 0
        

        let offset = 0.5 - center // use the "center" choice to create an offset
        let p1 = CGPoint(x: 0, y: 0 + offset)
        let p2 = CGPoint(x: 800 + offset, y: 0 + offset)

        // draw line of minimal stroke width
        let width = 1 / scale
        context.setLineWidth(width)
        context.setStrokeColor(color)
        context.beginPath()
        context.move(to: p1)
        context.addLine(to: p2)
        context.strokePath()
    

    override func draw(_ rect: CGRect) 
        let context = UIGraphicsGetCurrentContext()
        drawHairline(in: context!, scale: 2, color: UIColor.gray.cgColor)
    

【讨论】:

以上是关于Swift - UIView 绘制 1 像素宽度的线的主要内容,如果未能解决你的问题,请参考以下文章

Swift 中的 UIScrollView 内的 UIPanGesture

使用 C/C++ 和 WinApi 精确绘制 1 像素宽度的线

如何将 UIView 中绘制的像素与 UIImageView 中的图像结合起来,并将其保存为新图像?

[FMX]在 FMX 程序中绘制单像素宽度的直线 [FMX]在 FMX 程序中绘制单像素宽度的直线

如何在 Swift 中使用 SnapKit 使 UIView 具有相等的宽度和高度?

在 Swift 中的 UIView 上绘制网格