iOS: 如何正確的繪製1像素的線

一、Point Vs Pixel

iOS中當我們使用Quartz,UIKit,CoreAnimation等框架時,所有的座標系統採用Point來衡量。系統在實際渲染到設置時會幫助我們處理Point到Pixel的轉換。

這樣做的好處隔離變化,即我們在佈局的事後不需要關注當前設備是否爲Retina,直接按照一套座標系統來佈局即可。

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

One point doesnotnecessarily correspondtoone 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.f / [UIScreen mainScreen].scale

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

Why?

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

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

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

如下圖所示:


Positions definedbywhole-numbered points fall at the midpoint between pixels.Forexample,ifyou draw a one-pixel-wide vertical linefrom(1.0,1.0)to(1.0,10.0), yougeta fuzzy grey line.Ifyou draw a two-pixel-wide line, yougeta solid black line because it fully covers two pixels (oneoneither sideofthe specified point).Asa rule, lines that are an odd numberofphysical pixels wide appear softer than lineswithwidths measuredineven numbersofphysical pixels unless you adjust their positiontomake them cover pixels fully.

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

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

如何對齊呢?

Ona low-resolution display (witha scale factorof1.0), a one-point-wide lineisone pixel wide.Toavoid antialiasingwhenyou draw a one-point-wide horizontalorvertical line,ifthe lineisan odd numberofpixelsinwidth, you must offset the positionby0.5pointstoeither sideofa whole-numbered position.Ifthe lineisan even numberofpointsinwidth,toavoid a fuzzy line, you mustnotdoso.Ona high-resolution display (witha scale factorof2.0), a line thatisone point wideisnotantialiased at all because it occupies two full pixels (from-0.5to+0.5).Todraw a line that covers only asinglephysical pixel, you would needtomake it0.5pointsinthicknessandoffset its positionby0.25points. A comparison between the two typesofscreensisshowninFigure1-4.

翻譯一下

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

如下圖所示:


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

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

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


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

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

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

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

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

#defineSINGLE_LINE_WIDTH          (1 / [UIScreen mainScreen].scale)#defineSINGLE_LINE_ADJUST_OFFSET  ((1 / [UIScreen mainScreen].scale) / 2)

使用代碼如下:

CGFloatxPos =5;UIView*view = [[UIViewalloc] 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@interfaceSvGridView:UIView/**

* @brief 網格間距,默認30

*/@property(nonatomic,assign)CGFloatgridSpacing;/**

* @brief 網格線寬度,默認爲1 pixel (1.0f / [UIScreen mainScreen].scale)

*/@property(nonatomic,assign)CGFloatgridLineWidth;/**

* @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)@implementationSvGridView@synthesizegridColor = _gridColor;@synthesizegridSpacing = _gridSpacing;- (instancetype)initWithFrame:(CGRect)frame{self= [superinitWithFrame:frame];if(self) {self.backgroundColor = [UIColorclearColor];                _gridColor = [UIColorblueColor];        _gridLineWidth = SINGLE_LINE_WIDTH;        _gridSpacing =30;    }returnself;}- (void)setGridColor:(UIColor*)gridColor{    _gridColor = gridColor;        [selfsetNeedsDisplay];}- (void)setGridSpacing:(CGFloat)gridSpacing{    _gridSpacing = gridSpacing;        [selfsetNeedsDisplay];}- (void)setGridLineWidth:(CGFloat)gridLineWidth{    _gridLineWidth = gridLineWidth;        [selfsetNeedsDisplay];}// Only override drawRect: if you perform custom drawing.// An empty implementation adversely affects performance during animation.- (void)drawRect:(CGRect)rect{CGContextRefcontext =UIGraphicsGetCurrentContext();CGContextBeginPath(context);CGFloatlineMargin =self.gridSpacing;/**

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

* 僅當要繪製的線寬爲奇數像素時,繪製位置需要調整

*/CGFloatpixelAdjustOffset =0;if(((int)(self.gridLineWidth * [UIScreenmainScreen].scale) +1) %2==0) {        pixelAdjustOffset = SINGLE_LINE_ADJUST_OFFSET;    }CGFloatxPos = lineMargin - pixelAdjustOffset;CGFloatyPos = lineMargin - pixelAdjustOffset;while(xPos

使用方法如下:

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

三、一個問題

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

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

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

參考文檔:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章