├── .gitignore ├── FoldLabelDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── lee.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── FoldLabelDemo ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── FoldLabelCell.h ├── FoldLabelCell.m ├── FoldLabelCell.xib ├── FoldModel.h ├── FoldModel.m ├── Info.plist ├── ViewController.h ├── ViewController.m ├── YYText │ ├── Component │ │ ├── YYTextContainerView.h │ │ ├── YYTextContainerView.m │ │ ├── YYTextDebugOption.h │ │ ├── YYTextDebugOption.m │ │ ├── YYTextEffectWindow.h │ │ ├── YYTextEffectWindow.m │ │ ├── YYTextInput.h │ │ ├── YYTextInput.m │ │ ├── YYTextKeyboardManager.h │ │ ├── YYTextKeyboardManager.m │ │ ├── YYTextLayout.h │ │ ├── YYTextLayout.m │ │ ├── YYTextLine.h │ │ ├── YYTextLine.m │ │ ├── YYTextMagnifier.h │ │ ├── YYTextMagnifier.m │ │ ├── YYTextSelectionView.h │ │ └── YYTextSelectionView.m │ ├── String │ │ ├── YYTextArchiver.h │ │ ├── YYTextArchiver.m │ │ ├── YYTextAttribute.h │ │ ├── YYTextAttribute.m │ │ ├── YYTextParser.h │ │ ├── YYTextParser.m │ │ ├── YYTextRubyAnnotation.h │ │ ├── YYTextRubyAnnotation.m │ │ ├── YYTextRunDelegate.h │ │ └── YYTextRunDelegate.m │ ├── Utility │ │ ├── NSAttributedString+YYText.h │ │ ├── NSAttributedString+YYText.m │ │ ├── NSParagraphStyle+YYText.h │ │ ├── NSParagraphStyle+YYText.m │ │ ├── UIPasteboard+YYText.h │ │ ├── UIPasteboard+YYText.m │ │ ├── UIView+YYText.h │ │ ├── UIView+YYText.m │ │ ├── YYTextAsyncLayer.h │ │ ├── YYTextAsyncLayer.m │ │ ├── YYTextTransaction.h │ │ ├── YYTextTransaction.m │ │ ├── YYTextUtilities.h │ │ ├── YYTextUtilities.m │ │ ├── YYTextWeakProxy.h │ │ └── YYTextWeakProxy.m │ ├── YYLabel.h │ ├── YYLabel.m │ ├── YYText.h │ ├── YYTextView.h │ └── YYTextView.m └── main.m ├── FoldLabelDemoTests ├── FoldLabelDemoTests.m └── Info.plist ├── FoldLabelDemoUITests ├── FoldLabelDemoUITests.m └── Info.plist └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | # CocoaPods 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | # Pods/ 38 | 39 | # Carthage 40 | # 41 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 42 | # Carthage/Checkouts 43 | 44 | Carthage/Build 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 52 | 53 | fastlane/report.xml 54 | fastlane/Preview.html 55 | fastlane/screenshots/**/*.png 56 | fastlane/test_output 57 | 58 | # Code Injection 59 | # 60 | # After new code Injection tools there's a generated folder /iOSInjectionProject 61 | # https://github.com/johnno1962/injectionforxcode 62 | 63 | iOSInjectionProject/ 64 | -------------------------------------------------------------------------------- /FoldLabelDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FoldLabelDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FoldLabelDemo.xcodeproj/xcuserdata/lee.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /FoldLabelDemo.xcodeproj/xcuserdata/lee.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | FoldLabelDemo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /FoldLabelDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // FoldLabelDemo 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /FoldLabelDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // FoldLabelDemo 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /FoldLabelDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /FoldLabelDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /FoldLabelDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /FoldLabelDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /FoldLabelDemo/FoldLabelCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // FoldLabelCell.h 3 | // FoldLabelDemo 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "FoldModel.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | typedef void(^showContentCallback)(void); 14 | 15 | @interface FoldLabelCell : UITableViewCell 16 | @property (nonatomic, strong) FoldModel *model; 17 | @property (nonatomic, strong) showContentCallback showContentCallback; 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /FoldLabelDemo/FoldLabelCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // FoldLabelCell.m 3 | // FoldLabelDemo 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import "FoldLabelCell.h" 10 | #import 11 | #define kScreenWidth [UIScreen mainScreen].bounds.size.width 12 | 13 | static const NSInteger limitMaxLineCount = 3; 14 | 15 | @interface FoldLabelCell () 16 | @property (weak, nonatomic) IBOutlet UILabel *topicLabel; 17 | @property (weak, nonatomic) IBOutlet UILabel *contentLabel; 18 | //@property (nonatomic, copy) NSString *moreText; 19 | @property (nonatomic, strong) NSString *contentString; 20 | @property (weak, nonatomic) IBOutlet UIButton *foldButton; 21 | @property (weak, nonatomic) IBOutlet NSLayoutConstraint *foldButtonTopMargin; 22 | @property (weak, nonatomic) IBOutlet NSLayoutConstraint *foldButtonHeight; 23 | @end 24 | 25 | @implementation FoldLabelCell 26 | 27 | - (void)setModel:(FoldModel *)model { 28 | _model = model; 29 | self.topicLabel.text = model.title; 30 | self.contentLabel.text = model.content; 31 | BOOL isFold = [self shouldFoldContent:_contentLabel]; 32 | if (isFold) { 33 | //折叠 34 | if (!model.isFold) { 35 | [self.foldButton setTitle:@"展开" forState: UIControlStateNormal]; 36 | self.contentLabel.numberOfLines = limitMaxLineCount; 37 | } else { 38 | [self.foldButton setTitle:@"收起" forState: UIControlStateNormal]; 39 | self.contentLabel.numberOfLines = 0; 40 | } 41 | self.foldButton.hidden = NO; 42 | self.foldButtonHeight.constant = 20; 43 | self.foldButtonTopMargin.constant = 10; 44 | } else { 45 | //不需要折叠 46 | self.contentLabel.numberOfLines = 0; 47 | self.foldButton.hidden = YES; 48 | self.foldButtonHeight.constant = 0; 49 | self.foldButtonTopMargin.constant = 0; 50 | } 51 | } 52 | 53 | - (void)awakeFromNib { 54 | [super awakeFromNib]; 55 | // Initialization code 56 | self.selectionStyle = UITableViewCellSelectionStyleNone; 57 | } 58 | 59 | //点击按钮 60 | - (IBAction)foldButtonClicked:(id)sender { 61 | if (self.showContentCallback) { 62 | self.showContentCallback(); 63 | } 64 | } 65 | 66 | #pragma mark 是否需要折叠内容 67 | - (BOOL)shouldFoldContent:(UILabel *)label { 68 | NSArray *lineStringArray = [self getSeparatedLinesFromLabelWidth:kScreenWidth - 30 text:label.text]; 69 | if (lineStringArray.count > limitMaxLineCount) { 70 | return YES; 71 | } 72 | return NO; 73 | } 74 | 75 | #pragma mark 计算UILabel每一行显示的字符串 76 | - (NSArray *)getSeparatedLinesFromLabelWidth:(CGFloat)labelWidth text:(NSString *)textStr { 77 | NSString *text = textStr; 78 | UIFont *font = _contentLabel.font; 79 | if (text == nil) { 80 | return nil; 81 | } 82 | CTFontRef myFont = CTFontCreateWithName(( CFStringRef)([font fontName]), [font pointSize], NULL); 83 | NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:text]; 84 | NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; 85 | paragraphStyle.lineBreakMode = NSLineBreakByCharWrapping; 86 | [attStr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, attStr.length)]; 87 | [attStr addAttribute:(NSString *)kCTFontAttributeName 88 | value:(__bridge id)myFont 89 | range:NSMakeRange(0, attStr.length)]; 90 | CFRelease(myFont); 91 | CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(( CFAttributedStringRef)attStr); 92 | CGMutablePathRef path = CGPathCreateMutable(); 93 | CGPathAddRect(path, NULL, CGRectMake(0,0,labelWidth,100000)); 94 | CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL); 95 | NSArray *lines = ( NSArray *)CTFrameGetLines(frame); 96 | NSMutableArray *linesArray = [[NSMutableArray alloc]init]; 97 | for (id line in lines) { 98 | CTLineRef lineRef = (__bridge CTLineRef )line; 99 | CFRange lineRange = CTLineGetStringRange(lineRef); 100 | NSRange range = NSMakeRange(lineRange.location, lineRange.length); 101 | NSString *lineString = [text substringWithRange:range]; 102 | CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr, 103 | lineRange, 104 | kCTKernAttributeName, 105 | (CFTypeRef)([NSNumber numberWithFloat:0.0])); 106 | CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr, 107 | lineRange, 108 | kCTKernAttributeName, 109 | (CFTypeRef)([NSNumber numberWithInt:0.0])); 110 | [linesArray addObject:lineString]; 111 | } 112 | CGPathRelease(path); 113 | CFRelease(frame); 114 | CFRelease(frameSetter); 115 | return (NSArray *)linesArray; 116 | } 117 | @end 118 | 119 | -------------------------------------------------------------------------------- /FoldLabelDemo/FoldLabelCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 32 | 39 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /FoldLabelDemo/FoldModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // FoldModel.h 3 | // FoldLabelDemo 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | 14 | @interface FoldModel : NSObject 15 | @property (nonatomic, assign)BOOL isFold; 16 | @property (nonatomic, copy) NSString *title; 17 | @property (nonatomic, copy) NSString *content; 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /FoldLabelDemo/FoldModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // FoldModel.m 3 | // FoldLabelDemo 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import "FoldModel.h" 10 | 11 | @implementation FoldModel 12 | 13 | - (instancetype)init 14 | { 15 | self = [super init]; 16 | if (self) { 17 | _isFold = NO; 18 | } 19 | return self; 20 | } 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /FoldLabelDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /FoldLabelDemo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // FoldLabelDemo 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /FoldLabelDemo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // FoldLabelDemo 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "FoldLabelCell.h" 11 | @interface ViewController () 12 | @property (weak, nonatomic) IBOutlet UITableView *tableView; 13 | @property (nonatomic, strong) NSMutableArray *modelArray; 14 | @end 15 | 16 | @implementation ViewController 17 | 18 | - (void)viewDidLoad { 19 | [super viewDidLoad]; 20 | // Do any additional setup after loading the view. 21 | [self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass([FoldLabelCell class]) bundle:nil 22 | ] forCellReuseIdentifier: NSStringFromClass([FoldLabelCell class])]; 23 | self.tableView.estimatedRowHeight = 100; 24 | self.tableView.rowHeight = UITableViewAutomaticDimension; 25 | self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth; 26 | 27 | self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero]; 28 | 29 | self.modelArray = [NSMutableArray arrayWithCapacity:0]; 30 | 31 | NSArray *titleArray = @[@"【字母哥半场10分6篮板,正负值达+17】", 32 | @"八村垒15分7篮板,正负值-20全场最低", 33 | @"奥斯曼:每个人都为胜利做出了贡献,对阵美国将非常艰难", 34 | @"静夜思"]; 35 | NSArray *contentArray = @[@"世界杯F组小组赛第一轮希腊迎战黑山的比赛正在进行中,半场结束,希腊以42-16领先。效力于NBA密尔沃基雄鹿队的希腊前锋扬尼斯-阿德托昆博出场13分钟,6投4中,罚球2中2,得到10分6篮板2抢断,正负值为全队第二高的+17",@"日本阵中八村垒出场31分钟,10投3中,其中三分2中0,罚球10中9,得到15分7篮板2助攻2抢断,正负值为全场最低的-20。",@"土耳其以86-67大胜日本。赛后,土耳其男篮前锋切迪-奥斯曼接受了媒体采访。谈到本场比赛的胜利,奥斯曼说:“我们今天非常专注,在为期45天的准备阶段,我们一直在谈论日本男篮,最终我们击败了他们,这场胜利对于我们而言非常重要,我们的表现很好,每个人都做出了贡献。”谈到下一场小组赛对阵美国男篮,奥斯曼说:“一场艰难的比赛等待着我们,我们大概还有一天半的准备时间,我们会认真研究对手,努力打出最佳状态,争取拿下胜利。”", @"床前明月光\n疑似地上霜\n举头望明月\n低头思故乡。"]; 36 | 37 | for (int i = 0; i < titleArray.count; i++) { 38 | FoldModel *model = [FoldModel new]; 39 | model.title = titleArray[i]; 40 | model.content = contentArray[i]; 41 | [self.modelArray addObject:model]; 42 | } 43 | } 44 | 45 | #pragma mark UITableViewDataSource 46 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 47 | return self.modelArray.count; 48 | } 49 | 50 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 51 | FoldLabelCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([FoldLabelCell class])]; 52 | FoldModel *model = _modelArray[indexPath.row]; 53 | cell.model = model; 54 | __weak typeof(self) weakSelf = self; 55 | cell.showContentCallback = ^{ 56 | __strong typeof(weakSelf) strongSelf = weakSelf; 57 | if (!model.isFold) { 58 | model.isFold = YES; 59 | [strongSelf.modelArray replaceObjectAtIndex:indexPath.row withObject:model]; 60 | } else { 61 | model.isFold = NO; 62 | [strongSelf.modelArray replaceObjectAtIndex:indexPath.row withObject:model]; 63 | } 64 | [UIView performWithoutAnimation:^{ 65 | [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; 66 | }]; 67 | }; 68 | return cell; 69 | } 70 | @end 71 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextContainerView.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextContainerView.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/21. 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 | 12 | #import 13 | 14 | #if __has_include() 15 | #import 16 | #else 17 | #import "YYTextLayout.h" 18 | #endif 19 | 20 | NS_ASSUME_NONNULL_BEGIN 21 | 22 | /** 23 | A simple view to diaplay `YYTextLayout`. 24 | 25 | @discussion This view can become first responder. If this view is first responder, 26 | all the action (such as UIMenu's action) would forward to the `hostView` property. 27 | Typically, you should not use this class directly. 28 | 29 | @warning All the methods in this class should be called on main thread. 30 | */ 31 | @interface YYTextContainerView : UIView 32 | 33 | /// First responder's aciton will forward to this view. 34 | @property (nullable, nonatomic, weak) UIView *hostView; 35 | 36 | /// Debug option for layout debug. Set this property will let the view redraw it's contents. 37 | @property (nullable, nonatomic, copy) YYTextDebugOption *debugOption; 38 | 39 | /// Text vertical alignment. 40 | @property (nonatomic) YYTextVerticalAlignment textVerticalAlignment; 41 | 42 | /// Text layout. Set this property will let the view redraw it's contents. 43 | @property (nullable, nonatomic, strong) YYTextLayout *layout; 44 | 45 | /// The contents fade animation duration when the layout's contents changed. Default is 0 (no animation). 46 | @property (nonatomic) NSTimeInterval contentsFadeDuration; 47 | 48 | /// Convenience method to set `layout` and `contentsFadeDuration`. 49 | /// @param layout Same as `layout` property. 50 | /// @param fadeDuration Same as `contentsFadeDuration` property. 51 | - (void)setLayout:(nullable YYTextLayout *)layout withFadeDuration:(NSTimeInterval)fadeDuration; 52 | 53 | @end 54 | 55 | NS_ASSUME_NONNULL_END 56 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextContainerView.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextContainerView.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/21. 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 | 12 | #import "YYTextContainerView.h" 13 | 14 | @implementation YYTextContainerView { 15 | BOOL _attachmentChanged; 16 | NSMutableArray *_attachmentViews; 17 | NSMutableArray *_attachmentLayers; 18 | } 19 | 20 | - (instancetype)initWithFrame:(CGRect)frame { 21 | self = [super initWithFrame:frame]; 22 | if (!self) return nil; 23 | self.backgroundColor = [UIColor clearColor]; 24 | _attachmentViews = [NSMutableArray array]; 25 | _attachmentLayers = [NSMutableArray array]; 26 | return self; 27 | } 28 | 29 | - (void)setDebugOption:(YYTextDebugOption *)debugOption { 30 | BOOL needDraw = _debugOption.needDrawDebug; 31 | _debugOption = debugOption.copy; 32 | if (_debugOption.needDrawDebug != needDraw) { 33 | [self setNeedsDisplay]; 34 | } 35 | } 36 | 37 | - (void)setTextVerticalAlignment:(YYTextVerticalAlignment)textVerticalAlignment { 38 | if (_textVerticalAlignment == textVerticalAlignment) return; 39 | _textVerticalAlignment = textVerticalAlignment; 40 | [self setNeedsDisplay]; 41 | } 42 | 43 | - (void)setContentsFadeDuration:(NSTimeInterval)contentsFadeDuration { 44 | if (_contentsFadeDuration == contentsFadeDuration) return; 45 | _contentsFadeDuration = contentsFadeDuration; 46 | if (contentsFadeDuration <= 0) { 47 | [self.layer removeAnimationForKey:@"contents"]; 48 | } 49 | } 50 | 51 | - (void)setLayout:(YYTextLayout *)layout { 52 | if (_layout == layout) return; 53 | _layout = layout; 54 | _attachmentChanged = YES; 55 | [self setNeedsDisplay]; 56 | } 57 | 58 | - (void)setLayout:(YYTextLayout *)layout withFadeDuration:(NSTimeInterval)fadeDuration { 59 | self.contentsFadeDuration = fadeDuration; 60 | self.layout = layout; 61 | } 62 | 63 | - (void)drawRect:(CGRect)rect { 64 | // fade content 65 | [self.layer removeAnimationForKey:@"contents"]; 66 | if (_contentsFadeDuration > 0) { 67 | CATransition *transition = [CATransition animation]; 68 | transition.duration = _contentsFadeDuration; 69 | transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; 70 | transition.type = kCATransitionFade; 71 | [self.layer addAnimation:transition forKey:@"contents"]; 72 | } 73 | 74 | // update attachment 75 | if (_attachmentChanged) { 76 | for (UIView *view in _attachmentViews) { 77 | if (view.superview == self) [view removeFromSuperview]; 78 | } 79 | for (CALayer *layer in _attachmentLayers) { 80 | if (layer.superlayer == self.layer) [layer removeFromSuperlayer]; 81 | } 82 | [_attachmentViews removeAllObjects]; 83 | [_attachmentLayers removeAllObjects]; 84 | } 85 | 86 | // draw layout 87 | CGSize boundingSize = _layout.textBoundingSize; 88 | CGPoint point = CGPointZero; 89 | if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) { 90 | if (_layout.container.isVerticalForm) { 91 | point.x = -(self.bounds.size.width - boundingSize.width) * 0.5; 92 | } else { 93 | point.y = (self.bounds.size.height - boundingSize.height) * 0.5; 94 | } 95 | } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) { 96 | if (_layout.container.isVerticalForm) { 97 | point.x = -(self.bounds.size.width - boundingSize.width); 98 | } else { 99 | point.y = (self.bounds.size.height - boundingSize.height); 100 | } 101 | } 102 | [_layout drawInContext:UIGraphicsGetCurrentContext() size:self.bounds.size point:point view:self layer:self.layer debug:_debugOption cancel:nil]; 103 | 104 | // update attachment 105 | if (_attachmentChanged) { 106 | _attachmentChanged = NO; 107 | for (YYTextAttachment *a in _layout.attachments) { 108 | if ([a.content isKindOfClass:[UIView class]]) [_attachmentViews addObject:a.content]; 109 | if ([a.content isKindOfClass:[CALayer class]]) [_attachmentLayers addObject:a.content]; 110 | } 111 | } 112 | } 113 | 114 | - (void)setFrame:(CGRect)frame { 115 | CGSize oldSize = self.bounds.size; 116 | [super setFrame:frame]; 117 | if (!CGSizeEqualToSize(oldSize, self.bounds.size)) { 118 | [self setNeedsLayout]; 119 | } 120 | } 121 | 122 | - (void)setBounds:(CGRect)bounds { 123 | CGSize oldSize = self.bounds.size; 124 | [super setBounds:bounds]; 125 | if (!CGSizeEqualToSize(oldSize, self.bounds.size)) { 126 | [self setNeedsLayout]; 127 | } 128 | } 129 | 130 | #pragma mark - UIResponder forward 131 | 132 | - (BOOL)canBecomeFirstResponder { 133 | return YES; 134 | } 135 | 136 | - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { 137 | return [self.hostView canPerformAction:action withSender:sender]; 138 | } 139 | 140 | - (id)forwardingTargetForSelector:(SEL)aSelector { 141 | return self.hostView; 142 | } 143 | 144 | @end 145 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextDebugOption.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextDebugOption.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/8. 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 | 12 | #import 13 | 14 | @class YYTextDebugOption; 15 | 16 | NS_ASSUME_NONNULL_BEGIN 17 | 18 | /** 19 | The YYTextDebugTarget protocol defines the method a debug target should implement. 20 | A debug target can be add to the global container to receive the shared debug 21 | option changed notification. 22 | */ 23 | @protocol YYTextDebugTarget 24 | 25 | @required 26 | /** 27 | When the shared debug option changed, this method would be called on main thread. 28 | It should return as quickly as possible. The option's property should not be changed 29 | in this method. 30 | 31 | @param option The shared debug option. 32 | */ 33 | - (void)setDebugOption:(nullable YYTextDebugOption *)option; 34 | @end 35 | 36 | 37 | 38 | /** 39 | The debug option for YYText. 40 | */ 41 | @interface YYTextDebugOption : NSObject 42 | @property (nullable, nonatomic, strong) UIColor *baselineColor; ///< baseline color 43 | @property (nullable, nonatomic, strong) UIColor *CTFrameBorderColor; ///< CTFrame path border color 44 | @property (nullable, nonatomic, strong) UIColor *CTFrameFillColor; ///< CTFrame path fill color 45 | @property (nullable, nonatomic, strong) UIColor *CTLineBorderColor; ///< CTLine bounds border color 46 | @property (nullable, nonatomic, strong) UIColor *CTLineFillColor; ///< CTLine bounds fill color 47 | @property (nullable, nonatomic, strong) UIColor *CTLineNumberColor; ///< CTLine line number color 48 | @property (nullable, nonatomic, strong) UIColor *CTRunBorderColor; ///< CTRun bounds border color 49 | @property (nullable, nonatomic, strong) UIColor *CTRunFillColor; ///< CTRun bounds fill color 50 | @property (nullable, nonatomic, strong) UIColor *CTRunNumberColor; ///< CTRun number color 51 | @property (nullable, nonatomic, strong) UIColor *CGGlyphBorderColor; ///< CGGlyph bounds border color 52 | @property (nullable, nonatomic, strong) UIColor *CGGlyphFillColor; ///< CGGlyph bounds fill color 53 | 54 | - (BOOL)needDrawDebug; ///< `YES`: at least one debug color is visible. `NO`: all debug color is invisible/nil. 55 | - (void)clear; ///< Set all debug color to nil. 56 | 57 | /** 58 | Add a debug target. 59 | 60 | @discussion When `setSharedDebugOption:` is called, all added debug target will 61 | receive `setDebugOption:` in main thread. It maintains an unsafe_unretained 62 | reference to this target. The target must to removed before dealloc. 63 | 64 | @param target A debug target. 65 | */ 66 | + (void)addDebugTarget:(id)target; 67 | 68 | /** 69 | Remove a debug target which is added by `addDebugTarget:`. 70 | 71 | @param target A debug target. 72 | */ 73 | + (void)removeDebugTarget:(id)target; 74 | 75 | /** 76 | Returns the shared debug option. 77 | 78 | @return The shared debug option, default is nil. 79 | */ 80 | + (nullable YYTextDebugOption *)sharedDebugOption; 81 | 82 | /** 83 | Set a debug option as shared debug option. 84 | This method must be called on main thread. 85 | 86 | @discussion When call this method, the new option will set to all debug target 87 | which is added by `addDebugTarget:`. 88 | 89 | @param option A new debug option (nil is valid). 90 | */ 91 | + (void)setSharedDebugOption:(nullable YYTextDebugOption *)option; 92 | 93 | @end 94 | 95 | NS_ASSUME_NONNULL_END 96 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextDebugOption.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextDebugOption.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/8. 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 | 12 | #import "YYTextDebugOption.h" 13 | #import "YYTextWeakProxy.h" 14 | #import 15 | #import 16 | 17 | static pthread_mutex_t _sharedDebugLock; 18 | static CFMutableSetRef _sharedDebugTargets = nil; 19 | static YYTextDebugOption *_sharedDebugOption = nil; 20 | 21 | static const void* _sharedDebugSetRetain(CFAllocatorRef allocator, const void *value) { 22 | return value; 23 | } 24 | 25 | static void _sharedDebugSetRelease(CFAllocatorRef allocator, const void *value) { 26 | } 27 | 28 | void _sharedDebugSetFunction(const void *value, void *context) { 29 | id target = (__bridge id)(value); 30 | [target setDebugOption:_sharedDebugOption]; 31 | } 32 | 33 | static void _initSharedDebug() { 34 | static dispatch_once_t onceToken; 35 | dispatch_once(&onceToken, ^{ 36 | pthread_mutex_init(&_sharedDebugLock, NULL); 37 | CFSetCallBacks callbacks = kCFTypeSetCallBacks; 38 | callbacks.retain = _sharedDebugSetRetain; 39 | callbacks.release = _sharedDebugSetRelease; 40 | _sharedDebugTargets = CFSetCreateMutable(CFAllocatorGetDefault(), 0, &callbacks); 41 | }); 42 | } 43 | 44 | static void _setSharedDebugOption(YYTextDebugOption *option) { 45 | _initSharedDebug(); 46 | pthread_mutex_lock(&_sharedDebugLock); 47 | _sharedDebugOption = option.copy; 48 | CFSetApplyFunction(_sharedDebugTargets, _sharedDebugSetFunction, NULL); 49 | pthread_mutex_unlock(&_sharedDebugLock); 50 | } 51 | 52 | static YYTextDebugOption *_getSharedDebugOption() { 53 | _initSharedDebug(); 54 | pthread_mutex_lock(&_sharedDebugLock); 55 | YYTextDebugOption *op = _sharedDebugOption; 56 | pthread_mutex_unlock(&_sharedDebugLock); 57 | return op; 58 | } 59 | 60 | static void _addDebugTarget(id target) { 61 | _initSharedDebug(); 62 | pthread_mutex_lock(&_sharedDebugLock); 63 | CFSetAddValue(_sharedDebugTargets, (__bridge const void *)(target)); 64 | pthread_mutex_unlock(&_sharedDebugLock); 65 | } 66 | 67 | static void _removeDebugTarget(id target) { 68 | _initSharedDebug(); 69 | pthread_mutex_lock(&_sharedDebugLock); 70 | CFSetRemoveValue(_sharedDebugTargets, (__bridge const void *)(target)); 71 | pthread_mutex_unlock(&_sharedDebugLock); 72 | } 73 | 74 | 75 | @implementation YYTextDebugOption 76 | 77 | - (id)copyWithZone:(NSZone *)zone { 78 | YYTextDebugOption *op = [self.class new]; 79 | op.baselineColor = self.baselineColor; 80 | op.CTFrameBorderColor = self.CTFrameBorderColor; 81 | op.CTFrameFillColor = self.CTFrameFillColor; 82 | op.CTLineBorderColor = self.CTLineBorderColor; 83 | op.CTLineFillColor = self.CTLineFillColor; 84 | op.CTLineNumberColor = self.CTLineNumberColor; 85 | op.CTRunBorderColor = self.CTRunBorderColor; 86 | op.CTRunFillColor = self.CTRunFillColor; 87 | op.CTRunNumberColor = self.CTRunNumberColor; 88 | op.CGGlyphBorderColor = self.CGGlyphBorderColor; 89 | op.CGGlyphFillColor = self.CGGlyphFillColor; 90 | return op; 91 | } 92 | 93 | - (BOOL)needDrawDebug { 94 | if (self.baselineColor || 95 | self.CTFrameBorderColor || 96 | self.CTFrameFillColor || 97 | self.CTLineBorderColor || 98 | self.CTLineFillColor || 99 | self.CTLineNumberColor || 100 | self.CTRunBorderColor || 101 | self.CTRunFillColor || 102 | self.CTRunNumberColor || 103 | self.CGGlyphBorderColor || 104 | self.CGGlyphFillColor) return YES; 105 | return NO; 106 | } 107 | 108 | - (void)clear { 109 | self.baselineColor = nil; 110 | self.CTFrameBorderColor = nil; 111 | self.CTFrameFillColor = nil; 112 | self.CTLineBorderColor = nil; 113 | self.CTLineFillColor = nil; 114 | self.CTLineNumberColor = nil; 115 | self.CTRunBorderColor = nil; 116 | self.CTRunFillColor = nil; 117 | self.CTRunNumberColor = nil; 118 | self.CGGlyphBorderColor = nil; 119 | self.CGGlyphFillColor = nil; 120 | } 121 | 122 | + (void)addDebugTarget:(id)target { 123 | if (target) _addDebugTarget(target); 124 | } 125 | 126 | + (void)removeDebugTarget:(id)target { 127 | if (target) _removeDebugTarget(target); 128 | } 129 | 130 | + (YYTextDebugOption *)sharedDebugOption { 131 | return _getSharedDebugOption(); 132 | } 133 | 134 | + (void)setSharedDebugOption:(YYTextDebugOption *)option { 135 | NSAssert([NSThread isMainThread], @"This method must be called on the main thread"); 136 | _setSharedDebugOption(option); 137 | } 138 | 139 | @end 140 | 141 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextEffectWindow.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextEffectWindow.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/2/25. 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 | 12 | #import 13 | 14 | #if __has_include() 15 | #import 16 | #import 17 | #else 18 | #import "YYTextMagnifier.h" 19 | #import "YYTextSelectionView.h" 20 | #endif 21 | 22 | NS_ASSUME_NONNULL_BEGIN 23 | 24 | /** 25 | A window to display magnifier and extra contents for text view. 26 | 27 | @discussion Use `sharedWindow` to get the instance, don't create your own instance. 28 | Typically, you should not use this class directly. 29 | */ 30 | @interface YYTextEffectWindow : UIWindow 31 | 32 | /// Returns the shared instance (returns nil in App Extension). 33 | + (nullable instancetype)sharedWindow; 34 | 35 | /// Show the magnifier in this window with a 'popup' animation. @param mag A magnifier. 36 | - (void)showMagnifier:(YYTextMagnifier *)mag; 37 | /// Update the magnifier content and position. @param mag A magnifier. 38 | - (void)moveMagnifier:(YYTextMagnifier *)mag; 39 | /// Remove the magnifier from this window with a 'shrink' animation. @param mag A magnifier. 40 | - (void)hideMagnifier:(YYTextMagnifier *)mag; 41 | 42 | 43 | /// Show the selection dot in this window if the dot is clipped by the selection view. 44 | /// @param selection A selection view. 45 | - (void)showSelectionDot:(YYTextSelectionView *)selection; 46 | /// Remove the selection dot from this window. 47 | /// @param selection A selection view. 48 | - (void)hideSelectionDot:(YYTextSelectionView *)selection; 49 | 50 | @end 51 | 52 | NS_ASSUME_NONNULL_END 53 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextInput.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextInput.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/17. 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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | Text position affinity. For example, the offset appears after the last 18 | character on a line is backward affinity, before the first character on 19 | the following line is forward affinity. 20 | */ 21 | typedef NS_ENUM(NSInteger, YYTextAffinity) { 22 | YYTextAffinityForward = 0, ///< offset appears before the character 23 | YYTextAffinityBackward = 1, ///< offset appears after the character 24 | }; 25 | 26 | 27 | /** 28 | A YYTextPosition object represents a position in a text container; in other words, 29 | it is an index into the backing string in a text-displaying view. 30 | 31 | YYTextPosition has the same API as Apple's implementation in UITextView/UITextField, 32 | so you can alse use it to interact with UITextView/UITextField. 33 | */ 34 | @interface YYTextPosition : UITextPosition 35 | 36 | @property (nonatomic, readonly) NSInteger offset; 37 | @property (nonatomic, readonly) YYTextAffinity affinity; 38 | 39 | + (instancetype)positionWithOffset:(NSInteger)offset; 40 | + (instancetype)positionWithOffset:(NSInteger)offset affinity:(YYTextAffinity) affinity; 41 | 42 | - (NSComparisonResult)compare:(id)otherPosition; 43 | 44 | @end 45 | 46 | 47 | /** 48 | A YYTextRange object represents a range of characters in a text container; in other words, 49 | it identifies a starting index and an ending index in string backing a text-displaying view. 50 | 51 | YYTextRange has the same API as Apple's implementation in UITextView/UITextField, 52 | so you can alse use it to interact with UITextView/UITextField. 53 | */ 54 | @interface YYTextRange : UITextRange 55 | 56 | @property (nonatomic, readonly) YYTextPosition *start; 57 | @property (nonatomic, readonly) YYTextPosition *end; 58 | @property (nonatomic, readonly, getter=isEmpty) BOOL empty; 59 | 60 | + (instancetype)rangeWithRange:(NSRange)range; 61 | + (instancetype)rangeWithRange:(NSRange)range affinity:(YYTextAffinity) affinity; 62 | + (instancetype)rangeWithStart:(YYTextPosition *)start end:(YYTextPosition *)end; 63 | + (instancetype)defaultRange; ///< <{0,0} Forward> 64 | 65 | - (NSRange)asRange; 66 | 67 | @end 68 | 69 | 70 | /** 71 | A YYTextSelectionRect object encapsulates information about a selected range of 72 | text in a text-displaying view. 73 | 74 | YYTextSelectionRect has the same API as Apple's implementation in UITextView/UITextField, 75 | so you can alse use it to interact with UITextView/UITextField. 76 | */ 77 | @interface YYTextSelectionRect : UITextSelectionRect 78 | 79 | @property (nonatomic, readwrite) CGRect rect; 80 | @property (nonatomic, readwrite) UITextWritingDirection writingDirection; 81 | @property (nonatomic, readwrite) BOOL containsStart; 82 | @property (nonatomic, readwrite) BOOL containsEnd; 83 | @property (nonatomic, readwrite) BOOL isVertical; 84 | 85 | @end 86 | 87 | NS_ASSUME_NONNULL_END 88 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextInput.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextInput.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/17. 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 | 12 | #import "YYTextInput.h" 13 | #import "YYTextUtilities.h" 14 | 15 | 16 | @implementation YYTextPosition 17 | 18 | + (instancetype)positionWithOffset:(NSInteger)offset { 19 | return [self positionWithOffset:offset affinity:YYTextAffinityForward]; 20 | } 21 | 22 | + (instancetype)positionWithOffset:(NSInteger)offset affinity:(YYTextAffinity)affinity { 23 | YYTextPosition *p = [self new]; 24 | p->_offset = offset; 25 | p->_affinity = affinity; 26 | return p; 27 | } 28 | 29 | - (instancetype)copyWithZone:(NSZone *)zone { 30 | return [self.class positionWithOffset:_offset affinity:_affinity]; 31 | } 32 | 33 | - (NSString *)description { 34 | return [NSString stringWithFormat:@"<%@: %p> (%@%@)", self.class, self, @(_offset), _affinity == YYTextAffinityForward ? @"F":@"B"]; 35 | } 36 | 37 | - (NSUInteger)hash { 38 | return _offset * 2 + (_affinity == YYTextAffinityForward ? 1 : 0); 39 | } 40 | 41 | - (BOOL)isEqual:(YYTextPosition *)object { 42 | if (!object) return NO; 43 | return _offset == object.offset && _affinity == object.affinity; 44 | } 45 | 46 | - (NSComparisonResult)compare:(YYTextPosition *)otherPosition { 47 | if (!otherPosition) return NSOrderedAscending; 48 | if (_offset < otherPosition.offset) return NSOrderedAscending; 49 | if (_offset > otherPosition.offset) return NSOrderedDescending; 50 | if (_affinity == YYTextAffinityBackward && otherPosition.affinity == YYTextAffinityForward) return NSOrderedAscending; 51 | if (_affinity == YYTextAffinityForward && otherPosition.affinity == YYTextAffinityBackward) return NSOrderedDescending; 52 | return NSOrderedSame; 53 | } 54 | 55 | @end 56 | 57 | 58 | 59 | @implementation YYTextRange { 60 | YYTextPosition *_start; 61 | YYTextPosition *_end; 62 | } 63 | 64 | - (instancetype)init { 65 | self = [super init]; 66 | if (!self) return nil; 67 | _start = [YYTextPosition positionWithOffset:0]; 68 | _end = [YYTextPosition positionWithOffset:0]; 69 | return self; 70 | } 71 | 72 | - (YYTextPosition *)start { 73 | return _start; 74 | } 75 | 76 | - (YYTextPosition *)end { 77 | return _end; 78 | } 79 | 80 | - (BOOL)isEmpty { 81 | return _start.offset == _end.offset; 82 | } 83 | 84 | - (NSRange)asRange { 85 | return NSMakeRange(_start.offset, _end.offset - _start.offset); 86 | } 87 | 88 | + (instancetype)rangeWithRange:(NSRange)range { 89 | return [self rangeWithRange:range affinity:YYTextAffinityForward]; 90 | } 91 | 92 | + (instancetype)rangeWithRange:(NSRange)range affinity:(YYTextAffinity)affinity { 93 | YYTextPosition *start = [YYTextPosition positionWithOffset:range.location affinity:affinity]; 94 | YYTextPosition *end = [YYTextPosition positionWithOffset:range.location + range.length affinity:affinity]; 95 | return [self rangeWithStart:start end:end]; 96 | } 97 | 98 | + (instancetype)rangeWithStart:(YYTextPosition *)start end:(YYTextPosition *)end { 99 | if (!start || !end) return nil; 100 | if ([start compare:end] == NSOrderedDescending) { 101 | YYTEXT_SWAP(start, end); 102 | } 103 | YYTextRange *range = [YYTextRange new]; 104 | range->_start = start; 105 | range->_end = end; 106 | return range; 107 | } 108 | 109 | + (instancetype)defaultRange { 110 | return [self new]; 111 | } 112 | 113 | - (instancetype)copyWithZone:(NSZone *)zone { 114 | return [self.class rangeWithStart:_start end:_end]; 115 | } 116 | 117 | - (NSString *)description { 118 | return [NSString stringWithFormat:@"<%@: %p> (%@, %@)%@", self.class, self, @(_start.offset), @(_end.offset - _start.offset), _end.affinity == YYTextAffinityForward ? @"F":@"B"]; 119 | } 120 | 121 | - (NSUInteger)hash { 122 | return (sizeof(NSUInteger) == 8 ? OSSwapInt64(_start.hash) : OSSwapInt32(_start.hash)) + _end.hash; 123 | } 124 | 125 | - (BOOL)isEqual:(YYTextRange *)object { 126 | if (!object) return NO; 127 | return [_start isEqual:object.start] && [_end isEqual:object.end]; 128 | } 129 | 130 | @end 131 | 132 | 133 | 134 | @implementation YYTextSelectionRect 135 | 136 | @synthesize rect = _rect; 137 | @synthesize writingDirection = _writingDirection; 138 | @synthesize containsStart = _containsStart; 139 | @synthesize containsEnd = _containsEnd; 140 | @synthesize isVertical = _isVertical; 141 | 142 | - (id)copyWithZone:(NSZone *)zone { 143 | YYTextSelectionRect *one = [self.class new]; 144 | one.rect = _rect; 145 | one.writingDirection = _writingDirection; 146 | one.containsStart = _containsStart; 147 | one.containsEnd = _containsEnd; 148 | one.isVertical = _isVertical; 149 | return one; 150 | } 151 | 152 | @end 153 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextKeyboardManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextKeyboardManager.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/6/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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | System keyboard transition information. 18 | Use -[YYTextKeyboardManager convertRect:toView:] to convert frame to specified view. 19 | */ 20 | typedef struct { 21 | BOOL fromVisible; ///< Keyboard visible before transition. 22 | BOOL toVisible; ///< Keyboard visible after transition. 23 | CGRect fromFrame; ///< Keyboard frame before transition. 24 | CGRect toFrame; ///< Keyboard frame after transition. 25 | NSTimeInterval animationDuration; ///< Keyboard transition animation duration. 26 | UIViewAnimationCurve animationCurve; ///< Keyboard transition animation curve. 27 | UIViewAnimationOptions animationOption; ///< Keybaord transition animation option. 28 | } YYTextKeyboardTransition; 29 | 30 | 31 | /** 32 | The YYTextKeyboardObserver protocol defines the method you can use 33 | to receive system keyboard change information. 34 | */ 35 | @protocol YYTextKeyboardObserver 36 | @optional 37 | - (void)keyboardChangedWithTransition:(YYTextKeyboardTransition)transition; 38 | @end 39 | 40 | 41 | /** 42 | A YYTextKeyboardManager object lets you get the system keyboard information, 43 | and track the keyboard visible/frame/transition. 44 | 45 | @discussion You should access this class in main thread. 46 | Compatible: iPhone/iPad with iOS6/7/8/9. 47 | */ 48 | @interface YYTextKeyboardManager : NSObject 49 | 50 | - (instancetype)init UNAVAILABLE_ATTRIBUTE; 51 | + (instancetype)new UNAVAILABLE_ATTRIBUTE; 52 | 53 | /// Get the default manager (returns nil in App Extension). 54 | + (nullable instancetype)defaultManager; 55 | 56 | /// Get the keyboard window. nil if there's no keyboard window. 57 | @property (nullable, nonatomic, readonly) UIWindow *keyboardWindow; 58 | 59 | /// Get the keyboard view. nil if there's no keyboard view. 60 | @property (nullable, nonatomic, readonly) UIView *keyboardView; 61 | 62 | /// Whether the keyboard is visible. 63 | @property (nonatomic, readonly, getter=isKeyboardVisible) BOOL keyboardVisible; 64 | 65 | /// Get the keyboard frame. CGRectNull if there's no keyboard view. 66 | /// Use convertRect:toView: to convert frame to specified view. 67 | @property (nonatomic, readonly) CGRect keyboardFrame; 68 | 69 | 70 | /** 71 | Add an observer to manager to get keyboard change information. 72 | This method makes a weak reference to the observer. 73 | 74 | @param observer An observer. 75 | This method will do nothing if the observer is nil, or already added. 76 | */ 77 | - (void)addObserver:(id)observer; 78 | 79 | /** 80 | Remove an observer from manager. 81 | 82 | @param observer An observer. 83 | This method will do nothing if the observer is nil, or not in manager. 84 | */ 85 | - (void)removeObserver:(id)observer; 86 | 87 | /** 88 | Convert rect to specified view or window. 89 | 90 | @param rect The frame rect. 91 | @param view A specified view or window (pass nil to convert for main window). 92 | @return The converted rect in specifeid view. 93 | */ 94 | - (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view; 95 | 96 | @end 97 | 98 | NS_ASSUME_NONNULL_END 99 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextLine.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextLine.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/3/10. 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 | 12 | #import 13 | #import 14 | 15 | #if __has_include() 16 | #import 17 | #else 18 | #import "YYTextAttribute.h" 19 | #endif 20 | 21 | @class YYTextRunGlyphRange; 22 | 23 | NS_ASSUME_NONNULL_BEGIN 24 | 25 | /** 26 | A text line object wrapped `CTLineRef`, see `YYTextLayout` for more. 27 | */ 28 | @interface YYTextLine : NSObject 29 | 30 | + (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical; 31 | 32 | @property (nonatomic) NSUInteger index; ///< line index 33 | @property (nonatomic) NSUInteger row; ///< line row 34 | @property (nullable, nonatomic, strong) NSArray *> *verticalRotateRange; ///< Run rotate range 35 | 36 | @property (nonatomic, readonly) CTLineRef CTLine; ///< CoreText line 37 | @property (nonatomic, readonly) NSRange range; ///< string range 38 | @property (nonatomic, readonly) BOOL vertical; ///< vertical form 39 | 40 | @property (nonatomic, readonly) CGRect bounds; ///< bounds (ascent + descent) 41 | @property (nonatomic, readonly) CGSize size; ///< bounds.size 42 | @property (nonatomic, readonly) CGFloat width; ///< bounds.size.width 43 | @property (nonatomic, readonly) CGFloat height; ///< bounds.size.height 44 | @property (nonatomic, readonly) CGFloat top; ///< bounds.origin.y 45 | @property (nonatomic, readonly) CGFloat bottom; ///< bounds.origin.y + bounds.size.height 46 | @property (nonatomic, readonly) CGFloat left; ///< bounds.origin.x 47 | @property (nonatomic, readonly) CGFloat right; ///< bounds.origin.x + bounds.size.width 48 | 49 | @property (nonatomic) CGPoint position; ///< baseline position 50 | @property (nonatomic, readonly) CGFloat ascent; ///< line ascent 51 | @property (nonatomic, readonly) CGFloat descent; ///< line descent 52 | @property (nonatomic, readonly) CGFloat leading; ///< line leading 53 | @property (nonatomic, readonly) CGFloat lineWidth; ///< line width 54 | @property (nonatomic, readonly) CGFloat trailingWhitespaceWidth; 55 | 56 | @property (nullable, nonatomic, readonly) NSArray *attachments; ///< YYTextAttachment 57 | @property (nullable, nonatomic, readonly) NSArray *attachmentRanges; ///< NSRange(NSValue) 58 | @property (nullable, nonatomic, readonly) NSArray *attachmentRects; ///< CGRect(NSValue) 59 | 60 | @end 61 | 62 | 63 | typedef NS_ENUM(NSUInteger, YYTextRunGlyphDrawMode) { 64 | /// No rotate. 65 | YYTextRunGlyphDrawModeHorizontal = 0, 66 | 67 | /// Rotate vertical for single glyph. 68 | YYTextRunGlyphDrawModeVerticalRotate = 1, 69 | 70 | /// Rotate vertical for single glyph, and move the glyph to a better position, 71 | /// such as fullwidth punctuation. 72 | YYTextRunGlyphDrawModeVerticalRotateMove = 2, 73 | }; 74 | 75 | /** 76 | A range in CTRun, used for vertical form. 77 | */ 78 | @interface YYTextRunGlyphRange : NSObject 79 | @property (nonatomic) NSRange glyphRangeInRun; 80 | @property (nonatomic) YYTextRunGlyphDrawMode drawMode; 81 | + (instancetype)rangeWithRange:(NSRange)range drawMode:(YYTextRunGlyphDrawMode)mode; 82 | @end 83 | 84 | NS_ASSUME_NONNULL_END 85 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextLine.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYYTextLine.m 3 | // YYText 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 | 12 | #import "YYTextLine.h" 13 | #import "YYTextUtilities.h" 14 | 15 | 16 | @implementation YYTextLine { 17 | CGFloat _firstGlyphPos; // first glyph position for baseline, typically 0. 18 | } 19 | 20 | + (instancetype)lineWithCTLine:(CTLineRef)CTLine position:(CGPoint)position vertical:(BOOL)isVertical { 21 | if (!CTLine) return nil; 22 | YYTextLine *line = [self new]; 23 | line->_position = position; 24 | line->_vertical = isVertical; 25 | [line setCTLine:CTLine]; 26 | return line; 27 | } 28 | 29 | - (void)dealloc { 30 | if (_CTLine) CFRelease(_CTLine); 31 | } 32 | 33 | - (void)setCTLine:(_Nonnull CTLineRef)CTLine { 34 | if (_CTLine != CTLine) { 35 | if (CTLine) CFRetain(CTLine); 36 | if (_CTLine) CFRelease(_CTLine); 37 | _CTLine = CTLine; 38 | if (_CTLine) { 39 | _lineWidth = CTLineGetTypographicBounds(_CTLine, &_ascent, &_descent, &_leading); 40 | CFRange range = CTLineGetStringRange(_CTLine); 41 | _range = NSMakeRange(range.location, range.length); 42 | if (CTLineGetGlyphCount(_CTLine) > 0) { 43 | CFArrayRef runs = CTLineGetGlyphRuns(_CTLine); 44 | CTRunRef run = CFArrayGetValueAtIndex(runs, 0); 45 | CGPoint pos; 46 | CTRunGetPositions(run, CFRangeMake(0, 1), &pos); 47 | _firstGlyphPos = pos.x; 48 | } else { 49 | _firstGlyphPos = 0; 50 | } 51 | _trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(_CTLine); 52 | } else { 53 | _lineWidth = _ascent = _descent = _leading = _firstGlyphPos = _trailingWhitespaceWidth = 0; 54 | _range = NSMakeRange(0, 0); 55 | } 56 | [self reloadBounds]; 57 | } 58 | } 59 | 60 | - (void)setPosition:(CGPoint)position { 61 | _position = position; 62 | [self reloadBounds]; 63 | } 64 | 65 | - (void)reloadBounds { 66 | if (_vertical) { 67 | _bounds = CGRectMake(_position.x - _descent, _position.y, _ascent + _descent, _lineWidth); 68 | _bounds.origin.y += _firstGlyphPos; 69 | } else { 70 | _bounds = CGRectMake(_position.x, _position.y - _ascent, _lineWidth, _ascent + _descent); 71 | _bounds.origin.x += _firstGlyphPos; 72 | } 73 | 74 | _attachments = nil; 75 | _attachmentRanges = nil; 76 | _attachmentRects = nil; 77 | if (!_CTLine) return; 78 | CFArrayRef runs = CTLineGetGlyphRuns(_CTLine); 79 | NSUInteger runCount = CFArrayGetCount(runs); 80 | if (runCount == 0) return; 81 | 82 | NSMutableArray *attachments = [NSMutableArray new]; 83 | NSMutableArray *attachmentRanges = [NSMutableArray new]; 84 | NSMutableArray *attachmentRects = [NSMutableArray new]; 85 | for (NSUInteger r = 0; r < runCount; r++) { 86 | CTRunRef run = CFArrayGetValueAtIndex(runs, r); 87 | CFIndex glyphCount = CTRunGetGlyphCount(run); 88 | if (glyphCount == 0) continue; 89 | NSDictionary *attrs = (id)CTRunGetAttributes(run); 90 | YYTextAttachment *attachment = attrs[YYTextAttachmentAttributeName]; 91 | if (attachment) { 92 | CGPoint runPosition = CGPointZero; 93 | CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); 94 | 95 | CGFloat ascent, descent, leading, runWidth; 96 | CGRect runTypoBounds; 97 | runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading); 98 | 99 | if (_vertical) { 100 | YYTEXT_SWAP(runPosition.x, runPosition.y); 101 | runPosition.y = _position.y + runPosition.y; 102 | runTypoBounds = CGRectMake(_position.x + runPosition.x - descent, runPosition.y , ascent + descent, runWidth); 103 | } else { 104 | runPosition.x += _position.x; 105 | runPosition.y = _position.y - runPosition.y; 106 | runTypoBounds = CGRectMake(runPosition.x, runPosition.y - ascent, runWidth, ascent + descent); 107 | } 108 | 109 | NSRange runRange = YYTextNSRangeFromCFRange(CTRunGetStringRange(run)); 110 | [attachments addObject:attachment]; 111 | [attachmentRanges addObject:[NSValue valueWithRange:runRange]]; 112 | [attachmentRects addObject:[NSValue valueWithCGRect:runTypoBounds]]; 113 | } 114 | } 115 | _attachments = attachments.count ? attachments : nil; 116 | _attachmentRanges = attachmentRanges.count ? attachmentRanges : nil; 117 | _attachmentRects = attachmentRects.count ? attachmentRects : nil; 118 | } 119 | 120 | - (CGSize)size { 121 | return _bounds.size; 122 | } 123 | 124 | - (CGFloat)width { 125 | return CGRectGetWidth(_bounds); 126 | } 127 | 128 | - (CGFloat)height { 129 | return CGRectGetHeight(_bounds); 130 | } 131 | 132 | - (CGFloat)top { 133 | return CGRectGetMinY(_bounds); 134 | } 135 | 136 | - (CGFloat)bottom { 137 | return CGRectGetMaxY(_bounds); 138 | } 139 | 140 | - (CGFloat)left { 141 | return CGRectGetMinX(_bounds); 142 | } 143 | 144 | - (CGFloat)right { 145 | return CGRectGetMaxX(_bounds); 146 | } 147 | 148 | - (NSString *)description { 149 | NSMutableString *desc = @"".mutableCopy; 150 | NSRange range = self.range; 151 | [desc appendFormat:@" row:%zd range:%tu,%tu",self, self.row, range.location, range.length]; 152 | [desc appendFormat:@" position:%@",NSStringFromCGPoint(self.position)]; 153 | [desc appendFormat:@" bounds:%@",NSStringFromCGRect(self.bounds)]; 154 | return desc; 155 | } 156 | 157 | @end 158 | 159 | 160 | @implementation YYTextRunGlyphRange 161 | + (instancetype)rangeWithRange:(NSRange)range drawMode:(YYTextRunGlyphDrawMode)mode { 162 | YYTextRunGlyphRange *one = [self new]; 163 | one.glyphRangeInRun = range; 164 | one.drawMode = mode; 165 | return one; 166 | } 167 | @end 168 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextMagnifier.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextMagnifier.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/2/25. 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 | 12 | #import 13 | 14 | #if __has_include() 15 | #import 16 | #else 17 | #import "YYTextAttribute.h" 18 | #endif 19 | 20 | NS_ASSUME_NONNULL_BEGIN 21 | 22 | /// Magnifier type 23 | typedef NS_ENUM(NSInteger, YYTextMagnifierType) { 24 | YYTextMagnifierTypeCaret, ///< Circular magnifier 25 | YYTextMagnifierTypeRanged, ///< Round rectangle magnifier 26 | }; 27 | 28 | /** 29 | A magnifier view which can be displayed in `YYTextEffectWindow`. 30 | 31 | @discussion Use `magnifierWithType:` to create instance. 32 | Typically, you should not use this class directly. 33 | */ 34 | @interface YYTextMagnifier : UIView 35 | 36 | /// Create a mangifier with the specified type. @param type The magnifier type. 37 | + (id)magnifierWithType:(YYTextMagnifierType)type; 38 | 39 | @property (nonatomic, readonly) YYTextMagnifierType type; ///< Type of magnifier 40 | @property (nonatomic, readonly) CGSize fitSize; ///< The 'best' size for magnifier view. 41 | @property (nonatomic, readonly) CGSize snapshotSize; ///< The 'best' snapshot image size for magnifier. 42 | @property (nullable, nonatomic, strong) UIImage *snapshot; ///< The image in magnifier (readwrite). 43 | 44 | @property (nullable, nonatomic, weak) UIView *hostView; ///< The coordinate based view. 45 | @property (nonatomic) CGPoint hostCaptureCenter; ///< The snapshot capture center in `hostView`. 46 | @property (nonatomic) CGPoint hostPopoverCenter; ///< The popover center in `hostView`. 47 | @property (nonatomic) BOOL hostVerticalForm; ///< The host view is vertical form. 48 | @property (nonatomic) BOOL captureDisabled; ///< A hint for `YYTextEffectWindow` to disable capture. 49 | @property (nonatomic) BOOL captureFadeAnimation; ///< Show fade animation when the snapshot image changed. 50 | @end 51 | 52 | NS_ASSUME_NONNULL_END 53 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextMagnifier.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextMagnifier.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/2/25. 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 | 12 | #import "YYTextMagnifier.h" 13 | #import "YYTextUtilities.h" 14 | 15 | #define kCaptureDisableFadeTime 0.1 16 | 17 | 18 | @interface _YYTextMagnifierCaret : YYTextMagnifier { 19 | UIImageView *_contentView; 20 | UIImageView *_coverView; 21 | } 22 | @end 23 | 24 | @implementation _YYTextMagnifierCaret 25 | 26 | #define kMultiple 1.2 27 | #define kDiameter 113.0 28 | #define kPadding 7.0 29 | #define kSize CGSizeMake(kDiameter + kPadding * 2, kDiameter + kPadding * 2) 30 | 31 | - (instancetype)initWithFrame:(CGRect)frame { 32 | self = [super initWithFrame:frame]; 33 | _contentView = [UIImageView new]; 34 | _contentView.frame = CGRectMake(kPadding, kPadding, kDiameter, kDiameter); 35 | _contentView.layer.cornerRadius = kDiameter / 2; 36 | _contentView.clipsToBounds = YES; 37 | [self addSubview:_contentView]; 38 | 39 | _coverView = [UIImageView new]; 40 | _coverView.frame = (CGRect){.origin = CGPointZero, .size = kSize}; 41 | _coverView.image = [self.class coverImage]; 42 | [self addSubview:_coverView]; 43 | return self; 44 | } 45 | 46 | - (instancetype)init { 47 | self = [self initWithFrame:CGRectZero]; 48 | self.frame = (CGRect){.size = [self sizeThatFits:CGSizeZero]}; 49 | return self; 50 | } 51 | 52 | - (YYTextMagnifierType)type { 53 | return YYTextMagnifierTypeCaret; 54 | } 55 | 56 | - (CGSize)sizeThatFits:(CGSize)size { 57 | return kSize; 58 | } 59 | 60 | - (void)setSnapshot:(UIImage *)snapshot { 61 | if (self.captureFadeAnimation) { 62 | [_contentView.layer removeAnimationForKey:@"contents"]; 63 | CABasicAnimation *animation = [CABasicAnimation animation]; 64 | animation.duration = kCaptureDisableFadeTime; 65 | animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; 66 | [_contentView.layer addAnimation:animation forKey:@"contents"]; 67 | } 68 | _contentView.image = snapshot; 69 | } 70 | 71 | - (UIImage *)snapshot { 72 | return _contentView.image; 73 | } 74 | 75 | - (CGSize)snapshotSize { 76 | CGFloat length = floor(kDiameter / 1.2); 77 | return CGSizeMake(length, length); 78 | } 79 | 80 | - (CGSize)fitSize { 81 | return [self sizeThatFits:CGSizeZero]; 82 | } 83 | 84 | + (UIImage *)coverImage { 85 | static UIImage *image; 86 | static dispatch_once_t onceToken; 87 | dispatch_once(&onceToken, ^{ 88 | CGSize size = kSize; 89 | CGRect rect = (CGRect) {.size = size, .origin = CGPointZero}; 90 | rect = CGRectInset(rect, kPadding, kPadding); 91 | 92 | UIGraphicsBeginImageContextWithOptions(size, NO, 0); 93 | CGContextRef context = UIGraphicsGetCurrentContext(); 94 | 95 | CGPathRef boxPath = CGPathCreateWithRect(CGRectMake(0, 0, size.width, size.height), NULL); 96 | CGPathRef fillPath = CGPathCreateWithEllipseInRect(rect, NULL); 97 | CGPathRef strokePath = CGPathCreateWithEllipseInRect(YYTextCGRectPixelHalf(rect), NULL); 98 | 99 | // inner shadow 100 | CGContextSaveGState(context); { 101 | CGFloat blurRadius = 25; 102 | CGSize offset = CGSizeMake(0, 15); 103 | CGColorRef shadowColor = [UIColor colorWithWhite:0 alpha:0.16].CGColor; 104 | CGColorRef opaqueShadowColor = CGColorCreateCopyWithAlpha(shadowColor, 1.0); 105 | CGContextAddPath(context, fillPath); 106 | CGContextClip(context); 107 | CGContextSetAlpha(context, CGColorGetAlpha(shadowColor)); 108 | CGContextBeginTransparencyLayer(context, NULL); { 109 | CGContextSetShadowWithColor(context, offset, blurRadius, opaqueShadowColor); 110 | CGContextSetBlendMode(context, kCGBlendModeSourceOut); 111 | CGContextSetFillColorWithColor(context, opaqueShadowColor); 112 | CGContextAddPath(context, fillPath); 113 | CGContextFillPath(context); 114 | } CGContextEndTransparencyLayer(context); 115 | CGColorRelease(opaqueShadowColor); 116 | } CGContextRestoreGState(context); 117 | 118 | // outer shadow 119 | CGContextSaveGState(context); { 120 | CGContextAddPath(context, boxPath); 121 | CGContextAddPath(context, fillPath); 122 | CGContextEOClip(context); 123 | CGColorRef shadowColor = [UIColor colorWithWhite:0 alpha:0.32].CGColor; 124 | CGContextSetShadowWithColor(context, CGSizeMake(0, 1.5), 3, shadowColor); 125 | CGContextBeginTransparencyLayer(context, NULL); { 126 | CGContextAddPath(context, fillPath); 127 | [[UIColor colorWithWhite:0.7 alpha:1.000] setFill]; 128 | CGContextFillPath(context); 129 | } CGContextEndTransparencyLayer(context); 130 | } CGContextRestoreGState(context); 131 | 132 | // stroke 133 | CGContextSaveGState(context); { 134 | CGContextAddPath(context, strokePath); 135 | [[UIColor colorWithWhite:0.6 alpha:1] setStroke]; 136 | CGContextSetLineWidth(context, YYTextCGFloatFromPixel(1)); 137 | CGContextStrokePath(context); 138 | } CGContextRestoreGState(context); 139 | 140 | CFRelease(boxPath); 141 | CFRelease(fillPath); 142 | CFRelease(strokePath); 143 | 144 | image = UIGraphicsGetImageFromCurrentImageContext(); 145 | UIGraphicsEndImageContext(); 146 | 147 | }); 148 | return image; 149 | } 150 | 151 | 152 | #undef kMultiple 153 | #undef kDiameter 154 | #undef kPadding 155 | #undef kSize 156 | 157 | @end 158 | 159 | 160 | 161 | @interface _YYTextMagnifierRanged : YYTextMagnifier { 162 | UIImageView *_contentView; 163 | UIImageView *_coverView; 164 | } 165 | @end 166 | 167 | 168 | @implementation _YYTextMagnifierRanged 169 | #define kMultiple 1.2 170 | #define kSize CGSizeMake(141, 60) 171 | #define kPadding YYTextCGFloatPixelHalf(6.0) 172 | #define kRadius 6.0 173 | #define kHeight 32.0 174 | #define kArrow 14.0 175 | 176 | - (instancetype)initWithFrame:(CGRect)frame { 177 | self = [super initWithFrame:frame]; 178 | _contentView = [UIImageView new]; 179 | _contentView.frame = CGRectMake(kPadding, kPadding, kSize.width - 2 * kPadding, kHeight); 180 | _contentView.layer.cornerRadius = kRadius; 181 | _contentView.clipsToBounds = YES; 182 | [self addSubview:_contentView]; 183 | 184 | _coverView = [UIImageView new]; 185 | _coverView.frame = (CGRect){.origin = CGPointZero, .size = kSize}; 186 | _coverView.image = [self.class coverImage]; 187 | [self addSubview:_coverView]; 188 | 189 | self.layer.anchorPoint = CGPointMake(0.5, 1.2); 190 | return self; 191 | } 192 | 193 | - (instancetype)init { 194 | self = [self initWithFrame:CGRectZero]; 195 | self.frame = (CGRect){.size = [self sizeThatFits:CGSizeZero]}; 196 | return self; 197 | } 198 | 199 | - (YYTextMagnifierType)type { 200 | return YYTextMagnifierTypeRanged; 201 | } 202 | 203 | - (CGSize)sizeThatFits:(CGSize)size { 204 | return kSize; 205 | } 206 | 207 | - (void)setSnapshot:(UIImage *)snapshot { 208 | if (self.captureFadeAnimation) { 209 | [_contentView.layer removeAnimationForKey:@"contents"]; 210 | CABasicAnimation *animation = [CABasicAnimation animation]; 211 | animation.duration = kCaptureDisableFadeTime; 212 | animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; 213 | [_contentView.layer addAnimation:animation forKey:@"contents"]; 214 | } 215 | _contentView.image = snapshot; 216 | } 217 | 218 | - (UIImage *)snapshot { 219 | return _contentView.image; 220 | } 221 | 222 | - (CGSize)snapshotSize { 223 | CGSize size; 224 | size.width = floor((kSize.width - 2 * kPadding) / kMultiple); 225 | size.height = floor(kHeight / kMultiple); 226 | return size; 227 | } 228 | 229 | - (CGSize)fitSize { 230 | return [self sizeThatFits:CGSizeZero]; 231 | } 232 | 233 | + (UIImage *)coverImage { 234 | static UIImage *image; 235 | static dispatch_once_t onceToken; 236 | dispatch_once(&onceToken, ^{ 237 | CGSize size = kSize; 238 | CGRect rect = (CGRect) {.size = size, .origin = CGPointZero}; 239 | 240 | UIGraphicsBeginImageContextWithOptions(size, NO, 0); 241 | CGContextRef context = UIGraphicsGetCurrentContext(); 242 | 243 | CGPathRef boxPath = CGPathCreateWithRect(rect, NULL); 244 | 245 | CGMutablePathRef path = CGPathCreateMutable(); 246 | CGPathMoveToPoint(path, NULL, kPadding + kRadius, kPadding); 247 | CGPathAddLineToPoint(path, NULL, size.width - kPadding - kRadius, kPadding); 248 | CGPathAddQuadCurveToPoint(path, NULL, size.width - kPadding, kPadding, size.width - kPadding, kPadding + kRadius); 249 | CGPathAddLineToPoint(path, NULL, size.width - kPadding, kHeight); 250 | CGPathAddCurveToPoint(path, NULL, size.width - kPadding, kPadding + kHeight, size.width - kPadding - kRadius, kPadding + kHeight, size.width - kPadding - kRadius, kPadding + kHeight); 251 | CGPathAddLineToPoint(path, NULL, size.width / 2 + kArrow, kPadding + kHeight); 252 | CGPathAddLineToPoint(path, NULL, size.width / 2, kPadding + kHeight + kArrow); 253 | CGPathAddLineToPoint(path, NULL, size.width / 2 - kArrow, kPadding + kHeight); 254 | CGPathAddLineToPoint(path, NULL, kPadding + kRadius, kPadding + kHeight); 255 | CGPathAddQuadCurveToPoint(path, NULL, kPadding, kPadding + kHeight, kPadding, kHeight); 256 | CGPathAddLineToPoint(path, NULL, kPadding, kPadding + kRadius); 257 | CGPathAddQuadCurveToPoint(path, NULL, kPadding, kPadding, kPadding + kRadius, kPadding); 258 | CGPathCloseSubpath(path); 259 | 260 | CGMutablePathRef arrowPath = CGPathCreateMutable(); 261 | CGPathMoveToPoint(arrowPath, NULL, size.width / 2 - kArrow, YYTextCGFloatPixelFloor(kPadding) + kHeight); 262 | CGPathAddLineToPoint(arrowPath, NULL, size.width / 2 + kArrow, YYTextCGFloatPixelFloor(kPadding) + kHeight); 263 | CGPathAddLineToPoint(arrowPath, NULL, size.width / 2, kPadding + kHeight + kArrow); 264 | CGPathCloseSubpath(arrowPath); 265 | 266 | // inner shadow 267 | CGContextSaveGState(context); { 268 | CGFloat blurRadius = 25; 269 | CGSize offset = CGSizeMake(0, 15); 270 | CGColorRef shadowColor = [UIColor colorWithWhite:0 alpha:0.16].CGColor; 271 | CGColorRef opaqueShadowColor = CGColorCreateCopyWithAlpha(shadowColor, 1.0); 272 | CGContextAddPath(context, path); 273 | CGContextClip(context); 274 | CGContextSetAlpha(context, CGColorGetAlpha(shadowColor)); 275 | CGContextBeginTransparencyLayer(context, NULL); { 276 | CGContextSetShadowWithColor(context, offset, blurRadius, opaqueShadowColor); 277 | CGContextSetBlendMode(context, kCGBlendModeSourceOut); 278 | CGContextSetFillColorWithColor(context, opaqueShadowColor); 279 | CGContextAddPath(context, path); 280 | CGContextFillPath(context); 281 | } CGContextEndTransparencyLayer(context); 282 | CGColorRelease(opaqueShadowColor); 283 | } CGContextRestoreGState(context); 284 | 285 | // outer shadow 286 | CGContextSaveGState(context); { 287 | CGContextAddPath(context, boxPath); 288 | CGContextAddPath(context, path); 289 | CGContextEOClip(context); 290 | CGColorRef shadowColor = [UIColor colorWithWhite:0 alpha:0.32].CGColor; 291 | CGContextSetShadowWithColor(context, CGSizeMake(0, 1.5), 3, shadowColor); 292 | CGContextBeginTransparencyLayer(context, NULL); { 293 | CGContextAddPath(context, path); 294 | [[UIColor colorWithWhite:0.7 alpha:1.000] setFill]; 295 | CGContextFillPath(context); 296 | } CGContextEndTransparencyLayer(context); 297 | } CGContextRestoreGState(context); 298 | 299 | // arrow 300 | CGContextSaveGState(context); { 301 | CGContextAddPath(context, arrowPath); 302 | [[UIColor colorWithWhite:1 alpha:0.95] set]; 303 | CGContextFillPath(context); 304 | } CGContextRestoreGState(context); 305 | 306 | // stroke 307 | CGContextSaveGState(context); { 308 | CGContextAddPath(context, path); 309 | [[UIColor colorWithWhite:0.6 alpha:1] setStroke]; 310 | CGContextSetLineWidth(context, YYTextCGFloatFromPixel(1)); 311 | CGContextStrokePath(context); 312 | } CGContextRestoreGState(context); 313 | 314 | CFRelease(boxPath); 315 | CFRelease(path); 316 | CFRelease(arrowPath); 317 | 318 | image = UIGraphicsGetImageFromCurrentImageContext(); 319 | UIGraphicsEndImageContext(); 320 | 321 | }); 322 | return image; 323 | } 324 | 325 | #undef kMultiple 326 | #undef kSize 327 | #undef kPadding 328 | #undef kRadius 329 | #undef kHeight 330 | #undef kArrow 331 | 332 | @end 333 | 334 | 335 | @implementation YYTextMagnifier 336 | 337 | + (id)magnifierWithType:(YYTextMagnifierType)type { 338 | switch (type) { 339 | case YYTextMagnifierTypeCaret :return [_YYTextMagnifierCaret new]; 340 | case YYTextMagnifierTypeRanged :return [_YYTextMagnifierRanged new]; 341 | } 342 | return nil; 343 | } 344 | 345 | - (id)initWithFrame:(CGRect)frame { 346 | // class cluster 347 | if ([self isMemberOfClass:[YYTextMagnifier class]]) { 348 | @throw [NSException exceptionWithName:NSStringFromClass([self class]) reason:@"Attempting to instantiate an abstract class. Use a concrete subclass instead." userInfo:nil]; 349 | return nil; 350 | } 351 | self = [super initWithFrame:frame]; 352 | return self; 353 | } 354 | 355 | @end 356 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextSelectionView.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextSelectionView.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/2/25. 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 | 12 | #import 13 | 14 | #if __has_include() 15 | #import 16 | #import 17 | #else 18 | #import "YYTextAttribute.h" 19 | #import "YYTextInput.h" 20 | #endif 21 | 22 | NS_ASSUME_NONNULL_BEGIN 23 | 24 | /** 25 | A single dot view. The frame should be foursquare. 26 | Change the background color for display. 27 | 28 | @discussion Typically, you should not use this class directly. 29 | */ 30 | @interface YYSelectionGrabberDot : UIView 31 | /// Dont't access this property. It was used by `YYTextEffectWindow`. 32 | @property (nonatomic, strong) UIView *mirror; 33 | @end 34 | 35 | 36 | /** 37 | A grabber (stick with a dot). 38 | 39 | @discussion Typically, you should not use this class directly. 40 | */ 41 | @interface YYSelectionGrabber : UIView 42 | 43 | @property (nonatomic, readonly) YYSelectionGrabberDot *dot; ///< the dot view 44 | @property (nonatomic) YYTextDirection dotDirection; ///< don't support composite direction 45 | @property (nullable, nonatomic, strong) UIColor *color; ///< tint color, default is nil 46 | 47 | @end 48 | 49 | 50 | /** 51 | The selection view for text edit and select. 52 | 53 | @discussion Typically, you should not use this class directly. 54 | */ 55 | @interface YYTextSelectionView : UIView 56 | 57 | @property (nullable, nonatomic, weak) UIView *hostView; ///< the holder view 58 | @property (nullable, nonatomic, strong) UIColor *color; ///< the tint color 59 | @property (nonatomic, getter = isCaretBlinks) BOOL caretBlinks; ///< whether the caret is blinks 60 | @property (nonatomic, getter = isCaretVisible) BOOL caretVisible; ///< whether the caret is visible 61 | @property (nonatomic, getter = isVerticalForm) BOOL verticalForm; ///< weather the text view is vertical form 62 | 63 | @property (nonatomic) CGRect caretRect; ///< caret rect (width==0 or height==0) 64 | @property (nullable, nonatomic, copy) NSArray *selectionRects; ///< default is nil 65 | 66 | @property (nonatomic, readonly) UIView *caretView; 67 | @property (nonatomic, readonly) YYSelectionGrabber *startGrabber; 68 | @property (nonatomic, readonly) YYSelectionGrabber *endGrabber; 69 | 70 | - (BOOL)isGrabberContainsPoint:(CGPoint)point; 71 | - (BOOL)isStartGrabberContainsPoint:(CGPoint)point; 72 | - (BOOL)isEndGrabberContainsPoint:(CGPoint)point; 73 | - (BOOL)isCaretContainsPoint:(CGPoint)point; 74 | - (BOOL)isSelectionRectsContainsPoint:(CGPoint)point; 75 | 76 | @end 77 | 78 | NS_ASSUME_NONNULL_END 79 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Component/YYTextSelectionView.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextSelectionView.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/2/25. 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 | 12 | #import "YYTextSelectionView.h" 13 | #import "YYTextUtilities.h" 14 | #import "YYTextWeakProxy.h" 15 | 16 | #define kMarkAlpha 0.2 17 | #define kLineWidth 2.0 18 | #define kBlinkDuration 0.5 19 | #define kBlinkFadeDuration 0.2 20 | #define kBlinkFirstDelay 0.1 21 | #define kTouchTestExtend 14.0 22 | #define kTouchDotExtend 7.0 23 | 24 | 25 | @implementation YYSelectionGrabberDot 26 | 27 | - (instancetype)initWithFrame:(CGRect)frame { 28 | self = [super initWithFrame:frame]; 29 | if (!self) return nil; 30 | self.userInteractionEnabled = NO; 31 | self.mirror = [UIView new]; 32 | return self; 33 | } 34 | 35 | - (void)layoutSubviews { 36 | [super layoutSubviews]; 37 | CGFloat length = MIN(self.bounds.size.width, self.bounds.size.height); 38 | self.layer.cornerRadius = length * 0.5; 39 | self.mirror.bounds = self.bounds; 40 | self.mirror.layer.cornerRadius = self.layer.cornerRadius; 41 | } 42 | 43 | - (void)setBackgroundColor:(UIColor *)backgroundColor { 44 | [super setBackgroundColor:backgroundColor]; 45 | _mirror.backgroundColor = backgroundColor; 46 | } 47 | 48 | @end 49 | 50 | 51 | 52 | @implementation YYSelectionGrabber 53 | 54 | - (instancetype) initWithFrame:(CGRect)frame { 55 | self = [super initWithFrame:frame]; 56 | if (!self) return nil; 57 | _dot = [[YYSelectionGrabberDot alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; 58 | return self; 59 | } 60 | 61 | - (void)setDotDirection:(YYTextDirection)dotDirection { 62 | _dotDirection = dotDirection; 63 | [self addSubview:_dot]; 64 | CGRect frame = _dot.frame; 65 | CGFloat ofs = 0.5; 66 | if (dotDirection == YYTextDirectionTop) { 67 | frame.origin.y = -frame.size.height + ofs; 68 | frame.origin.x = (self.bounds.size.width - frame.size.width) / 2; 69 | } else if (dotDirection == YYTextDirectionRight) { 70 | frame.origin.x = self.bounds.size.width - ofs; 71 | frame.origin.y = (self.bounds.size.height - frame.size.height) / 2; 72 | } else if (dotDirection == YYTextDirectionBottom) { 73 | frame.origin.y = self.bounds.size.height - ofs; 74 | frame.origin.x = (self.bounds.size.width - frame.size.width) / 2; 75 | } else if (dotDirection == YYTextDirectionLeft) { 76 | frame.origin.x = -frame.size.width + ofs; 77 | frame.origin.y = (self.bounds.size.height - frame.size.height) / 2; 78 | } else { 79 | [_dot removeFromSuperview]; 80 | } 81 | _dot.frame = frame; 82 | } 83 | 84 | - (void)setColor:(UIColor *)color { 85 | self.backgroundColor = color; 86 | _dot.backgroundColor = color; 87 | _color = color; 88 | } 89 | 90 | - (void)layoutSubviews { 91 | [super layoutSubviews]; 92 | [self setDotDirection:_dotDirection]; 93 | } 94 | 95 | - (CGRect)touchRect { 96 | CGRect rect = CGRectInset(self.frame, -kTouchTestExtend, -kTouchTestExtend); 97 | UIEdgeInsets insets = {0}; 98 | if (_dotDirection == YYTextDirectionTop) { 99 | insets.top = -kTouchDotExtend; 100 | } else if (_dotDirection == YYTextDirectionRight) { 101 | insets.right = -kTouchDotExtend; 102 | } else if (_dotDirection == YYTextDirectionBottom) { 103 | insets.bottom = -kTouchDotExtend; 104 | } else if (_dotDirection == YYTextDirectionLeft) { 105 | insets.left = -kTouchDotExtend; 106 | } 107 | rect = UIEdgeInsetsInsetRect(rect, insets); 108 | return rect; 109 | } 110 | 111 | @end 112 | 113 | 114 | 115 | @interface YYTextSelectionView () 116 | @property (nonatomic, strong) NSTimer *caretTimer; 117 | @property (nonatomic, strong) UIView *caretView; 118 | @property (nonatomic, strong) YYSelectionGrabber *startGrabber; 119 | @property (nonatomic, strong) YYSelectionGrabber *endGrabber; 120 | @property (nonatomic, strong) NSMutableArray *markViews; 121 | @end 122 | 123 | @implementation YYTextSelectionView 124 | 125 | - (instancetype)initWithFrame:(CGRect)frame { 126 | self = [super initWithFrame:frame]; 127 | if (!self) return nil; 128 | 129 | self.userInteractionEnabled = NO; 130 | self.clipsToBounds = NO; 131 | _markViews = [NSMutableArray array]; 132 | _caretView = [UIView new]; 133 | _caretView.hidden = YES; 134 | _startGrabber = [YYSelectionGrabber new]; 135 | _startGrabber.dotDirection = YYTextDirectionTop; 136 | _startGrabber.hidden = YES; 137 | _endGrabber = [YYSelectionGrabber new]; 138 | _endGrabber.dotDirection = YYTextDirectionBottom; 139 | _endGrabber.hidden = YES; 140 | 141 | [self addSubview:_startGrabber]; 142 | [self addSubview:_endGrabber]; 143 | [self addSubview:_caretView]; 144 | 145 | return self; 146 | } 147 | 148 | - (void)dealloc { 149 | [_caretTimer invalidate]; 150 | } 151 | 152 | - (void)setColor:(UIColor *)color { 153 | _color = color; 154 | self.caretView.backgroundColor = color; 155 | self.startGrabber.color = color; 156 | self.endGrabber.color = color; 157 | [self.markViews enumerateObjectsUsingBlock: ^(UIView *v, NSUInteger idx, BOOL *stop) { 158 | v.backgroundColor = color; 159 | }]; 160 | } 161 | 162 | - (void)setCaretBlinks:(BOOL)caretBlinks { 163 | if (_caretBlinks != caretBlinks) { 164 | _caretView.alpha = 1; 165 | [self.class cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startBlinks) object:nil]; 166 | if (caretBlinks) { 167 | [self performSelector:@selector(_startBlinks) withObject:nil afterDelay:kBlinkFirstDelay]; 168 | } else { 169 | [_caretTimer invalidate]; 170 | _caretTimer = nil; 171 | } 172 | _caretBlinks = caretBlinks; 173 | } 174 | } 175 | 176 | - (void)_startBlinks { 177 | [_caretTimer invalidate]; 178 | if (_caretVisible) { 179 | _caretTimer = [NSTimer timerWithTimeInterval:kBlinkDuration target:[YYTextWeakProxy proxyWithTarget:self] selector:@selector(_doBlink) userInfo:nil repeats:YES]; 180 | [[NSRunLoop currentRunLoop] addTimer:_caretTimer forMode:NSDefaultRunLoopMode]; 181 | } else { 182 | _caretView.alpha = 1; 183 | } 184 | } 185 | 186 | - (void)_doBlink { 187 | [UIView animateWithDuration:kBlinkFadeDuration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations: ^{ 188 | if (_caretView.alpha == 1) _caretView.alpha = 0; 189 | else _caretView.alpha = 1; 190 | } completion:NULL]; 191 | } 192 | 193 | - (void)setCaretVisible:(BOOL)caretVisible { 194 | _caretVisible = caretVisible; 195 | self.caretView.hidden = !caretVisible; 196 | _caretView.alpha = 1; 197 | [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startBlinks) object:nil]; 198 | if (_caretBlinks) { 199 | [self performSelector:@selector(_startBlinks) withObject:nil afterDelay:kBlinkFirstDelay]; 200 | } 201 | } 202 | 203 | - (void)setVerticalForm:(BOOL)verticalForm { 204 | if (_verticalForm != verticalForm) { 205 | _verticalForm = verticalForm; 206 | [self setCaretRect:_caretRect]; 207 | self.startGrabber.dotDirection = verticalForm ? YYTextDirectionRight : YYTextDirectionTop; 208 | self.endGrabber.dotDirection = verticalForm ? YYTextDirectionLeft : YYTextDirectionBottom; 209 | } 210 | } 211 | 212 | - (CGRect)_standardCaretRect:(CGRect)caretRect { 213 | caretRect = CGRectStandardize(caretRect); 214 | if (_verticalForm) { 215 | if (caretRect.size.height == 0) { 216 | caretRect.size.height = kLineWidth; 217 | caretRect.origin.y -= kLineWidth * 0.5; 218 | } 219 | if (caretRect.origin.y < 0) { 220 | caretRect.origin.y = 0; 221 | } else if (caretRect.origin.y + caretRect.size.height > self.bounds.size.height) { 222 | caretRect.origin.y = self.bounds.size.height - caretRect.size.height; 223 | } 224 | } else { 225 | if (caretRect.size.width == 0) { 226 | caretRect.size.width = kLineWidth; 227 | caretRect.origin.x -= kLineWidth * 0.5; 228 | } 229 | if (caretRect.origin.x < 0) { 230 | caretRect.origin.x = 0; 231 | } else if (caretRect.origin.x + caretRect.size.width > self.bounds.size.width) { 232 | caretRect.origin.x = self.bounds.size.width - caretRect.size.width; 233 | } 234 | } 235 | caretRect = YYTextCGRectPixelRound(caretRect); 236 | if (isnan(caretRect.origin.x) || isinf(caretRect.origin.x)) caretRect.origin.x = 0; 237 | if (isnan(caretRect.origin.y) || isinf(caretRect.origin.y)) caretRect.origin.y = 0; 238 | if (isnan(caretRect.size.width) || isinf(caretRect.size.width)) caretRect.size.width = 0; 239 | if (isnan(caretRect.size.height) || isinf(caretRect.size.height)) caretRect.size.height = 0; 240 | return caretRect; 241 | } 242 | 243 | - (void)setCaretRect:(CGRect)caretRect { 244 | _caretRect = caretRect; 245 | self.caretView.frame = [self _standardCaretRect:caretRect]; 246 | CGFloat minWidth = MIN(self.caretView.bounds.size.width, self.caretView.bounds.size.height); 247 | self.caretView.layer.cornerRadius = minWidth / 2; 248 | } 249 | 250 | - (void)setSelectionRects:(NSArray *)selectionRects { 251 | _selectionRects = selectionRects.copy; 252 | [self.markViews enumerateObjectsUsingBlock: ^(UIView *v, NSUInteger idx, BOOL *stop) { 253 | [v removeFromSuperview]; 254 | }]; 255 | [self.markViews removeAllObjects]; 256 | self.startGrabber.hidden = YES; 257 | self.endGrabber.hidden = YES; 258 | 259 | [selectionRects enumerateObjectsUsingBlock: ^(YYTextSelectionRect *r, NSUInteger idx, BOOL *stop) { 260 | CGRect rect = r.rect; 261 | rect = CGRectStandardize(rect); 262 | rect = YYTextCGRectPixelRound(rect); 263 | if (r.containsStart || r.containsEnd) { 264 | rect = [self _standardCaretRect:rect]; 265 | if (r.containsStart) { 266 | self.startGrabber.hidden = NO; 267 | self.startGrabber.frame = rect; 268 | } 269 | if (r.containsEnd) { 270 | self.endGrabber.hidden = NO; 271 | self.endGrabber.frame = rect; 272 | } 273 | } else { 274 | if (rect.size.width > 0 && rect.size.height > 0) { 275 | UIView *mark = [[UIView alloc] initWithFrame:rect]; 276 | mark.backgroundColor = _color; 277 | mark.alpha = kMarkAlpha; 278 | [self insertSubview:mark atIndex:0]; 279 | [self.markViews addObject:mark]; 280 | } 281 | } 282 | }]; 283 | } 284 | 285 | - (BOOL)isGrabberContainsPoint:(CGPoint)point { 286 | return [self isStartGrabberContainsPoint:point] || [self isEndGrabberContainsPoint:point]; 287 | } 288 | 289 | - (BOOL)isStartGrabberContainsPoint:(CGPoint)point { 290 | if (_startGrabber.hidden) return NO; 291 | CGRect startRect = [_startGrabber touchRect]; 292 | CGRect endRect = [_endGrabber touchRect]; 293 | if (CGRectIntersectsRect(startRect, endRect)) { 294 | CGFloat distStart = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(startRect)); 295 | CGFloat distEnd = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(endRect)); 296 | if (distEnd <= distStart) return NO; 297 | } 298 | return CGRectContainsPoint(startRect, point); 299 | } 300 | 301 | - (BOOL)isEndGrabberContainsPoint:(CGPoint)point { 302 | if (_endGrabber.hidden) return NO; 303 | CGRect startRect = [_startGrabber touchRect]; 304 | CGRect endRect = [_endGrabber touchRect]; 305 | if (CGRectIntersectsRect(startRect, endRect)) { 306 | CGFloat distStart = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(startRect)); 307 | CGFloat distEnd = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(endRect)); 308 | if (distEnd > distStart) return NO; 309 | } 310 | return CGRectContainsPoint(endRect, point); 311 | } 312 | 313 | - (BOOL)isCaretContainsPoint:(CGPoint)point { 314 | if (_caretVisible) { 315 | CGRect rect = CGRectInset(_caretRect, -kTouchTestExtend, -kTouchTestExtend); 316 | return CGRectContainsPoint(rect, point); 317 | } 318 | return NO; 319 | } 320 | 321 | - (BOOL)isSelectionRectsContainsPoint:(CGPoint)point { 322 | if (_selectionRects.count == 0) return NO; 323 | for (YYTextSelectionRect *rect in _selectionRects) { 324 | if (CGRectContainsPoint(rect.rect, point)) return YES; 325 | } 326 | return NO; 327 | } 328 | 329 | @end 330 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/String/YYTextArchiver.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextArchiver.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/3/16. 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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | A subclass of `NSKeyedArchiver` which implement `NSKeyedArchiverDelegate` protocol. 18 | 19 | The archiver can encode the object which contains 20 | CGColor/CGImage/CTRunDelegateRef/.. (such as NSAttributedString). 21 | */ 22 | @interface YYTextArchiver : NSKeyedArchiver 23 | @end 24 | 25 | /** 26 | A subclass of `NSKeyedUnarchiver` which implement `NSKeyedUnarchiverDelegate` 27 | protocol. The unarchiver can decode the data which is encoded by 28 | `YYTextArchiver` or `NSKeyedArchiver`. 29 | */ 30 | @interface YYTextUnarchiver : NSKeyedUnarchiver 31 | @end 32 | 33 | NS_ASSUME_NONNULL_END 34 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/String/YYTextArchiver.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextArchiver.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/3/16. 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 | 12 | #import "YYTextArchiver.h" 13 | #import "YYTextRunDelegate.h" 14 | #import "YYTextRubyAnnotation.h" 15 | 16 | /** 17 | When call CTRunDelegateGetTypeID() on some devices (runs iOS6), I got the error: 18 | "dyld: lazy symbol binding failed: Symbol not found: _CTRunDelegateGetTypeID" 19 | 20 | Here's a workaround for this issue. 21 | */ 22 | static CFTypeID CTRunDelegateTypeID() { 23 | static CFTypeID typeID; 24 | static dispatch_once_t onceToken; 25 | dispatch_once(&onceToken, ^{ 26 | /* 27 | if ((long)CTRunDelegateGetTypeID + 1 > 1) { //avoid compiler optimization 28 | typeID = CTRunDelegateGetTypeID(); 29 | } 30 | */ 31 | YYTextRunDelegate *delegate = [YYTextRunDelegate new]; 32 | CTRunDelegateRef ref = delegate.CTRunDelegate; 33 | typeID = CFGetTypeID(ref); 34 | CFRelease(ref); 35 | }); 36 | return typeID; 37 | } 38 | 39 | static CFTypeID CTRubyAnnotationTypeID() { 40 | static CFTypeID typeID; 41 | static dispatch_once_t onceToken; 42 | dispatch_once(&onceToken, ^{ 43 | if ((long)CTRubyAnnotationGetTypeID + 1 > 1) { //avoid compiler optimization 44 | typeID = CTRunDelegateGetTypeID(); 45 | } else { 46 | typeID = kCFNotFound; 47 | } 48 | }); 49 | return typeID; 50 | } 51 | 52 | /** 53 | A wrapper for CGColorRef. Used for Archive/Unarchive/Copy. 54 | */ 55 | @interface _YYCGColor : NSObject 56 | @property (nonatomic, assign) CGColorRef CGColor; 57 | + (instancetype)colorWithCGColor:(CGColorRef)CGColor; 58 | @end 59 | 60 | @implementation _YYCGColor 61 | 62 | + (instancetype)colorWithCGColor:(CGColorRef)CGColor { 63 | _YYCGColor *color = [self new]; 64 | color.CGColor = CGColor; 65 | return color; 66 | } 67 | 68 | - (void)setCGColor:(CGColorRef)CGColor { 69 | if (_CGColor != CGColor) { 70 | if (CGColor) CGColor = (CGColorRef)CFRetain(CGColor); 71 | if (_CGColor) CFRelease(_CGColor); 72 | _CGColor = CGColor; 73 | } 74 | } 75 | 76 | - (void)dealloc { 77 | if (_CGColor) CFRelease(_CGColor); 78 | _CGColor = NULL; 79 | } 80 | 81 | - (id)copyWithZone:(NSZone *)zone { 82 | _YYCGColor *color = [self.class new]; 83 | color.CGColor = self.CGColor; 84 | return color; 85 | } 86 | 87 | - (void)encodeWithCoder:(NSCoder *)aCoder { 88 | UIColor *color = [UIColor colorWithCGColor:_CGColor]; 89 | [aCoder encodeObject:color forKey:@"color"]; 90 | } 91 | 92 | - (id)initWithCoder:(NSCoder *)aDecoder { 93 | self = [self init]; 94 | UIColor *color = [aDecoder decodeObjectForKey:@"color"]; 95 | self.CGColor = color.CGColor; 96 | return self; 97 | } 98 | 99 | @end 100 | 101 | /** 102 | A wrapper for CGImageRef. Used for Archive/Unarchive/Copy. 103 | */ 104 | @interface _YYCGImage : NSObject 105 | @property (nonatomic, assign) CGImageRef CGImage; 106 | + (instancetype)imageWithCGImage:(CGImageRef)CGImage; 107 | @end 108 | 109 | @implementation _YYCGImage 110 | 111 | + (instancetype)imageWithCGImage:(CGImageRef)CGImage { 112 | _YYCGImage *image = [self new]; 113 | image.CGImage = CGImage; 114 | return image; 115 | } 116 | 117 | - (void)setCGImage:(CGImageRef)CGImage { 118 | if (_CGImage != CGImage) { 119 | if (CGImage) CGImage = (CGImageRef)CFRetain(CGImage); 120 | if (_CGImage) CFRelease(_CGImage); 121 | _CGImage = CGImage; 122 | } 123 | } 124 | 125 | - (void)dealloc { 126 | if (_CGImage) CFRelease(_CGImage); 127 | } 128 | 129 | - (id)copyWithZone:(NSZone *)zone { 130 | _YYCGImage *image = [self.class new]; 131 | image.CGImage = self.CGImage; 132 | return image; 133 | } 134 | 135 | - (void)encodeWithCoder:(NSCoder *)aCoder { 136 | UIImage *image = [UIImage imageWithCGImage:_CGImage]; 137 | [aCoder encodeObject:image forKey:@"image"]; 138 | } 139 | 140 | - (id)initWithCoder:(NSCoder *)aDecoder { 141 | self = [self init]; 142 | UIImage *image = [aDecoder decodeObjectForKey:@"image"]; 143 | self.CGImage = image.CGImage; 144 | return self; 145 | } 146 | 147 | @end 148 | 149 | 150 | @implementation YYTextArchiver 151 | 152 | + (NSData *)archivedDataWithRootObject:(id)rootObject { 153 | if (!rootObject) return nil; 154 | NSMutableData *data = [NSMutableData data]; 155 | YYTextArchiver *archiver = [[[self class] alloc] initForWritingWithMutableData:data]; 156 | [archiver encodeRootObject:rootObject]; 157 | [archiver finishEncoding]; 158 | return data; 159 | } 160 | 161 | + (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path { 162 | NSData *data = [self archivedDataWithRootObject:rootObject]; 163 | if (!data) return NO; 164 | return [data writeToFile:path atomically:YES]; 165 | } 166 | 167 | - (instancetype)init { 168 | self = [super init]; 169 | self.delegate = self; 170 | return self; 171 | } 172 | 173 | - (instancetype)initForWritingWithMutableData:(NSMutableData *)data { 174 | self = [super initForWritingWithMutableData:data]; 175 | self.delegate = self; 176 | return self; 177 | } 178 | 179 | - (id)archiver:(NSKeyedArchiver *)archiver willEncodeObject:(id)object { 180 | CFTypeID typeID = CFGetTypeID((CFTypeRef)object); 181 | if (typeID == CTRunDelegateTypeID()) { 182 | CTRunDelegateRef runDelegate = (__bridge CFTypeRef)(object); 183 | id ref = CTRunDelegateGetRefCon(runDelegate); 184 | if (ref) return ref; 185 | } else if (typeID == CTRubyAnnotationTypeID()) { 186 | CTRubyAnnotationRef ctRuby = (__bridge CFTypeRef)(object); 187 | YYTextRubyAnnotation *ruby = [YYTextRubyAnnotation rubyWithCTRubyRef:ctRuby]; 188 | if (ruby) return ruby; 189 | } else if (typeID == CGColorGetTypeID()) { 190 | return [_YYCGColor colorWithCGColor:(CGColorRef)object]; 191 | } else if (typeID == CGImageGetTypeID()) { 192 | return [_YYCGImage imageWithCGImage:(CGImageRef)object]; 193 | } 194 | return object; 195 | } 196 | 197 | @end 198 | 199 | 200 | @implementation YYTextUnarchiver 201 | 202 | + (id)unarchiveObjectWithData:(NSData *)data { 203 | if (data.length == 0) return nil; 204 | YYTextUnarchiver *unarchiver = [[self alloc] initForReadingWithData:data]; 205 | return [unarchiver decodeObject]; 206 | } 207 | 208 | + (id)unarchiveObjectWithFile:(NSString *)path { 209 | NSData *data = [NSData dataWithContentsOfFile:path]; 210 | return [self unarchiveObjectWithData:data]; 211 | } 212 | 213 | - (instancetype)init { 214 | self = [super init]; 215 | self.delegate = self; 216 | return self; 217 | } 218 | 219 | - (instancetype)initForReadingWithData:(NSData *)data { 220 | self = [super initForReadingWithData:data]; 221 | self.delegate = self; 222 | return self; 223 | } 224 | 225 | - (id)unarchiver:(NSKeyedUnarchiver *)unarchiver didDecodeObject:(id) NS_RELEASES_ARGUMENT object NS_RETURNS_RETAINED { 226 | if ([object class] == [YYTextRunDelegate class]) { 227 | YYTextRunDelegate *runDelegate = object; 228 | CTRunDelegateRef ct = runDelegate.CTRunDelegate; 229 | id ctObj = (__bridge id)ct; 230 | if (ct) CFRelease(ct); 231 | return ctObj; 232 | } else if ([object class] == [YYTextRubyAnnotation class]) { 233 | YYTextRubyAnnotation *ruby = object; 234 | if ([UIDevice currentDevice].systemVersion.floatValue >= 8) { 235 | CTRubyAnnotationRef ct = ruby.CTRubyAnnotation; 236 | id ctObj = (__bridge id)(ct); 237 | if (ct) CFRelease(ct); 238 | return ctObj; 239 | } else { 240 | return object; 241 | } 242 | } else if ([object class] == [_YYCGColor class]) { 243 | _YYCGColor *color = object; 244 | return (id)color.CGColor; 245 | } else if ([object class] == [_YYCGImage class]) { 246 | _YYCGImage *image = object; 247 | return (id)image.CGImage; 248 | } 249 | return object; 250 | } 251 | 252 | @end 253 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/String/YYTextAttribute.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextAttribute.h 3 | // YYText 4 | // 5 | // Created by ibireme on 14/10/26. 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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | #pragma mark - Enum Define 17 | 18 | /// The attribute type 19 | typedef NS_OPTIONS(NSInteger, YYTextAttributeType) { 20 | YYTextAttributeTypeNone = 0, 21 | YYTextAttributeTypeUIKit = 1 << 0, ///< UIKit attributes, such as UILabel/UITextField/drawInRect. 22 | YYTextAttributeTypeCoreText = 1 << 1, ///< CoreText attributes, used by CoreText. 23 | YYTextAttributeTypeYYText = 1 << 2, ///< YYText attributes, used by YYText. 24 | }; 25 | 26 | /// Get the attribute type from an attribute name. 27 | extern YYTextAttributeType YYTextAttributeGetType(NSString *attributeName); 28 | 29 | /** 30 | Line style in YYText (similar to NSUnderlineStyle). 31 | */ 32 | typedef NS_OPTIONS (NSInteger, YYTextLineStyle) { 33 | // basic style (bitmask:0xFF) 34 | YYTextLineStyleNone = 0x00, ///< ( ) Do not draw a line (Default). 35 | YYTextLineStyleSingle = 0x01, ///< (──────) Draw a single line. 36 | YYTextLineStyleThick = 0x02, ///< (━━━━━━━) Draw a thick line. 37 | YYTextLineStyleDouble = 0x09, ///< (══════) Draw a double line. 38 | 39 | // style pattern (bitmask:0xF00) 40 | YYTextLineStylePatternSolid = 0x000, ///< (────────) Draw a solid line (Default). 41 | YYTextLineStylePatternDot = 0x100, ///< (‑ ‑ ‑ ‑ ‑ ‑) Draw a line of dots. 42 | YYTextLineStylePatternDash = 0x200, ///< (— — — —) Draw a line of dashes. 43 | YYTextLineStylePatternDashDot = 0x300, ///< (— ‑ — ‑ — ‑) Draw a line of alternating dashes and dots. 44 | YYTextLineStylePatternDashDotDot = 0x400, ///< (— ‑ ‑ — ‑ ‑) Draw a line of alternating dashes and two dots. 45 | YYTextLineStylePatternCircleDot = 0x900, ///< (••••••••••••) Draw a line of small circle dots. 46 | }; 47 | 48 | /** 49 | Text vertical alignment. 50 | */ 51 | typedef NS_ENUM(NSInteger, YYTextVerticalAlignment) { 52 | YYTextVerticalAlignmentTop = 0, ///< Top alignment. 53 | YYTextVerticalAlignmentCenter = 1, ///< Center alignment. 54 | YYTextVerticalAlignmentBottom = 2, ///< Bottom alignment. 55 | }; 56 | 57 | /** 58 | The direction define in YYText. 59 | */ 60 | typedef NS_OPTIONS(NSUInteger, YYTextDirection) { 61 | YYTextDirectionNone = 0, 62 | YYTextDirectionTop = 1 << 0, 63 | YYTextDirectionRight = 1 << 1, 64 | YYTextDirectionBottom = 1 << 2, 65 | YYTextDirectionLeft = 1 << 3, 66 | }; 67 | 68 | /** 69 | The trunction type, tells the truncation engine which type of truncation is being requested. 70 | */ 71 | typedef NS_ENUM (NSUInteger, YYTextTruncationType) { 72 | /// No truncate. 73 | YYTextTruncationTypeNone = 0, 74 | 75 | /// Truncate at the beginning of the line, leaving the end portion visible. 76 | YYTextTruncationTypeStart = 1, 77 | 78 | /// Truncate at the end of the line, leaving the start portion visible. 79 | YYTextTruncationTypeEnd = 2, 80 | 81 | /// Truncate in the middle of the line, leaving both the start and the end portions visible. 82 | YYTextTruncationTypeMiddle = 3, 83 | }; 84 | 85 | 86 | 87 | #pragma mark - Attribute Name Defined in YYText 88 | 89 | /// The value of this attribute is a `YYTextBackedString` object. 90 | /// Use this attribute to store the original plain text if it is replaced by something else (such as attachment). 91 | UIKIT_EXTERN NSString *const YYTextBackedStringAttributeName; 92 | 93 | /// The value of this attribute is a `YYTextBinding` object. 94 | /// Use this attribute to bind a range of text together, as if it was a single charactor. 95 | UIKIT_EXTERN NSString *const YYTextBindingAttributeName; 96 | 97 | /// The value of this attribute is a `YYTextShadow` object. 98 | /// Use this attribute to add shadow to a range of text. 99 | /// Shadow will be drawn below text glyphs. Use YYTextShadow.subShadow to add multi-shadow. 100 | UIKIT_EXTERN NSString *const YYTextShadowAttributeName; 101 | 102 | /// The value of this attribute is a `YYTextShadow` object. 103 | /// Use this attribute to add inner shadow to a range of text. 104 | /// Inner shadow will be drawn above text glyphs. Use YYTextShadow.subShadow to add multi-shadow. 105 | UIKIT_EXTERN NSString *const YYTextInnerShadowAttributeName; 106 | 107 | /// The value of this attribute is a `YYTextDecoration` object. 108 | /// Use this attribute to add underline to a range of text. 109 | /// The underline will be drawn below text glyphs. 110 | UIKIT_EXTERN NSString *const YYTextUnderlineAttributeName; 111 | 112 | /// The value of this attribute is a `YYTextDecoration` object. 113 | /// Use this attribute to add strikethrough (delete line) to a range of text. 114 | /// The strikethrough will be drawn above text glyphs. 115 | UIKIT_EXTERN NSString *const YYTextStrikethroughAttributeName; 116 | 117 | /// The value of this attribute is a `YYTextBorder` object. 118 | /// Use this attribute to add cover border or cover color to a range of text. 119 | /// The border will be drawn above the text glyphs. 120 | UIKIT_EXTERN NSString *const YYTextBorderAttributeName; 121 | 122 | /// The value of this attribute is a `YYTextBorder` object. 123 | /// Use this attribute to add background border or background color to a range of text. 124 | /// The border will be drawn below the text glyphs. 125 | UIKIT_EXTERN NSString *const YYTextBackgroundBorderAttributeName; 126 | 127 | /// The value of this attribute is a `YYTextBorder` object. 128 | /// Use this attribute to add a code block border to one or more line of text. 129 | /// The border will be drawn below the text glyphs. 130 | UIKIT_EXTERN NSString *const YYTextBlockBorderAttributeName; 131 | 132 | /// The value of this attribute is a `YYTextAttachment` object. 133 | /// Use this attribute to add attachment to text. 134 | /// It should be used in conjunction with a CTRunDelegate. 135 | UIKIT_EXTERN NSString *const YYTextAttachmentAttributeName; 136 | 137 | /// The value of this attribute is a `YYTextHighlight` object. 138 | /// Use this attribute to add a touchable highlight state to a range of text. 139 | UIKIT_EXTERN NSString *const YYTextHighlightAttributeName; 140 | 141 | /// The value of this attribute is a `NSValue` object stores CGAffineTransform. 142 | /// Use this attribute to add transform to each glyph in a range of text. 143 | UIKIT_EXTERN NSString *const YYTextGlyphTransformAttributeName; 144 | 145 | 146 | 147 | #pragma mark - String Token Define 148 | 149 | UIKIT_EXTERN NSString *const YYTextAttachmentToken; ///< Object replacement character (U+FFFC), used for text attachment. 150 | UIKIT_EXTERN NSString *const YYTextTruncationToken; ///< Horizontal ellipsis (U+2026), used for text truncation "…". 151 | 152 | 153 | 154 | #pragma mark - Attribute Value Define 155 | 156 | /** 157 | The tap/long press action callback defined in YYText. 158 | 159 | @param containerView The text container view (such as YYLabel/YYTextView). 160 | @param text The whole text. 161 | @param range The text range in `text` (if no range, the range.location is NSNotFound). 162 | @param rect The text frame in `containerView` (if no data, the rect is CGRectNull). 163 | */ 164 | typedef void(^YYTextAction)(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect); 165 | 166 | 167 | /** 168 | YYTextBackedString objects are used by the NSAttributedString class cluster 169 | as the values for text backed string attributes (stored in the attributed 170 | string under the key named YYTextBackedStringAttributeName). 171 | 172 | It may used for copy/paste plain text from attributed string. 173 | Example: If :) is replace by a custom emoji (such as😊), the backed string can be set to @":)". 174 | */ 175 | @interface YYTextBackedString : NSObject 176 | + (instancetype)stringWithString:(nullable NSString *)string; 177 | @property (nullable, nonatomic, copy) NSString *string; ///< backed string 178 | @end 179 | 180 | 181 | /** 182 | YYTextBinding objects are used by the NSAttributedString class cluster 183 | as the values for shadow attributes (stored in the attributed string under 184 | the key named YYTextBindingAttributeName). 185 | 186 | Add this to a range of text will make the specified characters 'binding together'. 187 | YYTextView will treat the range of text as a single character during text 188 | selection and edit. 189 | */ 190 | @interface YYTextBinding : NSObject 191 | + (instancetype)bindingWithDeleteConfirm:(BOOL)deleteConfirm; 192 | @property (nonatomic) BOOL deleteConfirm; ///< confirm the range when delete in YYTextView 193 | @end 194 | 195 | 196 | /** 197 | YYTextShadow objects are used by the NSAttributedString class cluster 198 | as the values for shadow attributes (stored in the attributed string under 199 | the key named YYTextShadowAttributeName or YYTextInnerShadowAttributeName). 200 | 201 | It's similar to `NSShadow`, but offers more options. 202 | */ 203 | @interface YYTextShadow : NSObject 204 | + (instancetype)shadowWithColor:(nullable UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius; 205 | 206 | @property (nullable, nonatomic, strong) UIColor *color; ///< shadow color 207 | @property (nonatomic) CGSize offset; ///< shadow offset 208 | @property (nonatomic) CGFloat radius; ///< shadow blur radius 209 | @property (nonatomic) CGBlendMode blendMode; ///< shadow blend mode 210 | @property (nullable, nonatomic, strong) YYTextShadow *subShadow; ///< a sub shadow which will be added above the parent shadow 211 | 212 | + (instancetype)shadowWithNSShadow:(NSShadow *)nsShadow; ///< convert NSShadow to YYTextShadow 213 | - (NSShadow *)nsShadow; ///< convert YYTextShadow to NSShadow 214 | @end 215 | 216 | 217 | /** 218 | YYTextDecorationLine objects are used by the NSAttributedString class cluster 219 | as the values for decoration line attributes (stored in the attributed string under 220 | the key named YYTextUnderlineAttributeName or YYTextStrikethroughAttributeName). 221 | 222 | When it's used as underline, the line is drawn below text glyphs; 223 | when it's used as strikethrough, the line is drawn above text glyphs. 224 | */ 225 | @interface YYTextDecoration : NSObject 226 | + (instancetype)decorationWithStyle:(YYTextLineStyle)style; 227 | + (instancetype)decorationWithStyle:(YYTextLineStyle)style width:(nullable NSNumber *)width color:(nullable UIColor *)color; 228 | @property (nonatomic) YYTextLineStyle style; ///< line style 229 | @property (nullable, nonatomic, strong) NSNumber *width; ///< line width (nil means automatic width) 230 | @property (nullable, nonatomic, strong) UIColor *color; ///< line color (nil means automatic color) 231 | @property (nullable, nonatomic, strong) YYTextShadow *shadow; ///< line shadow 232 | @end 233 | 234 | 235 | /** 236 | YYTextBorder objects are used by the NSAttributedString class cluster 237 | as the values for border attributes (stored in the attributed string under 238 | the key named YYTextBorderAttributeName or YYTextBackgroundBorderAttributeName). 239 | 240 | It can be used to draw a border around a range of text, or draw a background 241 | to a range of text. 242 | 243 | Example: 244 | ╭──────╮ 245 | │ Text │ 246 | ╰──────╯ 247 | */ 248 | @interface YYTextBorder : NSObject 249 | + (instancetype)borderWithLineStyle:(YYTextLineStyle)lineStyle lineWidth:(CGFloat)width strokeColor:(nullable UIColor *)color; 250 | + (instancetype)borderWithFillColor:(nullable UIColor *)color cornerRadius:(CGFloat)cornerRadius; 251 | @property (nonatomic) YYTextLineStyle lineStyle; ///< border line style 252 | @property (nonatomic) CGFloat strokeWidth; ///< border line width 253 | @property (nullable, nonatomic, strong) UIColor *strokeColor; ///< border line color 254 | @property (nonatomic) CGLineJoin lineJoin; ///< border line join 255 | @property (nonatomic) UIEdgeInsets insets; ///< border insets for text bounds 256 | @property (nonatomic) CGFloat cornerRadius; ///< border corder radius 257 | @property (nullable, nonatomic, strong) YYTextShadow *shadow; ///< border shadow 258 | @property (nullable, nonatomic, strong) UIColor *fillColor; ///< inner fill color 259 | @end 260 | 261 | 262 | /** 263 | YYTextAttachment objects are used by the NSAttributedString class cluster 264 | as the values for attachment attributes (stored in the attributed string under 265 | the key named YYTextAttachmentAttributeName). 266 | 267 | When display an attributed string which contains `YYTextAttachment` object, 268 | the content will be placed in text metric. If the content is `UIImage`, 269 | then it will be drawn to CGContext; if the content is `UIView` or `CALayer`, 270 | then it will be added to the text container's view or layer. 271 | */ 272 | @interface YYTextAttachment : NSObject 273 | + (instancetype)attachmentWithContent:(nullable id)content; 274 | @property (nullable, nonatomic, strong) id content; ///< Supported type: UIImage, UIView, CALayer 275 | @property (nonatomic) UIViewContentMode contentMode; ///< Content display mode. 276 | @property (nonatomic) UIEdgeInsets contentInsets; ///< The insets when drawing content. 277 | @property (nullable, nonatomic, strong) NSDictionary *userInfo; ///< The user information dictionary. 278 | @end 279 | 280 | 281 | /** 282 | YYTextHighlight objects are used by the NSAttributedString class cluster 283 | as the values for touchable highlight attributes (stored in the attributed string 284 | under the key named YYTextHighlightAttributeName). 285 | 286 | When display an attributed string in `YYLabel` or `YYTextView`, the range of 287 | highlight text can be toucheds down by users. If a range of text is turned into 288 | highlighted state, the `attributes` in `YYTextHighlight` will be used to modify 289 | (set or remove) the original attributes in the range for display. 290 | */ 291 | @interface YYTextHighlight : NSObject 292 | 293 | /** 294 | Attributes that you can apply to text in an attributed string when highlight. 295 | Key: Same as CoreText/YYText Attribute Name. 296 | Value: Modify attribute value when highlight (NSNull for remove attribute). 297 | */ 298 | @property (nullable, nonatomic, copy) NSDictionary *attributes; 299 | 300 | /** 301 | Creates a highlight object with specified attributes. 302 | 303 | @param attributes The attributes which will replace original attributes when highlight, 304 | If the value is NSNull, it will removed when highlight. 305 | */ 306 | + (instancetype)highlightWithAttributes:(nullable NSDictionary *)attributes; 307 | 308 | /** 309 | Convenience methods to create a default highlight with the specifeid background color. 310 | 311 | @param color The background border color. 312 | */ 313 | + (instancetype)highlightWithBackgroundColor:(nullable UIColor *)color; 314 | 315 | // Convenience methods below to set the `attributes`. 316 | - (void)setFont:(nullable UIFont *)font; 317 | - (void)setColor:(nullable UIColor *)color; 318 | - (void)setStrokeWidth:(nullable NSNumber *)width; 319 | - (void)setStrokeColor:(nullable UIColor *)color; 320 | - (void)setShadow:(nullable YYTextShadow *)shadow; 321 | - (void)setInnerShadow:(nullable YYTextShadow *)shadow; 322 | - (void)setUnderline:(nullable YYTextDecoration *)underline; 323 | - (void)setStrikethrough:(nullable YYTextDecoration *)strikethrough; 324 | - (void)setBackgroundBorder:(nullable YYTextBorder *)border; 325 | - (void)setBorder:(nullable YYTextBorder *)border; 326 | - (void)setAttachment:(nullable YYTextAttachment *)attachment; 327 | 328 | /** 329 | The user information dictionary, default is nil. 330 | */ 331 | @property (nullable, nonatomic, copy) NSDictionary *userInfo; 332 | 333 | /** 334 | Tap action when user tap the highlight, default is nil. 335 | If the value is nil, YYTextView or YYLabel will ask it's delegate to handle the tap action. 336 | */ 337 | @property (nullable, nonatomic, copy) YYTextAction tapAction; 338 | 339 | /** 340 | Long press action when user long press the highlight, default is nil. 341 | If the value is nil, YYTextView or YYLabel will ask it's delegate to handle the long press action. 342 | */ 343 | @property (nullable, nonatomic, copy) YYTextAction longPressAction; 344 | 345 | @end 346 | 347 | NS_ASSUME_NONNULL_END 348 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/String/YYTextParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextParser.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/3/6. 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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | The YYTextParser protocol declares the required method for YYTextView and YYLabel 18 | to modify the text during editing. 19 | 20 | You can implement this protocol to add code highlighting or emoticon replacement for 21 | YYTextView and YYLabel. See `YYTextSimpleMarkdownParser` and `YYTextSimpleEmoticonParser` for example. 22 | */ 23 | @protocol YYTextParser 24 | @required 25 | /** 26 | When text is changed in YYTextView or YYLabel, this method will be called. 27 | 28 | @param text The original attributed string. This method may parse the text and 29 | change the text attributes or content. 30 | 31 | @param selectedRange Current selected range in `text`. 32 | This method should correct the range if the text content is changed. If there's 33 | no selected range (such as YYLabel), this value is NULL. 34 | 35 | @return If the 'text' is modified in this method, returns `YES`, otherwise returns `NO`. 36 | */ 37 | - (BOOL)parseText:(nullable NSMutableAttributedString *)text selectedRange:(nullable NSRangePointer)selectedRange; 38 | @end 39 | 40 | 41 | 42 | /** 43 | A simple markdown parser. 44 | 45 | It'a very simple markdown parser, you can use this parser to highlight some 46 | small piece of markdown text. 47 | 48 | This markdown parser use regular expression to parse text, slow and weak. 49 | If you want to write a better parser, try these projests: 50 | https://github.com/NimbusKit/markdown 51 | https://github.com/dreamwieber/AttributedMarkdown 52 | https://github.com/indragiek/CocoaMarkdown 53 | 54 | Or you can use lex/yacc to generate your custom parser. 55 | */ 56 | @interface YYTextSimpleMarkdownParser : NSObject 57 | @property (nonatomic) CGFloat fontSize; ///< default is 14 58 | @property (nonatomic) CGFloat headerFontSize; ///< default is 20 59 | 60 | @property (nullable, nonatomic, strong) UIColor *textColor; 61 | @property (nullable, nonatomic, strong) UIColor *controlTextColor; 62 | @property (nullable, nonatomic, strong) UIColor *headerTextColor; 63 | @property (nullable, nonatomic, strong) UIColor *inlineTextColor; 64 | @property (nullable, nonatomic, strong) UIColor *codeTextColor; 65 | @property (nullable, nonatomic, strong) UIColor *linkTextColor; 66 | 67 | - (void)setColorWithBrightTheme; ///< reset the color properties to pre-defined value. 68 | - (void)setColorWithDarkTheme; ///< reset the color properties to pre-defined value. 69 | @end 70 | 71 | 72 | 73 | /** 74 | A simple emoticon parser. 75 | 76 | Use this parser to map some specified piece of string to image emoticon. 77 | Example: "Hello :smile:" -> "Hello 😀" 78 | 79 | It can also be used to extend the "unicode emoticon". 80 | */ 81 | @interface YYTextSimpleEmoticonParser : NSObject 82 | 83 | /** 84 | The custom emoticon mapper. 85 | The key is a specified plain string, such as @":smile:". 86 | The value is a UIImage which will replace the specified plain string in text. 87 | */ 88 | @property (nullable, copy) NSDictionary *emoticonMapper; 89 | @end 90 | 91 | NS_ASSUME_NONNULL_END 92 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/String/YYTextRubyAnnotation.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextRubyAnnotation.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/24. 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 | 12 | #import 13 | #import 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | /** 18 | Wrapper for CTRubyAnnotationRef. 19 | 20 | Example: 21 | 22 | YYTextRubyAnnotation *ruby = [YYTextRubyAnnotation new]; 23 | ruby.textBefore = @"zhù yīn"; 24 | CTRubyAnnotationRef ctRuby = ruby.CTRubyAnnotation; 25 | if (ctRuby) { 26 | /// add to attributed string 27 | CFRelease(ctRuby); 28 | } 29 | 30 | */ 31 | @interface YYTextRubyAnnotation : NSObject 32 | 33 | /// Specifies how the ruby text and the base text should be aligned relative to each other. 34 | @property (nonatomic) CTRubyAlignment alignment; 35 | 36 | /// Specifies how the ruby text can overhang adjacent characters. 37 | @property (nonatomic) CTRubyOverhang overhang; 38 | 39 | /// Specifies the size of the annotation text as a percent of the size of the base text. 40 | @property (nonatomic) CGFloat sizeFactor; 41 | 42 | 43 | /// The ruby text is positioned before the base text; 44 | /// i.e. above horizontal text and to the right of vertical text. 45 | @property (nullable, nonatomic, copy) NSString *textBefore; 46 | 47 | /// The ruby text is positioned after the base text; 48 | /// i.e. below horizontal text and to the left of vertical text. 49 | @property (nullable, nonatomic, copy) NSString *textAfter; 50 | 51 | /// The ruby text is positioned to the right of the base text whether it is horizontal or vertical. 52 | /// This is the way that Bopomofo annotations are attached to Chinese text in Taiwan. 53 | @property (nullable, nonatomic, copy) NSString *textInterCharacter; 54 | 55 | /// The ruby text follows the base text with no special styling. 56 | @property (nullable, nonatomic, copy) NSString *textInline; 57 | 58 | 59 | /** 60 | Create a ruby object from CTRuby object. 61 | 62 | @param ctRuby A CTRuby object. 63 | 64 | @return A ruby object, or nil when an error occurs. 65 | */ 66 | + (instancetype)rubyWithCTRubyRef:(CTRubyAnnotationRef)ctRuby NS_AVAILABLE_IOS(8_0); 67 | 68 | /** 69 | Create a CTRuby object from the instance. 70 | 71 | @return A new CTRuby object, or NULL when an error occurs. 72 | The returned value should be release after used. 73 | */ 74 | - (nullable CTRubyAnnotationRef)CTRubyAnnotation CF_RETURNS_RETAINED NS_AVAILABLE_IOS(8_0); 75 | 76 | @end 77 | 78 | NS_ASSUME_NONNULL_END 79 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/String/YYTextRubyAnnotation.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextRubyAnnotation.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/24. 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 | 12 | #import "YYTextRubyAnnotation.h" 13 | 14 | @implementation YYTextRubyAnnotation 15 | 16 | - (instancetype)init { 17 | self = super.init; 18 | self.alignment = kCTRubyAlignmentAuto; 19 | self.overhang = kCTRubyOverhangAuto; 20 | self.sizeFactor = 0.5; 21 | return self; 22 | } 23 | 24 | + (instancetype)rubyWithCTRubyRef:(CTRubyAnnotationRef)ctRuby { 25 | if (!ctRuby) return nil; 26 | YYTextRubyAnnotation *one = [self new]; 27 | one.alignment = CTRubyAnnotationGetAlignment(ctRuby); 28 | one.overhang = CTRubyAnnotationGetOverhang(ctRuby); 29 | one.sizeFactor = CTRubyAnnotationGetSizeFactor(ctRuby); 30 | one.textBefore = (__bridge NSString *)(CTRubyAnnotationGetTextForPosition(ctRuby, kCTRubyPositionBefore)); 31 | one.textAfter = (__bridge NSString *)(CTRubyAnnotationGetTextForPosition(ctRuby, kCTRubyPositionAfter)); 32 | one.textInterCharacter = (__bridge NSString *)(CTRubyAnnotationGetTextForPosition(ctRuby, kCTRubyPositionInterCharacter)); 33 | one.textInline = (__bridge NSString *)(CTRubyAnnotationGetTextForPosition(ctRuby, kCTRubyPositionInline)); 34 | return one; 35 | } 36 | 37 | - (CTRubyAnnotationRef)CTRubyAnnotation CF_RETURNS_RETAINED { 38 | if (((long)CTRubyAnnotationCreate + 1) == 1) return NULL; // system not support 39 | 40 | CFStringRef text[kCTRubyPositionCount]; 41 | text[kCTRubyPositionBefore] = (__bridge CFStringRef)(_textBefore); 42 | text[kCTRubyPositionAfter] = (__bridge CFStringRef)(_textAfter); 43 | text[kCTRubyPositionInterCharacter] = (__bridge CFStringRef)(_textInterCharacter); 44 | text[kCTRubyPositionInline] = (__bridge CFStringRef)(_textInline); 45 | CTRubyAnnotationRef ruby = CTRubyAnnotationCreate(_alignment, _overhang, _sizeFactor, text); 46 | return ruby; 47 | } 48 | 49 | - (id)copyWithZone:(NSZone *)zone { 50 | YYTextRubyAnnotation *one = [self.class new]; 51 | one.alignment = _alignment; 52 | one.overhang = _overhang; 53 | one.sizeFactor = _sizeFactor; 54 | one.textBefore = _textBefore; 55 | one.textAfter = _textAfter; 56 | one.textInterCharacter = _textInterCharacter; 57 | one.textInline = _textInline; 58 | return one; 59 | } 60 | 61 | - (void)encodeWithCoder:(NSCoder *)aCoder { 62 | [aCoder encodeObject:@(_alignment) forKey:@"alignment"]; 63 | [aCoder encodeObject:@(_overhang) forKey:@"overhang"]; 64 | [aCoder encodeObject:@(_sizeFactor) forKey:@"sizeFactor"]; 65 | [aCoder encodeObject:_textBefore forKey:@"textBefore"]; 66 | [aCoder encodeObject:_textAfter forKey:@"textAfter"]; 67 | [aCoder encodeObject:_textInterCharacter forKey:@"textInterCharacter"]; 68 | [aCoder encodeObject:_textInline forKey:@"textInline"]; 69 | } 70 | 71 | - (id)initWithCoder:(NSCoder *)aDecoder { 72 | self = [self init]; 73 | _alignment = ((NSNumber *)[aDecoder decodeObjectForKey:@"alignment"]).intValue; 74 | _overhang = ((NSNumber *)[aDecoder decodeObjectForKey:@"overhang"]).intValue; 75 | _sizeFactor = ((NSNumber *)[aDecoder decodeObjectForKey:@"sizeFactor"]).intValue; 76 | _textBefore = [aDecoder decodeObjectForKey:@"textBefore"]; 77 | _textAfter = [aDecoder decodeObjectForKey:@"textAfter"]; 78 | _textInterCharacter = [aDecoder decodeObjectForKey:@"textInterCharacter"]; 79 | _textInline = [aDecoder decodeObjectForKey:@"textInline"]; 80 | return self; 81 | } 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/String/YYTextRunDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextRunDelegate.h 3 | // YYText 4 | // 5 | // Created by ibireme on 14/10/14. 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 | 12 | #import 13 | #import 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | /** 18 | Wrapper for CTRunDelegateRef. 19 | 20 | Example: 21 | 22 | YYTextRunDelegate *delegate = [YYTextRunDelegate new]; 23 | delegate.ascent = 20; 24 | delegate.descent = 4; 25 | delegate.width = 20; 26 | CTRunDelegateRef ctRunDelegate = delegate.CTRunDelegate; 27 | if (ctRunDelegate) { 28 | /// add to attributed string 29 | CFRelease(ctRunDelegate); 30 | } 31 | 32 | */ 33 | @interface YYTextRunDelegate : NSObject 34 | 35 | /** 36 | Creates and returns the CTRunDelegate. 37 | 38 | @discussion You need call CFRelease() after used. 39 | The CTRunDelegateRef has a strong reference to this YYTextRunDelegate object. 40 | In CoreText, use CTRunDelegateGetRefCon() to get this YYTextRunDelegate object. 41 | 42 | @return The CTRunDelegate object. 43 | */ 44 | - (nullable CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED; 45 | 46 | /** 47 | Additional information about the the run delegate. 48 | */ 49 | @property (nullable, nonatomic, strong) NSDictionary *userInfo; 50 | 51 | /** 52 | The typographic ascent of glyphs in the run. 53 | */ 54 | @property (nonatomic) CGFloat ascent; 55 | 56 | /** 57 | The typographic descent of glyphs in the run. 58 | */ 59 | @property (nonatomic) CGFloat descent; 60 | 61 | /** 62 | The typographic width of glyphs in the run. 63 | */ 64 | @property (nonatomic) CGFloat width; 65 | 66 | @end 67 | 68 | NS_ASSUME_NONNULL_END 69 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/String/YYTextRunDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextRunDelegate.m 3 | // YYText 4 | // 5 | // Created by ibireme on 14/10/14. 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 | 12 | #import "YYTextRunDelegate.h" 13 | 14 | static void DeallocCallback(void *ref) { 15 | YYTextRunDelegate *self = (__bridge_transfer YYTextRunDelegate *)(ref); 16 | self = nil; // release 17 | } 18 | 19 | static CGFloat GetAscentCallback(void *ref) { 20 | YYTextRunDelegate *self = (__bridge YYTextRunDelegate *)(ref); 21 | return self.ascent; 22 | } 23 | 24 | static CGFloat GetDecentCallback(void *ref) { 25 | YYTextRunDelegate *self = (__bridge YYTextRunDelegate *)(ref); 26 | return self.descent; 27 | } 28 | 29 | static CGFloat GetWidthCallback(void *ref) { 30 | YYTextRunDelegate *self = (__bridge YYTextRunDelegate *)(ref); 31 | return self.width; 32 | } 33 | 34 | @implementation YYTextRunDelegate 35 | 36 | - (CTRunDelegateRef)CTRunDelegate CF_RETURNS_RETAINED { 37 | CTRunDelegateCallbacks callbacks; 38 | callbacks.version = kCTRunDelegateCurrentVersion; 39 | callbacks.dealloc = DeallocCallback; 40 | callbacks.getAscent = GetAscentCallback; 41 | callbacks.getDescent = GetDecentCallback; 42 | callbacks.getWidth = GetWidthCallback; 43 | return CTRunDelegateCreate(&callbacks, (__bridge_retained void *)(self.copy)); 44 | } 45 | 46 | - (void)encodeWithCoder:(NSCoder *)aCoder { 47 | [aCoder encodeObject:@(_ascent) forKey:@"ascent"]; 48 | [aCoder encodeObject:@(_descent) forKey:@"descent"]; 49 | [aCoder encodeObject:@(_width) forKey:@"width"]; 50 | [aCoder encodeObject:_userInfo forKey:@"userInfo"]; 51 | } 52 | 53 | - (id)initWithCoder:(NSCoder *)aDecoder { 54 | self = [super init]; 55 | _ascent = ((NSNumber *)[aDecoder decodeObjectForKey:@"ascent"]).floatValue; 56 | _descent = ((NSNumber *)[aDecoder decodeObjectForKey:@"descent"]).floatValue; 57 | _width = ((NSNumber *)[aDecoder decodeObjectForKey:@"width"]).floatValue; 58 | _userInfo = [aDecoder decodeObjectForKey:@"userInfo"]; 59 | return self; 60 | } 61 | 62 | - (id)copyWithZone:(NSZone *)zone { 63 | typeof(self) one = [self.class new]; 64 | one.ascent = self.ascent; 65 | one.descent = self.descent; 66 | one.width = self.width; 67 | one.userInfo = self.userInfo; 68 | return one; 69 | } 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/NSParagraphStyle+YYText.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSParagraphStyle+YYText.h 3 | // YYText 4 | // 5 | // Created by ibireme on 14/10/7. 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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | Provides extensions for `NSParagraphStyle` to work with CoreText. 18 | */ 19 | @interface NSParagraphStyle (YYText) 20 | 21 | /** 22 | Creates a new NSParagraphStyle object from the CoreText Style. 23 | 24 | @param CTStyle CoreText Paragraph Style. 25 | 26 | @return a new NSParagraphStyle 27 | */ 28 | + (nullable NSParagraphStyle *)yy_styleWithCTStyle:(CTParagraphStyleRef)CTStyle; 29 | 30 | /** 31 | Creates and returns a CoreText Paragraph Style. (need call CFRelease() after used) 32 | */ 33 | - (nullable CTParagraphStyleRef)yy_CTStyle CF_RETURNS_RETAINED; 34 | 35 | @end 36 | 37 | NS_ASSUME_NONNULL_END 38 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/NSParagraphStyle+YYText.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSParagraphStyle+YYText.m 3 | // YYText 4 | // 5 | // Created by ibireme on 14/10/7. 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 | 12 | #import "NSParagraphStyle+YYText.h" 13 | #import "YYTextAttribute.h" 14 | #import 15 | 16 | // Dummy class for category 17 | @interface NSParagraphStyle_YYText : NSObject @end 18 | @implementation NSParagraphStyle_YYText @end 19 | 20 | 21 | @implementation NSParagraphStyle (YYText) 22 | 23 | + (NSParagraphStyle *)yy_styleWithCTStyle:(CTParagraphStyleRef)CTStyle { 24 | if (CTStyle == NULL) return nil; 25 | 26 | NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; 27 | 28 | #pragma clang diagnostic push 29 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 30 | CGFloat lineSpacing; 31 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &lineSpacing)) { 32 | style.lineSpacing = lineSpacing; 33 | } 34 | #pragma clang diagnostic pop 35 | 36 | CGFloat paragraphSpacing; 37 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), ¶graphSpacing)) { 38 | style.paragraphSpacing = paragraphSpacing; 39 | } 40 | 41 | CTTextAlignment alignment; 42 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &alignment)) { 43 | style.alignment = NSTextAlignmentFromCTTextAlignment(alignment); 44 | } 45 | 46 | CGFloat firstLineHeadIndent; 47 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &firstLineHeadIndent)) { 48 | style.firstLineHeadIndent = firstLineHeadIndent; 49 | } 50 | 51 | CGFloat headIndent; 52 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &headIndent)) { 53 | style.headIndent = headIndent; 54 | } 55 | 56 | CGFloat tailIndent; 57 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierTailIndent, sizeof(CGFloat), &tailIndent)) { 58 | style.tailIndent = tailIndent; 59 | } 60 | 61 | CTLineBreakMode lineBreakMode; 62 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBreakMode)) { 63 | style.lineBreakMode = (NSLineBreakMode)lineBreakMode; 64 | } 65 | 66 | CGFloat minimumLineHeight; 67 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &minimumLineHeight)) { 68 | style.minimumLineHeight = minimumLineHeight; 69 | } 70 | 71 | CGFloat maximumLineHeight; 72 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &maximumLineHeight)) { 73 | style.maximumLineHeight = maximumLineHeight; 74 | } 75 | 76 | CTWritingDirection baseWritingDirection; 77 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(CTWritingDirection), &baseWritingDirection)) { 78 | style.baseWritingDirection = (NSWritingDirection)baseWritingDirection; 79 | } 80 | 81 | CGFloat lineHeightMultiple; 82 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(CGFloat), &lineHeightMultiple)) { 83 | style.lineHeightMultiple = lineHeightMultiple; 84 | } 85 | 86 | CGFloat paragraphSpacingBefore; 87 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), ¶graphSpacingBefore)) { 88 | style.paragraphSpacingBefore = paragraphSpacingBefore; 89 | } 90 | 91 | if ([style respondsToSelector:@selector(tabStops)]) { 92 | CFArrayRef tabStops; 93 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierTabStops, sizeof(CFArrayRef), &tabStops)) { 94 | if ([style respondsToSelector:@selector(setTabStops:)]) { 95 | NSMutableArray *tabs = [NSMutableArray new]; 96 | [((__bridge NSArray *)(tabStops))enumerateObjectsUsingBlock : ^(id obj, NSUInteger idx, BOOL *stop) { 97 | CTTextTabRef ctTab = (__bridge CFTypeRef)obj; 98 | 99 | NSTextTab *tab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentFromCTTextAlignment(CTTextTabGetAlignment(ctTab)) location:CTTextTabGetLocation(ctTab) options:(__bridge id)CTTextTabGetOptions(ctTab)]; 100 | [tabs addObject:tab]; 101 | }]; 102 | if (tabs.count) { 103 | style.tabStops = tabs; 104 | } 105 | } 106 | } 107 | 108 | CGFloat defaultTabInterval; 109 | if (CTParagraphStyleGetValueForSpecifier(CTStyle, kCTParagraphStyleSpecifierDefaultTabInterval, sizeof(CGFloat), &defaultTabInterval)) { 110 | if ([style respondsToSelector:@selector(setDefaultTabInterval:)]) { 111 | style.defaultTabInterval = defaultTabInterval; 112 | } 113 | } 114 | } 115 | 116 | return style; 117 | } 118 | 119 | - (CTParagraphStyleRef)yy_CTStyle CF_RETURNS_RETAINED { 120 | CTParagraphStyleSetting set[kCTParagraphStyleSpecifierCount] = { 0 }; 121 | int count = 0; 122 | 123 | #pragma clang diagnostic push 124 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 125 | CGFloat lineSpacing = self.lineSpacing; 126 | set[count].spec = kCTParagraphStyleSpecifierLineSpacing; 127 | set[count].valueSize = sizeof(CGFloat); 128 | set[count].value = &lineSpacing; 129 | count++; 130 | #pragma clang diagnostic pop 131 | 132 | CGFloat paragraphSpacing = self.paragraphSpacing; 133 | set[count].spec = kCTParagraphStyleSpecifierParagraphSpacing; 134 | set[count].valueSize = sizeof(CGFloat); 135 | set[count].value = ¶graphSpacing; 136 | count++; 137 | 138 | CTTextAlignment alignment = NSTextAlignmentToCTTextAlignment(self.alignment); 139 | set[count].spec = kCTParagraphStyleSpecifierAlignment; 140 | set[count].valueSize = sizeof(CTTextAlignment); 141 | set[count].value = &alignment; 142 | count++; 143 | 144 | CGFloat firstLineHeadIndent = self.firstLineHeadIndent; 145 | set[count].spec = kCTParagraphStyleSpecifierFirstLineHeadIndent; 146 | set[count].valueSize = sizeof(CGFloat); 147 | set[count].value = &firstLineHeadIndent; 148 | count++; 149 | 150 | CGFloat headIndent = self.headIndent; 151 | set[count].spec = kCTParagraphStyleSpecifierHeadIndent; 152 | set[count].valueSize = sizeof(CGFloat); 153 | set[count].value = &headIndent; 154 | count++; 155 | 156 | CGFloat tailIndent = self.tailIndent; 157 | set[count].spec = kCTParagraphStyleSpecifierTailIndent; 158 | set[count].valueSize = sizeof(CGFloat); 159 | set[count].value = &tailIndent; 160 | count++; 161 | 162 | CTLineBreakMode paraLineBreak = (CTLineBreakMode)self.lineBreakMode; 163 | set[count].spec = kCTParagraphStyleSpecifierLineBreakMode; 164 | set[count].valueSize = sizeof(CTLineBreakMode); 165 | set[count].value = ¶LineBreak; 166 | count++; 167 | 168 | CGFloat minimumLineHeight = self.minimumLineHeight; 169 | set[count].spec = kCTParagraphStyleSpecifierMinimumLineHeight; 170 | set[count].valueSize = sizeof(CGFloat); 171 | set[count].value = &minimumLineHeight; 172 | count++; 173 | 174 | CGFloat maximumLineHeight = self.maximumLineHeight; 175 | set[count].spec = kCTParagraphStyleSpecifierMaximumLineHeight; 176 | set[count].valueSize = sizeof(CGFloat); 177 | set[count].value = &maximumLineHeight; 178 | count++; 179 | 180 | CTWritingDirection paraWritingDirection = (CTWritingDirection)self.baseWritingDirection; 181 | set[count].spec = kCTParagraphStyleSpecifierBaseWritingDirection; 182 | set[count].valueSize = sizeof(CTWritingDirection); 183 | set[count].value = ¶WritingDirection; 184 | count++; 185 | 186 | CGFloat lineHeightMultiple = self.lineHeightMultiple; 187 | set[count].spec = kCTParagraphStyleSpecifierLineHeightMultiple; 188 | set[count].valueSize = sizeof(CGFloat); 189 | set[count].value = &lineHeightMultiple; 190 | count++; 191 | 192 | CGFloat paragraphSpacingBefore = self.paragraphSpacingBefore; 193 | set[count].spec = kCTParagraphStyleSpecifierParagraphSpacingBefore; 194 | set[count].valueSize = sizeof(CGFloat); 195 | set[count].value = ¶graphSpacingBefore; 196 | count++; 197 | 198 | if([self respondsToSelector:@selector(tabStops)]) { 199 | NSMutableArray *tabs = [NSMutableArray array]; 200 | if ([self respondsToSelector:@selector(tabStops)]) { 201 | NSInteger numTabs = self.tabStops.count; 202 | if (numTabs) { 203 | [self.tabStops enumerateObjectsUsingBlock: ^(NSTextTab *tab, NSUInteger idx, BOOL *stop) { 204 | CTTextTabRef ctTab = CTTextTabCreate(NSTextAlignmentToCTTextAlignment(tab.alignment), tab.location, (__bridge CFTypeRef)tab.options); 205 | [tabs addObject:(__bridge id)ctTab]; 206 | CFRelease(ctTab); 207 | }]; 208 | 209 | CFArrayRef tabStops = (__bridge CFArrayRef)(tabs); 210 | set[count].spec = kCTParagraphStyleSpecifierTabStops; 211 | set[count].valueSize = sizeof(CFArrayRef); 212 | set[count].value = &tabStops; 213 | count++; 214 | } 215 | } 216 | 217 | if ([self respondsToSelector:@selector(defaultTabInterval)]) { 218 | CGFloat defaultTabInterval = self.defaultTabInterval; 219 | set[count].spec = kCTParagraphStyleSpecifierDefaultTabInterval; 220 | set[count].valueSize = sizeof(CGFloat); 221 | set[count].value = &defaultTabInterval; 222 | count++; 223 | } 224 | } 225 | 226 | CTParagraphStyleRef style = CTParagraphStyleCreate(set, count); 227 | return style; 228 | } 229 | 230 | @end 231 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/UIPasteboard+YYText.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIPasteboard+YYText.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/2. 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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | Extend UIPasteboard to support image and attributed string. 18 | */ 19 | @interface UIPasteboard (YYText) 20 | 21 | @property (nullable, nonatomic, copy) NSData *yy_PNGData; ///< PNG file data 22 | @property (nullable, nonatomic, copy) NSData *yy_JPEGData; ///< JPEG file data 23 | @property (nullable, nonatomic, copy) NSData *yy_GIFData; ///< GIF file data 24 | @property (nullable, nonatomic, copy) NSData *yy_WEBPData; ///< WebP file data 25 | @property (nullable, nonatomic, copy) NSData *yy_ImageData; ///< image file data 26 | 27 | /// Attributed string, 28 | /// Set this attributed will also set the string property which is copy from the attributed string. 29 | /// If the attributed string contains one or more image, it will also set the `images` property. 30 | @property (nullable, nonatomic, copy) NSAttributedString *yy_AttributedString; 31 | 32 | @end 33 | 34 | 35 | /// The name identifying the attributed string in pasteboard. 36 | UIKIT_EXTERN NSString *const YYTextPasteboardTypeAttributedString; 37 | 38 | /// The UTI Type identifying WebP data in pasteboard. 39 | UIKIT_EXTERN NSString *const YYTextUTTypeWEBP; 40 | 41 | NS_ASSUME_NONNULL_END 42 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/UIPasteboard+YYText.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIPasteboard+YYText.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/2. 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 | 12 | #import "UIPasteboard+YYText.h" 13 | #import "NSAttributedString+YYText.h" 14 | #import 15 | 16 | 17 | #if __has_include("YYImage.h") 18 | #import "YYImage.h" 19 | #define YYTextAnimatedImageAvailable 1 20 | #elif __has_include() 21 | #import 22 | #define YYTextAnimatedImageAvailable 1 23 | #elif __has_include() 24 | #import 25 | #define YYTextAnimatedImageAvailable 1 26 | #else 27 | #define YYTextAnimatedImageAvailable 0 28 | #endif 29 | 30 | 31 | // Dummy class for category 32 | @interface UIPasteboard_YYText : NSObject @end 33 | @implementation UIPasteboard_YYText @end 34 | 35 | 36 | NSString *const YYTextPasteboardTypeAttributedString = @"com.ibireme.NSAttributedString"; 37 | NSString *const YYTextUTTypeWEBP = @"com.google.webp"; 38 | 39 | @implementation UIPasteboard (YYText) 40 | 41 | 42 | - (void)setYy_PNGData:(NSData *)PNGData { 43 | [self setData:PNGData forPasteboardType:(id)kUTTypePNG]; 44 | } 45 | 46 | - (NSData *)yy_PNGData { 47 | return [self dataForPasteboardType:(id)kUTTypePNG]; 48 | } 49 | 50 | - (void)setYy_JPEGData:(NSData *)JPEGData { 51 | [self setData:JPEGData forPasteboardType:(id)kUTTypeJPEG]; 52 | } 53 | 54 | - (NSData *)yy_JPEGData { 55 | return [self dataForPasteboardType:(id)kUTTypeJPEG]; 56 | } 57 | 58 | - (void)setYy_GIFData:(NSData *)GIFData { 59 | [self setData:GIFData forPasteboardType:(id)kUTTypeGIF]; 60 | } 61 | 62 | - (NSData *)yy_GIFData { 63 | return [self dataForPasteboardType:(id)kUTTypeGIF]; 64 | } 65 | 66 | - (void)setYy_WEBPData:(NSData *)WEBPData { 67 | [self setData:WEBPData forPasteboardType:YYTextUTTypeWEBP]; 68 | } 69 | 70 | - (NSData *)yy_WEBPData { 71 | return [self dataForPasteboardType:YYTextUTTypeWEBP]; 72 | } 73 | 74 | - (void)setYy_ImageData:(NSData *)imageData { 75 | [self setData:imageData forPasteboardType:(id)kUTTypeImage]; 76 | } 77 | 78 | - (NSData *)yy_ImageData { 79 | return [self dataForPasteboardType:(id)kUTTypeImage]; 80 | } 81 | 82 | - (void)setYy_AttributedString:(NSAttributedString *)attributedString { 83 | self.string = [attributedString yy_plainTextForRange:NSMakeRange(0, attributedString.length)]; 84 | NSData *data = [attributedString yy_archiveToData]; 85 | if (data) { 86 | NSDictionary *item = @{YYTextPasteboardTypeAttributedString : data}; 87 | [self addItems:@[item]]; 88 | } 89 | [attributedString enumerateAttribute:YYTextAttachmentAttributeName inRange:NSMakeRange(0, attributedString.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(YYTextAttachment *attachment, NSRange range, BOOL *stop) { 90 | 91 | // save image 92 | UIImage *simpleImage = nil; 93 | if ([attachment.content isKindOfClass:[UIImage class]]) { 94 | simpleImage = attachment.content; 95 | } else if ([attachment.content isKindOfClass:[UIImageView class]]) { 96 | simpleImage = ((UIImageView *)attachment.content).image; 97 | } 98 | if (simpleImage) { 99 | NSDictionary *item = @{@"com.apple.uikit.image" : simpleImage}; 100 | [self addItems:@[item]]; 101 | } 102 | 103 | #if YYTextAnimatedImageAvailable 104 | // save animated image 105 | if ([attachment.content isKindOfClass:[UIImageView class]]) { 106 | UIImageView *imageView = attachment.content; 107 | Class aniImageClass = NSClassFromString(@"YYImage"); 108 | UIImage *image = imageView.image; 109 | if (aniImageClass && [image isKindOfClass:aniImageClass]) { 110 | NSData *data = [image valueForKey:@"animatedImageData"]; 111 | NSNumber *type = [image valueForKey:@"animatedImageType"]; 112 | if (data) { 113 | switch (type.unsignedIntegerValue) { 114 | case YYImageTypeGIF: { 115 | NSDictionary *item = @{(id)kUTTypeGIF : data}; 116 | [self addItems:@[item]]; 117 | } break; 118 | case YYImageTypePNG: { // APNG 119 | NSDictionary *item = @{(id)kUTTypePNG : data}; 120 | [self addItems:@[item]]; 121 | } break; 122 | case YYImageTypeWebP: { 123 | NSDictionary *item = @{(id)YYTextUTTypeWEBP : data}; 124 | [self addItems:@[item]]; 125 | } break; 126 | default: break; 127 | } 128 | } 129 | } 130 | } 131 | #endif 132 | 133 | }]; 134 | } 135 | 136 | - (NSAttributedString *)yy_AttributedString { 137 | for (NSDictionary *items in self.items) { 138 | NSData *data = items[YYTextPasteboardTypeAttributedString]; 139 | if (data) { 140 | return [NSAttributedString yy_unarchiveFromData:data]; 141 | } 142 | } 143 | return nil; 144 | } 145 | 146 | @end 147 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/UIView+YYText.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+YYText.h 3 | // YYText 4 | // 5 | // Created by ibireme on 13/4/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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | Provides extensions for `UIView`. 18 | */ 19 | @interface UIView (YYText) 20 | 21 | /** 22 | Returns the view's view controller (may be nil). 23 | */ 24 | @property (nullable, nonatomic, readonly) UIViewController *yy_viewController; 25 | 26 | /** 27 | Returns the visible alpha on screen, taking into account superview and window. 28 | */ 29 | @property (nonatomic, readonly) CGFloat yy_visibleAlpha; 30 | 31 | /** 32 | Converts a point from the receiver's coordinate system to that of the specified view or window. 33 | 34 | @param point A point specified in the local coordinate system (bounds) of the receiver. 35 | @param view The view or window into whose coordinate system point is to be converted. 36 | If view is nil, this method instead converts to window base coordinates. 37 | @return The point converted to the coordinate system of view. 38 | */ 39 | - (CGPoint)yy_convertPoint:(CGPoint)point toViewOrWindow:(UIView *)view; 40 | 41 | /** 42 | Converts a point from the coordinate system of a given view or window to that of the receiver. 43 | 44 | @param point A point specified in the local coordinate system (bounds) of view. 45 | @param view The view or window with point in its coordinate system. 46 | If view is nil, this method instead converts from window base coordinates. 47 | @return The point converted to the local coordinate system (bounds) of the receiver. 48 | */ 49 | - (CGPoint)yy_convertPoint:(CGPoint)point fromViewOrWindow:(UIView *)view; 50 | 51 | /** 52 | Converts a rectangle from the receiver's coordinate system to that of another view or window. 53 | 54 | @param rect A rectangle specified in the local coordinate system (bounds) of the receiver. 55 | @param view The view or window that is the target of the conversion operation. If view is nil, this method instead converts to window base coordinates. 56 | @return The converted rectangle. 57 | */ 58 | - (CGRect)yy_convertRect:(CGRect)rect toViewOrWindow:(UIView *)view; 59 | 60 | /** 61 | Converts a rectangle from the coordinate system of another view or window to that of the receiver. 62 | 63 | @param rect A rectangle specified in the local coordinate system (bounds) of view. 64 | @param view The view or window with rect in its coordinate system. 65 | If view is nil, this method instead converts from window base coordinates. 66 | @return The converted rectangle. 67 | */ 68 | - (CGRect)yy_convertRect:(CGRect)rect fromViewOrWindow:(UIView *)view; 69 | 70 | @end 71 | 72 | NS_ASSUME_NONNULL_END 73 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/UIView+YYText.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+YYText.m 3 | // YYText 4 | // 5 | // Created by ibireme on 13/4/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 | 12 | #import "UIView+YYText.h" 13 | 14 | // Dummy class for category 15 | @interface UIView_YYText : NSObject @end 16 | @implementation UIView_YYText @end 17 | 18 | 19 | @implementation UIView (YYText) 20 | 21 | - (UIViewController *)yy_viewController { 22 | for (UIView *view = self; view; view = view.superview) { 23 | UIResponder *nextResponder = [view nextResponder]; 24 | if ([nextResponder isKindOfClass:[UIViewController class]]) { 25 | return (UIViewController *)nextResponder; 26 | } 27 | } 28 | return nil; 29 | } 30 | 31 | - (CGFloat)yy_visibleAlpha { 32 | if ([self isKindOfClass:[UIWindow class]]) { 33 | if (self.hidden) return 0; 34 | return self.alpha; 35 | } 36 | if (!self.window) return 0; 37 | CGFloat alpha = 1; 38 | UIView *v = self; 39 | while (v) { 40 | if (v.hidden) { 41 | alpha = 0; 42 | break; 43 | } 44 | alpha *= v.alpha; 45 | v = v.superview; 46 | } 47 | return alpha; 48 | } 49 | 50 | - (CGPoint)yy_convertPoint:(CGPoint)point toViewOrWindow:(UIView *)view { 51 | if (!view) { 52 | if ([self isKindOfClass:[UIWindow class]]) { 53 | return [((UIWindow *)self) convertPoint:point toWindow:nil]; 54 | } else { 55 | return [self convertPoint:point toView:nil]; 56 | } 57 | } 58 | 59 | UIWindow *from = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window; 60 | UIWindow *to = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window; 61 | if ((!from || !to) || (from == to)) return [self convertPoint:point toView:view]; 62 | point = [self convertPoint:point toView:from]; 63 | point = [to convertPoint:point fromWindow:from]; 64 | point = [view convertPoint:point fromView:to]; 65 | return point; 66 | } 67 | 68 | - (CGPoint)yy_convertPoint:(CGPoint)point fromViewOrWindow:(UIView *)view { 69 | if (!view) { 70 | if ([self isKindOfClass:[UIWindow class]]) { 71 | return [((UIWindow *)self) convertPoint:point fromWindow:nil]; 72 | } else { 73 | return [self convertPoint:point fromView:nil]; 74 | } 75 | } 76 | 77 | UIWindow *from = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window; 78 | UIWindow *to = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window; 79 | if ((!from || !to) || (from == to)) return [self convertPoint:point fromView:view]; 80 | point = [from convertPoint:point fromView:view]; 81 | point = [to convertPoint:point fromWindow:from]; 82 | point = [self convertPoint:point fromView:to]; 83 | return point; 84 | } 85 | 86 | - (CGRect)yy_convertRect:(CGRect)rect toViewOrWindow:(UIView *)view { 87 | if (!view) { 88 | if ([self isKindOfClass:[UIWindow class]]) { 89 | return [((UIWindow *)self) convertRect:rect toWindow:nil]; 90 | } else { 91 | return [self convertRect:rect toView:nil]; 92 | } 93 | } 94 | 95 | UIWindow *from = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window; 96 | UIWindow *to = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window; 97 | if (!from || !to) return [self convertRect:rect toView:view]; 98 | if (from == to) return [self convertRect:rect toView:view]; 99 | rect = [self convertRect:rect toView:from]; 100 | rect = [to convertRect:rect fromWindow:from]; 101 | rect = [view convertRect:rect fromView:to]; 102 | return rect; 103 | } 104 | 105 | - (CGRect)yy_convertRect:(CGRect)rect fromViewOrWindow:(UIView *)view { 106 | if (!view) { 107 | if ([self isKindOfClass:[UIWindow class]]) { 108 | return [((UIWindow *)self) convertRect:rect fromWindow:nil]; 109 | } else { 110 | return [self convertRect:rect fromView:nil]; 111 | } 112 | } 113 | 114 | UIWindow *from = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window; 115 | UIWindow *to = [self isKindOfClass:[UIWindow class]] ? (id)self : self.window; 116 | if ((!from || !to) || (from == to)) return [self convertRect:rect fromView:view]; 117 | rect = [from convertRect:rect fromView:view]; 118 | rect = [to convertRect:rect fromWindow:from]; 119 | rect = [self convertRect:rect fromView:to]; 120 | return rect; 121 | } 122 | 123 | @end -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/YYTextAsyncLayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextAsyncLayer.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/11. 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 | 12 | #import 13 | #import 14 | 15 | @class YYTextAsyncLayerDisplayTask; 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | /** 20 | The YYTextAsyncLayer class is a subclass of CALayer used for render contents asynchronously. 21 | 22 | @discussion When the layer need update it's contents, it will ask the delegate 23 | for a async display task to render the contents in a background queue. 24 | */ 25 | @interface YYTextAsyncLayer : CALayer 26 | /// Whether the render code is executed in background. Default is YES. 27 | @property BOOL displaysAsynchronously; 28 | @end 29 | 30 | 31 | /** 32 | The YYTextAsyncLayer's delegate protocol. The delegate of the YYTextAsyncLayer (typically a UIView) 33 | must implements the method in this protocol. 34 | */ 35 | @protocol YYTextAsyncLayerDelegate 36 | @required 37 | /// This method is called to return a new display task when the layer's contents need update. 38 | - (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask; 39 | @end 40 | 41 | 42 | /** 43 | A display task used by YYTextAsyncLayer to render the contents in background queue. 44 | */ 45 | @interface YYTextAsyncLayerDisplayTask : NSObject 46 | 47 | /** 48 | This block will be called before the asynchronous drawing begins. 49 | It will be called on the main thread. 50 | 51 | block param layer: The layer. 52 | */ 53 | @property (nullable, nonatomic, copy) void (^willDisplay)(CALayer *layer); 54 | 55 | /** 56 | This block is called to draw the layer's contents. 57 | 58 | @discussion This block may be called on main thread or background thread, 59 | so is should be thread-safe. 60 | 61 | block param context: A new bitmap content created by layer. 62 | block param size: The content size (typically same as layer's bound size). 63 | block param isCancelled: If this block returns `YES`, the method should cancel the 64 | drawing process and return as quickly as possible. 65 | */ 66 | @property (nullable, nonatomic, copy) void (^display)(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)); 67 | 68 | /** 69 | This block will be called after the asynchronous drawing finished. 70 | It will be called on the main thread. 71 | 72 | block param layer: The layer. 73 | block param finished: If the draw process is cancelled, it's `NO`, otherwise it's `YES`; 74 | */ 75 | @property (nullable, nonatomic, copy) void (^didDisplay)(CALayer *layer, BOOL finished); 76 | 77 | @end 78 | 79 | NS_ASSUME_NONNULL_END 80 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/YYTextAsyncLayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextAsyncLayer.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/11. 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 | 12 | #import "YYTextAsyncLayer.h" 13 | #import 14 | 15 | 16 | /// Global display queue, used for content rendering. 17 | static dispatch_queue_t YYTextAsyncLayerGetDisplayQueue() { 18 | #define MAX_QUEUE_COUNT 16 19 | static int queueCount; 20 | static dispatch_queue_t queues[MAX_QUEUE_COUNT]; 21 | static dispatch_once_t onceToken; 22 | static int32_t counter = 0; 23 | dispatch_once(&onceToken, ^{ 24 | queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount; 25 | queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount; 26 | if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) { 27 | for (NSUInteger i = 0; i < queueCount; i++) { 28 | dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); 29 | queues[i] = dispatch_queue_create("com.ibireme.text.render", attr); 30 | } 31 | } else { 32 | for (NSUInteger i = 0; i < queueCount; i++) { 33 | queues[i] = dispatch_queue_create("com.ibireme.text.render", DISPATCH_QUEUE_SERIAL); 34 | dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); 35 | } 36 | } 37 | }); 38 | uint32_t cur = (uint32_t)OSAtomicIncrement32(&counter); 39 | return queues[(cur) % queueCount]; 40 | #undef MAX_QUEUE_COUNT 41 | } 42 | 43 | static dispatch_queue_t YYTextAsyncLayerGetReleaseQueue() { 44 | #ifdef YYDispatchQueuePool_h 45 | return YYDispatchQueueGetForQOS(NSQualityOfServiceDefault); 46 | #else 47 | return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); 48 | #endif 49 | } 50 | 51 | 52 | /// a thread safe incrementing counter. 53 | @interface _YYTextSentinel : NSObject 54 | /// Returns the current value of the counter. 55 | @property (atomic, readonly) int32_t value; 56 | /// Increase the value atomically. @return The new value. 57 | - (int32_t)increase; 58 | @end 59 | 60 | @implementation _YYTextSentinel { 61 | int32_t _value; 62 | } 63 | - (int32_t)value { 64 | return _value; 65 | } 66 | - (int32_t)increase { 67 | return OSAtomicIncrement32(&_value); 68 | } 69 | @end 70 | 71 | 72 | @implementation YYTextAsyncLayerDisplayTask 73 | @end 74 | 75 | 76 | @implementation YYTextAsyncLayer { 77 | _YYTextSentinel *_sentinel; 78 | } 79 | 80 | #pragma mark - Override 81 | 82 | + (id)defaultValueForKey:(NSString *)key { 83 | if ([key isEqualToString:@"displaysAsynchronously"]) { 84 | return @(YES); 85 | } else { 86 | return [super defaultValueForKey:key]; 87 | } 88 | } 89 | 90 | - (instancetype)init { 91 | self = [super init]; 92 | static CGFloat scale; //global 93 | static dispatch_once_t onceToken; 94 | dispatch_once(&onceToken, ^{ 95 | scale = [UIScreen mainScreen].scale; 96 | }); 97 | self.contentsScale = scale; 98 | _sentinel = [_YYTextSentinel new]; 99 | _displaysAsynchronously = YES; 100 | return self; 101 | } 102 | 103 | - (void)dealloc { 104 | [_sentinel increase]; 105 | } 106 | 107 | - (void)setNeedsDisplay { 108 | [self _cancelAsyncDisplay]; 109 | [super setNeedsDisplay]; 110 | } 111 | 112 | - (void)display { 113 | super.contents = super.contents; 114 | [self _displayAsync:_displaysAsynchronously]; 115 | } 116 | 117 | #pragma mark - Private 118 | 119 | - (void)_displayAsync:(BOOL)async { 120 | __strong id delegate = (id)self.delegate; 121 | YYTextAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask]; 122 | if (!task.display) { 123 | if (task.willDisplay) task.willDisplay(self); 124 | self.contents = nil; 125 | if (task.didDisplay) task.didDisplay(self, YES); 126 | return; 127 | } 128 | 129 | if (async) { 130 | if (task.willDisplay) task.willDisplay(self); 131 | _YYTextSentinel *sentinel = _sentinel; 132 | int32_t value = sentinel.value; 133 | BOOL (^isCancelled)() = ^BOOL() { 134 | return value != sentinel.value; 135 | }; 136 | CGSize size = self.bounds.size; 137 | BOOL opaque = self.opaque; 138 | CGFloat scale = self.contentsScale; 139 | CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL; 140 | if (size.width < 1 || size.height < 1) { 141 | CGImageRef image = (__bridge_retained CGImageRef)(self.contents); 142 | self.contents = nil; 143 | if (image) { 144 | dispatch_async(YYTextAsyncLayerGetReleaseQueue(), ^{ 145 | CFRelease(image); 146 | }); 147 | } 148 | if (task.didDisplay) task.didDisplay(self, YES); 149 | CGColorRelease(backgroundColor); 150 | return; 151 | } 152 | 153 | dispatch_async(YYTextAsyncLayerGetDisplayQueue(), ^{ 154 | if (isCancelled()) { 155 | CGColorRelease(backgroundColor); 156 | return; 157 | } 158 | UIGraphicsBeginImageContextWithOptions(size, opaque, scale); 159 | CGContextRef context = UIGraphicsGetCurrentContext(); 160 | if (opaque && context) { 161 | CGContextSaveGState(context); { 162 | if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) { 163 | CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); 164 | CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale)); 165 | CGContextFillPath(context); 166 | } 167 | if (backgroundColor) { 168 | CGContextSetFillColorWithColor(context, backgroundColor); 169 | CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale)); 170 | CGContextFillPath(context); 171 | } 172 | } CGContextRestoreGState(context); 173 | CGColorRelease(backgroundColor); 174 | } 175 | task.display(context, size, isCancelled); 176 | if (isCancelled()) { 177 | UIGraphicsEndImageContext(); 178 | dispatch_async(dispatch_get_main_queue(), ^{ 179 | if (task.didDisplay) task.didDisplay(self, NO); 180 | }); 181 | return; 182 | } 183 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 184 | UIGraphicsEndImageContext(); 185 | if (isCancelled()) { 186 | dispatch_async(dispatch_get_main_queue(), ^{ 187 | if (task.didDisplay) task.didDisplay(self, NO); 188 | }); 189 | return; 190 | } 191 | dispatch_async(dispatch_get_main_queue(), ^{ 192 | if (isCancelled()) { 193 | if (task.didDisplay) task.didDisplay(self, NO); 194 | } else { 195 | self.contents = (__bridge id)(image.CGImage); 196 | if (task.didDisplay) task.didDisplay(self, YES); 197 | } 198 | }); 199 | }); 200 | } else { 201 | [_sentinel increase]; 202 | if (task.willDisplay) task.willDisplay(self); 203 | UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale); 204 | CGContextRef context = UIGraphicsGetCurrentContext(); 205 | if (self.opaque && context) { 206 | CGSize size = self.bounds.size; 207 | size.width *= self.contentsScale; 208 | size.height *= self.contentsScale; 209 | CGContextSaveGState(context); { 210 | if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) { 211 | CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); 212 | CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height)); 213 | CGContextFillPath(context); 214 | } 215 | if (self.backgroundColor) { 216 | CGContextSetFillColorWithColor(context, self.backgroundColor); 217 | CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height)); 218 | CGContextFillPath(context); 219 | } 220 | } CGContextRestoreGState(context); 221 | } 222 | task.display(context, self.bounds.size, ^{return NO;}); 223 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 224 | UIGraphicsEndImageContext(); 225 | self.contents = (__bridge id)(image.CGImage); 226 | if (task.didDisplay) task.didDisplay(self, YES); 227 | } 228 | } 229 | 230 | - (void)_cancelAsyncDisplay { 231 | [_sentinel increase]; 232 | } 233 | 234 | @end 235 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/YYTextTransaction.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextTransaction.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/18. 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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | YYTextTransaction let you perform a selector once before current runloop sleep. 18 | */ 19 | @interface YYTextTransaction : NSObject 20 | 21 | /** 22 | Creates and returns a transaction with a specified target and selector. 23 | 24 | @param target A specified target, the target is retained until runloop end. 25 | @param selector A selector for target. 26 | 27 | @return A new transaction, or nil if an error occurs. 28 | */ 29 | + (YYTextTransaction *)transactionWithTarget:(id)target selector:(SEL)selector; 30 | 31 | /** 32 | Commit the trancaction to main runloop. 33 | 34 | @discussion It will perform the selector on the target once before main runloop's 35 | current loop sleep. If the same transaction (same target and same selector) has 36 | already commit to runloop in this loop, this method do nothing. 37 | */ 38 | - (void)commit; 39 | 40 | @end 41 | 42 | NS_ASSUME_NONNULL_END 43 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/YYTextTransaction.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextTransaction.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/18. 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 | 12 | #import "YYTextTransaction.h" 13 | 14 | 15 | @interface YYTextTransaction() 16 | @property (nonatomic, strong) id target; 17 | @property (nonatomic, assign) SEL selector; 18 | @end 19 | 20 | static NSMutableSet *transactionSet = nil; 21 | 22 | static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { 23 | if (transactionSet.count == 0) return; 24 | NSSet *currentSet = transactionSet; 25 | transactionSet = [NSMutableSet new]; 26 | [currentSet enumerateObjectsUsingBlock:^(YYTextTransaction *transaction, BOOL *stop) { 27 | #pragma clang diagnostic push 28 | #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 29 | [transaction.target performSelector:transaction.selector]; 30 | #pragma clang diagnostic pop 31 | }]; 32 | } 33 | 34 | static void YYTextTransactionSetup() { 35 | static dispatch_once_t onceToken; 36 | dispatch_once(&onceToken, ^{ 37 | transactionSet = [NSMutableSet new]; 38 | CFRunLoopRef runloop = CFRunLoopGetMain(); 39 | CFRunLoopObserverRef observer; 40 | 41 | observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), 42 | kCFRunLoopBeforeWaiting | kCFRunLoopExit, 43 | true, // repeat 44 | 0xFFFFFF, // after CATransaction(2000000) 45 | YYRunLoopObserverCallBack, NULL); 46 | CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes); 47 | CFRelease(observer); 48 | }); 49 | } 50 | 51 | 52 | @implementation YYTextTransaction 53 | 54 | + (YYTextTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{ 55 | if (!target || !selector) return nil; 56 | YYTextTransaction *t = [YYTextTransaction new]; 57 | t.target = target; 58 | t.selector = selector; 59 | return t; 60 | } 61 | 62 | - (void)commit { 63 | if (!_target || !_selector) return; 64 | YYTextTransactionSetup(); 65 | [transactionSet addObject:self]; 66 | } 67 | 68 | - (NSUInteger)hash { 69 | long v1 = (long)((void *)_selector); 70 | long v2 = (long)_target; 71 | return v1 ^ v2; 72 | } 73 | 74 | - (BOOL)isEqual:(id)object { 75 | if (self == object) return YES; 76 | if (![object isMemberOfClass:self.class]) return NO; 77 | YYTextTransaction *other = object; 78 | return other.selector == _selector && other.target == _target; 79 | } 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/YYTextUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextUtilities.m 3 | // YYText 4 | // 5 | // Created by ibireme on 15/4/6. 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 | 12 | #import "YYTextUtilities.h" 13 | #import 14 | #import "UIView+YYText.h" 15 | 16 | NSCharacterSet *YYTextVerticalFormRotateCharacterSet() { 17 | static NSMutableCharacterSet *set; 18 | static dispatch_once_t onceToken; 19 | dispatch_once(&onceToken, ^{ 20 | set = [NSMutableCharacterSet new]; 21 | [set addCharactersInRange:NSMakeRange(0x1100, 256)]; // Hangul Jamo 22 | [set addCharactersInRange:NSMakeRange(0x2460, 160)]; // Enclosed Alphanumerics 23 | [set addCharactersInRange:NSMakeRange(0x2600, 256)]; // Miscellaneous Symbols 24 | [set addCharactersInRange:NSMakeRange(0x2700, 192)]; // Dingbats 25 | [set addCharactersInRange:NSMakeRange(0x2E80, 128)]; // CJK Radicals Supplement 26 | [set addCharactersInRange:NSMakeRange(0x2F00, 224)]; // Kangxi Radicals 27 | [set addCharactersInRange:NSMakeRange(0x2FF0, 16)]; // Ideographic Description Characters 28 | [set addCharactersInRange:NSMakeRange(0x3000, 64)]; // CJK Symbols and Punctuation 29 | [set removeCharactersInRange:NSMakeRange(0x3008, 10)]; 30 | [set removeCharactersInRange:NSMakeRange(0x3014, 12)]; 31 | [set addCharactersInRange:NSMakeRange(0x3040, 96)]; // Hiragana 32 | [set addCharactersInRange:NSMakeRange(0x30A0, 96)]; // Katakana 33 | [set addCharactersInRange:NSMakeRange(0x3100, 48)]; // Bopomofo 34 | [set addCharactersInRange:NSMakeRange(0x3130, 96)]; // Hangul Compatibility Jamo 35 | [set addCharactersInRange:NSMakeRange(0x3190, 16)]; // Kanbun 36 | [set addCharactersInRange:NSMakeRange(0x31A0, 32)]; // Bopomofo Extended 37 | [set addCharactersInRange:NSMakeRange(0x31C0, 48)]; // CJK Strokes 38 | [set addCharactersInRange:NSMakeRange(0x31F0, 16)]; // Katakana Phonetic Extensions 39 | [set addCharactersInRange:NSMakeRange(0x3200, 256)]; // Enclosed CJK Letters and Months 40 | [set addCharactersInRange:NSMakeRange(0x3300, 256)]; // CJK Compatibility 41 | [set addCharactersInRange:NSMakeRange(0x3400, 2582)]; // CJK Unified Ideographs Extension A 42 | [set addCharactersInRange:NSMakeRange(0x4E00, 20941)]; // CJK Unified Ideographs 43 | [set addCharactersInRange:NSMakeRange(0xAC00, 11172)]; // Hangul Syllables 44 | [set addCharactersInRange:NSMakeRange(0xD7B0, 80)]; // Hangul Jamo Extended-B 45 | [set addCharactersInString:@""]; // U+F8FF (Private Use Area) 46 | [set addCharactersInRange:NSMakeRange(0xF900, 512)]; // CJK Compatibility Ideographs 47 | [set addCharactersInRange:NSMakeRange(0xFE10, 16)]; // Vertical Forms 48 | [set addCharactersInRange:NSMakeRange(0xFF00, 240)]; // Halfwidth and Fullwidth Forms 49 | [set addCharactersInRange:NSMakeRange(0x1F200, 256)]; // Enclosed Ideographic Supplement 50 | [set addCharactersInRange:NSMakeRange(0x1F300, 768)]; // Enclosed Ideographic Supplement 51 | [set addCharactersInRange:NSMakeRange(0x1F600, 80)]; // Emoticons (Emoji) 52 | [set addCharactersInRange:NSMakeRange(0x1F680, 128)]; // Transport and Map Symbols 53 | 54 | // See http://unicode-table.com/ for more information. 55 | }); 56 | return set; 57 | } 58 | 59 | NSCharacterSet *YYTextVerticalFormRotateAndMoveCharacterSet() { 60 | static NSMutableCharacterSet *set; 61 | static dispatch_once_t onceToken; 62 | dispatch_once(&onceToken, ^{ 63 | set = [NSMutableCharacterSet new]; 64 | [set addCharactersInString:@",。、."]; 65 | }); 66 | return set; 67 | } 68 | 69 | // return 0 when succeed 70 | static int matrix_invert(__CLPK_integer N, double *matrix) { 71 | __CLPK_integer error = 0; 72 | __CLPK_integer pivot_tmp[6 * 6]; 73 | __CLPK_integer *pivot = pivot_tmp; 74 | double workspace_tmp[6 * 6]; 75 | double *workspace = workspace_tmp; 76 | bool need_free = false; 77 | 78 | if (N > 6) { 79 | need_free = true; 80 | pivot = malloc(N * N * sizeof(__CLPK_integer)); 81 | if (!pivot) return -1; 82 | workspace = malloc(N * sizeof(double)); 83 | if (!workspace) { 84 | free(pivot); 85 | return -1; 86 | } 87 | } 88 | 89 | dgetrf_(&N, &N, matrix, &N, pivot, &error); 90 | 91 | if (error == 0) { 92 | dgetri_(&N, matrix, &N, pivot, workspace, &N, &error); 93 | } 94 | 95 | if (need_free) { 96 | free(pivot); 97 | free(workspace); 98 | } 99 | return error; 100 | } 101 | 102 | CGAffineTransform YYTextCGAffineTransformGetFromPoints(CGPoint before[3], CGPoint after[3]) { 103 | if (before == NULL || after == NULL) return CGAffineTransformIdentity; 104 | 105 | CGPoint p1, p2, p3, q1, q2, q3; 106 | p1 = before[0]; p2 = before[1]; p3 = before[2]; 107 | q1 = after[0]; q2 = after[1]; q3 = after[2]; 108 | 109 | double A[36]; 110 | A[ 0] = p1.x; A[ 1] = p1.y; A[ 2] = 0; A[ 3] = 0; A[ 4] = 1; A[ 5] = 0; 111 | A[ 6] = 0; A[ 7] = 0; A[ 8] = p1.x; A[ 9] = p1.y; A[10] = 0; A[11] = 1; 112 | A[12] = p2.x; A[13] = p2.y; A[14] = 0; A[15] = 0; A[16] = 1; A[17] = 0; 113 | A[18] = 0; A[19] = 0; A[20] = p2.x; A[21] = p2.y; A[22] = 0; A[23] = 1; 114 | A[24] = p3.x; A[25] = p3.y; A[26] = 0; A[27] = 0; A[28] = 1; A[29] = 0; 115 | A[30] = 0; A[31] = 0; A[32] = p3.x; A[33] = p3.y; A[34] = 0; A[35] = 1; 116 | 117 | int error = matrix_invert(6, A); 118 | if (error) return CGAffineTransformIdentity; 119 | 120 | double B[6]; 121 | B[0] = q1.x; B[1] = q1.y; B[2] = q2.x; B[3] = q2.y; B[4] = q3.x; B[5] = q3.y; 122 | 123 | double M[6]; 124 | M[0] = A[ 0] * B[0] + A[ 1] * B[1] + A[ 2] * B[2] + A[ 3] * B[3] + A[ 4] * B[4] + A[ 5] * B[5]; 125 | M[1] = A[ 6] * B[0] + A[ 7] * B[1] + A[ 8] * B[2] + A[ 9] * B[3] + A[10] * B[4] + A[11] * B[5]; 126 | M[2] = A[12] * B[0] + A[13] * B[1] + A[14] * B[2] + A[15] * B[3] + A[16] * B[4] + A[17] * B[5]; 127 | M[3] = A[18] * B[0] + A[19] * B[1] + A[20] * B[2] + A[21] * B[3] + A[22] * B[4] + A[23] * B[5]; 128 | M[4] = A[24] * B[0] + A[25] * B[1] + A[26] * B[2] + A[27] * B[3] + A[28] * B[4] + A[29] * B[5]; 129 | M[5] = A[30] * B[0] + A[31] * B[1] + A[32] * B[2] + A[33] * B[3] + A[34] * B[4] + A[35] * B[5]; 130 | 131 | CGAffineTransform transform = CGAffineTransformMake(M[0], M[2], M[1], M[3], M[4], M[5]); 132 | return transform; 133 | } 134 | 135 | CGAffineTransform YYTextCGAffineTransformGetFromViews(UIView *from, UIView *to) { 136 | if (!from || !to) return CGAffineTransformIdentity; 137 | 138 | CGPoint before[3], after[3]; 139 | before[0] = CGPointMake(0, 0); 140 | before[1] = CGPointMake(0, 1); 141 | before[2] = CGPointMake(1, 0); 142 | after[0] = [from yy_convertPoint:before[0] toViewOrWindow:to]; 143 | after[1] = [from yy_convertPoint:before[1] toViewOrWindow:to]; 144 | after[2] = [from yy_convertPoint:before[2] toViewOrWindow:to]; 145 | 146 | return YYTextCGAffineTransformGetFromPoints(before, after); 147 | } 148 | 149 | UIViewContentMode YYTextCAGravityToUIViewContentMode(NSString *gravity) { 150 | static NSDictionary *dic; 151 | static dispatch_once_t onceToken; 152 | dispatch_once(&onceToken, ^{ 153 | dic = @{ kCAGravityCenter:@(UIViewContentModeCenter), 154 | kCAGravityTop:@(UIViewContentModeTop), 155 | kCAGravityBottom:@(UIViewContentModeBottom), 156 | kCAGravityLeft:@(UIViewContentModeLeft), 157 | kCAGravityRight:@(UIViewContentModeRight), 158 | kCAGravityTopLeft:@(UIViewContentModeTopLeft), 159 | kCAGravityTopRight:@(UIViewContentModeTopRight), 160 | kCAGravityBottomLeft:@(UIViewContentModeBottomLeft), 161 | kCAGravityBottomRight:@(UIViewContentModeBottomRight), 162 | kCAGravityResize:@(UIViewContentModeScaleToFill), 163 | kCAGravityResizeAspect:@(UIViewContentModeScaleAspectFit), 164 | kCAGravityResizeAspectFill:@(UIViewContentModeScaleAspectFill) }; 165 | }); 166 | if (!gravity) return UIViewContentModeScaleToFill; 167 | return (UIViewContentMode)((NSNumber *)dic[gravity]).integerValue; 168 | } 169 | 170 | NSString *YYTextUIViewContentModeToCAGravity(UIViewContentMode contentMode) { 171 | switch (contentMode) { 172 | case UIViewContentModeScaleToFill: return kCAGravityResize; 173 | case UIViewContentModeScaleAspectFit: return kCAGravityResizeAspect; 174 | case UIViewContentModeScaleAspectFill: return kCAGravityResizeAspectFill; 175 | case UIViewContentModeRedraw: return kCAGravityResize; 176 | case UIViewContentModeCenter: return kCAGravityCenter; 177 | case UIViewContentModeTop: return kCAGravityTop; 178 | case UIViewContentModeBottom: return kCAGravityBottom; 179 | case UIViewContentModeLeft: return kCAGravityLeft; 180 | case UIViewContentModeRight: return kCAGravityRight; 181 | case UIViewContentModeTopLeft: return kCAGravityTopLeft; 182 | case UIViewContentModeTopRight: return kCAGravityTopRight; 183 | case UIViewContentModeBottomLeft: return kCAGravityBottomLeft; 184 | case UIViewContentModeBottomRight: return kCAGravityBottomRight; 185 | default: return kCAGravityResize; 186 | } 187 | } 188 | 189 | CGRect YYTextCGRectFitWithContentMode(CGRect rect, CGSize size, UIViewContentMode mode) { 190 | rect = CGRectStandardize(rect); 191 | size.width = size.width < 0 ? -size.width : size.width; 192 | size.height = size.height < 0 ? -size.height : size.height; 193 | CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); 194 | switch (mode) { 195 | case UIViewContentModeScaleAspectFit: 196 | case UIViewContentModeScaleAspectFill: { 197 | if (rect.size.width < 0.01 || rect.size.height < 0.01 || 198 | size.width < 0.01 || size.height < 0.01) { 199 | rect.origin = center; 200 | rect.size = CGSizeZero; 201 | } else { 202 | CGFloat scale; 203 | if (mode == UIViewContentModeScaleAspectFit) { 204 | if (size.width / size.height < rect.size.width / rect.size.height) { 205 | scale = rect.size.height / size.height; 206 | } else { 207 | scale = rect.size.width / size.width; 208 | } 209 | } else { 210 | if (size.width / size.height < rect.size.width / rect.size.height) { 211 | scale = rect.size.width / size.width; 212 | } else { 213 | scale = rect.size.height / size.height; 214 | } 215 | } 216 | size.width *= scale; 217 | size.height *= scale; 218 | rect.size = size; 219 | rect.origin = CGPointMake(center.x - size.width * 0.5, center.y - size.height * 0.5); 220 | } 221 | } break; 222 | case UIViewContentModeCenter: { 223 | rect.size = size; 224 | rect.origin = CGPointMake(center.x - size.width * 0.5, center.y - size.height * 0.5); 225 | } break; 226 | case UIViewContentModeTop: { 227 | rect.origin.x = center.x - size.width * 0.5; 228 | rect.size = size; 229 | } break; 230 | case UIViewContentModeBottom: { 231 | rect.origin.x = center.x - size.width * 0.5; 232 | rect.origin.y += rect.size.height - size.height; 233 | rect.size = size; 234 | } break; 235 | case UIViewContentModeLeft: { 236 | rect.origin.y = center.y - size.height * 0.5; 237 | rect.size = size; 238 | } break; 239 | case UIViewContentModeRight: { 240 | rect.origin.y = center.y - size.height * 0.5; 241 | rect.origin.x += rect.size.width - size.width; 242 | rect.size = size; 243 | } break; 244 | case UIViewContentModeTopLeft: { 245 | rect.size = size; 246 | } break; 247 | case UIViewContentModeTopRight: { 248 | rect.origin.x += rect.size.width - size.width; 249 | rect.size = size; 250 | } break; 251 | case UIViewContentModeBottomLeft: { 252 | rect.origin.y += rect.size.height - size.height; 253 | rect.size = size; 254 | } break; 255 | case UIViewContentModeBottomRight: { 256 | rect.origin.x += rect.size.width - size.width; 257 | rect.origin.y += rect.size.height - size.height; 258 | rect.size = size; 259 | } break; 260 | case UIViewContentModeScaleToFill: 261 | case UIViewContentModeRedraw: 262 | default: { 263 | rect = rect; 264 | } 265 | } 266 | return rect; 267 | } 268 | 269 | CGFloat YYTextScreenScale() { 270 | static CGFloat scale; 271 | static dispatch_once_t onceToken; 272 | dispatch_once(&onceToken, ^{ 273 | scale = [UIScreen mainScreen].scale; 274 | }); 275 | return scale; 276 | } 277 | 278 | CGSize YYTextScreenSize() { 279 | static CGSize size; 280 | static dispatch_once_t onceToken; 281 | dispatch_once(&onceToken, ^{ 282 | size = [UIScreen mainScreen].bounds.size; 283 | if (size.height < size.width) { 284 | CGFloat tmp = size.height; 285 | size.height = size.width; 286 | size.width = tmp; 287 | } 288 | }); 289 | return size; 290 | } 291 | 292 | 293 | BOOL YYTextIsAppExtension() { 294 | static BOOL isAppExtension = NO; 295 | static dispatch_once_t onceToken; 296 | dispatch_once(&onceToken, ^{ 297 | Class cls = NSClassFromString(@"UIApplication"); 298 | if(!cls || ![cls respondsToSelector:@selector(sharedApplication)]) isAppExtension = YES; 299 | if ([[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]) isAppExtension = YES; 300 | }); 301 | return isAppExtension; 302 | } 303 | 304 | UIApplication *YYTextSharedApplication() { 305 | #pragma clang diagnostic push 306 | #pragma clang diagnostic ignored "-Wundeclared-selector" 307 | return YYTextIsAppExtension() ? nil : [UIApplication performSelector:@selector(sharedApplication)]; 308 | #pragma clang diagnostic pop 309 | } 310 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/YYTextWeakProxy.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextWeakProxy.h 3 | // YYText 4 | // 5 | // Created by ibireme on 14/10/18. 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 | 12 | #import 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | A proxy used to hold a weak object. 18 | It can be used to avoid retain cycles, such as the target in NSTimer or CADisplayLink. 19 | 20 | sample code: 21 | 22 | @implementation MyView { 23 | NSTimer *_timer; 24 | } 25 | 26 | - (void)initTimer { 27 | YYTextWeakProxy *proxy = [YYTextWeakProxy proxyWithTarget:self]; 28 | _timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES]; 29 | } 30 | 31 | - (void)tick:(NSTimer *)timer {...} 32 | @end 33 | */ 34 | @interface YYTextWeakProxy : NSProxy 35 | 36 | /** 37 | The proxy target. 38 | */ 39 | @property (nullable, nonatomic, weak, readonly) id target; 40 | 41 | /** 42 | Creates a new weak proxy for target. 43 | 44 | @param target Target object. 45 | 46 | @return A new proxy object. 47 | */ 48 | - (instancetype)initWithTarget:(id)target; 49 | 50 | /** 51 | Creates a new weak proxy for target. 52 | 53 | @param target Target object. 54 | 55 | @return A new proxy object. 56 | */ 57 | + (instancetype)proxyWithTarget:(id)target; 58 | 59 | @end 60 | 61 | NS_ASSUME_NONNULL_END 62 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/Utility/YYTextWeakProxy.m: -------------------------------------------------------------------------------- 1 | // 2 | // YYTextWeakProxy.m 3 | // YYText 4 | // 5 | // Created by ibireme on 14/10/18. 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 | 12 | #import "YYTextWeakProxy.h" 13 | 14 | 15 | @implementation YYTextWeakProxy 16 | 17 | - (instancetype)initWithTarget:(id)target { 18 | _target = target; 19 | return self; 20 | } 21 | 22 | + (instancetype)proxyWithTarget:(id)target { 23 | return [[YYTextWeakProxy alloc] initWithTarget:target]; 24 | } 25 | 26 | - (id)forwardingTargetForSelector:(SEL)selector { 27 | return _target; 28 | } 29 | 30 | - (void)forwardInvocation:(NSInvocation *)invocation { 31 | void *null = NULL; 32 | [invocation setReturnValue:&null]; 33 | } 34 | 35 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { 36 | return [NSObject instanceMethodSignatureForSelector:@selector(init)]; 37 | } 38 | 39 | - (BOOL)respondsToSelector:(SEL)aSelector { 40 | return [_target respondsToSelector:aSelector]; 41 | } 42 | 43 | - (BOOL)isEqual:(id)object { 44 | return [_target isEqual:object]; 45 | } 46 | 47 | - (NSUInteger)hash { 48 | return [_target hash]; 49 | } 50 | 51 | - (Class)superclass { 52 | return [_target superclass]; 53 | } 54 | 55 | - (Class)class { 56 | return [_target class]; 57 | } 58 | 59 | - (BOOL)isKindOfClass:(Class)aClass { 60 | return [_target isKindOfClass:aClass]; 61 | } 62 | 63 | - (BOOL)isMemberOfClass:(Class)aClass { 64 | return [_target isMemberOfClass:aClass]; 65 | } 66 | 67 | - (BOOL)conformsToProtocol:(Protocol *)aProtocol { 68 | return [_target conformsToProtocol:aProtocol]; 69 | } 70 | 71 | - (BOOL)isProxy { 72 | return YES; 73 | } 74 | 75 | - (NSString *)description { 76 | return [_target description]; 77 | } 78 | 79 | - (NSString *)debugDescription { 80 | return [_target debugDescription]; 81 | } 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/YYLabel.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYLabel.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/2/25. 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 | 12 | #import 13 | 14 | #if __has_include() 15 | #import 16 | #import 17 | #import 18 | #else 19 | #import "YYTextParser.h" 20 | #import "YYTextLayout.h" 21 | #import "YYTextAttribute.h" 22 | #endif 23 | 24 | NS_ASSUME_NONNULL_BEGIN 25 | 26 | #if !TARGET_INTERFACE_BUILDER 27 | 28 | /** 29 | The YYLabel class implements a read-only text view. 30 | 31 | @discussion The API and behavior is similar to UILabel, but provides more features: 32 | 33 | * It supports asynchronous layout and rendering (to avoid blocking UI thread). 34 | * It extends the CoreText attributes to support more text effects. 35 | * It allows to add UIImage, UIView and CALayer as text attachments. 36 | * It allows to add 'highlight' link to some range of text to allow user interact with. 37 | * It allows to add container path and exclusion paths to control text container's shape. 38 | * It supports vertical form layout to display CJK text. 39 | 40 | See NSAttributedString+YYText.h for more convenience methods to set the attributes. 41 | See YYTextAttribute.h and YYTextLayout.h for more information. 42 | */ 43 | @interface YYLabel : UIView 44 | 45 | #pragma mark - Accessing the Text Attributes 46 | ///============================================================================= 47 | /// @name Accessing the Text Attributes 48 | ///============================================================================= 49 | 50 | /** 51 | The text displayed by the label. Default is nil. 52 | Set a new value to this property also replaces the text in `attributedText`. 53 | Get the value returns the plain text in `attributedText`. 54 | */ 55 | @property (nullable, nonatomic, copy) NSString *text; 56 | 57 | /** 58 | The font of the text. Default is 17-point system font. 59 | Set a new value to this property also causes the new font to be applied to the entire `attributedText`. 60 | Get the value returns the font at the head of `attributedText`. 61 | */ 62 | @property (null_resettable, nonatomic, strong) UIFont *font; 63 | 64 | /** 65 | The color of the text. Default is black. 66 | Set a new value to this property also causes the new color to be applied to the entire `attributedText`. 67 | Get the value returns the color at the head of `attributedText`. 68 | */ 69 | @property (null_resettable, nonatomic, strong) UIColor *textColor; 70 | 71 | /** 72 | The shadow color of the text. Default is nil. 73 | Set a new value to this property also causes the shadow color to be applied to the entire `attributedText`. 74 | Get the value returns the shadow color at the head of `attributedText`. 75 | */ 76 | @property (nullable, nonatomic, strong) UIColor *shadowColor; 77 | 78 | /** 79 | The shadow offset of the text. Default is CGSizeZero. 80 | Set a new value to this property also causes the shadow offset to be applied to the entire `attributedText`. 81 | Get the value returns the shadow offset at the head of `attributedText`. 82 | */ 83 | @property (nonatomic) CGSize shadowOffset; 84 | 85 | /** 86 | The shadow blur of the text. Default is 0. 87 | Set a new value to this property also causes the shadow blur to be applied to the entire `attributedText`. 88 | Get the value returns the shadow blur at the head of `attributedText`. 89 | */ 90 | @property (nonatomic) CGFloat shadowBlurRadius; 91 | 92 | /** 93 | The technique to use for aligning the text. Default is NSTextAlignmentNatural. 94 | Set a new value to this property also causes the new alignment to be applied to the entire `attributedText`. 95 | Get the value returns the alignment at the head of `attributedText`. 96 | */ 97 | @property (nonatomic) NSTextAlignment textAlignment; 98 | 99 | /** 100 | The text vertical aligmnent in container. Default is YYTextVerticalAlignmentCenter. 101 | */ 102 | @property (nonatomic) YYTextVerticalAlignment textVerticalAlignment; 103 | 104 | /** 105 | The styled text displayed by the label. 106 | Set a new value to this property also replaces the value of the `text`, `font`, `textColor`, 107 | `textAlignment` and other properties in label. 108 | 109 | @discussion It only support the attributes declared in CoreText and YYTextAttribute. 110 | See `NSAttributedString+YYText` for more convenience methods to set the attributes. 111 | */ 112 | @property (nullable, nonatomic, copy) NSAttributedString *attributedText; 113 | 114 | /** 115 | The technique to use for wrapping and truncating the label's text. 116 | Default is NSLineBreakByTruncatingTail. 117 | */ 118 | @property (nonatomic) NSLineBreakMode lineBreakMode; 119 | 120 | /** 121 | The truncation token string used when text is truncated. Default is nil. 122 | When the value is nil, the label use "…" as default truncation token. 123 | */ 124 | @property (nullable, nonatomic, copy) NSAttributedString *truncationToken; 125 | 126 | /** 127 | The maximum number of lines to use for rendering text. Default value is 1. 128 | 0 means no limit. 129 | */ 130 | @property (nonatomic) NSUInteger numberOfLines; 131 | 132 | /** 133 | When `text` or `attributedText` is changed, the parser will be called to modify the text. 134 | It can be used to add code highlighting or emoticon replacement to text view. 135 | The default value is nil. 136 | 137 | See `YYTextParser` protocol for more information. 138 | */ 139 | @property (nullable, nonatomic, strong) id textParser; 140 | 141 | /** 142 | The current text layout in text view. It can be used to query the text layout information. 143 | Set a new value to this property also replaces most properties in this label, such as `text`, 144 | `color`, `attributedText`, `lineBreakMode`, `textContainerPath`, `exclusionPaths` and so on. 145 | */ 146 | @property (nullable, nonatomic, strong) YYTextLayout *textLayout; 147 | 148 | 149 | #pragma mark - Configuring the Text Container 150 | ///============================================================================= 151 | /// @name Configuring the Text Container 152 | ///============================================================================= 153 | 154 | /** 155 | A UIBezierPath object that specifies the shape of the text frame. Default value is nil. 156 | */ 157 | @property (nullable, nonatomic, copy) UIBezierPath *textContainerPath; 158 | 159 | /** 160 | An array of UIBezierPath objects representing the exclusion paths inside the 161 | receiver's bounding rectangle. Default value is nil. 162 | */ 163 | @property (nullable, nonatomic, copy) NSArray *exclusionPaths; 164 | 165 | /** 166 | The inset of the text container's layout area within the text view's content area. 167 | Default value is UIEdgeInsetsZero. 168 | */ 169 | @property (nonatomic) UIEdgeInsets textContainerInset; 170 | 171 | /** 172 | Whether the receiver's layout orientation is vertical form. Default is NO. 173 | It may used to display CJK text. 174 | */ 175 | @property (nonatomic, getter=isVerticalForm) BOOL verticalForm; 176 | 177 | /** 178 | The text line position modifier used to modify the lines' position in layout. 179 | Default value is nil. 180 | See `YYTextLinePositionModifier` protocol for more information. 181 | */ 182 | @property (nullable, nonatomic, copy) id linePositionModifier; 183 | 184 | /** 185 | The debug option to display CoreText layout result. 186 | The default value is [YYTextDebugOption sharedDebugOption]. 187 | */ 188 | @property (nullable, nonatomic, copy) YYTextDebugOption *debugOption; 189 | 190 | 191 | #pragma mark - Getting the Layout Constraints 192 | ///============================================================================= 193 | /// @name Getting the Layout Constraints 194 | ///============================================================================= 195 | 196 | /** 197 | The preferred maximum width (in points) for a multiline label. 198 | 199 | @discussion This property affects the size of the label when layout constraints 200 | are applied to it. During layout, if the text extends beyond the width 201 | specified by this property, the additional text is flowed to one or more new 202 | lines, thereby increasing the height of the label. If the text is vertical 203 | form, this value will match to text height. 204 | */ 205 | @property (nonatomic) CGFloat preferredMaxLayoutWidth; 206 | 207 | 208 | #pragma mark - Interacting with Text Data 209 | ///============================================================================= 210 | /// @name Interacting with Text Data 211 | ///============================================================================= 212 | 213 | /** 214 | When user tap the label, this action will be called (similar to tap gesture). 215 | The default value is nil. 216 | */ 217 | @property (nullable, nonatomic, copy) YYTextAction textTapAction; 218 | 219 | /** 220 | When user long press the label, this action will be called (similar to long press gesture). 221 | The default value is nil. 222 | */ 223 | @property (nullable, nonatomic, copy) YYTextAction textLongPressAction; 224 | 225 | /** 226 | When user tap the highlight range of text, this action will be called. 227 | The default value is nil. 228 | */ 229 | @property (nullable, nonatomic, copy) YYTextAction highlightTapAction; 230 | 231 | /** 232 | When user long press the highlight range of text, this action will be called. 233 | The default value is nil. 234 | */ 235 | @property (nullable, nonatomic, copy) YYTextAction highlightLongPressAction; 236 | 237 | 238 | #pragma mark - Configuring the Display Mode 239 | ///============================================================================= 240 | /// @name Configuring the Display Mode 241 | ///============================================================================= 242 | 243 | /** 244 | A Boolean value indicating whether the layout and rendering codes are running 245 | asynchronously on background threads. 246 | 247 | The default value is `NO`. 248 | */ 249 | @property (nonatomic) BOOL displaysAsynchronously; 250 | 251 | /** 252 | If the value is YES, and the layer is rendered asynchronously, then it will 253 | set label.layer.contents to nil before display. 254 | 255 | The default value is `YES`. 256 | 257 | @discussion When the asynchronously display is enabled, the layer's content will 258 | be updated after the background render process finished. If the render process 259 | can not finished in a vsync time (1/60 second), the old content will be still kept 260 | for display. You may manually clear the content by set the layer.contents to nil 261 | after you update the label's properties, or you can just set this property to YES. 262 | */ 263 | @property (nonatomic) BOOL clearContentsBeforeAsynchronouslyDisplay; 264 | 265 | /** 266 | If the value is YES, and the layer is rendered asynchronously, then it will add 267 | a fade animation on layer when the contents of layer changed. 268 | 269 | The default value is `YES`. 270 | */ 271 | @property (nonatomic) BOOL fadeOnAsynchronouslyDisplay; 272 | 273 | /** 274 | If the value is YES, then it will add a fade animation on layer when some range 275 | of text become highlighted. 276 | 277 | The default value is `YES`. 278 | */ 279 | @property (nonatomic) BOOL fadeOnHighlight; 280 | 281 | /** 282 | Ignore common properties (such as text, font, textColor, attributedText...) and 283 | only use "textLayout" to display content. 284 | 285 | The default value is `NO`. 286 | 287 | @discussion If you control the label content only through "textLayout", then 288 | you may set this value to YES for higher performance. 289 | */ 290 | @property (nonatomic) BOOL ignoreCommonProperties; 291 | 292 | /* 293 | Tips: 294 | 295 | 1. If you only need a UILabel alternative to display rich text and receive link touch event, 296 | you do not need to adjust the display mode properties. 297 | 298 | 2. If you have performance issues, you may enable the asynchronous display mode 299 | by setting the `displaysAsynchronously` to YES. 300 | 301 | 3. If you want to get the highest performance, you should do text layout with 302 | `YYTextLayout` class in background thread. Here's an example: 303 | 304 | YYLabel *label = [YYLabel new]; 305 | label.displaysAsynchronously = YES; 306 | label.ignoreCommonProperties = YES; 307 | 308 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 309 | 310 | // Create attributed string. 311 | NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text"]; 312 | text.yy_font = [UIFont systemFontOfSize:16]; 313 | text.yy_color = [UIColor grayColor]; 314 | [text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)]; 315 | 316 | // Create text container 317 | YYTextContainer *container = [YYTextContainer new]; 318 | container.size = CGSizeMake(100, CGFLOAT_MAX); 319 | container.maximumNumberOfRows = 0; 320 | 321 | // Generate a text layout. 322 | YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:text]; 323 | 324 | dispatch_async(dispatch_get_main_queue(), ^{ 325 | label.size = layout.textBoundingSize; 326 | label.textLayout = layout; 327 | }); 328 | }); 329 | 330 | */ 331 | 332 | @end 333 | 334 | 335 | #else // TARGET_INTERFACE_BUILDER 336 | IB_DESIGNABLE 337 | @interface YYLabel : UIView 338 | @property (nullable, nonatomic, copy) IBInspectable NSString *text; 339 | @property (null_resettable, nonatomic, strong) IBInspectable UIColor *textColor; 340 | @property (nullable, nonatomic, strong) IBInspectable NSString *fontName_; 341 | @property (nonatomic) IBInspectable CGFloat fontSize_; 342 | @property (nonatomic) IBInspectable BOOL fontIsBold_; 343 | @property (nonatomic) IBInspectable NSUInteger numberOfLines; 344 | @property (nonatomic) IBInspectable NSInteger lineBreakMode; 345 | @property (nonatomic) IBInspectable CGFloat preferredMaxLayoutWidth; 346 | @property (nonatomic, getter=isVerticalForm) IBInspectable BOOL verticalForm; 347 | @property (nonatomic) IBInspectable NSInteger textAlignment; 348 | @property (nonatomic) IBInspectable NSInteger textVerticalAlignment; 349 | @property (nullable, nonatomic, strong) IBInspectable UIColor *shadowColor; 350 | @property (nonatomic) IBInspectable CGPoint shadowOffset; 351 | @property (nonatomic) IBInspectable CGFloat shadowBlurRadius; 352 | @property (nullable, nonatomic, copy) IBInspectable NSAttributedString *attributedText; 353 | @property (nonatomic) IBInspectable CGFloat insetTop_; 354 | @property (nonatomic) IBInspectable CGFloat insetBottom_; 355 | @property (nonatomic) IBInspectable CGFloat insetLeft_; 356 | @property (nonatomic) IBInspectable CGFloat insetRight_; 357 | @property (nonatomic) IBInspectable BOOL debugEnabled_; 358 | 359 | @property (null_resettable, nonatomic, strong) UIFont *font; 360 | @property (nullable, nonatomic, copy) NSAttributedString *truncationToken; 361 | @property (nullable, nonatomic, strong) id textParser; 362 | @property (nullable, nonatomic, strong) YYTextLayout *textLayout; 363 | @property (nullable, nonatomic, copy) UIBezierPath *textContainerPath; 364 | @property (nullable, nonatomic, copy) NSArray *exclusionPaths; 365 | @property (nonatomic) UIEdgeInsets textContainerInset; 366 | @property (nullable, nonatomic, copy) id linePositionModifier; 367 | @property (nonnull, nonatomic, copy) YYTextDebugOption *debugOption; 368 | @property (nullable, nonatomic, copy) YYTextAction textTapAction; 369 | @property (nullable, nonatomic, copy) YYTextAction textLongPressAction; 370 | @property (nullable, nonatomic, copy) YYTextAction highlightTapAction; 371 | @property (nullable, nonatomic, copy) YYTextAction highlightLongPressAction; 372 | @property (nonatomic) BOOL displaysAsynchronously; 373 | @property (nonatomic) BOOL clearContentsBeforeAsynchronouslyDisplay; 374 | @property (nonatomic) BOOL fadeOnAsynchronouslyDisplay; 375 | @property (nonatomic) BOOL fadeOnHighlight; 376 | @property (nonatomic) BOOL ignoreCommonProperties; 377 | @end 378 | #endif // !TARGET_INTERFACE_BUILDER 379 | 380 | NS_ASSUME_NONNULL_END 381 | -------------------------------------------------------------------------------- /FoldLabelDemo/YYText/YYText.h: -------------------------------------------------------------------------------- 1 | // 2 | // YYText.h 3 | // YYText 4 | // 5 | // Created by ibireme on 15/2/25. 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 | 12 | #import 13 | 14 | #if __has_include() 15 | FOUNDATION_EXPORT double YYTextVersionNumber; 16 | FOUNDATION_EXPORT const unsigned char YYTextVersionString[]; 17 | #import 18 | #import 19 | #import 20 | #import 21 | #import 22 | #import 23 | #import 24 | #import 25 | #import 26 | #import 27 | #import 28 | #import 29 | #import 30 | #import 31 | #import 32 | #import 33 | #else 34 | #import "YYLabel.h" 35 | #import "YYTextView.h" 36 | #import "YYTextAttribute.h" 37 | #import "YYTextArchiver.h" 38 | #import "YYTextParser.h" 39 | #import "YYTextRunDelegate.h" 40 | #import "YYTextRubyAnnotation.h" 41 | #import "YYTextLayout.h" 42 | #import "YYTextLine.h" 43 | #import "YYTextInput.h" 44 | #import "YYTextDebugOption.h" 45 | #import "YYTextKeyboardManager.h" 46 | #import "YYTextUtilities.h" 47 | #import "NSAttributedString+YYText.h" 48 | #import "NSParagraphStyle+YYText.h" 49 | #import "UIPasteboard+YYText.h" 50 | #endif 51 | -------------------------------------------------------------------------------- /FoldLabelDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // FoldLabelDemo 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /FoldLabelDemoTests/FoldLabelDemoTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // FoldLabelDemoTests.m 3 | // FoldLabelDemoTests 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface FoldLabelDemoTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation FoldLabelDemoTests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | - (void)tearDown { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | - (void)testExample { 26 | // This is an example of a functional test case. 27 | // Use XCTAssert and related functions to verify your tests produce the correct results. 28 | } 29 | 30 | - (void)testPerformanceExample { 31 | // This is an example of a performance test case. 32 | [self measureBlock:^{ 33 | // Put the code you want to measure the time of here. 34 | }]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /FoldLabelDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /FoldLabelDemoUITests/FoldLabelDemoUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // FoldLabelDemoUITests.m 3 | // FoldLabelDemoUITests 4 | // 5 | // Created by Lee on 2019/9/1. 6 | // Copyright © 2019 Lee. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface FoldLabelDemoUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation FoldLabelDemoUITests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | 20 | // In UI tests it is usually best to stop immediately when a failure occurs. 21 | self.continueAfterFailure = NO; 22 | 23 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 24 | [[[XCUIApplication alloc] init] launch]; 25 | 26 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 27 | } 28 | 29 | - (void)tearDown { 30 | // Put teardown code here. This method is called after the invocation of each test method in the class. 31 | } 32 | 33 | - (void)testExample { 34 | // Use recording to get started writing UI tests. 35 | // Use XCTAssert and related functions to verify your tests produce the correct results. 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /FoldLabelDemoUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FoldLabelDemo 2 | 仿微信朋友圈UITableViewCell中文字展开收起功能 3 | 最近做社交APP涉及到一个功能,用户发表长文字后显示一定行数,超出一定行数后显示“更多”按钮,同时用户点击后展开文字,再点击收起内容;参考一下朋友圈的交互效果做了个demo,其中关键在于UILabel行数的计算,考虑到不同文字占用字符数不同,并且包含表情等,使用CoreText计算更为合适; 4 | ![屏幕录制 2019-09-06 上午12.11.19.2019-09-06 00_21_03.gif](https://upload-images.jianshu.io/upload_images/1506056-d30c252d8e4003dd.gif?imageMogr2/auto-orient/strip) 5 | 6 | 核心思路是通过coreText计算UILabel当前显示文字所需要的行数,如果大于最大行数就设置numberOfLines; 7 | --------------------------------------------------------------------------------