YYTextLayout.h 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. //
  2. // YYTextLayout.h
  3. // YYKit <https://github.com/ibireme/YYKit>
  4. //
  5. // Created by ibireme on 15/3/3.
  6. // Copyright (c) 2015 ibireme.
  7. //
  8. // This source code is licensed under the MIT-style license found in the
  9. // LICENSE file in the root directory of this source tree.
  10. //
  11. #import <UIKit/UIKit.h>
  12. #import <CoreText/CoreText.h>
  13. #if __has_include(<YYKit/YYKit.h>)
  14. #import <YYKit/YYTextDebugOption.h>
  15. #import <YYKit/YYTextLine.h>
  16. #import <YYKit/YYTextInput.h>
  17. #else
  18. #import "YYTextDebugOption.h"
  19. #import "YYTextLine.h"
  20. #import "YYTextInput.h"
  21. #endif
  22. @protocol YYTextLinePositionModifier;
  23. NS_ASSUME_NONNULL_BEGIN
  24. /**
  25. The max text container size in layout.
  26. */
  27. extern const CGSize YYTextContainerMaxSize;
  28. /**
  29. The YYTextContainer class defines a region in which text is laid out.
  30. YYTextLayout class uses one or more YYTextContainer objects to generate layouts.
  31. A YYTextContainer defines rectangular regions (`size` and `insets`) or
  32. nonrectangular shapes (`path`), and you can define exclusion paths inside the
  33. text container's bounding rectangle so that text flows around the exclusion
  34. path as it is laid out.
  35. All methods in this class is thread-safe.
  36. Example:
  37. ┌─────────────────────────────┐ <------- container
  38. │ │
  39. │ asdfasdfasdfasdfasdfa <------------ container insets
  40. │ asdfasdfa asdfasdfa │
  41. │ asdfas asdasd │
  42. │ asdfa <----------------------- container exclusion path
  43. │ asdfas adfasd │
  44. │ asdfasdfa asdfasdfa │
  45. │ asdfasdfasdfasdfasdfa │
  46. │ │
  47. └─────────────────────────────┘
  48. */
  49. @interface YYTextContainer : NSObject <NSCoding, NSCopying>
  50. /// Creates a container with the specified size. @param size The size.
  51. + (instancetype)containerWithSize:(CGSize)size;
  52. /// Creates a container with the specified size and insets. @param size The size. @param insets The text insets.
  53. + (instancetype)containerWithSize:(CGSize)size insets:(UIEdgeInsets)insets;
  54. /// Creates a container with the specified path. @param path The path.
  55. + (instancetype)containerWithPath:(nullable UIBezierPath *)path;
  56. /// The constrained size. (if the size is larger than YYTextContainerMaxSize, it will be clipped)
  57. @property CGSize size;
  58. /// The insets for constrained size. The inset value should not be negative. Default is UIEdgeInsetsZero.
  59. @property UIEdgeInsets insets;
  60. /// Custom constrained path. Set this property to ignore `size` and `insets`. Default is nil.
  61. @property (nullable, copy) UIBezierPath *path;
  62. /// An array of `UIBezierPath` for path exclusion. Default is nil.
  63. @property (nullable, copy) NSArray<UIBezierPath *> *exclusionPaths;
  64. /// Path line width. Default is 0;
  65. @property CGFloat pathLineWidth;
  66. /// YES:(PathFillEvenOdd) Text is filled in the area that would be painted if the path were given to CGContextEOFillPath.
  67. /// NO: (PathFillWindingNumber) Text is fill in the area that would be painted if the path were given to CGContextFillPath.
  68. /// Default is YES;
  69. @property (getter=isPathFillEvenOdd) BOOL pathFillEvenOdd;
  70. /// Whether the text is vertical form (may used for CJK text layout). Default is NO.
  71. @property (getter=isVerticalForm) BOOL verticalForm;
  72. /// Maximum number of rows, 0 means no limit. Default is 0.
  73. @property NSUInteger maximumNumberOfRows;
  74. /// The line truncation type, default is none.
  75. @property YYTextTruncationType truncationType;
  76. /// The truncation token. If nil, the layout will use "…" instead. Default is nil.
  77. @property (nullable, copy) NSAttributedString *truncationToken;
  78. /// This modifier is applied to the lines before the layout is completed,
  79. /// give you a chance to modify the line position. Default is nil.
  80. @property (nullable, copy) id<YYTextLinePositionModifier> linePositionModifier;
  81. @end
  82. /**
  83. The YYTextLinePositionModifier protocol declares the required method to modify
  84. the line position in text layout progress. See `YYTextLinePositionSimpleModifier` for example.
  85. */
  86. @protocol YYTextLinePositionModifier <NSObject, NSCopying>
  87. @required
  88. /**
  89. This method will called before layout is completed. The method should be thread-safe.
  90. @param lines An array of YYTextLine.
  91. @param text The full text.
  92. @param container The layout container.
  93. */
  94. - (void)modifyLines:(NSArray<YYTextLine *> *)lines fromText:(NSAttributedString *)text inContainer:(YYTextContainer *)container;
  95. @end
  96. /**
  97. A simple implementation of `YYTextLinePositionModifier`. It can fix each line's position
  98. to a specified value, lets each line of height be the same.
  99. */
  100. @interface YYTextLinePositionSimpleModifier : NSObject <YYTextLinePositionModifier>
  101. @property (assign) CGFloat fixedLineHeight; ///< The fixed line height (distance between two baseline).
  102. @end
  103. /**
  104. YYTextLayout class is a readonly class stores text layout result.
  105. All the property in this class is readonly, and should not be changed.
  106. The methods in this class is thread-safe (except some of the draw methods).
  107. example: (layout with a circle exclusion path)
  108. ┌──────────────────────────┐ <------ container
  109. │ [--------Line0--------] │ <- Row0
  110. │ [--------Line1--------] │ <- Row1
  111. │ [-Line2-] [-Line3-] │ <- Row2
  112. │ [-Line4] [Line5-] │ <- Row3
  113. │ [-Line6-] [-Line7-] │ <- Row4
  114. │ [--------Line8--------] │ <- Row5
  115. │ [--------Line9--------] │ <- Row6
  116. └──────────────────────────┘
  117. */
  118. @interface YYTextLayout : NSObject <NSCoding>
  119. #pragma mark - Generate text layout
  120. ///=============================================================================
  121. /// @name Generate text layout
  122. ///=============================================================================
  123. /**
  124. Generate a layout with the given container size and text.
  125. @param size The text container's size
  126. @param text The text (if nil, returns nil).
  127. @return A new layout, or nil when an error occurs.
  128. */
  129. + (nullable YYTextLayout *)layoutWithContainerSize:(CGSize)size
  130. text:(NSAttributedString *)text;
  131. /**
  132. Generate a layout with the given container and text.
  133. @param container The text container (if nil, returns nil).
  134. @param text The text (if nil, returns nil).
  135. @return A new layout, or nil when an error occurs.
  136. */
  137. + (nullable YYTextLayout *)layoutWithContainer:(YYTextContainer *)container
  138. text:(NSAttributedString *)text;
  139. /**
  140. Generate a layout with the given container and text.
  141. @param container The text container (if nil, returns nil).
  142. @param text The text (if nil, returns nil).
  143. @param range The text range (if out of range, returns nil). If the
  144. length of the range is 0, it means the length is no limit.
  145. @return A new layout, or nil when an error occurs.
  146. */
  147. + (nullable YYTextLayout *)layoutWithContainer:(YYTextContainer *)container
  148. text:(NSAttributedString *)text
  149. range:(NSRange)range;
  150. /**
  151. Generate layouts with the given containers and text.
  152. @param containers An array of YYTextContainer object (if nil, returns nil).
  153. @param text The text (if nil, returns nil).
  154. @return An array of YYTextLayout object (the count is same as containers),
  155. or nil when an error occurs.
  156. */
  157. + (nullable NSArray<YYTextLayout *> *)layoutWithContainers:(NSArray<YYTextContainer *> *)containers
  158. text:(NSAttributedString *)text;
  159. /**
  160. Generate layouts with the given containers and text.
  161. @param containers An array of YYTextContainer object (if nil, returns nil).
  162. @param text The text (if nil, returns nil).
  163. @param range The text range (if out of range, returns nil). If the
  164. length of the range is 0, it means the length is no limit.
  165. @return An array of YYTextLayout object (the count is same as containers),
  166. or nil when an error occurs.
  167. */
  168. + (nullable NSArray<YYTextLayout *> *)layoutWithContainers:(NSArray<YYTextContainer *> *)containers
  169. text:(NSAttributedString *)text
  170. range:(NSRange)range;
  171. - (instancetype)init UNAVAILABLE_ATTRIBUTE;
  172. + (instancetype)new UNAVAILABLE_ATTRIBUTE;
  173. #pragma mark - Text layout attributes
  174. ///=============================================================================
  175. /// @name Text layout attributes
  176. ///=============================================================================
  177. ///< The text container
  178. @property (nonatomic, strong, readonly) YYTextContainer *container;
  179. ///< The full text
  180. @property (nonatomic, strong, readonly) NSAttributedString *text;
  181. ///< The text range in full text
  182. @property (nonatomic, readonly) NSRange range;
  183. ///< CTFrameSetter
  184. @property (nonatomic, readonly) CTFramesetterRef frameSetter;
  185. ///< CTFrame
  186. @property (nonatomic, readonly) CTFrameRef frame;
  187. ///< Array of `YYTextLine`, no truncated
  188. @property (nonatomic, strong, readonly) NSArray<YYTextLine *> *lines;
  189. ///< YYTextLine with truncated token, or nil
  190. @property (nullable, nonatomic, strong, readonly) YYTextLine *truncatedLine;
  191. ///< Array of `YYTextAttachment`
  192. @property (nullable, nonatomic, strong, readonly) NSArray<YYTextAttachment *> *attachments;
  193. ///< Array of NSRange(wrapped by NSValue) in text
  194. @property (nullable, nonatomic, strong, readonly) NSArray<NSValue *> *attachmentRanges;
  195. ///< Array of CGRect(wrapped by NSValue) in container
  196. @property (nullable, nonatomic, strong, readonly) NSArray<NSValue *> *attachmentRects;
  197. ///< Set of Attachment (UIImage/UIView/CALayer)
  198. @property (nullable, nonatomic, strong, readonly) NSSet *attachmentContentsSet;
  199. ///< Number of rows
  200. @property (nonatomic, readonly) NSUInteger rowCount;
  201. ///< Visible text range
  202. @property (nonatomic, readonly) NSRange visibleRange;
  203. ///< Bounding rect (glyphs)
  204. @property (nonatomic, readonly) CGRect textBoundingRect;
  205. ///< Bounding size (glyphs and insets, ceil to pixel)
  206. @property (nonatomic, readonly) CGSize textBoundingSize;
  207. ///< Has highlight attribute
  208. @property (nonatomic, readonly) BOOL containsHighlight;
  209. ///< Has block border attribute
  210. @property (nonatomic, readonly) BOOL needDrawBlockBorder;
  211. ///< Has background border attribute
  212. @property (nonatomic, readonly) BOOL needDrawBackgroundBorder;
  213. ///< Has shadow attribute
  214. @property (nonatomic, readonly) BOOL needDrawShadow;
  215. ///< Has underline attribute
  216. @property (nonatomic, readonly) BOOL needDrawUnderline;
  217. ///< Has visible text
  218. @property (nonatomic, readonly) BOOL needDrawText;
  219. ///< Has attachment attribute
  220. @property (nonatomic, readonly) BOOL needDrawAttachment;
  221. ///< Has inner shadow attribute
  222. @property (nonatomic, readonly) BOOL needDrawInnerShadow;
  223. ///< Has strickthrough attribute
  224. @property (nonatomic, readonly) BOOL needDrawStrikethrough;
  225. ///< Has border attribute
  226. @property (nonatomic, readonly) BOOL needDrawBorder;
  227. #pragma mark - Query information from text layout
  228. ///=============================================================================
  229. /// @name Query information from text layout
  230. ///=============================================================================
  231. /**
  232. The first line index for row.
  233. @param row A row index.
  234. @return The line index, or NSNotFound if not found.
  235. */
  236. - (NSUInteger)lineIndexForRow:(NSUInteger)row;
  237. /**
  238. The number of lines for row.
  239. @param row A row index.
  240. @return The number of lines, or NSNotFound when an error occurs.
  241. */
  242. - (NSUInteger)lineCountForRow:(NSUInteger)row;
  243. /**
  244. The row index for line.
  245. @param line A row index.
  246. @return The row index, or NSNotFound if not found.
  247. */
  248. - (NSUInteger)rowIndexForLine:(NSUInteger)line;
  249. /**
  250. The line index for a specified point.
  251. @discussion It returns NSNotFound if there's no text at the point.
  252. @param point A point in the container.
  253. @return The line index, or NSNotFound if not found.
  254. */
  255. - (NSUInteger)lineIndexForPoint:(CGPoint)point;
  256. /**
  257. The line index closest to a specified point.
  258. @param point A point in the container.
  259. @return The line index, or NSNotFound if no line exist in layout.
  260. */
  261. - (NSUInteger)closestLineIndexForPoint:(CGPoint)point;
  262. /**
  263. The offset in container for a text position in a specified line.
  264. @discussion The offset is the text position's baseline point.x.
  265. If the container is vertical form, the offset is the baseline point.y;
  266. @param position The text position in string.
  267. @param lineIndex The line index.
  268. @return The offset in container, or CGFLOAT_MAX if not found.
  269. */
  270. - (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)lineIndex;
  271. /**
  272. The text position for a point in a specified line.
  273. @discussion This method just call CTLineGetStringIndexForPosition() and does
  274. NOT consider the emoji, line break character, binding text...
  275. @param point A point in the container.
  276. @param lineIndex The line index.
  277. @return The text position, or NSNotFound if not found.
  278. */
  279. - (NSUInteger)textPositionForPoint:(CGPoint)point lineIndex:(NSUInteger)lineIndex;
  280. /**
  281. The closest text position to a specified point.
  282. @discussion This method takes into account the restrict of emoji, line break
  283. character, binding text and text affinity.
  284. @param point A point in the container.
  285. @return A text position, or nil if not found.
  286. */
  287. - (nullable YYTextPosition *)closestPositionToPoint:(CGPoint)point;
  288. /**
  289. Returns the new position when moving selection grabber in text view.
  290. @discussion There are two grabber in the text selection period, user can only
  291. move one grabber at the same time.
  292. @param point A point in the container.
  293. @param oldPosition The old text position for the moving grabber.
  294. @param otherPosition The other position in text selection view.
  295. @return A text position, or nil if not found.
  296. */
  297. - (nullable YYTextPosition *)positionForPoint:(CGPoint)point
  298. oldPosition:(YYTextPosition *)oldPosition
  299. otherPosition:(YYTextPosition *)otherPosition;
  300. /**
  301. Returns the character or range of characters that is at a given point in the container.
  302. If there is no text at the point, returns nil.
  303. @discussion This method takes into account the restrict of emoji, line break
  304. character, binding text and text affinity.
  305. @param point A point in the container.
  306. @return An object representing a range that encloses a character (or characters)
  307. at point. Or nil if not found.
  308. */
  309. - (nullable YYTextRange *)textRangeAtPoint:(CGPoint)point;
  310. /**
  311. Returns the closest character or range of characters that is at a given point in
  312. the container.
  313. @discussion This method takes into account the restrict of emoji, line break
  314. character, binding text and text affinity.
  315. @param point A point in the container.
  316. @return An object representing a range that encloses a character (or characters)
  317. at point. Or nil if not found.
  318. */
  319. - (nullable YYTextRange *)closestTextRangeAtPoint:(CGPoint)point;
  320. /**
  321. If the position is inside an emoji, composed character sequences, line break '\\r\\n'
  322. or custom binding range, then returns the range by extend the position. Otherwise,
  323. returns a zero length range from the position.
  324. @param position A text-position object that identifies a location in layout.
  325. @return A text-range object that extend the position. Or nil if an error occurs
  326. */
  327. - (nullable YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)position;
  328. /**
  329. Returns a text range at a given offset in a specified direction from another
  330. text position to its farthest extent in a certain direction of layout.
  331. @param position A text-position object that identifies a location in layout.
  332. @param direction A constant that indicates a direction of layout (right, left, up, down).
  333. @param offset A character offset from position.
  334. @return A text-range object that represents the distance from position to the
  335. farthest extent in direction. Or nil if an error occurs.
  336. */
  337. - (nullable YYTextRange *)textRangeByExtendingPosition:(YYTextPosition *)position
  338. inDirection:(UITextLayoutDirection)direction
  339. offset:(NSInteger)offset;
  340. /**
  341. Returns the line index for a given text position.
  342. @discussion This method takes into account the text affinity.
  343. @param position A text-position object that identifies a location in layout.
  344. @return The line index, or NSNotFound if not found.
  345. */
  346. - (NSUInteger)lineIndexForPosition:(YYTextPosition *)position;
  347. /**
  348. Returns the baseline position for a given text position.
  349. @param position An object that identifies a location in the layout.
  350. @return The baseline position for text, or CGPointZero if not found.
  351. */
  352. - (CGPoint)linePositionForPosition:(YYTextPosition *)position;
  353. /**
  354. Returns a rectangle used to draw the caret at a given insertion point.
  355. @param position An object that identifies a location in the layout.
  356. @return A rectangle that defines the area for drawing the caret. The width is
  357. always zero in normal container, the height is always zero in vertical form container.
  358. If not found, it returns CGRectNull.
  359. */
  360. - (CGRect)caretRectForPosition:(YYTextPosition *)position;
  361. /**
  362. Returns the first rectangle that encloses a range of text in the layout.
  363. @param range An object that represents a range of text in layout.
  364. @return The first rectangle in a range of text. You might use this rectangle to
  365. draw a correction rectangle. The "first" in the name refers the rectangle
  366. enclosing the first line when the range encompasses multiple lines of text.
  367. If not found, it returns CGRectNull.
  368. */
  369. - (CGRect)firstRectForRange:(YYTextRange *)range;
  370. /**
  371. Returns the rectangle union that encloses a range of text in the layout.
  372. @param range An object that represents a range of text in layout.
  373. @return A rectangle that defines the area than encloses the range.
  374. If not found, it returns CGRectNull.
  375. */
  376. - (CGRect)rectForRange:(YYTextRange *)range;
  377. /**
  378. Returns an array of selection rects corresponding to the range of text.
  379. The start and end rect can be used to show grabber.
  380. @param range An object representing a range in text.
  381. @return An array of `YYTextSelectionRect` objects that encompass the selection.
  382. If not found, the array is empty.
  383. */
  384. - (NSArray<YYTextSelectionRect *> *)selectionRectsForRange:(YYTextRange *)range;
  385. /**
  386. Returns an array of selection rects corresponding to the range of text.
  387. @param range An object representing a range in text.
  388. @return An array of `YYTextSelectionRect` objects that encompass the selection.
  389. If not found, the array is empty.
  390. */
  391. - (NSArray<YYTextSelectionRect *> *)selectionRectsWithoutStartAndEndForRange:(YYTextRange *)range;
  392. /**
  393. Returns the start and end selection rects corresponding to the range of text.
  394. The start and end rect can be used to show grabber.
  395. @param range An object representing a range in text.
  396. @return An array of `YYTextSelectionRect` objects contains the start and end to
  397. the selection. If not found, the array is empty.
  398. */
  399. - (NSArray<YYTextSelectionRect *> *)selectionRectsWithOnlyStartAndEndForRange:(YYTextRange *)range;
  400. #pragma mark - Draw text layout
  401. ///=============================================================================
  402. /// @name Draw text layout
  403. ///=============================================================================
  404. /**
  405. Draw the layout and show the attachments.
  406. @discussion If the `view` parameter is not nil, then the attachment views will
  407. add to this `view`, and if the `layer` parameter is not nil, then the attachment
  408. layers will add to this `layer`.
  409. @warning This method should be called on main thread if `view` or `layer` parameter
  410. is not nil and there's UIView or CALayer attachments in layout.
  411. Otherwise, it can be called on any thread.
  412. @param context The draw context. Pass nil to avoid text and image drawing.
  413. @param size The context size.
  414. @param point The point at which to draw the layout.
  415. @param view The attachment views will add to this view.
  416. @param layer The attachment layers will add to this layer.
  417. @param debug The debug option. Pass nil to avoid debug drawing.
  418. @param cancel The cancel checker block. It will be called in drawing progress.
  419. If it returns YES, the further draw progress will be canceled.
  420. Pass nil to ignore this feature.
  421. */
  422. - (void)drawInContext:(nullable CGContextRef)context
  423. size:(CGSize)size
  424. point:(CGPoint)point
  425. view:(nullable UIView *)view
  426. layer:(nullable CALayer *)layer
  427. debug:(nullable YYTextDebugOption *)debug
  428. cancel:(nullable BOOL (^)(void))cancel;
  429. /**
  430. Draw the layout text and image (without view or layer attachments).
  431. @discussion This method is thread safe and can be called on any thread.
  432. @param context The draw context. Pass nil to avoid text and image drawing.
  433. @param size The context size.
  434. @param debug The debug option. Pass nil to avoid debug drawing.
  435. */
  436. - (void)drawInContext:(nullable CGContextRef)context
  437. size:(CGSize)size
  438. debug:(nullable YYTextDebugOption *)debug;
  439. /**
  440. Show view and layer attachments.
  441. @warning This method must be called on main thread.
  442. @param view The attachment views will add to this view.
  443. @param layer The attachment layers will add to this layer.
  444. */
  445. - (void)addAttachmentToView:(nullable UIView *)view layer:(nullable CALayer *)layer;
  446. /**
  447. Remove attachment views and layers from their super container.
  448. @warning This method must be called on main thread.
  449. */
  450. - (void)removeAttachmentFromViewAndLayer;
  451. @end
  452. NS_ASSUME_NONNULL_END