(轉(zhuǎn)載)iOS: 如何正確的繪制1像素的線

一剩燥、Point Vs Pixel

iOS中當(dāng)我們使用Quartz澳骤,UIKit,CoreAnimation等框架時(shí)反番,所有的坐標(biāo)系統(tǒng)采用Point來(lái)衡量沙热。系統(tǒng)在實(shí)際渲染到設(shè)置時(shí)會(huì)幫助我們處理Point到Pixel的轉(zhuǎn)換。
這樣做的好處隔離變化罢缸,即我們?cè)诓季值氖潞蟛恍枰P(guān)注當(dāng)前設(shè)備是否為Retina篙贸,直接按照一套坐標(biāo)系統(tǒng)來(lái)布局即可。

實(shí)際使用中我們需要牢記下面這一點(diǎn):

One point does not necessarily correspond to one physical pixel.

1px Point的線在非Retina屏幕則是一個(gè)像素枫疆,在Retina屏幕上則可能是2個(gè)或者3個(gè)爵川,取決于系統(tǒng)設(shè)備的DPI。

iOS系統(tǒng)中息楔,UIScreen寝贡,UIView,UIImage钞螟,CALayer類都提供相關(guān)屬性來(lái)獲取scale factor兔甘。
原生的繪制技術(shù)天然的幫我們處理了scale factor,例如在drawRect:方法中鳞滨,UIKit自動(dòng)的根據(jù)當(dāng)前運(yùn)行的設(shè)備設(shè)置了正切的scale factor。所以我們?cè)赿rawRect: 方法中繪制的任何內(nèi)容都會(huì)被自動(dòng)縮放到設(shè)備的物理屏幕上蟆淀。

基于以上信息可以看出拯啦,我們大部分情況下都不需要去關(guān)注pixel澡匪,然而存在部分情況需要考慮像素的轉(zhuǎn)化。

例如畫1個(gè)像素的分割線

看到這個(gè)問(wèn)題你的第一想法可能是褒链,直接根據(jù)當(dāng)前屏幕的縮放因子計(jì)算出1 像素線對(duì)應(yīng)的Point唁情,然后設(shè)置線寬即可。
代碼如下:

1.0f / [UIScreen mainScreen].scale

表面上看著一切正常了甫匹,但是通過(guò)實(shí)際的設(shè)備測(cè)試你會(huì)發(fā)現(xiàn)渲染出來(lái)的線寬并不是1個(gè)像素甸鸟。Why?

為了獲得良好的視覺(jué)效果,繪圖系統(tǒng)通常都會(huì)采用一個(gè)叫“antialiasing(反鋸齒)”的技術(shù)兵迅,iOS也不例外抢韭。
顯示屏幕有很多小的顯示單元組成,可以接單的理解為一個(gè)單元就代表一個(gè)像素恍箭。如果要畫一條黑線刻恭,條線剛好落在了一列或者一行顯示顯示單元之內(nèi),將會(huì)渲染出標(biāo)準(zhǔn)的一個(gè)像素的黑線扯夭。
但如果線落在了兩個(gè)行或列的中間時(shí)鳍贾,那么會(huì)得到一條“失真”的線,其實(shí)是兩個(gè)像素寬的灰線交洗。

如下圖所示:

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.

官方解釋如上骑科,簡(jiǎn)單翻譯一下:

規(guī)定:奇數(shù)像素寬度的線在渲染的時(shí)候?qū)?huì)表現(xiàn)為柔和的寬度擴(kuò)展到向上的整數(shù)寬度的線,除非你手動(dòng)的調(diào)整線的位置构拳,使線剛好落在一行或列的顯示單元內(nèi)纵散。

如何對(duì)齊呢?

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.

翻譯一下

在非高清屏上隐圾,一個(gè)Point對(duì)應(yīng)一個(gè)像素伍掀。為了防止“antialiasing”導(dǎo)致的奇數(shù)像素的線渲染時(shí)出現(xiàn)失真,你需要設(shè)置偏移0.5 Point暇藏。
在高清屏幕上蜜笤,要繪制一個(gè)像素的線,需要設(shè)置線寬為0.5個(gè)Point盐碱,同事設(shè)置偏移為0.25 Point把兔。
如果線寬為偶數(shù)Point的話,則不要去設(shè)置偏移瓮顽,否則線條也會(huì)失真县好。

如下圖所示:


看了上述一通解釋,我們了解了1像素寬的線條失真的原因暖混,及解決辦法缕贡。
至此問(wèn)題貌似都解決了?再想想為什么在非Retina和Retina屏幕上調(diào)整位置時(shí)值不一樣,前者為0.5Point晾咪,后者為0.25Point收擦,那么scale為3的6 Plus設(shè)備又該調(diào)整多少呢?
要回答這個(gè)問(wèn)題谍倦,我們需要理解調(diào)整多少依舊什么原則塞赂。


再回過(guò)頭來(lái)看看這上面的圖片,圖片中每一格子代表一個(gè)像素昼蛀,而頂部標(biāo)記的則代碼我們布局時(shí)的坐標(biāo)宴猾。
可以看到左邊的非Retina屏幕,我們要在(3,0)這個(gè)位置畫一條一個(gè)像素寬的豎線時(shí)叼旋,由于渲染的最小單位是像素仇哆,而(3,0)這個(gè)坐標(biāo)恰好位于兩個(gè)像素中間,此時(shí)系統(tǒng)會(huì)對(duì)坐標(biāo)3左右兩列的像素對(duì)填充送淆,為了不至于線顯得太寬税产,為對(duì)線的顏色淡化。那么根據(jù)上述信息我們可以得出偷崩,如果要畫出一個(gè)像素寬的線辟拷,就得把繪制的坐標(biāo)移動(dòng)到(2.5, 0)或者(3.5,0)這個(gè)位置,這樣系統(tǒng)渲染的時(shí)候剛好可以填充一列像素阐斜,也就是標(biāo)準(zhǔn)的一個(gè)像素的線衫冻。

基于上面的分析,我們可以得出“Scale為3的6 Plus”設(shè)備如果要繪制1個(gè)像素寬的線條時(shí)谒出,位置調(diào)整也應(yīng)該是0.5像素隅俘,對(duì)應(yīng)該的Point計(jì)算如下:

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

奉上一個(gè)畫一像素線的一個(gè)宏:

#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線條

貼上一個(gè)寫的GridView的代碼笤喳,代碼中對(duì)Grid線條的奇數(shù)像素做了偏移为居,防止出現(xiàn)線條模糊的情況。

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 網(wǎng)格間距杀狡,默認(rèn)30
     */
    @property (nonatomic, assign) CGFloat  gridSpacing;
    /**
     * @brief 網(wǎng)格線寬度蒙畴,默認(rèn)為1 pixel (1.0f / [UIScreen mainScreen].scale)
     */
    @property (nonatomic, assign) CGFloat  gridLineWidth;
    /**
     * @brief 網(wǎng)格顏色,默認(rèn)藍(lán)色
     */
    @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
* 僅當(dāng)要繪制的線寬為奇數(shù)像素時(shí)呜象,繪制位置需要調(diào)整
*/
    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];

參考文檔:

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

注:smileEvday保留本文的一切權(quán)利

轉(zhuǎn)載請(qǐng)著名原文出處

本文所有內(nèi)容僅代表個(gè)人觀點(diǎn)膳凝,如有有不對(duì)的地方,歡迎指出恭陡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蹬音,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子休玩,更是在濱河造成了極大的恐慌著淆,老刑警劉巖劫狠,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異牧抽,居然都是意外死亡嘉熊,警方通過(guò)查閱死者的電腦和手機(jī)遥赚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門扬舒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人凫佛,你說(shuō)我怎么就攤上這事讲坎。” “怎么了愧薛?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵晨炕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我毫炉,道長(zhǎng)瓮栗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任瞄勾,我火速辦了婚禮费奸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘进陡。我一直安慰自己愿阐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布趾疚。 她就那樣靜靜地躺著缨历,像睡著了一般。 火紅的嫁衣襯著肌膚如雪糙麦。 梳的紋絲不亂的頭發(fā)上辛孵,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音赡磅,去河邊找鬼魄缚。 笑死,一個(gè)胖子當(dāng)著我的面吹牛仆邓,可吹牛的內(nèi)容都是我干的鲜滩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼节值,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼徙硅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起搞疗,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嗓蘑,失蹤者是張志新(化名)和其女友劉穎须肆,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桩皿,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡豌汇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泄隔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拒贱。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖佛嬉,靈堂內(nèi)的尸體忽然破棺而出逻澳,到底是詐尸還是另有隱情,我是刑警寧澤暖呕,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布斜做,位于F島的核電站,受9級(jí)特大地震影響湾揽,放射性物質(zhì)發(fā)生泄漏瓤逼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一库物、第九天 我趴在偏房一處隱蔽的房頂上張望霸旗。 院中可真熱鬧,春花似錦艳狐、人聲如沸定硝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蔬啡。三九已至,卻和暖如春镀虐,著一層夾襖步出監(jiān)牢的瞬間箱蟆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工刮便, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留空猜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓恨旱,卻偏偏與公主長(zhǎng)得像辈毯,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子搜贤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容