简体   繁体   English

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

[英]Swift - UIView draw 1 pixel-width line

I'm trying to draw a line on UIView using Swift like that:我正在尝试使用 Swift 在 UIView 上画一条线,如下所示:

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

But drawn line is 2 pixels-width.但是绘制的线是 2 像素宽。 I tried another way:我尝试了另一种方式:

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

But result was the same.但结果是一样的。 What am I doing wrong and how to draw a 1 pixel-width line on UIView using Swift?我在做什么错以及如何使用 Swift 在 UIView 上绘制 1 像素宽度的线?

Two issues here:这里有两个问题:

  1. CoreGraphics drawing functions work in terms of points (the unit of screen layout which is constant in approximate physical size across all devices), not in pixels. CoreGraphics 绘图函数以点数(屏幕布局的单位,在所有设备上的近似物理尺寸不变)而不是以像素为单位工作。 The number of pixels per point is different on devices with different screen scales: iPad 2 and iPad mini are the only 1x devices supported in iOS 7 and later, iPhone 4, iPad 3, iPad mini 2 and later are 2x, except for iPhone 6/6s/7 Plus which is 3x.每个点的像素数在不同屏幕比例的设备上是不同的:iPad 2 和 iPad mini 是 iOS 7 及更高版本中唯一支持的 1x 设备,iPhone 4、iPad 3、iPad mini 2 及更高版本是 2x,iPhone 6 除外/6s/7 Plus 是 3 倍。 So if you want a one-device-pixel hairline, you need a 0.5-point line width on most current devices (and 0.33-point width on iPhone 6 Plus).因此,如果您想要一个设备像素的细线,则在大多数当前设备上都需要 0.5 磅的线宽(而在 iPhone 6 Plus 上则需要 0.33 磅的线宽)。

  2. The width of a line is centered in the square area of one point.一条线的宽度以一个点的正方形区域为中心。 So if you have a line from (10.0, 10.0) to (10.0, 20.0), it'll actually lie in between the pixels with x-coordinate 10.0 and 9.0 — when rendered, antialiasing will shade both the 10.0 and 9.0 columns of pixels at 50% of the line color, instead of shading one column at 100%.所以如果你有一条从 (10.0, 10.0) 到 (10.0, 20.0) 的线,它实际上会位于 x 坐标为 10.0 和 9.0 的像素之间——渲染时,抗锯齿会同时遮蔽 10.0 和 9.0 列的像素在线条颜色的 50% 处,而不是在 100% 处对一列进行着色。 To fix this, you need to position your line so it's entirely within a pixel.要解决此问题,您需要定位您的线条,使其完全位于一个像素内。 (A device pixel, not a layout point.) (设备像素,而不是布局点。)

So, to get a one-pixel hairline, you need to both reduce your line width and offset the points you're drawing by an amount that varies based on the scale factor of your screen.因此,要获得 1 像素的细线,您需要减小线宽,并将绘制的点偏移一定量,该量因屏幕的比例因子而异。 Here's an extension of your test case that does that:这是您的测试用例的扩展:

// 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()
}

The centerChoice calculation generalizes the issue of having to choose which sub-point pixel to draw your line on. centerChoice计算概括了必须选择在哪个子点像素上绘制线条的问题。 You have to draw through the center of the point (offset 0.5) to shade a whole pixel on a 1x display or shade only the middle pixel of the point on a 3x display, but on a 2x display offset 0.5 is in between the two pixels that make up one point.您必须通过点的中心(偏移 0.5)绘制以在 1x 显示器上为整个像素着色,或者在 3x 显示器上仅对点的中间像素进行着色,但在 2x 显示器上偏移 0.5 位于两个像素之间这构成了一点。 So for 2x, you have to choose offset 0.25 or offset 0.75 — the center line does that for even or odd scale factors in general.因此,对于 2x,您必须选择偏移 0.25 或偏移 0.75 — center通常对偶数或奇数比例因子执行此操作。

Note #1: I changed your test case to draw a vertical line, because it's easier to see the effect of antialiasing that way.注意#1:我将您的测试用例更改为绘制一条垂直线,因为这样更容易看到抗锯齿的效果。 A diagonal line will get some antialiasing no matter what, but a vertical or horizontal line will get no antialiasing if it's of the right width and in the right place.无论如何,对角线都会得到一些抗锯齿,但是如果垂直或水平线的宽度和位置正确,则不会得到抗锯齿。

Note #2: iPhone 6/6s/7 Plus has a logical scale of 3.0 and a physical display scale of about 2.61 — you might want to play around with screen.scale versus screen.nativeScale to see which gets you better looking results.注意 #2:iPhone 6/6s/7 Plus 的逻辑比例为 3.0,物理显示比例约为 2.61 — 您可能想尝试使用screen.scalescreen.nativeScale来查看哪个能让您获得更好的效果。

I'm use this code for view with 1px bottom line.我正在使用此代码查看底线为 1px 的视图。 Other examples work bad on x3 displays such as 6 Plus其他示例在 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()
    }
}

As additional of rickster answer.作为rickster 的补充回答。

You can use this class in storeboard:你可以在 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)
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM