天天看點

iOS 繪制1像素的線

一、Point Vs Pixel

iOS中當我們使用Quartz,UIKit,CoreAnimation等架構時,所有的坐标系統采用Point來衡量。系統在實際渲染到設定時會幫助我們處理Point到Pixel的轉換。

這樣做的好處隔離變化,即我們在布局的事後不需要關注目前裝置是否為Retina,直接按照一套坐标系統來布局即可。

實際使用中我們需要牢記下面這一點:

One point does not necessarily correspond to one physical pixel.      

1 Point的線在非Retina螢幕則是一個像素,在Retina螢幕上則可能是2個或者3個,取決于系統裝置的DPI。

iOS系統中,UIScreen,UIView,UIImage,CALayer類都提供相關屬性來擷取scale factor。

原生的繪制技術天然的幫我們處理了scale factor,例如在drawRect:方法中,UIKit自動的根據目前運作的裝置設定了正切的scale factor。是以我們在drawRect: 方法中繪制的任何内容都會被自動縮放到裝置的實體螢幕上。

基于以上資訊可以看出,我們大部分情況下都不需要去關注pixel,然而存在部分情況需要考慮像素的轉化。

例如畫1個像素的分割線      

看到這個問題你的第一想法可能是,直接根據目前螢幕的縮放因子計算出1 像素線對應的Point,然後設定線寬即可。

代碼如下:

1.0f / [UIScreen mainScreen].scale      

表面上看着一切正常了,但是通過實際的裝置測試你會發現渲染出來的線寬并不是1個像素。

Why?

為了獲得良好的視覺效果,繪圖系統通常都會采用一個叫“antialiasing(反鋸齒)”的技術,iOS也不例外。

顯示螢幕有很多小的顯示單元組成,可以接單的了解為一個單元就代表一個像素。如果要畫一條黑線,條線剛好落在了一列或者一行顯示顯示單元之内,将會渲染出标準的一個像素的黑線。

但如果線落在了兩個行或列的中間時,那麼會得到一條“失真”的線,其實是兩個像素寬的灰線。

如下圖所示:

iOS 繪制1像素的線
Positions defined by whole-numbered points fall at the midpoint between pixels. For example, if you draw a one-pixel-wide vertical line from (1.0, 1.0) to (1.0, 10.0), you get a fuzzy grey line. If you draw a two-pixel-wide line, you get a solid black line because it fully covers two pixels (one on either side of the specified point). As a rule, lines that are an odd number of physical pixels wide appear softer than lines with widths measured in even numbers of physical pixels unless you adjust their position to make them cover pixels fully.      

官方解釋如上,簡單翻譯一下:

規定:奇數像素寬度的線在渲染的時候将會表現為柔和的寬度擴充到向上的整數寬度的線,除非你手動的調整線的位置,使線剛好落在一行或列的顯示單元内。      

如何對齊呢?

On a low-resolution display (with a scale factor of 1.0), a one-point-wide line is one pixel wide. To avoid antialiasing when you draw a one-point-wide horizontal or vertical line, if the line is an odd number of pixels in width, you must offset the position by 0.5 points to either side of a whole-numbered position. If the line is an even number of points in width, to avoid a fuzzy line, you must not do so.
On a high-resolution display (with a scale factor of 2.0), a line that is one point wide is not antialiased at all because it occupies two full pixels (from -0.5 to +0.5). To draw a line that covers only a single physical pixel, you would need to make it 0.5 points in thickness and offset its position by 0.25 points. A comparison between the two types of screens is shown in Figure 1-4.      

翻譯一下

在非高清屏上,一個Point對應一個像素。為了防止“antialiasing”導緻的奇數像素的線渲染時出現失真,你需要設定偏移0.5 Point。
在高清螢幕上,要繪制一個像素的線,需要設定線寬為0.5個Point,同僚設定偏移為0.25 Point。
如果線寬為偶數Point的話,則不要去設定偏移,否則線條也會失真。      
iOS 繪制1像素的線

