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.
1 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.
翻譯一下
|
|
如下圖所示:
看了上述一通解釋,我們了解了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)];
#p#
二、正確的繪制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 @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];
三、一個(gè)問(wèn)題
好了,到這兒本文的全部知識(shí)就結(jié)束了,***我還有一個(gè)問(wèn)題。
設(shè)計(jì)師為什么一定要一個(gè)像素的線?
一個(gè)像素的線可能在非Retina設(shè)備上顯示寬度看著合適,在Retina屏幕上顯示可能會(huì)比較細(xì)。是不是一定需要一個(gè)像素的線,需要根據(jù)情況來(lái)處理。