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也不例外。
顯示屏幕有很多小的顯示單元組成,可以接單的理解為一個單元就代表一個像素。如果要畫一條黑線,條線剛好落在了一列或者一行顯示顯示單元之內,將會渲染出標準的一個像素的黑線。
但如果線落在了兩個行或列的中間時,那么會得到一條“失真”的線,其實是兩個像素寬的灰線。
如下圖所示:
- 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.
翻譯一下
|
|
如下圖所示:
看了上述一通解釋,我們了解了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.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)];
#p#
二、正確的繪制Grid線條
貼上一個寫的GridView的代碼,代碼中對Grid線條的奇數像素做了偏移,防止出現線條模糊的情況。
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 網格間距,默認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屏幕上顯示可能會比較細。是不是一定需要一個像素的線,需要根據情況來處理。