看了上述一通解釋,我們了解了1像素寬的線條失真的原因,及解決辦法。

至此問題貌似都解決了?再想想為什麼在非Retina和Retina螢幕上調整位置時值不一樣,前者為0.5Point,後者為0.25Point,那麼scale為3的6 Plus裝置又該調整多少呢?

要回答這個問題,我們需要了解調整多少依舊什麼原則。

iOS 繪制1像素的線

再回過頭來看看這上面的圖檔,圖檔中每一格子代表一個像素,而頂部标記的則代碼我們布局時的坐标。

可以看到左邊的非Retina螢幕,我們要在(3,0)這個位置畫一條一個像素寬的豎線時,由于渲染的最小機關是像素,而(3,0)這個坐标恰好位于兩個像素中間,此時系統會對坐标3左右兩列的像素對填充,為了不至于線顯得太寬,為對線的顔色淡化。那麼根據上述資訊我們可以得出,如果要畫出一個像素寬的線,就得把繪制的坐标移動到(2.5, 0)或者(3.5,0)這個位置,這樣系統渲染的時候剛好可以填充一列像素,也就是标準的一個像素的線。

基于上面的分析,我們可以得出“Scale為3的6 Plus”裝置如果要繪制1個像素寬的線條時,位置調整也應該是0.5像素,對應該的Point計算如下:

(1.0f / [UIScreen mainScreen].scale) / 2;      

奉上一個畫一像素線的一個宏:

#define SINGLE_LINE_WIDTH           (1 / [UIScreen mainScreen].scale)
#define SINGLE_LINE_ADJUST_OFFSET   ((1 / [UIScreen mainScreen].scale) / 2)      

使用代碼如下:

CGFloat xPos = 5;
  UIView *view = [[UIView alloc] initWithFrame:CGrect(x - SINGLE_LINE_ADJUST_OFFSET, 0, SINGLE_LINE_WIDTH, 100)];      

二、正确的繪制Grid線條

貼上一個寫的GridView的代碼,代碼中對Grid線條的奇數像素做了偏移,防止出現線條模糊的情況。

SvGridView.h

//
//  SvGridView.h
//  SvSinglePixel
//
//  Created by xiaoyong.cxy on 6/23/15.
//  Copyright (c) 2015 smileEvday. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface SvGridView : UIView

/**
 * @brief 網格間距,預設30
 */
@property (nonatomic, assign) CGFloat   gridSpacing;

/**
 * @brief 網格線寬度,預設為1 pixel (1.0f / [UIScreen mainScreen].scale)
 */
@property (nonatomic, assign) CGFloat   gridLineWidth;

/**
 * @brief 網格顔色,預設藍色
 */
@property (nonatomic, strong) UIColor   *gridColor;

@end      

SvGridView.m

//
//  SvGridView.m
//  SvSinglePixel
//
//  Created by xiaoyong.cxy on 6/23/15.
//  Copyright (c) 2015 smileEvday. All rights reserved.
//

#import "SvGridView.h"

#define SINGLE_LINE_WIDTH           (1 / [UIScreen mainScreen].scale)
#define SINGLE_LINE_ADJUST_OFFSET   ((1 / [UIScreen mainScreen].scale) / 2)

@implementation SvGridView

@synthesize gridColor = _gridColor;
@synthesize gridSpacing = _gridSpacing;

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor clearColor];

        _gridColor = [UIColor blueColor];
        _gridLineWidth = SINGLE_LINE_WIDTH;
        _gridSpacing = 30;
    }

    return self;
}

- (void)setGridColor:(UIColor *)gridColor
{
    _gridColor = gridColor;

    [self setNeedsDisplay];
}

- (void)setGridSpacing:(CGFloat)gridSpacing
{
    _gridSpacing = gridSpacing;

    [self setNeedsDisplay];
}

- (void)setGridLineWidth:(CGFloat)gridLineWidth
{
    _gridLineWidth = gridLineWidth;

    [self setNeedsDisplay];
}


// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextBeginPath(context);
    CGFloat lineMargin = self.gridSpacing;

    /**
     *  https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GraphicsDrawingOverview/GraphicsDrawingOverview.html
     * 僅當要繪制的線寬為奇數像素時,繪制位置需要調整
     */
    CGFloat pixelAdjustOffset = 0;
    if (((int)(self.gridLineWidth * [UIScreen mainScreen].scale) + 1) % 2 == 0) {
        pixelAdjustOffset = SINGLE_LINE_ADJUST_OFFSET;
    }

    CGFloat xPos = lineMargin - pixelAdjustOffset;
    CGFloat yPos = lineMargin - pixelAdjustOffset;
    while (xPos < self.bounds.size.width) {
CGContextMoveToPoint(context, xPos, 0);
CGContextAddLineToPoint(context, xPos, self.bounds.size.height);
xPos += lineMargin;
}

while (yPos < self.bounds.size.height) {
        CGContextMoveToPoint(context, 0, yPos);
        CGContextAddLineToPoint(context, self.bounds.size.width, yPos);
        yPos += lineMargin;
    }

    CGContextSetLineWidth(context, self.gridLineWidth);
    CGContextSetStrokeColorWithColor(context, self.gridColor.CGColor);
    CGContextStrokePath(context);
}


@end
      

使用方法如下:

SvGridView *gridView = [[SvGridView alloc] initWithFrame:self.view.bounds];
gridView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
gridView.alpha = 0.6;
gridView.gridColor = [UIColor greenColor];
[self.view addSubview:gridView];      

三、一個問題

好了,到這兒本文的全部知識就結束了,最後我還有一個問題。

設計師為什麼一定要一個像素的線?      

一個像素的線可能在非Retina裝置上顯示寬度看着合适,在Retina螢幕上顯示可能會比較細。是不是一定需要一個像素的線,需要根據情況來處理。

參考文檔:

​​https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GraphicsDrawingOverview/GraphicsDrawingOverview.html​​

One point does not necessarily correspond to one physical pixel.      
例如畫1個像素的分割線      
1.0f / [UIScreen mainScreen].scale      
iOS 繪制1像素的線
Positions defined by whole-numbered points fall at the midpoint between pixels. For example, if you draw a one-pixel-wide vertical line from (1.0, 1.0) to (1.0, 10.0), you get a fuzzy grey line. If you draw a two-pixel-wide line, you get a solid black line because it fully covers two pixels (one on either side of the specified point). As a rule, lines that are an odd number of physical pixels wide appear softer than lines with widths measured in even numbers of physical pixels unless you adjust their position to make them cover pixels fully.      
規定:奇數像素寬度的線在渲染的時候将會表現為柔和的寬度擴充到向上的整數寬度的線,除非你手動的調整線的位置,使線剛好落在一行或列的顯示單元内。      
On a low-resolution display (with a scale factor of 1.0), a one-point-wide line is one pixel wide. To avoid antialiasing when you draw a one-point-wide horizontal or vertical line, if the line is an odd number of pixels in width, you must offset the position by 0.5 points to either side of a whole-numbered position. If the line is an even number of points in width, to avoid a fuzzy line, you must not do so.
On a high-resolution display (with a scale factor of 2.0), a line that is one point wide is not antialiased at all because it occupies two full pixels (from -0.5 to +0.5). To draw a line that covers only a single physical pixel, you would need to make it 0.5 points in thickness and offset its position by 0.25 points. A comparison between the two types of screens is shown in Figure 1-4.      
在非高清屏上,一個Point對應一個像素。為了防止“antialiasing”導緻的奇數像素的線渲染時出現失真,你需要設定偏移0.5 Point。
在高清螢幕上,要繪制一個像素的線,需要設定線寬為0.5個Point,同僚設定偏移為0.25 Point。
如果線寬為偶數Point的話,則不要去設定偏移,否則線條也會失真。      
iOS 繪制1像素的線
iOS 繪制1像素的線
(1.0f / [UIScreen mainScreen].scale) / 2;      
#define SINGLE_LINE_WIDTH           (1 / [UIScreen mainScreen].scale)
#define SINGLE_LINE_ADJUST_OFFSET   ((1 / [UIScreen mainScreen].scale) / 2)      
CGFloat xPos = 5;
  UIView *view = [[UIView alloc] initWithFrame:CGrect(x - SINGLE_LINE_ADJUST_OFFSET, 0, SINGLE_LINE_WIDTH, 100)];      

//
//  SvGridView.h
//  SvSinglePixel
//
//  Created by xiaoyong.cxy on 6/23/15.
//  Copyright (c) 2015 smileEvday. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface SvGridView : UIView

/**
 * @brief 網格間距,預設30
 */
@property (nonatomic, assign) CGFloat   gridSpacing;

/**
 * @brief 網格線寬度,預設為1 pixel (1.0f / [UIScreen mainScreen].scale)
 */
@property (nonatomic, assign) CGFloat   gridLineWidth;

/**
 * @brief 網格顔色,預設藍色
 */
@property (nonatomic, strong) UIColor   *gridColor;

@end      
//
//  SvGridView.m
//  SvSinglePixel
//
//  Created by xiaoyong.cxy on 6/23/15.
//  Copyright (c) 2015 smileEvday. All rights reserved.
//

#import "SvGridView.h"

#define SINGLE_LINE_WIDTH           (1 / [UIScreen mainScreen].scale)
#define SINGLE_LINE_ADJUST_OFFSET   ((1 / [UIScreen mainScreen].scale) / 2)

@implementation SvGridView

@synthesize gridColor = _gridColor;
@synthesize gridSpacing = _gridSpacing;

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor clearColor];

        _gridColor = [UIColor blueColor];
        _gridLineWidth = SINGLE_LINE_WIDTH;
        _gridSpacing = 30;
    }

    return self;
}

- (void)setGridColor:(UIColor *)gridColor
{
    _gridColor = gridColor;

    [self setNeedsDisplay];
}

- (void)setGridSpacing:(CGFloat)gridSpacing
{
    _gridSpacing = gridSpacing;

    [self setNeedsDisplay];
}

- (void)setGridLineWidth:(CGFloat)gridLineWidth
{
    _gridLineWidth = gridLineWidth;

    [self setNeedsDisplay];
}


// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextBeginPath(context);
    CGFloat lineMargin = self.gridSpacing;

    /**
     *  https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GraphicsDrawingOverview/GraphicsDrawingOverview.html
     * 僅當要繪制的線寬為奇數像素時,繪制位置需要調整
     */
    CGFloat pixelAdjustOffset = 0;
    if (((int)(self.gridLineWidth * [UIScreen mainScreen].scale) + 1) % 2 == 0) {
        pixelAdjustOffset = SINGLE_LINE_ADJUST_OFFSET;
    }

    CGFloat xPos = lineMargin - pixelAdjustOffset;
    CGFloat yPos = lineMargin - pixelAdjustOffset;
    while (xPos < self.bounds.size.width) {
CGContextMoveToPoint(context, xPos, 0);
CGContextAddLineToPoint(context, xPos, self.bounds.size.height);
xPos += lineMargin;
}

while (yPos < self.bounds.size.height) {
        CGContextMoveToPoint(context, 0, yPos);
        CGContextAddLineToPoint(context, self.bounds.size.width, yPos);
        yPos += lineMargin;
    }

    CGContextSetLineWidth(context, self.gridLineWidth);
    CGContextSetStrokeColorWithColor(context, self.gridColor.CGColor);
    CGContextStrokePath(context);
}


@end
      
SvGridView *gridView = [[SvGridView alloc] initWithFrame:self.view.bounds];
gridView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
gridView.alpha = 0.6;
gridView.gridColor = [UIColor greenColor];
[self.view addSubview:gridView];      

設計師為什麼一定要一個像素的線?