├── .gitignore
├── AWRichText.podspec
├── AWRichText.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ └── contents.xcworkspacedata
├── AWRichText
├── AppDelegate.h
├── AppDelegate.m
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
├── ViewController.h
├── ViewController.m
└── main.m
├── Demo
├── Resources
│ ├── bangbangda.gif
│ ├── emoji
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ ├── 6.png
│ │ ├── 7.png
│ │ ├── 8.png
│ │ ├── 9.png
│ │ ├── gaolou.png
│ │ ├── guduo.png
│ │ ├── hetang.png
│ │ ├── hua.png
│ │ ├── meiren.png
│ │ ├── mingzhu.png
│ │ ├── shui.png
│ │ ├── tiaowu.png
│ │ ├── wa.gif
│ │ ├── xingxing.png
│ │ └── yezi.png
│ ├── familyBg.png
│ ├── familyLevel.png
│ ├── femal.png
│ ├── fengtimo.jpg
│ ├── level.png
│ ├── qishi.png
│ ├── roomadmin.png
│ ├── vip.png
│ ├── yufan.gif
│ ├── zan.gif
│ └── zijue.png
├── TestView.h
├── TestView.m
└── chat
│ ├── ChatView.h
│ ├── ChatView.m
│ ├── FakeChatModel.h
│ ├── FakeChatModel.m
│ └── tool
│ ├── AWDemoTool.h
│ ├── AWDemoTool.m
│ ├── UIImage+AWGif.h
│ └── UIImage+AWGif.m
├── LICENSE
├── README.md
└── RichText
├── AWRichText.h
├── AWRichText.m
├── AWRichTextLabel.h
├── AWRichTextLabel.m
├── components
├── AWRTAttachmentComponent.h
├── AWRTAttachmentComponent.m
├── AWRTComponent.h
├── AWRTComponent.m
├── AWRTImageComponent.h
├── AWRTImageComponent.m
├── AWRTTextComponent.h
├── AWRTTextComponent.m
├── AWRTViewComponent.h
└── AWRTViewComponent.m
└── tool
├── AWRTWeekRefrence.h
├── AWRTWeekRefrence.m
├── AWSimpleKVO.h
└── AWSimpleKVO.m
/.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 | *.xcuserstate
23 |
24 | ## Obj-C/Swift specific
25 | *.hmap
26 | *.ipa
27 | *.dSYM.zip
28 | *.dSYM
29 |
30 | # CocoaPods
31 | #
32 | # We recommend against adding the Pods directory to your .gitignore. However
33 | # you should judge for yourself, the pros and cons are mentioned at:
34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
35 | #
36 | # Pods/
37 |
38 | # Carthage
39 | #
40 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
41 | # Carthage/Checkouts
42 |
43 | Carthage/Build
44 |
45 | # fastlane
46 | #
47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
48 | # screenshots whenever they are needed.
49 | # For more information about the recommended setup visit:
50 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
51 |
52 | fastlane/report.xml
53 | fastlane/screenshots
54 |
55 | #Code Injection
56 | #
57 | # After new code Injection tools there's a generated folder /iOSInjectionProject
58 | # https://github.com/johnno1962/injectionforxcode
59 |
60 | iOSInjectionProject/
61 |
62 | .DS_Store
63 |
--------------------------------------------------------------------------------
/AWRichText.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "AWRichText"
3 | s.version = "1.0.1"
4 | s.summary = "基于CoreText,面向对象,极简,易用,高效,并不仅仅局限于图文混排的富文本排版神器。"
5 | s.description = <<-DESC
6 | 解决NSAttributedString的如下问题:
7 | 1. 太难用了,属性那么多,而且使用字典构造,每次用都要查一下文档。更不要说大规模使用了。
8 | 2. 不支持GIF动图
9 | 3. 不支持局部点击
10 | 4. 不支持UIView与文字进行混排
11 | DESC
12 | s.homepage = "https://github.com/hardman/AWRichText.git"
13 | s.screenshots = "https://raw.githubusercontent.com/hardman/OutLinkImages/master/AWRichText/AWRichText-demo.gif"
14 | s.license = "MIT"
15 | s.author = "wanghongyu"
16 | s.platform = :ios, "8.0"
17 | s.source = { :git => "https://github.com/hardman/AWRichText.git", :tag => "#{s.version}" }
18 | s.source_files = "RichText", "RichText/**/*.{h,m}"
19 | s.public_header_files = "RichText/**/*.h"
20 | end
21 |
--------------------------------------------------------------------------------
/AWRichText.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/AWRichText/AppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.h
3 | // AWRichText
4 | //
5 | // Created by kaso on 30/10/17.
6 | // Copyright © 2017年 airwind. 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 |
--------------------------------------------------------------------------------
/AWRichText/AppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.m
3 | // AWRichText
4 | //
5 | // Created by kaso on 30/10/17.
6 | // Copyright © 2017年 airwind. 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 |
--------------------------------------------------------------------------------
/AWRichText/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 | }
--------------------------------------------------------------------------------
/AWRichText/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 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/AWRichText/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 |
--------------------------------------------------------------------------------
/AWRichText/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 |
--------------------------------------------------------------------------------
/AWRichText/ViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.h
3 | // AWRichText
4 | //
5 | // Created by kaso on 30/10/17.
6 | // Copyright © 2017年 airwind. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface ViewController : UIViewController
12 |
13 |
14 | @end
15 |
16 |
--------------------------------------------------------------------------------
/AWRichText/ViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.m
3 | // AWRichText
4 | //
5 | // Created by kaso on 30/10/17.
6 | // Copyright © 2017年 airwind. All rights reserved.
7 | //
8 |
9 | #import "ViewController.h"
10 |
11 | #import "TestView.h"
12 |
13 | @interface ViewController ()
14 | @end
15 |
16 | @implementation ViewController
17 |
18 | - (void)viewDidLoad {
19 | [super viewDidLoad];
20 | [TestView testWithSuperView:self.view];
21 | }
22 |
23 | -(BOOL)shouldAutorotate{
24 | return NO;
25 | }
26 |
27 | - (void)didReceiveMemoryWarning {
28 | [super didReceiveMemoryWarning];
29 | // Dispose of any resources that can be recreated.
30 | }
31 |
32 | @end
33 |
--------------------------------------------------------------------------------
/AWRichText/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // AWRichText
4 | //
5 | // Created by kaso on 30/10/17.
6 | // Copyright © 2017年 airwind. 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 |
--------------------------------------------------------------------------------
/Demo/Resources/bangbangda.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/bangbangda.gif
--------------------------------------------------------------------------------
/Demo/Resources/emoji/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/1.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/2.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/3.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/4.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/5.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/6.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/7.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/8.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/9.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/gaolou.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/gaolou.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/guduo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/guduo.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/hetang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/hetang.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/hua.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/hua.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/meiren.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/meiren.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/mingzhu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/mingzhu.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/shui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/shui.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/tiaowu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/tiaowu.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/wa.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/wa.gif
--------------------------------------------------------------------------------
/Demo/Resources/emoji/xingxing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/xingxing.png
--------------------------------------------------------------------------------
/Demo/Resources/emoji/yezi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/emoji/yezi.png
--------------------------------------------------------------------------------
/Demo/Resources/familyBg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/familyBg.png
--------------------------------------------------------------------------------
/Demo/Resources/familyLevel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/familyLevel.png
--------------------------------------------------------------------------------
/Demo/Resources/femal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/femal.png
--------------------------------------------------------------------------------
/Demo/Resources/fengtimo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/fengtimo.jpg
--------------------------------------------------------------------------------
/Demo/Resources/level.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/level.png
--------------------------------------------------------------------------------
/Demo/Resources/qishi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/qishi.png
--------------------------------------------------------------------------------
/Demo/Resources/roomadmin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/roomadmin.png
--------------------------------------------------------------------------------
/Demo/Resources/vip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/vip.png
--------------------------------------------------------------------------------
/Demo/Resources/yufan.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/yufan.gif
--------------------------------------------------------------------------------
/Demo/Resources/zan.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/zan.gif
--------------------------------------------------------------------------------
/Demo/Resources/zijue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hardman/AWRichText/d759705a221c75be95b5480319a585f596b7beaa/Demo/Resources/zijue.png
--------------------------------------------------------------------------------
/Demo/TestView.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 |
9 | @interface TestView : UIView
10 |
11 | +(void) testWithSuperView:(UIView *)superView;
12 | @end
13 |
--------------------------------------------------------------------------------
/Demo/TestView.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "TestView.h"
8 |
9 | #import "AWRichText.h"
10 |
11 | #import "AWDemoTool.h"
12 |
13 | #import "UIImage+AWGif.h"
14 |
15 | #import "ChatView.h"
16 |
17 | ///是否打开DebugFrame
18 | static BOOL sAWTestAlwaysShowDebugFrame = NO;
19 |
20 | #pragma mark - 面向对象的富文本排版,远离令人头疼的NSAttributedString
21 |
22 | #pragma mark - 长文本图文混排cell 演示诸多基本功能(自定义UIView,精确点击,mode切换)
23 | /// 展示功能:
24 | /// 富文本创建
25 | /// 文本样式
26 | /// 图片样式
27 | /// 自定义UIView样式
28 | /// gif样式
29 | /// 文字截断样式
30 | /// 计算富文本高度
31 | /// 同一个Component存在多种可自由切换Mode
32 | /// 精确点击事件
33 | /// 段落属性
34 | @interface TestLongRTTableCell: UITableViewCell
35 | @property (nonatomic, strong) AWRichTextLabel *rtLabel;
36 | @property (nonatomic, weak) AWRichText *richText;
37 | @property (nonatomic, unsafe_unretained) CGFloat cellHei;
38 | @end
39 |
40 | @implementation TestLongRTTableCell
41 |
42 | -(void)createWithMaxWid:(CGFloat) maxWid{
43 | if (!_rtLabel) {
44 | ///构造richtext
45 | AWRichText *rt = [[AWRichText alloc] init];
46 | _richText = rt;
47 | [self createRichText];
48 |
49 | ///创建label
50 | _rtLabel = rt.createRichTextLabel;
51 | [self addSubview:_rtLabel];
52 |
53 | ///计算cell(富文本)高度
54 | [_richText attributedString];
55 | _richText.truncatingTokenComp = [[AWRTTextComponent alloc] init].AWText(@"~~~").AWFont([UIFont systemFontOfSize:12]).AWColor(colorWithRgbCode(@"#00f"));
56 | ///注意,autolayout中可使用rtMaxWidth这个属性,也可以使用rtFrame
57 | ///若此处将rtFrame的高度改成一个非零较小的值如60,会有截断效果。
58 | ///截断字符由truncatingTokenComp决定,如不传,默认为 "..."。
59 | _rtLabel.rtFrame = CGRectMake(10, 5, maxWid - 20, 0);
60 | _cellHei = _rtLabel.frame.size.height + 10;
61 | }
62 |
63 | ///启动动画
64 | [_richText letAnimStartOrStop:YES];
65 | }
66 |
67 | -(AWRTTextComponent *)addTextCompWithText:(NSString *)text{
68 | AWRTTextComponent *textComp = ((AWRTTextComponent *)[self.richText addComponentFromPoolWithType:AWRTComponentTypeText])
69 | .AWText(text)
70 | .AWColor(colorWithRgbCode(@"#222"))
71 | .AWShadowColor(colorWithRgbCode(@"#555"))
72 | .AWShadowOffset([NSValue valueWithCGSize:CGSizeMake(0, 2)])
73 | .AWShadowBlurRadius(@(3));
74 |
75 | if (@available(iOS 8.2, *)) {
76 | textComp.AWFont([UIFont systemFontOfSize:14 weight:UIFontWeightBold]);
77 | } else {
78 | textComp.AWFont([UIFont systemFontOfSize:14]);
79 | }
80 |
81 | return textComp;
82 | }
83 |
84 | -(AWRTTextComponent *)addLinkCompWithText:(NSString *)text onClick:(void (^)(void))onClick{
85 | AWRTTextComponent *linkComp = [self addTextCompWithText:text]
86 | .AWColor(colorWithRgbCode(@"#55F"));
87 |
88 | #define TOUCHING_MODE (@"touchingLinkMode")
89 | #define DEFAULT_MODE ((NSString *)AWRTComponentDefaultMode)
90 |
91 | [linkComp storeAllAttributesToMode:DEFAULT_MODE replace:YES];
92 |
93 | [linkComp beginUpdateMode:TOUCHING_MODE block:^{
94 | linkComp.AWUnderlineStyle(@(NSUnderlineStyleSingle))
95 | .AWUnderlineColor(colorWithRgbCode(@"#55F"));
96 | }];
97 |
98 | linkComp.touchable = YES;
99 | linkComp.touchCallback = ^(AWRTComponent *comp, AWRTLabelTouchEvent touchEvent) {
100 | if (awIsTouchingIn(touchEvent)) {
101 | comp.currentMode = TOUCHING_MODE;
102 | }else{
103 | comp.currentMode = DEFAULT_MODE;
104 | }
105 |
106 | if (touchEvent == AWRTLabelTouchEventEndedIn) {
107 | if (onClick) {
108 | onClick();
109 | }
110 | }
111 | };
112 |
113 | return linkComp;
114 | }
115 |
116 | -(AWRTImageComponent *)addImgCompWithImageName:(NSString *)name{
117 | AWRTImageComponent *imgComponent = ((AWRTImageComponent *) [self.richText addComponentFromPoolWithType:AWRTComponentTypeImage])
118 | .AWImage([UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:nil]])
119 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependFont))
120 | .AWFont([UIFont systemFontOfSize:12]);
121 | return imgComponent;
122 | }
123 |
124 | -(AWRTViewComponent *)addGifCompWithImageName:(NSString *)name{
125 | UIImage *gifImg = [UIImage animatedGIFWithData:[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:nil]]];
126 | UIImageView *imageView = [[UIImageView alloc] init];
127 | imageView.frame = CGRectMake(0, 0, gifImg.size.width * 0.3f, gifImg.size.height * 0.3f);
128 | imageView.animationImages = gifImg.images;
129 | imageView.animationDuration = gifImg.duration;
130 | imageView.image = gifImg.images.firstObject;
131 |
132 | AWRTViewComponent *viewComp = ((AWRTViewComponent *)[self.richText addComponentFromPoolWithType:AWRTComponentTypeView])
133 | .AWView(imageView)
134 | .AWFont([UIFont systemFontOfSize:20])
135 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependContent))
136 | .AWAlignment(@(AWRTAttachmentAlignCenter))
137 | .AWPaddingRight(@1);
138 |
139 | return viewComp;
140 | }
141 |
142 | -(AWRTViewComponent *)addButtonCompWithBtnTitle:(NSString *)title font:(UIFont *)font color:(UIColor *)color target:(id)target action:(SEL)action{
143 | UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 120, 22)];
144 | btn.titleLabel.font = font;
145 | [btn setTitle:title forState:UIControlStateNormal];
146 | [btn setTitleColor:color forState:UIControlStateNormal];
147 | [btn setTitleColor:colorWithRgbCode(@"#aaa") forState:UIControlStateHighlighted];
148 | [btn setBackgroundColor:colorWithRgbCode(@"#FFB800")];
149 | btn.layer.cornerRadius = 5;
150 | btn.layer.borderWidth = 1/[UIScreen mainScreen].scale;
151 | btn.layer.borderColor = colorWithRgbCode(@"#C9C9C9").CGColor;
152 | [btn addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
153 |
154 | AWRTViewComponent *viewComp = ((AWRTViewComponent *)[self.richText addComponentFromPoolWithType:AWRTComponentTypeView])
155 | .AWView(btn)
156 | .AWFont([UIFont systemFontOfSize:12])
157 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependContent))
158 | .AWAlignment(@(AWRTAttachmentAlignCenter))
159 | .AWPaddingRight(@1);
160 |
161 | return viewComp;
162 |
163 | }
164 |
165 | -(void) createRichText{
166 | [self addTextCompWithText:@"曲曲折折的荷塘"];
167 | [self addImgCompWithImageName:@"hetang.png"];
168 | [self addTextCompWithText:@"上面,"];
169 | [self addTextCompWithText:@"弥望的是田田的叶子"];
170 | [self addImgCompWithImageName:@"yezi.png"];
171 | [self addTextCompWithText:@"。叶子出水"];
172 | [self addImgCompWithImageName:@"shui.png"];
173 | [self addTextCompWithText:@"很高,像亭亭舞女"];
174 | [self addImgCompWithImageName:@"tiaowu.png"];
175 | [self addTextCompWithText:@"的裙。层层的叶子"];
176 | [self addImgCompWithImageName:@"yezi.png"];
177 | [self addTextCompWithText:@"中间,零星地点缀着些白花"];
178 | [self addImgCompWithImageName:@"hua.png"];
179 | [self addTextCompWithText:@",有袅娜地开着的,有羞涩地打着朵儿"];
180 | [self addImgCompWithImageName:@"guduo.png"];
181 | [self addTextCompWithText:@"的;"];
182 | UIFont *btnFont = nil;
183 | if (@available(iOS 8.2, *)) {
184 | btnFont = [UIFont systemFontOfSize:12 weight:UIFontWeightBold];
185 | } else {
186 | btnFont = [UIFont systemFontOfSize:12];
187 | }
188 | [self addButtonCompWithBtnTitle:@"一个无处安放的按钮" font:btnFont color:colorWithRgbCode(@"#000") target:self action:@selector(clickBtn:)];
189 | [self addTextCompWithText:@"正如一粒粒的明珠"];
190 | [self addImgCompWithImageName:@"mingzhu.png"];
191 | [self addTextCompWithText:@",又如碧天里的星星"];
192 | [self addImgCompWithImageName:@"xingxing.png"];
193 | [self addTextCompWithText:@",又如刚出浴的美人"];
194 | [self addImgCompWithImageName:@"meiren.png"];
195 | [self addTextCompWithText:@"。"];
196 | [self addLinkCompWithText:@"微风过处,送来缕缕清香,仿佛远处高楼" onClick:^{
197 | NSLog(@"点击到了一个链接");
198 | }];
199 | [self addImgCompWithImageName:@"gaolou.png"];
200 | [self addTextCompWithText:@"上渺茫的歌声"];
201 | [self addGifCompWithImageName:@"wa.gif"];
202 | [self addTextCompWithText:@"似的。"];
203 |
204 | self.richText.lineSpace = 5;
205 | }
206 |
207 | -(void) clickBtn:(UIButton *)btn{
208 | NSLog(@"点击到了一个按钮:[%@]", btn.titleLabel.text);
209 | }
210 |
211 | -(void) resetShowDebugFrame{
212 | _richText.alwaysShowDebugFrames = sAWTestAlwaysShowDebugFrame;
213 | }
214 |
215 | @end
216 |
217 | #pragma mark - 列表类控件杀手锏,极大简化列表类UI的创建 可方便处理:【头像+前缀icon x N +昵称】这种排版
218 | /// 列表类图文混排杀手锏
219 | /// 可方便处理:【头像+前缀icon x N +昵称】这种排版
220 | @interface TestHeaderIconRTTableCell:UITableViewCell
221 | @property (nonatomic, strong) AWRichTextLabel *rtLabel;
222 | @property (nonatomic, weak) AWRichText *richText;
223 | @end
224 |
225 | @implementation TestHeaderIconRTTableCell
226 |
227 | -(void) createRtLabelWithMaxWid:(CGFloat) maxWid{
228 | if (!_rtLabel) {
229 | ///创建richtext
230 | AWRichText *richText = [[AWRichText alloc] init];
231 | _richText = richText;
232 | [self createRichText];
233 |
234 | ///创建label
235 | _rtLabel = richText.createRichTextLabel;
236 | [self addSubview:_rtLabel];
237 | ///注意,autolayout中可使用rtMaxWidth这个属性,也可以使用rtFrame
238 | _rtLabel.rtFrame = CGRectMake(10, 5, maxWid - 20, 0);
239 | }
240 | }
241 |
242 | -(void) createRichText{
243 | UIImageView *headerImgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 60, 60)];
244 | headerImgView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"fengtimo.jpg" ofType:nil]];
245 | headerImgView.layer.borderWidth = 3;
246 | headerImgView.layer.borderColor = colorWithRgbCode(@"#FF5722").CGColor;
247 | headerImgView.layer.cornerRadius = 30;
248 | headerImgView.layer.masksToBounds = YES;
249 |
250 | /// 看似代码很长,但是同一类 component 初始化都是类似的。
251 | /// 可以很容易地封装成函数调用,如TestLongRTTableCell中Component的创建过程
252 | AWRTViewComponent *viewComp = [[AWRTViewComponent alloc] init]
253 | .AWView(headerImgView)
254 | .AWFont([UIFont systemFontOfSize:14])
255 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependContent))
256 | .AWAlignment(@(AWRTAttachmentAlignCenter));
257 | [self.richText addComponent:viewComp];
258 |
259 | AWRTImageComponent *imgComp = [[AWRTImageComponent alloc] init]
260 | .AWImage([UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"roomadmin.png" ofType:nil]])
261 | .AWFont([UIFont systemFontOfSize:14])
262 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependFont))
263 | .AWAlignment(@(AWRTAttachmentAlignCenter))
264 | .AWPaddingLeft(@1);
265 | [self.richText addComponent:imgComp];
266 |
267 | AWRTTextComponent *aliasComp = [[AWRTTextComponent alloc] init];
268 | aliasComp.AWText(@"中了提莫的毒无药可救").AWColor(@"#797f89").AWFont([UIFont systemFontOfSize:14]).AWPaddingLeft(@1);
269 | [self.richText addComponent:aliasComp];
270 | }
271 |
272 | -(void) resetShowDebugFrame{
273 | _richText.alwaysShowDebugFrames = sAWTestAlwaysShowDebugFrame;
274 | }
275 |
276 | @end
277 |
278 | #pragma mark - ChatView 一个聊天列表的例子
279 | /// 一个聊天列表的例子
280 | @interface TestChatViewCell:UITableViewCell
281 | @property (nonatomic, strong) UIView *containerView;
282 | @property (nonatomic, weak) ChatView *chatView;
283 | @end
284 |
285 | @implementation TestChatViewCell
286 |
287 | -(void)createContainerViewWithMaxWid:(CGFloat)maxWid{
288 | if (!_containerView) {
289 | _containerView = [[UIView alloc] initWithFrame:CGRectMake(10, 10, maxWid - 20, 200)];
290 | [self addSubview:_containerView];
291 |
292 | [ChatView testWithSuperView:_containerView];
293 | _chatView = _containerView.subviews.firstObject;
294 | }
295 | }
296 |
297 | -(void) resetShowDebugFrame{
298 | _chatView.alwaysShowDebugFrame = sAWTestAlwaysShowDebugFrame;
299 | }
300 |
301 | @end
302 |
303 | #pragma mark - 控制按钮排版演示,用AWRichText很容易做到重复元素横向排版
304 |
305 | typedef enum : NSUInteger {
306 | TestControlCellBtnTypeShowDebugFrame,
307 | } TestControlCellBtnType;
308 |
309 | @class TestControlCell;
310 | @protocol TestControlCellDelegate
311 | @optional
312 | -(void) cell:(TestControlCell *)cell onClickBtnType:(TestControlCellBtnType)type;
313 | @end
314 |
315 | @interface TestControlCell: UITableViewCell
316 | @property (nonatomic, strong) UIScrollView *scrollView;
317 | @property (nonatomic, strong) AWRichTextLabel *rtLabel;
318 | @property (nonatomic, weak) AWRichText *richText;
319 |
320 | @property (nonatomic, weak) id delegate;
321 | @end
322 |
323 | @implementation TestControlCell
324 |
325 | -(void) createRtLabelWithMaxWid:(CGFloat) maxWid{
326 | if (!_scrollView) {
327 | _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(10, 5, maxWid - 20, 50)];
328 | [self addSubview:_scrollView];
329 | }
330 | if (!_rtLabel) {
331 | ///创建richtext
332 | AWRichText *richText = [[AWRichText alloc] init];
333 | _richText = richText;
334 | [self createRichText];
335 |
336 | ///创建label
337 | _rtLabel = richText.createRichTextLabel;
338 | [_scrollView addSubview:_rtLabel];
339 | ///注意,autolayout中可使用rtMaxWidth这个属性,也可以使用rtFrame
340 | [_richText attributedString];
341 |
342 | [_rtLabel sizeToFit];
343 |
344 | _scrollView.contentSize = _rtLabel.frame.size;
345 | }
346 | }
347 |
348 | -(AWRTViewComponent *)addBtnCompWithView:(UIView *)view{
349 | AWRTViewComponent *viewComp = [[AWRTViewComponent alloc] init]
350 | .AWView(view)
351 | .AWFont([UIFont systemFontOfSize:14])
352 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependContent))
353 | .AWAlignment(@(AWRTAttachmentAlignCenter))
354 | .AWPaddingLeft(@1)
355 | ;
356 | [self.richText addComponent:viewComp];
357 | return viewComp;
358 | }
359 |
360 | -(UIButton *)btnWithTitle:(NSString *)title titleColor:(UIColor *)titleColor titleHighlightColor:(UIColor *)titleHighlightColor size:(CGSize)size target:(id)target action:(SEL)action{
361 | UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)];
362 | [btn setTitle:title forState:UIControlStateNormal];
363 | [btn setTitleColor:titleColor forState:UIControlStateNormal];
364 | [btn setTitleColor:titleHighlightColor forState:UIControlStateHighlighted];
365 | [btn setBackgroundColor:colorWithRgbCode(@"#009688")];
366 | [btn addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
367 | btn.layer.cornerRadius = 5;
368 | return btn;
369 | }
370 |
371 | -(void) createRichText{
372 | ///debugFrame按钮
373 | UIButton *debugFrameBtn = [self btnWithTitle:sAWTestAlwaysShowDebugFrame ? @"关闭DebugFrame": @"打开DebugFrame" titleColor:colorWithRgbCode(@"#fff") titleHighlightColor:colorWithRgbCode(@"#aaa") size:CGSizeMake(160, 40) target:self action:@selector(onClickDebugFrameBtn:)];
374 | [self addBtnCompWithView:debugFrameBtn];
375 |
376 | ///一些演示按钮
377 | for (NSInteger i = 0; i < 3; i++) {
378 | UIButton *otherBtn = [self btnWithTitle:@"一些演示按钮" titleColor:colorWithRgbCode(@"#fff") titleHighlightColor:colorWithRgbCode(@"#aaa") size:CGSizeMake(160, 40) target:nil action:nil];
379 | [self addBtnCompWithView:otherBtn];
380 | }
381 | }
382 |
383 | -(void) onClickDebugFrameBtn:(UIButton *)btn{
384 | sAWTestAlwaysShowDebugFrame = !sAWTestAlwaysShowDebugFrame;
385 | if (sAWTestAlwaysShowDebugFrame) {
386 | [btn setTitle:@"关闭DebugFrame" forState: UIControlStateNormal];
387 | }else{
388 | [btn setTitle:@"打开DebugFrame" forState: UIControlStateNormal];
389 | }
390 | [self.delegate cell:self onClickBtnType:TestControlCellBtnTypeShowDebugFrame];
391 | }
392 |
393 | -(void) resetShowDebugFrame{
394 | _richText.alwaysShowDebugFrames = sAWTestAlwaysShowDebugFrame;
395 | }
396 |
397 | @end
398 |
399 | @interface TestTableHeaderView: UITableViewHeaderFooterView
400 | @property (nonatomic, strong) AWRichTextLabel *label;
401 | @property (nonatomic, weak) AWRichText *richText;
402 | @end
403 |
404 | @implementation TestTableHeaderView
405 |
406 | -(void)createLabelWithMaxWid:(CGFloat)maxWid{
407 | if (!_label) {
408 | _label = [[AWRichTextLabel alloc] init];
409 | _label.rtFrame = CGRectMake(10, 0, maxWid, 0);
410 | [self addSubview:_label];
411 | }
412 | }
413 |
414 | -(void)setRichText:(AWRichText *)richText{
415 | if (_richText == richText) {
416 | return;
417 | }
418 | _richText = richText;
419 | self.label.richText = richText;
420 | }
421 |
422 | @end
423 |
424 | @interface TestView()
425 | @property (nonatomic, strong) UITableView *tableView;
426 | @end
427 |
428 | @implementation TestView
429 |
430 | -(void) initView{
431 | CGFloat safeTop = 20;
432 | if (@available(iOS 11.0, *)) {
433 | safeTop = 44;
434 | }
435 | UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, safeTop, self.bounds.size.width, self.bounds.size.height - safeTop) style:UITableViewStylePlain];
436 | tableView.delegate = self;
437 | tableView.dataSource = self;
438 | [tableView registerClass:[TestLongRTTableCell class] forCellReuseIdentifier:@"TestLongRTTableCell"];
439 | [tableView registerClass:[TestHeaderIconRTTableCell class] forCellReuseIdentifier:@"TestHeaderIconRTTableCell"];
440 | [tableView registerClass:[TestControlCell class] forCellReuseIdentifier:@"TestControlCell"];
441 | [tableView registerClass:[TestTableHeaderView class] forHeaderFooterViewReuseIdentifier:@"TestTableHeaderView"];
442 | [tableView registerClass:[TestChatViewCell class] forCellReuseIdentifier:@"TestChatViewCell"];
443 | [self addSubview:tableView];
444 | _tableView = tableView;
445 | }
446 |
447 | #pragma mark - tableview delegate
448 | -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
449 | return 4;
450 | }
451 |
452 | -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
453 | if (section == 0) {
454 | return 1;
455 | }else if(section == 1){
456 | return 3;
457 | }else if(section == 2){
458 | return 1;
459 | }else if(section == 3){
460 | return 1;
461 | }
462 | return 1;
463 | }
464 |
465 | -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
466 | if (indexPath.section == 0) {
467 | TestLongRTTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TestLongRTTableCell"];
468 | cell.selectionStyle = UITableViewCellSelectionStyleNone;
469 | [cell createWithMaxWid:tableView.bounds.size.width];
470 | [cell resetShowDebugFrame];
471 | return cell;
472 | }else if(indexPath.section == 1){
473 | TestHeaderIconRTTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TestHeaderIconRTTableCell"];
474 | cell.selectionStyle = UITableViewCellSelectionStyleNone;
475 | [cell createRtLabelWithMaxWid:tableView.bounds.size.width];
476 | [cell resetShowDebugFrame];
477 | return cell;
478 | }else if(indexPath.section == 2){
479 | TestChatViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TestChatViewCell"];
480 | cell.selectionStyle = UITableViewCellSelectionStyleNone;
481 | [cell resetShowDebugFrame];
482 | [cell createContainerViewWithMaxWid:tableView.bounds.size.width];
483 | return cell;
484 | }else if(indexPath.section == 3){
485 | TestControlCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TestControlCell"];
486 | cell.selectionStyle = UITableViewCellSelectionStyleNone;
487 | [cell createRtLabelWithMaxWid:tableView.bounds.size.width];
488 | [cell resetShowDebugFrame];
489 | cell.delegate = self;
490 | return cell;
491 | }
492 | return nil;
493 | }
494 |
495 | -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
496 | if (indexPath.section == 0) {
497 | TestLongRTTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TestLongRTTableCell"];
498 | [cell createWithMaxWid:tableView.bounds.size.width];
499 | return cell.cellHei;
500 | }else if(indexPath.section == 1){
501 | return 70.f;
502 | }else if(indexPath.section == 2){
503 | return 220.f;
504 | }else if(indexPath.section == 3){
505 | return 60.f;
506 | }
507 | return 0.1f;
508 | }
509 |
510 | -(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
511 | return 0.1f;
512 | }
513 |
514 | -(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
515 | return 20.f;
516 | }
517 |
518 | -(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
519 | TestTableHeaderView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"TestTableHeaderView"];
520 | AWRichText *rtText = [[AWRichText alloc] init];
521 |
522 | AWRTImageComponent *imgComp = [[AWRTImageComponent alloc] init]
523 | .AWImage([UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%ld.png", section + 1] ofType:nil]])
524 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependFont))
525 | .AWFont([UIFont systemFontOfSize:16]);
526 | ;
527 |
528 | [rtText addComponent:imgComp];
529 |
530 | AWRTTextComponent *textComp = [[AWRTTextComponent alloc] init];
531 | textComp.AWText([NSString stringWithFormat:@"第%ld个section", section + 1]);
532 |
533 | if (@available(iOS 8.2, *)) {
534 | textComp.AWFont([UIFont systemFontOfSize:16 weight:UIFontWeightBold]);
535 | } else {
536 | textComp.AWFont([UIFont systemFontOfSize:16]);
537 | }
538 | [rtText addComponent:textComp];
539 |
540 | rtText.alwaysShowDebugFrames = sAWTestAlwaysShowDebugFrame;
541 |
542 | [headerView createLabelWithMaxWid:tableView.bounds.size.width];
543 | [headerView setRichText:rtText];
544 | return headerView;
545 | }
546 |
547 | -(void)cell:(TestControlCell *)cell onClickBtnType:(TestControlCellBtnType)type{
548 | if (type == TestControlCellBtnTypeShowDebugFrame) {
549 | [self.tableView reloadData];
550 | }
551 | }
552 |
553 | +(void) testWithSuperView:(UIView *)superView{
554 | TestView *tv = [[TestView alloc] initWithFrame:superView.bounds];
555 | [superView addSubview:tv];
556 | [tv initView];
557 | }
558 |
559 | @end
560 |
--------------------------------------------------------------------------------
/Demo/chat/ChatView.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 |
9 | #import "AWRichText.h"
10 |
11 | typedef enum : NSUInteger {
12 | ChatViewCellRTCompTypeText,//文本
13 | ChatViewCellRTCompTypeGif,//gif
14 | ChatViewCellRTCompTypeImage,//image
15 | ChatViewCellRTCompTypeUIView,//UIView
16 | } ChatViewCellRTCompType;
17 |
18 | typedef void(^OnTouchCompBlock)(AWRTComponent *comp, AWRTLabelTouchEvent touchEvent);
19 |
20 | @interface ChatViewComponentModel : NSObject
21 | ///类型
22 | @property (nonatomic, unsafe_unretained) ChatViewCellRTCompType type;
23 |
24 | ///字体
25 | @property (nonatomic, strong) UIFont *font;
26 | @property (nonatomic, strong) UIColor *backgroundColor;
27 |
28 | ///阴影
29 | @property (nonatomic, unsafe_unretained) CGFloat shadowBlurRadius;
30 | @property (nonatomic, strong) UIColor *shadowColor;
31 | @property (nonatomic, unsafe_unretained) CGSize shadowOffset;
32 |
33 | ///文本 or link
34 | @property (nonatomic, copy) NSString *text;
35 | @property (nonatomic, strong) UIColor *color;
36 |
37 | ///图片 or Gif or UIView
38 | @property (nonatomic, unsafe_unretained) AWRTAttchmentBoundsDepend depend;
39 |
40 | ///图片 or Gif
41 | @property (nonatomic, unsafe_unretained) CGRect bounds;
42 | @property (nonatomic, strong) UIImage *image;
43 |
44 | ///UIView
45 | @property (nonatomic, strong) UIView *view;
46 |
47 | ///点击
48 | @property (nonatomic, strong) OnTouchCompBlock onTouchComp;
49 |
50 | ///任意gif
51 | +(ChatViewComponentModel *)gifComponentModelWithFont:(UIFont *)font image:(UIImage *)image;
52 | ///任意view
53 | +(ChatViewComponentModel *)viewComponentModelWithFont:(UIFont *)font view:(UIView *)view;
54 | ///任意图片
55 | +(ChatViewComponentModel *)imageComponentModelWithFont:(UIFont *)font image:(UIImage *)image;
56 | ///任意文本
57 | +(ChatViewComponentModel *)textComponentModelWithFont:(UIFont *)font text:(NSString *)text color:(UIColor *)color;
58 | @end
59 |
60 | @class ChatCellRichTextBuilder;
61 | @interface ChatViewModel : NSObject
62 |
63 | ///包含的components
64 | @property (nonatomic, strong) NSArray *compModels;
65 |
66 | ///richtext builder 用于创建richtext,并计算 cell高度
67 | -(ChatCellRichTextBuilder *)richtextBuilder;
68 |
69 | ///最大宽度
70 | @property (nonatomic, unsafe_unretained) CGFloat maxWid;
71 |
72 | +(instancetype) modelWithCompModels:(NSArray *)array maxWid:(CGFloat) maxWid;
73 |
74 | @end
75 |
76 | @interface ChatCellRichTextBuilder : NSObject
77 | -(AWRichText *) richtext;
78 | -(CGFloat) cellHeight;
79 | +(instancetype) builderWithModel:(ChatViewModel *) model maxWid:(CGFloat) maxWid;
80 | @end
81 |
82 | @interface ChatView : UIView
83 | -(void) addModel:(ChatViewModel *)model;
84 |
85 | @property (nonatomic, unsafe_unretained) BOOL alwaysShowDebugFrame;
86 |
87 | +(void) testWithSuperView:(UIView *)view;
88 | @end
89 |
--------------------------------------------------------------------------------
/Demo/chat/ChatView.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "ChatView.h"
8 | #import "AWRichText.h"
9 |
10 | #include "FakeChatModel.h"
11 |
12 | #define CHAT_VIEW_CELL_FONT [UIFont systemFontOfSize:14]
13 |
14 | @implementation ChatViewComponentModel
15 |
16 | ///任意文本
17 | +(ChatViewComponentModel *)textComponentModelWithFont:(UIFont *)font text:(NSString *)text color:(UIColor *)color{
18 | ChatViewComponentModel *textCompModel = [[ChatViewComponentModel alloc] init];
19 | textCompModel.type = ChatViewCellRTCompTypeText;
20 | textCompModel.font = font;
21 | textCompModel.text = text;
22 | textCompModel.color = color;
23 | return textCompModel;
24 | }
25 |
26 | ///任意图片
27 | +(ChatViewComponentModel *)imageComponentModelWithFont:(UIFont *)font image:(UIImage *)image{
28 | ChatViewComponentModel *imageCompModel = [[ChatViewComponentModel alloc] init];
29 | imageCompModel.type = ChatViewCellRTCompTypeImage;
30 | imageCompModel.depend = AWRTAttchmentBoundsDependFont;
31 | imageCompModel.image = image;
32 | return imageCompModel;
33 | }
34 |
35 | ///任意view
36 | +(ChatViewComponentModel *)viewComponentModelWithFont:(UIFont *)font view:(UIView *)view{
37 | ChatViewComponentModel *viewCompModel = [[ChatViewComponentModel alloc] init];
38 | viewCompModel.type = ChatViewCellRTCompTypeUIView;
39 | viewCompModel.depend = AWRTAttchmentBoundsDependFont;
40 | viewCompModel.view = view;
41 | return viewCompModel;
42 | }
43 |
44 | ///任意gif
45 | +(ChatViewComponentModel *)gifComponentModelWithFont:(UIFont *)font image:(UIImage *)image{
46 | ChatViewComponentModel *imageCompModel = [[ChatViewComponentModel alloc] init];
47 | imageCompModel.type = ChatViewCellRTCompTypeGif;
48 | imageCompModel.depend = AWRTAttchmentBoundsDependFont;
49 | imageCompModel.image = image;
50 | return imageCompModel;
51 | }
52 | @end
53 |
54 | @interface ChatViewModel()
55 | @property (nonatomic, strong) ChatCellRichTextBuilder *builder;
56 | @end
57 |
58 | @implementation ChatViewModel
59 |
60 | +(instancetype) modelWithCompModels:(NSArray *)array maxWid:(CGFloat) maxWid{
61 | ChatViewModel *model = [[ChatViewModel alloc] init];
62 | model.compModels = array;
63 | model.maxWid = maxWid;
64 | model.builder = [ChatCellRichTextBuilder builderWithModel:model maxWid:maxWid];
65 | return model;
66 | }
67 |
68 | -(ChatCellRichTextBuilder *)richtextBuilder{
69 | return _builder;
70 | }
71 |
72 | @end
73 |
74 | ///构造richtext 并计算cell高度
75 | @interface ChatCellRichTextBuilder()
76 | @property (nonatomic, weak) ChatViewModel *model;
77 | @property (nonatomic, unsafe_unretained) CGFloat maxWid;
78 | @property (nonatomic, unsafe_unretained) CGFloat cellHeight;
79 | @property (nonatomic, strong) AWRichText *richtext;
80 | @end
81 |
82 | @implementation ChatCellRichTextBuilder
83 |
84 | +(instancetype)builderWithModel:(ChatViewModel *)model maxWid:(CGFloat) maxWid{
85 | if (![model isKindOfClass:[ChatViewModel class]] || maxWid <= 0) {
86 | return nil;
87 | }
88 | ChatCellRichTextBuilder *builder = [[ChatCellRichTextBuilder alloc] init];
89 | builder.model = model;
90 | builder.maxWid = maxWid;
91 | [builder build];
92 | return builder;
93 | }
94 |
95 | -(void) build{
96 | AWRichText *richText = [[AWRichText alloc] init];
97 | for (NSInteger i = 0; i < _model.compModels.count; i++) {
98 | ChatViewComponentModel *compModel = _model.compModels[i];
99 | switch (compModel.type) {
100 | case ChatViewCellRTCompTypeGif:{
101 | AWRTViewComponent *comp = (AWRTViewComponent *)[richText addComponentFromPoolWithType:AWRTComponentTypeView];
102 | __block UIImageView *imageView = nil;
103 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{
104 | imageView = [[UIImageView alloc] init];
105 | imageView.frame = CGRectMake(0, 0, compModel.image.size.width, compModel.image.size.height);
106 |
107 | imageView.animationImages = compModel.image.images;
108 | imageView.animationDuration = compModel.image.duration;
109 | imageView.image = compModel.image.images.firstObject;
110 | }];
111 |
112 | [[NSOperationQueue mainQueue] waitUntilAllOperationsAreFinished];
113 |
114 | comp.AWView(imageView)
115 | .AWFont(CHAT_VIEW_CELL_FONT)
116 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependContent))
117 | .AWAlignment(@(AWRTAttachmentAlignCenter))
118 | .AWPaddingRight(@1);
119 |
120 | if (compModel.onTouchComp) {
121 | comp.touchable = YES;
122 | comp.touchCallback = compModel.onTouchComp;
123 | }
124 | }
125 | break;
126 | case ChatViewCellRTCompTypeText:{
127 | AWRTTextComponent *comp = (AWRTTextComponent *)[richText addComponentFromPoolWithType:AWRTComponentTypeText];
128 | comp.AWText(compModel.text)
129 | .AWFont(CHAT_VIEW_CELL_FONT)
130 | .AWColor(compModel.color)
131 | .AWBackgroundColor(compModel.backgroundColor)
132 | .AWShadowColor(compModel.shadowColor)
133 | .AWShadowOffset([NSValue valueWithCGSize:compModel.shadowOffset])
134 | .AWShadowBlurRadius(@(compModel.shadowBlurRadius))
135 | .AWPaddingRight(@1)
136 | ;
137 |
138 | if (compModel.onTouchComp) {
139 | comp.touchable = YES;
140 | comp.touchCallback = compModel.onTouchComp;
141 | }
142 | }
143 | break;
144 | case ChatViewCellRTCompTypeImage:{
145 | AWRTImageComponent *comp = (AWRTImageComponent *)[richText addComponentFromPoolWithType:AWRTComponentTypeImage];
146 | comp.AWImage(compModel.image)
147 | .AWFont(CHAT_VIEW_CELL_FONT)
148 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependFont))
149 | .AWAlignment(@(AWRTAttachmentAlignBottom))
150 | .AWPaddingRight(@1);
151 |
152 | if (compModel.onTouchComp) {
153 | comp.touchable = YES;
154 | comp.touchCallback = compModel.onTouchComp;
155 | }
156 | }
157 | break;
158 | case ChatViewCellRTCompTypeUIView:{
159 | AWRTViewComponent *comp = (AWRTViewComponent *)[richText addComponentFromPoolWithType:AWRTComponentTypeView];
160 | comp.AWView(compModel.view)
161 | .AWFont(CHAT_VIEW_CELL_FONT)
162 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependContent))
163 | .AWAlignment(@(AWRTAttachmentAlignCenter))
164 | .AWPaddingRight(@1);
165 |
166 | if (compModel.onTouchComp) {
167 | comp.touchable = YES;
168 | comp.touchCallback = compModel.onTouchComp;
169 | }
170 | }
171 | break;
172 | }
173 | }
174 |
175 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{
176 | [richText attributedString];
177 | }];
178 |
179 | [[NSOperationQueue mainQueue] waitUntilAllOperationsAreFinished];
180 |
181 | _cellHeight = [richText sizeThatFits:CGSizeMake(_maxWid, 0)].height;
182 | _richtext = richText;
183 | }
184 |
185 | @end
186 |
187 | static NSString *sChatViewTableCellId = @"ChatViewTableCell";
188 | @interface ChatViewTableCell: UITableViewCell
189 | @property (nonatomic, unsafe_unretained) CGFloat height;
190 | @property (nonatomic, strong) AWRichTextLabel *rtLabel;
191 | @property (nonatomic, weak) AWRichText *richText;
192 | @end
193 |
194 | @implementation ChatViewTableCell
195 |
196 | -(void)setRichText:(AWRichText *)richText maxWid:(CGFloat)maxWid{
197 | if ([_richText isEqual:richText]) {
198 | return;
199 | }
200 | _richText = richText;
201 | if (_rtLabel) {
202 | _rtLabel.richText = richText;
203 | }else{
204 | _rtLabel = richText.createRichTextLabel;
205 | _rtLabel.rtFrame = CGRectMake(0, 5, maxWid, 0);
206 | [self addSubview:_rtLabel];
207 | }
208 | }
209 |
210 | -(void) startAnimating{
211 | [_richText letAnimStartOrStop:YES];
212 | }
213 |
214 | @end
215 |
216 | @interface ChatView()
217 | @end
218 |
219 | @implementation ChatView{
220 | UITableView *_tableView;
221 | NSMutableArray *_tableDataArray;
222 | BOOL _autoScrollToBtm;
223 |
224 | UIButton *_moreMsgBtn;
225 | }
226 |
227 | -(void)setAlwaysShowDebugFrame:(BOOL)alwaysShowDebugFrame{
228 | if (_alwaysShowDebugFrame == alwaysShowDebugFrame) {
229 | return;
230 | }
231 | _alwaysShowDebugFrame = alwaysShowDebugFrame;
232 |
233 | [_tableView reloadData];
234 | }
235 |
236 | -(void) addModel:(ChatViewModel *)model{
237 | ///最多展示100条
238 | while (_tableDataArray.count >= 100) {
239 | [_tableDataArray removeObjectAtIndex:0];
240 | }
241 | [_tableDataArray addObject:model];
242 | [_tableView reloadData];
243 |
244 | if (_autoScrollToBtm) {
245 | dispatch_async(dispatch_get_main_queue(), ^{
246 | [self scrollToBtm];
247 | });
248 | }else{
249 | if (!self.isTableViewAtBottom) {
250 | [self showMoreMsgBtn];
251 | }
252 | }
253 | }
254 |
255 | -(void) scrollToBtm{
256 | [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:_tableDataArray.count - 1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:NO];
257 | [self hideMoreMsgBtn];
258 | _autoScrollToBtm = YES;
259 | }
260 |
261 | -(void) openAutoScrollToBtn{
262 | _autoScrollToBtm = YES;
263 | }
264 |
265 | -(void) closeAutoScrollToBtn{
266 | _autoScrollToBtm = NO;
267 | }
268 |
269 | -(BOOL) isTableViewAtBottom{
270 | return _tableView.contentOffset.y + _tableView.frame.size.height >= _tableView.contentSize.height;
271 | }
272 |
273 | -(void) showMoreMsgBtn{
274 | _moreMsgBtn.hidden = NO;
275 | }
276 |
277 | -(void) hideMoreMsgBtn{
278 | _moreMsgBtn.hidden = YES;
279 | }
280 |
281 | -(instancetype)initWithFrame:(CGRect)frame{
282 | self = [super initWithFrame:frame];
283 | [self initView];
284 | return self;
285 | }
286 |
287 | -(void) initView{
288 | [self initTableView];
289 | }
290 |
291 | #pragma mark - UITableView
292 | -(void) initTableView{
293 | _tableDataArray = [[NSMutableArray alloc] init];
294 |
295 | _tableView = [[UITableView alloc] initWithFrame:self.bounds];
296 | _tableView.delegate = self;
297 | _tableView.dataSource = self;
298 | [_tableView registerClass:[ChatViewTableCell class] forCellReuseIdentifier:sChatViewTableCellId];
299 | [self addSubview:_tableView];
300 |
301 | _autoScrollToBtm = YES;
302 |
303 | CGFloat moreMsgWid = 80, moreMsgHei = 26;
304 | _moreMsgBtn = [[UIButton alloc] initWithFrame:CGRectMake(_tableView.frame.size.width / 2 - moreMsgWid / 2, _tableView.frame.size.height - moreMsgHei, moreMsgWid, moreMsgHei)];
305 | [_moreMsgBtn setBackgroundColor:colorWithRgbCode(@"#ff6600")];
306 | [_moreMsgBtn setTitle:@"有更多消息" forState:UIControlStateNormal];
307 | [_moreMsgBtn setTitleColor:colorWithRgbCode(@"#ffffff") forState:UIControlStateNormal];
308 | [_moreMsgBtn setTitleColor:colorWithRgbCode(@"#aaaaaa") forState:UIControlStateHighlighted];
309 | _moreMsgBtn.titleLabel.font = [UIFont systemFontOfSize:12];
310 | _moreMsgBtn.layer.cornerRadius = 5;
311 | _moreMsgBtn.layer.masksToBounds = YES;
312 | [_moreMsgBtn addTarget:self action:@selector(onClickMoreMsgBtn) forControlEvents:UIControlEventTouchUpInside];
313 | [self addSubview:_moreMsgBtn];
314 |
315 | _moreMsgBtn.hidden = YES;
316 | }
317 |
318 | -(void) onClickMoreMsgBtn{
319 | [self scrollToBtm];
320 | }
321 |
322 | -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
323 | return 1;
324 | }
325 |
326 | -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
327 | return _tableDataArray.count;
328 | }
329 |
330 | -(ChatViewTableCell *) cellWithTableView:(UITableView *)tableView row:(NSInteger)row{
331 | ChatViewTableCell *cell = [tableView dequeueReusableCellWithIdentifier:sChatViewTableCellId];
332 | if (row < _tableDataArray.count) {
333 | ChatViewModel *model = _tableDataArray[row];
334 | [cell setRichText:model.richtextBuilder.richtext maxWid:model.maxWid];
335 |
336 | cell.richText.alwaysShowDebugFrames = self.alwaysShowDebugFrame;
337 |
338 | [cell startAnimating];
339 |
340 | cell.backgroundColor = colorWithRgbCode(@"#f7f7f7");
341 | }else{
342 | [cell setRichText:nil maxWid:0];
343 | }
344 |
345 | return cell;
346 | }
347 |
348 | -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
349 | return [self cellWithTableView:tableView row:indexPath.row];
350 | }
351 |
352 | -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
353 | if (indexPath.row < _tableDataArray.count) {
354 | ChatViewModel *model = _tableDataArray[indexPath.row];
355 | return model.richtextBuilder.cellHeight + 10;
356 | }else{
357 | return 0.1;
358 | }
359 | }
360 |
361 | #pragma mark - scrollview delegate
362 | -(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
363 | _autoScrollToBtm = NO;
364 | }
365 |
366 | -(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
367 | if (!decelerate && self.isTableViewAtBottom) {
368 | _autoScrollToBtm = YES;
369 | [self hideMoreMsgBtn];
370 | }
371 | }
372 |
373 | -(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
374 | if (self.isTableViewAtBottom) {
375 | _autoScrollToBtm = YES;
376 | [self hideMoreMsgBtn];
377 | }
378 | }
379 |
380 | #pragma mark - test
381 | +(void) testWithSuperView:(UIView *)view{
382 | ChatView *chatView = [[ChatView alloc] initWithFrame:CGRectMake(0, 0, view.frame.size.width, view.frame.size.height)];
383 | [view addSubview:chatView];
384 |
385 | [self addChatViewModelWithChatView:chatView];
386 | }
387 |
388 | +(void) pollChatViewModelWithChatView:(ChatView *)chatView{
389 | __weak typeof(self) weakSelf = self;
390 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
391 | [weakSelf addChatViewModelWithChatView:chatView];
392 | });
393 | }
394 |
395 | +(void) addChatViewModelWithChatView:(ChatView *)chatView{
396 | static NSInteger count = 0;
397 | CGFloat maxWid = chatView.bounds.size.width;
398 | static dispatch_queue_t squeue = nil;
399 | if(squeue == nil) squeue = dispatch_queue_create("test.chatview.squeue", DISPATCH_QUEUE_SERIAL);
400 | dispatch_async(squeue, ^{
401 | ChatViewModel *model = nil;
402 | if (randomBool()) {
403 | model = [FakeChatModel randomChatModelWithMaxWid:maxWid];
404 | }else{
405 | model = [FakeChatModel randomGiftModelWithMaxWid:maxWid];
406 | }
407 | dispatch_async(dispatch_get_main_queue(), ^{
408 | [chatView addModel:model];
409 | if (count++ < NSIntegerMax) {
410 | [self pollChatViewModelWithChatView:chatView];
411 | }
412 | });
413 | });
414 | }
415 | @end
416 |
--------------------------------------------------------------------------------
/Demo/chat/FakeChatModel.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 | #import "ChatView.h"
9 |
10 | #import "AWDemoTool.h"
11 |
12 | ///随机颜色
13 | static inline UIColor *fakeRandomColor(){
14 | return [UIColor colorWithRed: (arc4random() % 255) / 510.f green:(arc4random() % 255) / 510.f blue:(arc4random() % 255) / 510.f alpha:1];
15 | }
16 |
17 | ///随机名字
18 | static inline NSString *fakeRandomName(){
19 | NSArray *names = @[@"了不起的盖茨比", @"超级花美男", @"marsll", @"起点hck", @"星辰变", @"不喜人潮", @"阿门阿啊阿啊阿啊阿"];
20 | return names[arc4random() % names.count];
21 | }
22 |
23 | ///随机聊天文字
24 | static inline NSString *fakeRandomSayWords(){
25 | NSArray *contents = @[@"杨幂在唱歌???柳岩在唱歌???傻傻分不清楚,天呀!!!这是谁?",
26 | @"大神太厉害了~",
27 | @"主播不玩炫测了吗?????",
28 | @"炫测这英雄废了。。。。",
29 | @"星辰变有人看过吗",
30 | @"玩啥游戏的都有啊阿啊 枪火游侠",
31 | @"80过图 200开商店 加V 56234442"];
32 | return contents[arc4random() % contents.count];
33 | }
34 |
35 | ///随机礼物名 对应gif
36 | static inline NSString *fakeRandomGiftName(){
37 | NSArray *giftNames = @[@"bangbangda", @"yufan", @"zan"];
38 | return giftNames[arc4random() % giftNames.count];
39 | }
40 |
41 | @interface FakeChatUserModel: NSObject
42 | @property (nonatomic, strong) NSFont *font;
43 | @property (nonatomic, copy) NSString *name;
44 | @property (nonatomic, strong) UIColor *nameColor;
45 | @property (nonatomic, unsafe_unretained) BOOL isVip;
46 | @property (nonatomic, unsafe_unretained) BOOL isFemal;
47 | @property (nonatomic, unsafe_unretained) NSInteger level;
48 | @property (nonatomic, unsafe_unretained) NSInteger juewei;
49 | @property (nonatomic, copy) NSString *familyName;
50 | @property (nonatomic, unsafe_unretained) NSInteger familyLevel;
51 | @property (nonatomic, unsafe_unretained) BOOL isAdmin;
52 |
53 | +(FakeChatUserModel *) randomModel;
54 | @end
55 |
56 | @interface FakeChatModel : NSObject
57 | ///普通聊天
58 | +(ChatViewModel *)chatModelWithUserModel:(FakeChatUserModel *)userModel content:(NSString *)content contentColor:(UIColor *)contentColor toUserModel:(FakeChatUserModel *)toUserModel maxWid:(CGFloat)maxWid;
59 | +(ChatViewModel *)randomChatModelWithMaxWid:(CGFloat)maxWid;
60 | ///送礼
61 | +(ChatViewModel *)chatModelWithUserModel:(FakeChatUserModel *)userModel toUserModel:(FakeChatUserModel *)toUserModel giftName:(NSString *) giftName count:(NSInteger) count countColor:(UIColor *)countColor maxWid:(CGFloat)maxWid;
62 | +(ChatViewModel *)randomGiftModelWithMaxWid:(CGFloat)maxWid;
63 |
64 |
65 | //获取家族
66 | +(UIView *)viewWithFamilyName:(NSString *)familyName familyLevel:(NSInteger)familyLevel;
67 | ///获取等级view
68 | +(UIView *)imageViewWithLevel:(NSInteger) level;
69 | @end
70 |
--------------------------------------------------------------------------------
/Demo/chat/FakeChatModel.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "FakeChatModel.h"
8 | #import "UIImage+AWGif.h"
9 |
10 | #define CHAT_FONT [UIFont systemFontOfSize:14]
11 |
12 | #define CHAT_COMMON_COLOR colorWithRgbCode(@"#888888")
13 |
14 | @implementation FakeChatUserModel
15 |
16 | +(FakeChatUserModel *)randomModel{
17 | FakeChatUserModel *model = [[FakeChatUserModel alloc] init];
18 | model.name = fakeRandomName();
19 | model.nameColor = colorWithRgbCode(@"#2b94ff"); //fakeRandomColor();
20 | model.isVip = randomBool();
21 | model.isFemal = randomBool();
22 | model.level = randomInteger(0, 199);
23 | model.juewei = randomBool();
24 | model.familyName = fakeRandomName();
25 | model.familyLevel = randomInteger(1, 99);
26 | model.isAdmin = randomBool();
27 | return model;
28 | }
29 |
30 | @end
31 |
32 | @interface FakeChatModel()
33 | @property (nonatomic, strong) NSOperationQueue *mainQueue;
34 | @end
35 |
36 | @implementation FakeChatModel
37 |
38 | -(NSOperationQueue *)mainQueue{
39 | if (!_mainQueue) {
40 | _mainQueue = [NSOperationQueue mainQueue];
41 | }
42 | return _mainQueue;
43 | }
44 |
45 | #pragma mark - 聊天
46 |
47 | +(NSString *)pathWithName:(NSString *)name{
48 | return [[NSBundle mainBundle] pathForResource:name ofType:nil];
49 | }
50 |
51 | ///获取等级view
52 | +(UIView *)imageViewWithLevel:(NSInteger) level{
53 | UIImageView *imageView = [[UIImageView alloc] init];
54 | imageView.layer.contents = (__bridge id _Nullable)([[UIImage imageWithContentsOfFile:[self pathWithName:@"level.png"]] CGImage]);
55 | static CGFloat wid = 40, hei = 16, off = 3, totalHei = 3588;
56 | imageView.layer.contentsRect = CGRectMake(0, level * (hei + off) / totalHei, 1, hei/totalHei);
57 | imageView.layer.contentsGravity = kCAGravityBottom;
58 | imageView.frame = CGRectMake(0, 0, wid, hei);
59 | return imageView;
60 | }
61 |
62 | ///获取vip图片
63 | +(UIImage *)imageForVip{
64 | return [UIImage imageWithContentsOfFile:[self pathWithName:@"vip.png"]];
65 | }
66 |
67 | ///爵位图片
68 | +(UIImage *)imageWithJuewei:(NSInteger) juewei{
69 | NSString *path = nil;
70 | if (juewei == 0) {
71 | path = [self pathWithName:@"qishi.png"];
72 | }else if(juewei == 1){
73 | path = [self pathWithName:@"zijue.png"];
74 | }else{
75 | return nil;
76 | }
77 |
78 | return [UIImage imageWithContentsOfFile:path];
79 | }
80 |
81 | ///获取femal图片
82 | +(UIImage *)imageForFemal{
83 | return [UIImage imageWithContentsOfFile:[self pathWithName:@"femal.png"]];
84 | }
85 |
86 | ///获取房管图片
87 | +(UIImage *)imageForAdmin{
88 | return [UIImage imageWithContentsOfFile:[self pathWithName:@"roomadmin.png"]];
89 | }
90 |
91 | ///family
92 | +(UIView *)viewWithFamilyName:(NSString *)familyName familyLevel:(NSInteger)familyLevel{
93 | ///UIView
94 | UIView *familyView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 65, 24)];
95 | ///背景
96 | UIImageView *bgImgView = [[UIImageView alloc] init];
97 | bgImgView.image = [UIImage imageWithContentsOfFile:[self pathWithName:@"familyBg.png"]];
98 | [familyView addSubview:bgImgView];
99 | [bgImgView sizeToFit];
100 | CGRect bgImgViewFrame = bgImgView.frame;
101 | bgImgView.frame = CGRectMake(0, 5, bgImgViewFrame.size.width, bgImgViewFrame.size.height);
102 |
103 | ///familyName
104 | UILabel *nameLabel = [[UILabel alloc] init];
105 | nameLabel.text = [familyName substringToIndex:3];
106 | nameLabel.font = [UIFont systemFontOfSize:12];
107 | nameLabel.textColor = [UIColor whiteColor];
108 | [familyView addSubview:nameLabel];
109 | [nameLabel sizeToFit];
110 | CGRect nameLabelFrame = nameLabel.frame;
111 | nameLabel.frame = CGRectMake((bgImgViewFrame.size.width - nameLabelFrame.size.width) / 2, 5, nameLabelFrame.size.width, nameLabelFrame.size.height);
112 |
113 | ///levelBg
114 | UIImageView *levelBgImgView = [[UIImageView alloc] init];
115 | levelBgImgView.image = [UIImage imageWithContentsOfFile:[self pathWithName:@"familyLevel.png"]];
116 | [familyView addSubview:levelBgImgView];
117 | [levelBgImgView sizeToFit];
118 | CGRect levelBgImgFrame = levelBgImgView.frame;
119 | levelBgImgView.frame = CGRectMake(42, 0, levelBgImgFrame.size.width, levelBgImgFrame.size.height);
120 |
121 | ///levelLabel
122 | UILabel *levelLabel = [[UILabel alloc] init];
123 | levelLabel.text = [NSString stringWithFormat:@"%ld", familyLevel];
124 | levelLabel.font = [UIFont boldSystemFontOfSize:12];
125 | levelLabel.textColor = [UIColor whiteColor];
126 | [familyView addSubview:levelLabel];
127 | [levelLabel sizeToFit];
128 | CGRect levelLabelFrame = levelLabel.frame;
129 | levelLabel.frame = CGRectMake(42 + (levelBgImgFrame.size.width - levelLabelFrame.size.width) / 2, 5, levelLabelFrame.size.width, levelLabelFrame.size.height);
130 |
131 | return familyView;
132 | }
133 |
134 | +(UIImage *)giftImageWithName:(NSString *)name{
135 | return [UIImage animatedGIFWithData:[NSData dataWithContentsOfFile:[self pathWithName:[NSString stringWithFormat:@"%@.gif", name]]]];
136 | }
137 |
138 | ///用户名+前缀图片
139 | +(void) addUserNameToChatModelWithUserModel:(FakeChatUserModel *)userModel
140 | toArray:(NSMutableArray *)toArray{
141 | ///房管
142 | if (userModel.isAdmin) {
143 | ChatViewComponentModel *adminCompModel = [ChatViewComponentModel imageComponentModelWithFont:CHAT_FONT image:[self.class imageForAdmin]];
144 | [toArray addObject:adminCompModel];
145 | }
146 |
147 | ///femal
148 | if (userModel.isFemal) {
149 | ChatViewComponentModel *femalCompModel = [ChatViewComponentModel imageComponentModelWithFont:CHAT_FONT image:[self.class imageForFemal]];
150 | [toArray addObject:femalCompModel];
151 | }
152 |
153 | ///等级
154 | if (userModel.level >= 0 && userModel.level <= 119) {
155 | __block UIView *levelView = nil;
156 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{
157 | levelView = [self.class imageViewWithLevel: userModel.level];
158 | }];
159 |
160 | [[NSOperationQueue mainQueue] waitUntilAllOperationsAreFinished];
161 |
162 | ChatViewComponentModel *levelCompModel = [ChatViewComponentModel viewComponentModelWithFont:CHAT_FONT view:levelView];
163 | [toArray addObject:levelCompModel];
164 | }
165 |
166 | ///vip
167 | if (userModel.isVip) {
168 | ChatViewComponentModel *vipCompModel = [ChatViewComponentModel imageComponentModelWithFont:CHAT_FONT image:[self.class imageForVip]];
169 | [toArray addObject:vipCompModel];
170 | }
171 |
172 | ///爵位
173 | if (userModel.juewei == 0 || userModel.juewei == 1) {
174 | ChatViewComponentModel * jueweiCompModel = [ChatViewComponentModel imageComponentModelWithFont:CHAT_FONT image:[self.class imageWithJuewei:userModel.juewei]];
175 | [toArray addObject:jueweiCompModel];
176 | }
177 |
178 | ///family
179 | if ([userModel.familyName isKindOfClass:[NSString class]] && userModel.familyName.length > 0 && userModel.familyLevel > 0) {
180 | __block UIView *familyView = nil;
181 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{
182 | familyView = [self.class viewWithFamilyName:userModel.familyName familyLevel:userModel.familyLevel];
183 | }];
184 |
185 | [[NSOperationQueue mainQueue] waitUntilAllOperationsAreFinished];
186 |
187 | ChatViewComponentModel *familyCompModel = [ChatViewComponentModel viewComponentModelWithFont:CHAT_FONT view:familyView];
188 | [toArray addObject:familyCompModel];
189 | }
190 |
191 | ///name
192 | ChatViewComponentModel *usernameCompModel = [ChatViewComponentModel textComponentModelWithFont:CHAT_FONT text:userModel.name color:userModel.nameColor];
193 |
194 | [toArray addObject:usernameCompModel];
195 | }
196 |
197 | ///普通聊天
198 | +(ChatViewModel *)chatModelWithUserModel:(FakeChatUserModel *)userModel content:(NSString *)content contentColor:(UIColor *)contentColor toUserModel:(FakeChatUserModel *)toUserModel maxWid:(CGFloat)maxWid{
199 | NSMutableArray *compModelsArray = [[NSMutableArray alloc] init];
200 |
201 | ///name
202 | [self addUserNameToChatModelWithUserModel:userModel toArray:compModelsArray];
203 |
204 | ///对 xxx 说
205 | if (toUserModel) {
206 | /// 对
207 | ChatViewComponentModel *toCompModel = [ChatViewComponentModel textComponentModelWithFont:CHAT_FONT text:@"对" color:CHAT_COMMON_COLOR];
208 | [compModelsArray addObject:toCompModel];
209 |
210 | /// toUser
211 | [self addUserNameToChatModelWithUserModel:toUserModel toArray:compModelsArray];
212 | }
213 |
214 | /// 说
215 | ChatViewComponentModel *sayCompModel = [ChatViewComponentModel textComponentModelWithFont:CHAT_FONT text:@"说:" color:CHAT_COMMON_COLOR];
216 | [compModelsArray addObject:sayCompModel];
217 |
218 | ///content
219 | ChatViewComponentModel *contentCompModel = [ChatViewComponentModel textComponentModelWithFont:CHAT_FONT text:content color:contentColor];
220 | [compModelsArray addObject:contentCompModel];
221 |
222 | return [ChatViewModel modelWithCompModels:compModelsArray maxWid:maxWid];
223 | }
224 |
225 | ///随机聊天model
226 | +(ChatViewModel *)randomChatModelWithMaxWid:(CGFloat)maxWid{
227 | return [self chatModelWithUserModel:[FakeChatUserModel randomModel] content:fakeRandomSayWords() contentColor:colorWithRgbCode(@"#000000") toUserModel: randomBool() ? [FakeChatUserModel randomModel] : nil maxWid:maxWid];
228 | }
229 |
230 | ///送礼
231 | +(ChatViewModel *)chatModelWithUserModel:(FakeChatUserModel *)userModel toUserModel:(FakeChatUserModel *)toUserModel giftName:(NSString *) giftName count:(NSInteger) count countColor:(UIColor *)countColor maxWid:(CGFloat)maxWid{
232 |
233 | NSMutableArray *compModelsArray = [[NSMutableArray alloc] init];
234 | ///name
235 | [self addUserNameToChatModelWithUserModel:userModel toArray:compModelsArray];
236 |
237 | ///送给 xxx
238 | if (toUserModel) {
239 | /// 送给
240 | ChatViewComponentModel *toCompModel = [ChatViewComponentModel textComponentModelWithFont:CHAT_FONT text:@"送给" color:CHAT_COMMON_COLOR];
241 | [compModelsArray addObject:toCompModel];
242 |
243 | /// toUser
244 | [self addUserNameToChatModelWithUserModel:toUserModel toArray:compModelsArray];
245 | }
246 |
247 | /// 礼物
248 | ChatViewComponentModel *giftCompModel = [ChatViewComponentModel gifComponentModelWithFont:CHAT_FONT image:[self.class giftImageWithName:giftName]];
249 | [compModelsArray addObject:giftCompModel];
250 |
251 | /// x N
252 | ChatViewComponentModel *countCompModel = [ChatViewComponentModel textComponentModelWithFont:CHAT_FONT text:[NSString stringWithFormat:@"x%ld", count] color:countColor];
253 | [compModelsArray addObject:countCompModel];
254 |
255 | return [ChatViewModel modelWithCompModels:compModelsArray maxWid:maxWid];
256 | }
257 |
258 | +(ChatViewModel *)randomGiftModelWithMaxWid:(CGFloat)maxWid{
259 | return [self chatModelWithUserModel:[FakeChatUserModel randomModel] toUserModel:[FakeChatUserModel randomModel] giftName:fakeRandomGiftName() count:randomInteger(1, 10000) countColor:colorWithRgbCode(@"#eb6100") maxWid:maxWid];
260 | }
261 |
262 | @end
263 |
--------------------------------------------------------------------------------
/Demo/chat/tool/AWDemoTool.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 |
9 | /// 根据颜色代码#rrggbbaa创建UIColor对象
10 | static inline UIColor *colorWithRgbaCode(NSString *code){
11 | NSString *cCode = code;
12 |
13 | if (![cCode hasPrefix:@"#"]) {
14 | return nil;
15 | }
16 |
17 | if (cCode.length == 5) {
18 | NSString *first = [cCode substringWithRange:NSMakeRange(1, 1)];
19 | NSString *second = [cCode substringWithRange:NSMakeRange(2, 1)];
20 | NSString *third = [cCode substringWithRange:NSMakeRange(3, 1)];
21 | NSString *four = [cCode substringWithRange:NSMakeRange(4, 1)];
22 | cCode = [NSString stringWithFormat:@"#%@%@%@%@%@%@%@%@", first, first, second, second, third, third, four, four];
23 | }
24 |
25 | if (cCode.length != 9) {
26 | return nil;
27 | }
28 |
29 | static NSDictionary * convertDict = nil;
30 | if (!convertDict) {
31 | convertDict = @{@"0":@0,@"1":@1,@"2":@2,@"3":@3,@"4":@4,@"5":@5,@"6":@6,@"7":@7,@"8":@8,@"9":@9,@"a":@10,@"b":@11,@"c":@12,@"d":@13,@"e":@14,@"f":@15};
32 | }
33 |
34 | NSString *lowerCode = [cCode lowercaseString];
35 |
36 | CGFloat colors[4] = {0};
37 |
38 | for (NSInteger i = 1; i < code.length; i += 2) {
39 | NSString *onePart = [lowerCode substringWithRange:NSMakeRange(i, 1)];
40 | if (convertDict[onePart] == nil) {
41 | return nil;
42 | }
43 | NSString *twoPart = [lowerCode substringWithRange:NSMakeRange(i + 1, 1)];
44 | if (convertDict[twoPart] == nil) {
45 | return nil;
46 | }
47 | NSUInteger cvalue = [convertDict[onePart] unsignedCharValue] * 16;
48 | cvalue += [convertDict[twoPart] unsignedCharValue];
49 | colors[i / 2] = cvalue / 255.f;
50 | }
51 |
52 | return [UIColor colorWithRed:colors[0] green:colors[1] blue:colors[2] alpha:colors[3]];
53 | }
54 |
55 | /// 根据颜色代码#rrggbb创建UIColor对象
56 | static inline UIColor *colorWithRgbCode(NSString *code){
57 | NSString *cCode = code;
58 |
59 | if (![cCode hasPrefix:@"#"]) {
60 | return nil;
61 | }
62 |
63 | if (cCode.length == 4) {
64 | NSString *first = [cCode substringWithRange:NSMakeRange(1, 1)];
65 | NSString *second = [cCode substringWithRange:NSMakeRange(2, 1)];
66 | NSString *third = [cCode substringWithRange:NSMakeRange(3, 1)];
67 | cCode = [NSString stringWithFormat:@"#%@%@%@%@%@%@", first, first, second, second, third, third];
68 | }
69 |
70 | if (cCode.length != 7) {
71 | return nil;
72 | }
73 | return colorWithRgbaCode([NSString stringWithFormat:@"%@ff", cCode]);
74 | }
75 |
76 | ///随机integer闭区间
77 | static inline NSInteger randomInteger(NSInteger start, NSInteger end){
78 | return start + arc4random() % (end - start + 1);
79 | }
80 |
81 | ///随机bool
82 | static inline BOOL randomBool(){
83 | return randomInteger(0, 1) == 0;
84 | }
85 |
86 | @interface AWDemoTool : NSObject
87 |
88 | @end
89 |
--------------------------------------------------------------------------------
/Demo/chat/tool/AWDemoTool.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWDemoTool.h"
8 |
9 | @implementation AWDemoTool
10 |
11 | @end
12 |
--------------------------------------------------------------------------------
/Demo/chat/tool/UIImage+AWGif.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 |
9 | ///gif代码参考SDWebImage
10 | @interface UIImage(AWGif)
11 |
12 | + (UIImage *)animatedGIFNamed:(NSString *)name;
13 | + (UIImage *)animatedGIFWithData:(NSData *)data;
14 |
15 | @end
16 |
--------------------------------------------------------------------------------
/Demo/chat/tool/UIImage+AWGif.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "UIImage+AWGif.h"
8 |
9 | @implementation UIImage(AWGif)
10 | + (UIImage *)animatedGIFWithData:(NSData *)data {
11 | if (!data) {
12 | return nil;
13 | }
14 |
15 | CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
16 |
17 | size_t count = CGImageSourceGetCount(source);
18 |
19 | UIImage *animatedImage;
20 |
21 | if (count <= 1) {
22 | animatedImage = [[UIImage alloc] initWithData:data];
23 | }
24 | else {
25 | NSMutableArray *images = [NSMutableArray array];
26 |
27 | NSTimeInterval duration = 0.0f;
28 |
29 | for (size_t i = 0; i < count; i++) {
30 | CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
31 | if (!image) {
32 | continue;
33 | }
34 |
35 | duration += [self frameDurationAtIndex:i source:source];
36 |
37 | [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
38 |
39 | CGImageRelease(image);
40 | }
41 |
42 | if (!duration) {
43 | duration = (1.0f / 10.0f) * count;
44 | }
45 |
46 | animatedImage = [UIImage animatedImageWithImages:images duration:duration];
47 | }
48 |
49 | CFRelease(source);
50 |
51 | return animatedImage;
52 | }
53 |
54 | + (float)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
55 | float frameDuration = 0.1f;
56 | CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
57 | NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
58 | NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
59 |
60 | NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
61 | if (delayTimeUnclampedProp) {
62 | frameDuration = [delayTimeUnclampedProp floatValue];
63 | }
64 | else {
65 |
66 | NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
67 | if (delayTimeProp) {
68 | frameDuration = [delayTimeProp floatValue];
69 | }
70 | }
71 |
72 | // Many annoying ads specify a 0 duration to make an image flash as quickly as possible.
73 | // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify
74 | // a duration of <= 10 ms. See and
75 | // for more information.
76 |
77 | if (frameDuration < 0.011f) {
78 | frameDuration = 0.100f;
79 | }
80 |
81 | CFRelease(cfFrameProperties);
82 | return frameDuration;
83 | }
84 |
85 | + (UIImage *)animatedGIFNamed:(NSString *)name {
86 | if (![name hasSuffix:@".gif"]) {
87 | return [UIImage imageNamed:name];
88 | }
89 |
90 | CGFloat scale = [UIScreen mainScreen].scale;
91 |
92 | if (scale > 1.0f) {
93 | NSString *retinaPath = [[NSBundle mainBundle] pathForResource:[name stringByAppendingString:@"@2x"] ofType:nil];
94 |
95 | NSData *data = [NSData dataWithContentsOfFile:retinaPath];
96 |
97 | if (data) {
98 | return [UIImage animatedGIFWithData:data];
99 | }
100 |
101 | NSString *path = [[NSBundle mainBundle] pathForResource:name ofType:nil];
102 |
103 | data = [NSData dataWithContentsOfFile:path];
104 |
105 | if (data) {
106 | return [UIImage animatedGIFWithData:data];
107 | }
108 |
109 | return [UIImage imageNamed:name];
110 | }
111 | else {
112 | NSString *path = [[NSBundle mainBundle] pathForResource:name ofType:nil];
113 |
114 | NSData *data = [NSData dataWithContentsOfFile:path];
115 |
116 | if (data) {
117 | return [UIImage animatedGIFWithData:data];
118 | }
119 |
120 | return [UIImage imageNamed:name];
121 | }
122 | }
123 | @end
124 |
125 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 hard_man
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | AWRichText
2 | --
3 |
4 | > 基于CoreText,面向对象,极简,易用,高效,支持精确点击,UIView混排,GIF动图,并不仅仅局限于图文混排的富文本排版神器。
5 | >
6 | > 代码地址:https://github.com/hardman/AWRichText -- 喜欢的同学可以给个star。
7 | >
8 | > 接下来会在blog中更新一些具体实现细节。
9 | >
10 |
11 | 简述
12 | --
13 | 很多app中都有聊天功能,图文混排也是常见的需求。
14 |
15 | iOS原生类:NSAttributedString 就是支持图文混排的。很多应用会用它来实现自己的功能。
16 |
17 | 但是直接使用它会有很多不方便的地方,大概有下面几个:
18 |
19 | 1. 太难用,属性那么多,而且使用字典构造,每次用都要查一下文档。更不要说大规模使用了
20 | 2. 不支持GIF动图
21 | 3. 不支持局部精确点击
22 | 4. 不支持UIView与文字进行混排
23 |
24 | AWRichText完全解决了这些问题,它的特点如下:
25 |
26 | 1. 基于 NSAttributedString+CoreText 绘制
27 | 2. 面向对象+链式操作,不需记忆繁多的属性名即可快速生成各种文本效果
28 | 3. 支持GIF 及 任意UIView 的图文混排
29 | 4. 支持精确点击
30 |
31 | AWRichText是可以让你在项目中大规模使用的,并不仅仅局限于图文混排的工具。
32 |
33 | 它适合于如下场景:文字+文字,图片+文字,组件(UIView及其子类)+文字。
34 |
35 | 因此可出现在:**文档排版**,**聊天排版**,**列表展示**,**广告文案** 等各个位置。
36 |
37 | 功能演示
38 | --
39 | 
40 |
41 | 图中的Demo(直接下载github代码运行即可)包含4部分:
42 |
43 | * 第一部分展示了长文本图文混排功能。展示了文字样式,UIView(一个无处安放的按钮)混排,精确点击(蓝紫色字体),GIF动图(小龙)。
44 | * 第二部分展示了富文本的更多使用方式。可以在任意头像+昵称这种列表中使用,省去动态建立UIImageView和UILabel的麻烦。
45 | * 第三部分展示了一个简单的的仿斗鱼聊天功能。展示了如何创建复杂的富文本及获取富文本尺寸等功能。
46 | * 第四部分展示了纯UIView单行排版功能:对于按钮横排的需求很实用,另外点击"打开DebugFrame"按钮,会触发线框模式,能够看到每个文字的位置和大小。
47 |
48 | Demo中所有元素都是使用AWRichText构造的。
49 |
50 | 使用方法
51 | --
52 |
53 | 1.直接引入文件
54 | --
55 | 将代码中的 "RichText" 文件夹直接拖入你的工程。
56 | 引入头文件 "AWRichText.h" 即可使用。
57 |
58 | 2.使用pod
59 | --
60 | 在Podfile文件中加入:
61 |
62 | ```
63 | source 'https://github.com/CocoaPods/Specs.git'
64 | platform :ios, '8.0'
65 |
66 | target 'TargetName' do
67 | pod 'AWRichText', '~> 1.0.1'
68 | end
69 | ```
70 | 然后命令行执行如下命令:
71 |
72 | ```
73 | pod install
74 | ```
75 |
76 | 基本用法
77 | --
78 |
79 | ```Objective-C
80 | #include "AWRichText.h"
81 |
82 | ...
83 | ...
84 |
85 | AWRichText *richText = [[AWRichText alloc] init];
86 |
87 | //创建红色文本hello,文本类型 text和font是必须设置的。
88 | AWRTTextComponent *rTextComp = [[AWRTTextComponent alloc] init]
89 | .AWText(@"hello")
90 | .AWColor([UIColor redColor])
91 | .AWFont([UIFont systemFontOfSize:12])
92 | .AWPaddingRight(@1);
93 | [richText addComponent: rTextComp];
94 |
95 | //创建蓝色文本world
96 | AWRTTextComponent *bTextComp = [[AWRTTextComponent alloc] init]
97 | .AWText(@" world")
98 | .AWColor([UIColor blueColor ])
99 | .AWFont([UIFont systemFontOfSize:12])
100 | .AWPaddingRight(@1);
101 | [richText addComponent:bTextComp];
102 |
103 | //创建图片,图片类型也请设置font,否则可能显示异常
104 | AWRTImageComponent *imgComp = [[AWRTImageComponent alloc] init]
105 | .AWImage([UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"fengtimo.jpg" ofType:nil]])
106 | .AWFont([UIFont systemFontOfSize:12])
107 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependFont))
108 | .AWAlignment(@(AWRTAttachmentAlignCenter))
109 | .AWPaddingRight(@1);
110 | [richText addComponent:imgComp];
111 |
112 | //创建UIButton
113 | UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 120, 22)];
114 | btn.titleLabel.font = [UIFont systemFontOfSize:12];
115 | [btn setTitle:@"这是一个button" forState:UIControlStateNormal];
116 | [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
117 | [btn setTitleColor:[UIColor colorWithRed:170.f/255 green:170.f/255 blue:170.f/255 alpha:1] forState:UIControlStateHighlighted];
118 | [btn setBackgroundColor:[UIColor colorWithRed:1 green:184.f/255 blue:0 alpha:1]];
119 | btn.layer.cornerRadius = 5;
120 | btn.layer.borderWidth = 1/[UIScreen mainScreen].scale;
121 | btn.layer.borderColor = [UIColor colorWithRed:201.f/255 green:201.f/255 blue:201.f/255 alpha:1].CGColor;
122 |
123 | //根据button创建ViewComponent,View类型也请设置font,否则可能显示异常
124 | //另外 AWRTxxxComponent组件也可以从Pool中取,直接调用addComponentFromPoolWithType:方法。
125 | //此种方法适合AWRichText的components变化频繁的情况。
126 | //正常情况使用 alloc init的方式生成即可。
127 | ((AWRTViewComponent *)[richText addComponentFromPoolWithType:AWRTComponentTypeView])
128 | .AWView(btn)
129 | .AWFont([UIFont systemFontOfSize:12])
130 | .AWBoundsDepend(@(AWRTAttchmentBoundsDependContent))
131 | .AWAlignment(@(AWRTAttachmentAlignCenter));
132 |
133 | //创建label,AWRichTextLabel是UILabel的子类
134 | AWRichTextLabel *awLabel = [richText createRichTextLabel];
135 | //请务必设置rtFrame属性,设置后会自动计算frame的尺寸
136 | //宽度为非0,高度为0表示高度自适应。另外若宽度设置特别大,超出文字内容,最终生成的宽度仍然是以文字内容宽度为准。
137 | //宽度为0表示单行。
138 | //系统属性numberOfLines无效
139 | awLabel.rtFrame = CGRectMake(100, 100, 100, 0);
140 |
141 | awLabel.backgroundColor = [UIColor colorWithRed:0.9f green:0.9f blue:0.9f alpha:1];
142 |
143 | [superView addSubview:awLabel];
144 | ...
145 | ...
146 |
147 | ```
148 |
149 | 上述代码效果:
150 |
151 | 1 . 当rtFrame为 CGRectMake(100,100,100,0)时:
152 |
153 | 
154 |
155 | 2 . 当rtFrame为 CGRectMake(100,100,1000,0)时:
156 |
157 | 
158 |
159 | 其他用法及效果实现,详见github中的demo。
160 |
161 |
162 |
--------------------------------------------------------------------------------
/RichText/AWRichText.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 | #import "AWRTComponent.h"
9 | #import "AWRTTextComponent.h"
10 | #import "AWRTImageComponent.h"
11 | #import "AWRTViewComponent.h"
12 | #import "AWRichTextLabel.h"
13 | #import "AWRTWeekRefrence.h"
14 |
15 | typedef enum : NSUInteger {
16 | AWRTComponentTypeText,
17 | AWRTComponentTypeImage,
18 | AWRTComponentTypeView,
19 | } AWRTComponentType;
20 |
21 | typedef enum : NSUInteger {
22 | AWRichTextBuildStateIniting,
23 | AWRichTextBuildStateStable,
24 | AWRichTextBuildStateWillBuilding,
25 | AWRichTextBuildStateBuilding,
26 | AWRichTextBuildStateBuilt,
27 | } AWRichTextBuildState;
28 |
29 | @protocol AWRichTextDelegate
30 | @optional
31 | -(void) updatedForAWRichText:(AWRichText *)richText;
32 | -(void) awRichText:(AWRichText *)richText fmBuildState:(AWRichTextBuildState)from toBuildState:(AWRichTextBuildState)to;
33 | @end
34 |
35 | typedef AWRichText *(^AWRichTextPropertyChain)(id);
36 |
37 | /// 缓存类
38 | @interface AWRTComponentPool : NSObject
39 | -(AWRTComponent *)retainComponentWithType:(AWRTComponentType)type;
40 | -(void) releaseComponent:(AWRTComponent *)comp;
41 |
42 | -(AWRTTextComponent *)retainTextComponent;
43 | -(void) releaseTextComponent:(AWRTTextComponent *)textComponent;
44 |
45 | -(AWRTImageComponent *)retainImageComponent;
46 | -(void) releaseImageComponent:(AWRTImageComponent *)imageComponent;
47 |
48 | -(AWRTViewComponent *)retainViewComponent;
49 | -(void) releaseViewComponent:(AWRTViewComponent *)viewComponent;
50 | @end;
51 |
52 | ///创建富文本
53 | @interface AWRichText : NSObject
54 |
55 | #pragma mark - 承载label
56 | ///当AWRichtext创建好了之后可以通过此方法直接创建Label
57 | -(AWRichTextLabel *) createRichTextLabel;
58 |
59 | -(void) addListener:(id)listener;
60 | -(void) removeListener:(id)listener;
61 |
62 | ///组件(component)被点击时的回调,component.touchable必须为YES才能接收到回调
63 | @property (nonatomic, strong) void (^touchCallback)(AWRTComponent * comp, AWRTLabelTouchEvent touchEvent);
64 |
65 | #pragma mark - 构造富文本
66 | -(BOOL) addComponent:(AWRTComponent *)component;
67 | -(BOOL) addComponents:(NSArray *)components;
68 | -(BOOL) addComponentsFromRichText:(AWRichText *)richText;
69 | -(BOOL) removeComponent:(AWRTComponent *)component;
70 | -(BOOL) removeAllComponents;
71 | -(AWRTComponent *)componentWithIndex:(NSInteger) index;
72 | -(AWRTComponent *)componentWithTag:(NSString *)tag;
73 | -(NSInteger) componentCount;
74 | -(void) enumationComponentsWithBlock:(void(^)(AWRTComponent *comp, BOOL *stop))block;
75 | -(void) enumationComponentsWithBlock:(void(^)(AWRTComponent *comp, BOOL *stop))block reverse:(BOOL)reverse;
76 |
77 | @property (nonatomic, readonly, strong) AWRTComponentPool *pool;
78 | -(AWRTComponent *) addComponentFromPoolWithType:(AWRTComponentType)type;
79 | -(BOOL) removeAndAddToPoolWithComponent:(AWRTComponent *)comp;
80 | -(BOOL) removeAllComponentsAndAddToPool;
81 |
82 | #pragma mark - 属性操作
83 | ///行距
84 | @property (nonatomic, unsafe_unretained) CGFloat lineSpace;
85 | ///文本对齐
86 | @property (nonatomic, unsafe_unretained) NSTextAlignment alignment;
87 | ///breakMode
88 | @property (nonatomic, unsafe_unretained) NSLineBreakMode lineBreakMode;
89 | ///paragraphStyle
90 | @property (nonatomic, strong) NSMutableParagraphStyle *paragraphStyle;
91 | ///文字过多时的省略符号,如果不指定,默认为"..."
92 | @property (nonatomic, strong) AWRTComponent *truncatingTokenComp;
93 | ///是否强制显示debugFrame
94 | @property (nonatomic, unsafe_unretained) BOOL alwaysShowDebugFrames;
95 | ///是否自动播放gif动画
96 | @property (nonatomic, unsafe_unretained) BOOL isGifAnimAutoRun;
97 |
98 | #pragma mark - 属性的链式操作
99 | ///链式操作
100 | -(AWRichTextPropertyChain) AWLineSpace;
101 | -(AWRichTextPropertyChain) AWAlignment;
102 | -(AWRichTextPropertyChain) AWLineBreakMode;
103 | -(AWRichTextPropertyChain) AWParagraphStyle;
104 | -(AWRichTextPropertyChain) AWTruncatingTokenComp;
105 | -(AWRichTextPropertyChain) AWAlwaysShowDebugFrames;
106 | -(AWRichTextPropertyChain) AWIsGifAnimAutoRun;
107 |
108 | #pragma mark - 获取NSAttributedString,提前计算RichText的size时一般直接调用此方法。
109 | ///可直接调用此方法获取attributedString,直接触发build及drawRect
110 | ///因为计算富文本尺寸需要_attributedString不为空,因此想要提前计算RichText的size时,可直接调用此方法。
111 | -(NSAttributedString *)attributedString;
112 | //清空生成的attributedString,一般用于重建组件
113 | -(void) clearAttributedString;
114 |
115 | #pragma mark - 绘制
116 | -(void) drawRect:(CGRect) rect label:(UILabel *)label;
117 |
118 | #pragma mark - 尺寸计算
119 | -(CGSize)sizeThatFits:(CGSize)size;
120 | -(CGSize)intrinsicContentSizeWithPreferMaxWidth:(CGFloat)maxWidth;
121 |
122 | //实际绘制行数,会在计算尺寸和绘制中赋值,只有成功触发计算尺寸及绘制后,值才是正确的
123 | -(NSUInteger) drawingLineCount;
124 |
125 | #pragma mark - 动画
126 | ///如果components中有gif,如果某些情况下动画停止了(比如label移除后又添加),可以使用此函数。
127 | -(void) letAnimStartOrStop:(BOOL) isStart;
128 | @end
129 |
--------------------------------------------------------------------------------
/RichText/AWRichTextLabel.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 |
9 | @class AWRichText;
10 | @interface AWRichTextLabel: UILabel
11 |
12 | +(instancetype) labelWithRichText:(AWRichText *)richText rtFrame:(CGRect)rtFrame;
13 | +(instancetype) labelWithRichText:(AWRichText *)richText;
14 |
15 | -(instancetype) initWithRichText:(AWRichText *)richText rtFrame:(CGRect)rtFrame;
16 | -(instancetype) initWithRichText:(AWRichText *)richText;
17 |
18 | @property (nonatomic, strong) AWRichText *richText;
19 |
20 | @property (nonatomic, unsafe_unretained) CGRect rtFrame;
21 |
22 | @property (nonatomic, unsafe_unretained) CGFloat rtMaxWidth;
23 |
24 | @property (nonatomic, unsafe_unretained) BOOL usingAutoLayout;
25 |
26 | @end
27 |
28 |
--------------------------------------------------------------------------------
/RichText/AWRichTextLabel.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRichTextLabel.h"
8 | #import "AWRichText.h"
9 | #import
10 |
11 | #define AWRichTextEC @"AWRichTextEC"
12 |
13 | @interface AWRichTextLabel()
14 | @property (nonatomic, weak) AWRTComponent *touchingComponent;
15 |
16 | @property (nonatomic, unsafe_unretained) BOOL isRedrawingRTLabel;
17 | @end
18 |
19 | @implementation AWRichTextLabel
20 |
21 | @synthesize richText=_richText;
22 |
23 | +(instancetype) labelWithRichText:(AWRichText *)richText rtFrame:(CGRect)rtFrame{
24 | return [[self alloc] initWithRichText:richText rtFrame:rtFrame];
25 | }
26 |
27 | +(instancetype) labelWithRichText:(AWRichText *)richText{
28 | return [[self alloc] initWithRichText:richText];
29 | }
30 |
31 | -(instancetype) initWithRichText:(AWRichText *)richText{
32 | return [self initWithRichText:richText rtFrame:CGRectZero];
33 | }
34 |
35 | -(instancetype)initWithRichText:(AWRichText *)richText rtFrame:(CGRect)rtFrame{
36 | self = [super init];
37 | if (self) {
38 | _rtFrame = rtFrame;
39 |
40 | self.richText = richText;
41 | }
42 | return self;
43 | }
44 |
45 | ///设置或修改richtext
46 | -(void)setRichText:(AWRichText *)richText{
47 | if ([self.richText isEqual:richText]) {
48 | return;
49 | }
50 |
51 | _richText = richText;
52 |
53 | [richText addListener:(id)self];
54 |
55 | self.userInteractionEnabled = YES;
56 |
57 | if ([self.richText checkIfInitingState]) {
58 | [self.richText setNeedsBuild];
59 | }else{
60 | [self redrawRichTextLabel];
61 | }
62 | }
63 |
64 | -(void)setRtFrame:(CGRect)rtFrame{
65 | if (CGRectEqualToRect(_rtFrame, rtFrame)) {
66 | return;
67 | }
68 |
69 | _rtFrame = rtFrame;
70 |
71 | [self redrawRichTextLabel];
72 | }
73 |
74 | -(void)setRtMaxWidth:(CGFloat)rtMaxWidth{
75 | if (_rtFrame.size.width == rtMaxWidth) {
76 | return;
77 | }
78 |
79 | _rtFrame = CGRectMake(_rtFrame.origin.x, _rtFrame.origin.y, rtMaxWidth, _rtFrame.size.height);
80 | }
81 |
82 | -(CGFloat)rtMaxWidth{
83 | return _rtFrame.size.width;
84 | }
85 |
86 | -(BOOL) usingAutoLayout{
87 | if (self.constraints.count > 0) {
88 | return YES;
89 | }
90 | return _usingAutoLayout;
91 | }
92 |
93 | #pragma mark - touch 处理
94 | -(AWRTComponent *)componentWithTouchPoint:(CGPoint) point{
95 | __block AWRTComponent *retComponent = nil;
96 | [self.richText enumationComponentsWithBlock:^(AWRTComponent *comp, BOOL *stop) {
97 | if (comp.touchable && (comp.touchCallback || self.richText.touchCallback)) {
98 | for (NSValue *touchRects in comp.touchRects) {
99 | if ([touchRects respondsToSelector:@selector(CGRectValue)]) {
100 | CGRect rect = touchRects.CGRectValue;
101 | if (CGRectContainsPoint(rect, point)) {
102 | retComponent = comp;
103 | *stop = YES;
104 | break;
105 | }
106 | }
107 | }
108 | }
109 | }];
110 | return retComponent;
111 | }
112 |
113 | -(void)notifyRichTextTouchComponent:(AWRTComponent *)comp touchEvent:(AWRTLabelTouchEvent) touchEvent{
114 | if (comp.touchCallback) {
115 | comp.touchCallback(comp, touchEvent);
116 | }
117 | if (self.richText.touchCallback) {
118 | self.richText.touchCallback(comp, touchEvent);
119 | }
120 | }
121 |
122 | -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
123 | UITouch *touch = touches.anyObject;
124 | CGPoint point = [touch locationInView:self];
125 | self.touchingComponent = [self componentWithTouchPoint:point];
126 | if (self.touchingComponent) {
127 | [self notifyRichTextTouchComponent:self.touchingComponent touchEvent:AWRTLabelTouchEventBegan];
128 | }
129 | }
130 |
131 | -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
132 | if (self.touchingComponent) {
133 | UITouch *touch = touches.anyObject;
134 | CGPoint point = [touch locationInView:self];
135 | AWRTComponent *touchingComp = [self componentWithTouchPoint:point];
136 | CGPoint prePoint = [touch previousLocationInView:self];
137 | AWRTComponent *preTouchingComp = [self componentWithTouchPoint:prePoint];
138 | BOOL isMovedIn = self.touchingComponent == touchingComp && self.touchingComponent != preTouchingComp;
139 | BOOL isMovedOut = self.touchingComponent != touchingComp && self.touchingComponent == preTouchingComp;
140 | if (isMovedIn) {
141 | [self notifyRichTextTouchComponent:self.touchingComponent touchEvent:AWRTLabelTouchEventMovedIn];
142 | }else if(isMovedOut){
143 | [self notifyRichTextTouchComponent:self.touchingComponent touchEvent:AWRTLabelTouchEventMovedOut];
144 | }else{
145 | [self notifyRichTextTouchComponent:self.touchingComponent touchEvent:AWRTLabelTouchEventMoved];
146 | }
147 | }
148 | }
149 |
150 | -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
151 | if (self.touchingComponent) {
152 | UITouch *touch = touches.anyObject;
153 | CGPoint point = [touch locationInView:self];
154 | AWRTComponent *touchingComp = [self componentWithTouchPoint:point];
155 | if (self.touchingComponent == touchingComp) {
156 | [self notifyRichTextTouchComponent:self.touchingComponent touchEvent:AWRTLabelTouchEventEndedIn];
157 | }else{
158 | [self notifyRichTextTouchComponent:self.touchingComponent touchEvent:AWRTLabelTouchEventEndedOut];
159 | }
160 | }
161 |
162 | self.touchingComponent = nil;
163 | }
164 |
165 | -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
166 | if (self.touchingComponent) {
167 | [self notifyRichTextTouchComponent:self.touchingComponent touchEvent:AWRTLabelTouchEventCancelled];
168 | }
169 |
170 | self.touchingComponent = nil;
171 | }
172 |
173 | #pragma mark - awrichtext delegate
174 | /// 收到richText更新的消息,触发drawRect进行重绘
175 | -(void)updatedForAWRichText:(AWRichText *)richText{
176 | if (richText == self.richText) {
177 | [self redrawRichTextLabel];
178 | }
179 | }
180 |
181 | #pragma mark - awrichtext update
182 | -(void) redrawRichTextLabel{
183 | if ([self.richText checkIfInitingState]) {
184 | return;
185 | }
186 |
187 | if (self.isRedrawingRTLabel) {
188 | return;
189 | }
190 |
191 | self.isRedrawingRTLabel = YES;
192 |
193 | if (self.usingAutoLayout) {
194 | self.preferredMaxLayoutWidth = _rtFrame.size.width;
195 | [self invalidateIntrinsicContentSize];
196 | }else{
197 | self.frame = _rtFrame;
198 | if (self.frame.size.width == 0 || self.frame.size.height == 0) {
199 | [self sizeToFit];
200 | }
201 | }
202 |
203 | [self setNeedsDisplay];
204 |
205 | self.isRedrawingRTLabel = NO;
206 | }
207 |
208 | #pragma mark - 绘制相关
209 | ///覆盖系统函数,使用coretext绘制文本
210 | -(void)drawRect:(CGRect)rect{
211 | [self.richText drawRect:self.bounds label:self];
212 | }
213 |
214 | ///根据属性,自行计算label的size
215 | -(CGSize)sizeThatFits:(CGSize)size{
216 | return [self.richText sizeThatFits:size];
217 | }
218 |
219 | ///autolayout中固有尺寸,用于自适应尺寸
220 | -(CGSize)intrinsicContentSize{
221 | return [self.richText intrinsicContentSizeWithPreferMaxWidth:self.preferredMaxLayoutWidth];
222 | }
223 |
224 | #pragma mark - coding
225 | -(instancetype)initWithCoder:(NSCoder *)aDecoder{
226 | if (self = [super initWithCoder:aDecoder]) {
227 | self.richText = [aDecoder decodeObjectForKey:AWRichTextEC];
228 | }
229 | return self;
230 | }
231 |
232 | -(void)encodeWithCoder:(NSCoder *)aCoder{
233 | [aCoder encodeObject:self.richText forKey:AWRichTextEC];
234 | }
235 |
236 | @end
237 |
238 |
--------------------------------------------------------------------------------
/RichText/components/AWRTAttachmentComponent.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTComponent.h"
8 |
9 | #import
10 |
11 | typedef enum : NSUInteger {
12 | AWRTAttchmentBoundsDependContent,
13 | AWRTAttchmentBoundsDependFont,
14 | AWRTAttchmentBoundsDependSet,
15 | } AWRTAttchmentBoundsDepend;
16 |
17 | typedef enum : NSUInteger {
18 | AWRTAttachmentAlignCenter,
19 | AWRTAttachmentAlignBottom,
20 | AWRTAttachmentAlignTop,
21 | } AWRTAttachmentAlign;
22 |
23 | typedef enum : NSUInteger {
24 | AWRTAttachmentViewScaleTypeByWidth,
25 | AWRTAttachmentViewScaleTypeByHeight,
26 | AWRTAttachmentViewScaleTypeAuto,
27 | } AWRTAttachmentViewScaleType;
28 |
29 | ///链式操作
30 | @class AWRTAttachmentComponent;
31 | typedef AWRTAttachmentComponent *(^AWRTAttachmentComponentChain)(id);
32 |
33 | /// component最终生成的附件,会传递给AWRichText作为绘制图像的依据(尺寸和内容)
34 | @interface AWRTAttachment : NSObject
35 | /// UIImage或UIView
36 | @property (nonatomic, strong) id content;
37 |
38 | /// 缩放模式
39 | @property (nonatomic, unsafe_unretained) AWRTAttachmentViewScaleType scaleType;
40 |
41 | /// 尺寸
42 | @property (nonatomic, unsafe_unretained) CGRect bounds;
43 |
44 | /// 对齐方式
45 | @property (nonatomic, unsafe_unretained) AWRTAttachmentAlign alignment;
46 |
47 | /// 字体信息
48 | @property (nonatomic, unsafe_unretained) CGFloat fontAscent;
49 | @property (nonatomic, unsafe_unretained) CGFloat fontDescent;
50 |
51 | /// 是否绘制框线
52 | @property (nonatomic, unsafe_unretained) BOOL debugFrame;
53 |
54 | /// core text attachment 载体
55 | @property (nonatomic, unsafe_unretained) CTRunDelegateRef ctRunDelegateRef;
56 |
57 | /// 计算出的字体信息
58 | @property (nonatomic, readonly, unsafe_unretained) CGFloat attachmentAscent;
59 | @property (nonatomic, readonly, unsafe_unretained) CGFloat attachmentDescent;
60 |
61 | @end
62 |
63 | @interface AWRTAttachmentComponent : AWRTComponent
64 |
65 | @property (nonatomic, strong) id content;
66 |
67 | /// 可用于AWRTViewComponent,AWImageComponent暂时未用此属性
68 | /// 用于AWRTViewComponent时,表示如果View的frame同coreText计算出来的位置大小不同时,View的缩放方式。
69 | /// 一般说来:boundsDepend为AWRTAttchmentBoundsDependFont和AWRTAttchmentBoundsDependSet时才会出现此种情况。
70 | /// AWRTAttachmentViewScaleTypeByWidth 表示按照宽度比例等比缩放
71 | /// AWRTAttachmentViewScaleTypeByWidth 表示按照高度比例等比缩放
72 | /// AWRTAttachmentViewScaleTypeByAuto 表示不缩放,只修改frame
73 | @property (nonatomic, unsafe_unretained) AWRTAttachmentViewScaleType scaleType;
74 |
75 | /// 缩放比例
76 | @property (nonatomic, unsafe_unretained) CGFloat contentSizeScale;
77 |
78 | /// 生成的attachment,此对象会被传递给AWRichText做绘制时的上下文
79 | @property (nonatomic, readonly, strong) AWRTAttachment *attachment;
80 |
81 | /// alignment 同周围文字的对齐方式
82 | @property (nonatomic, unsafe_unretained) AWRTAttachmentAlign alignment;
83 |
84 | /// bounds计算方法
85 | /// AWRTAttchmentBoundsDependContent 表示根据View或Image自身的尺寸绘制尺寸
86 | /// AWRTAttchmentBoundsDependFont 表示根据字体高度计算绘制的尺寸
87 | /// AWRTAttchmentBoundsDependSet 表示绘制的尺寸使用当前设置的bounds属性
88 | @property (nonatomic, unsafe_unretained) AWRTAttchmentBoundsDepend boundsDepend;
89 |
90 | /// 绘制尺寸
91 | @property (nonatomic, unsafe_unretained) CGRect bounds;
92 |
93 | /// 绘制位置偏移
94 | @property (nonatomic, unsafe_unretained) CGPoint offset;
95 |
96 | #pragma mark - build attach
97 | /// 创建AWRTAttachment
98 | -(void) buildAttachment;
99 |
100 | #pragma mark - override
101 | /// 获取View或Image的本身尺寸
102 | -(CGRect) contentPresetBounds;
103 |
104 | #pragma mark - chains
105 | -(AWRTAttachmentComponentChain)AWContent;
106 | -(AWRTAttachmentComponentChain)AWContentSizeScale;
107 | -(AWRTAttachmentComponentChain)AWBounds;
108 | -(AWRTAttachmentComponentChain)AWAlignment;
109 | -(AWRTAttachmentComponentChain)AWBoundsDepend;
110 | -(AWRTAttachmentComponentChain)AWOffsets;
111 | -(AWRTAttachmentComponentChain)AWScaleType;
112 |
113 | -(AWRTAttachmentComponentChain)AWFont;
114 | -(AWRTAttachmentComponentChain)AWPaddingLeft;
115 | -(AWRTAttachmentComponentChain)AWPaddingRight;
116 | -(AWRTAttachmentComponentChain)AWDebugFrame;
117 | @end
118 |
--------------------------------------------------------------------------------
/RichText/components/AWRTAttachmentComponent.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTAttachmentComponent.h"
8 |
9 | #define AWRTImageAttachBounds @"AWRTImageAttachBounds"
10 | #define AWRTImageAttachAlignment @"AWRTImageAttachAlignment"
11 | #define AWRTImageAttachFontAscent @"AWRTImageAttachFontAscent"
12 | #define AWRTImageAttachFontDescent @"AWRTImageAttachFontDescent"
13 | #define AWRTImageAttachDebugFrame @"AWRTImageAttachDebugFrame"
14 | #define AWRTImageAttachContent @"AWRTImageAttachContent"
15 | #define AWRTImageAttachScaleType @"AWRTImageAttachScaleType"
16 |
17 | #define AWRTAttachmentCompAlignment @"AWRTAttachmentCompAlignment"
18 | #define AWRTAttachmentCompBoundsDepend @"AWRTAttachmentCompBoundsDepend"
19 | #define AWRTAttachmentCompOffset @"AWRTAttachmentCompOffset"
20 | #define AWRTAttachmentCompBounds @"AWRTAttachmentCompBounds"
21 |
22 | @interface AWRTAttachment()
23 | @property (nonatomic, unsafe_unretained) CGFloat attachmentAscent;
24 | @property (nonatomic, unsafe_unretained) CGFloat attachmentDescent;
25 | @end
26 |
27 | @implementation AWRTAttachment
28 |
29 | -(void) calcAescentDesccent{
30 | CGFloat fontAscent = fabs(self.fontAscent);
31 | CGFloat fontDescent = fabs(self.fontDescent);
32 | CGFloat ascent = self.bounds.size.height;
33 | CGFloat descent = 0;
34 | switch (self.alignment) {
35 | case AWRTAttachmentAlignTop:
36 | ascent = fontAscent;
37 | descent = fontAscent - self.bounds.size.height;
38 | break;
39 | case AWRTAttachmentAlignBottom:
40 | ascent = self.bounds.size.height - fontDescent;
41 | descent = -fontDescent;
42 | break;
43 | case AWRTAttachmentAlignCenter:{
44 | CGFloat tmp = ((fontAscent + fontDescent) / 2 - fontDescent);
45 | CGFloat halfhei = self.bounds.size.height / 2;
46 | ascent = tmp + halfhei;
47 | descent = tmp - halfhei;
48 | }
49 | break;
50 | }
51 |
52 | self.attachmentAscent = ascent;
53 | self.attachmentDescent = -descent;
54 | }
55 |
56 | -(instancetype)initWithCoder:(NSCoder *)aDecoder{
57 | if (self = [super init]) {
58 | self.bounds = [[aDecoder decodeObjectForKey:AWRTImageAttachBounds] CGRectValue];
59 | self.alignment = [aDecoder decodeIntegerForKey:AWRTImageAttachAlignment];
60 | self.fontAscent = [aDecoder decodeFloatForKey:AWRTImageAttachFontAscent];
61 | self.fontDescent = [aDecoder decodeFloatForKey:AWRTImageAttachFontDescent];
62 | self.debugFrame = [aDecoder decodeBoolForKey:AWRTImageAttachDebugFrame];
63 | self.content = [aDecoder decodeObjectForKey:AWRTImageAttachContent];
64 | self.scaleType = [aDecoder decodeIntegerForKey:AWRTImageAttachScaleType];
65 | }
66 | return self;
67 | }
68 |
69 | -(void)encodeWithCoder:(NSCoder *)aCoder{
70 | [aCoder encodeObject:[NSValue valueWithCGRect:self.bounds] forKey:AWRTImageAttachBounds];
71 | [aCoder encodeInteger:self.alignment forKey:AWRTImageAttachAlignment];
72 | [aCoder encodeFloat:self.fontAscent forKey:AWRTImageAttachFontAscent];
73 | [aCoder encodeFloat:self.fontDescent forKey:AWRTImageAttachFontDescent];
74 | [aCoder encodeBool:self.debugFrame forKey:AWRTImageAttachDebugFrame];
75 | [aCoder encodeObject:self.content forKey:AWRTImageAttachContent];
76 | [aCoder encodeInteger:self.scaleType forKey:AWRTImageAttachScaleType];
77 | }
78 |
79 | -(id)copyWithZone:(NSZone *)zone{
80 | AWRTAttachment *newAttachment = [[AWRTAttachment alloc] init];
81 | newAttachment.bounds = self.bounds;
82 | newAttachment.alignment = self.alignment;
83 | newAttachment.fontAscent = self.fontAscent;
84 | newAttachment.fontDescent = self.fontDescent;
85 | newAttachment.debugFrame = self.debugFrame;
86 | newAttachment.scaleType = self.scaleType;
87 | return newAttachment;
88 | }
89 |
90 | @end
91 |
92 | #pragma mark ct run delegate funcs
93 | static CGFloat AWRTImageCTRunAscentCallback(void *ref){
94 | AWRTAttachment *attachment = (__bridge AWRTAttachment *)ref;
95 | return attachment.attachmentAscent;
96 | }
97 |
98 | static CGFloat AWRTImageCTRunDescentCallback(void *ref){
99 | AWRTAttachment *attachment = (__bridge AWRTAttachment *)ref;
100 | return attachment.attachmentDescent;
101 | }
102 |
103 | static CGFloat AWRTImageCTRunWidthCallback(void *ref){
104 | AWRTAttachment *attachment = (__bridge AWRTAttachment *)ref;
105 | return attachment.bounds.size.width;
106 | }
107 |
108 | static void AWDeallocCallback(void *ref) {
109 | AWRTAttachment *self = (__bridge_transfer AWRTAttachment *)(ref);
110 | self = nil; // release
111 | }
112 |
113 | @interface AWRTAttachmentComponent()
114 | @property (nonatomic, strong) AWRTAttachment *attachment;
115 | @end
116 |
117 | @implementation AWRTAttachmentComponent
118 |
119 | -(AWRTAttachmentComponentChain)AWContent{
120 | return ^(id content){
121 | self.content = content;
122 | return self;
123 | };
124 | }
125 |
126 | -(AWRTAttachmentComponentChain)AWScaleType{
127 | return ^(id scaleType){
128 | if ([scaleType respondsToSelector:@selector(integerValue)]) {
129 | self.scaleType = [scaleType integerValue];
130 | }
131 | return self;
132 | };
133 | }
134 |
135 | -(AWRTAttachmentComponentChain)AWContentSizeScale{
136 | return ^(id contentSizeScale){
137 | self.contentSizeScale = [contentSizeScale floatValue];
138 | return self;
139 | };
140 | }
141 |
142 | -(AWRTAttachmentComponentChain)AWBounds{
143 | return ^(id bounds){
144 | if ([bounds respondsToSelector:@selector(CGRectValue)]) {
145 | self.bounds = [bounds CGRectValue];
146 | }
147 | return self;
148 | };
149 | }
150 |
151 | -(AWRTAttachmentComponentChain)AWBoundsDepend{
152 | return ^(id boundsDepend){
153 | if ([boundsDepend respondsToSelector:@selector(integerValue)]) {
154 | self.boundsDepend = [boundsDepend integerValue];
155 | }
156 | return self;
157 | };
158 | }
159 |
160 | -(AWRTAttachmentComponentChain)AWOffsets{
161 | return ^(id off){
162 | if ([off respondsToSelector:@selector(CGPointValue)]) {
163 | self.offset = [off CGPointValue];
164 | }
165 | return self;
166 | };
167 | }
168 |
169 | -(AWRTAttachmentComponentChain)AWAlignment{
170 | return ^(id alignment){
171 | if ([alignment respondsToSelector:@selector(integerValue)]) {
172 | self.alignment = [alignment integerValue];
173 | }
174 | return self;
175 | };
176 | }
177 |
178 | -(AWRTAttachmentComponentChain)AWPaddingLeft{
179 | return (AWRTAttachmentComponentChain) [super AWPaddingLeft];
180 | }
181 |
182 | -(AWRTAttachmentComponentChain)AWPaddingRight{
183 | return (AWRTAttachmentComponentChain) [super AWPaddingRight];
184 | }
185 |
186 | -(AWRTAttachmentComponentChain)AWFont{
187 | return (AWRTAttachmentComponentChain)[super AWFont];
188 | }
189 |
190 | -(AWRTAttachmentComponentChain)AWDebugFrame{
191 | return (AWRTAttachmentComponentChain)[super AWDebugFrame];
192 | }
193 |
194 | -(NSSet *)editableAttributes{
195 | NSMutableSet *set = [NSMutableSet setWithArray:@[@"bounds", @"alignment", @"boundsDepend", @"offset", @"content", @"scaleType"]];
196 | [set unionSet:[super editableAttributes]];
197 | return set;
198 | }
199 |
200 | #pragma mark - override
201 | -(CGSize)contentSize{
202 | return CGSizeZero;
203 | }
204 |
205 | -(CGFloat)contentScale{
206 | return 1;
207 | }
208 |
209 | -(CGRect)contentPresetBounds{
210 | return CGRectZero;
211 | }
212 |
213 | #pragma mark - attach
214 | -(void) buildAttachment{
215 | if (!_attachment) {
216 | _attachment = [[AWRTAttachment alloc] init];
217 | }
218 | CGFloat fontAscent = self.font.ascender;
219 | CGFloat fontDescent = fabs(self.font.descender);
220 |
221 | //font的ascender,descender与coreText计算的存在误差。
222 | CGFloat off = (self.font.lineHeight - self.font.pointSize) / 2;
223 |
224 | _attachment.fontAscent = fontAscent - off;
225 | _attachment.fontDescent = fontDescent - off;
226 |
227 | _attachment.debugFrame = self.debugFrame;
228 |
229 | _attachment.content = self.content;
230 | _attachment.alignment = self.alignment;
231 | _attachment.bounds = self.bounds;
232 |
233 | _attachment.scaleType = self.scaleType;
234 |
235 | [_attachment calcAescentDesccent];
236 | }
237 |
238 | -(BOOL) calculateBounds {
239 | CGFloat scale = self.contentSizeScale;
240 | if (scale <= 0) {
241 | scale = 1;
242 | }
243 | CGRect presetBounds = self.contentPresetBounds;
244 | CGFloat contentWid = presetBounds.size.width * scale;
245 | CGFloat contentHei = presetBounds.size.height * scale;
246 | switch (self.boundsDepend) {
247 | case AWRTAttchmentBoundsDependSet:
248 | if (CGRectEqualToRect(self.bounds, CGRectZero)) {
249 | NSLog(@"_bounds cannot be Zero when boundsDepend is AWRTImageBoundsDependSet");
250 | return NO;
251 | }
252 | break;
253 | case AWRTAttchmentBoundsDependFont:{
254 | if (!self.font) {
255 | NSLog(@"_font cannot be nil when boundsDepend is AWRTImageBoundsDependFont");
256 | return NO;
257 | }
258 | if (contentWid <= 0 || contentHei <= 0) {
259 | // NSLog(@"contentWid and contentHei cannot be 0 when boundsDepend is AWRTImageBoundsDependFont");
260 | return NO;
261 | }
262 | CGFloat rate = contentWid / contentHei;
263 | CGFloat srcHei = fabs(self.font.ascender) + fabs(self.font.descender);
264 | contentHei = srcHei * scale;
265 | contentWid = contentHei * rate;
266 | self.bounds = CGRectMake(0, self.font.ascender - (srcHei / 2) * (scale - 1), contentWid, contentHei);
267 | }
268 | break;
269 | case AWRTAttchmentBoundsDependContent:{
270 | if (contentWid <= 0 || contentHei <= 0) {
271 | // NSLog(@"contentWid and contentHei cannot be 0 when boundsDepend is AWRTImageBoundsDependContent");
272 | return NO;
273 | }
274 | self.bounds = CGRectMake(presetBounds.origin.x, presetBounds.origin.y - (presetBounds.size.height / 2) * (scale - 1), contentWid, contentHei);
275 | }
276 | break;
277 | default:
278 | break;
279 | }
280 |
281 | if (CGRectEqualToRect(self.bounds, CGRectZero)) {
282 | NSLog(@" _bounds cant be Zero when calculateBounds");
283 | return NO;
284 | }
285 |
286 | if (!CGPointEqualToPoint(self.offset, CGPointZero)) {
287 | self.bounds = CGRectMake(self.bounds.origin.x + self.offset.x, self.bounds.origin.y + self.offset.y, self.bounds.size.width, self.bounds.size.height);
288 | }
289 | return YES;
290 | }
291 |
292 | -(NSAttributedString *)build{
293 | if (![self calculateBounds]) {
294 | return nil;
295 | }
296 |
297 | [self buildAttachment];
298 | if (!_attachment) {
299 | return nil;
300 | }
301 |
302 | CTRunDelegateCallbacks callbacks;
303 | callbacks.version = kCTRunDelegateCurrentVersion;
304 | callbacks.getAscent = AWRTImageCTRunAscentCallback;
305 | callbacks.getDescent = AWRTImageCTRunDescentCallback;
306 | callbacks.getWidth = AWRTImageCTRunWidthCallback;
307 | callbacks.dealloc = AWDeallocCallback;
308 | unichar objectReplacementChar = 0xFFFC;
309 | NSString *objectReplacementString = [NSString stringWithCharacters:&objectReplacementChar length:1];
310 |
311 | CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge_retained void *)_attachment);
312 | _attachment.ctRunDelegateRef = delegate;
313 |
314 | NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:objectReplacementString];
315 | [attributedString addAttribute:(NSString *)kCTRunDelegateAttributeName value:(__bridge id _Nonnull)(delegate) range:NSMakeRange(0, attributedString.length)];
316 | if (delegate) {
317 | CFRelease(delegate);
318 | }
319 |
320 | return attributedString;
321 | }
322 |
323 | #pragma mark - encode
324 | -(instancetype)initWithCoder:(NSCoder *)aDecoder{
325 | if (self = [super initWithCoder:aDecoder]) {
326 | self.alignment = [aDecoder decodeIntegerForKey:AWRTAttachmentCompAlignment];
327 | self.boundsDepend = [aDecoder decodeIntegerForKey:AWRTAttachmentCompBoundsDepend];
328 | self.offset = [[aDecoder decodeObjectForKey:AWRTAttachmentCompOffset] CGPointValue];
329 | self.bounds = [[aDecoder decodeObjectForKey:AWRTAttachmentCompBounds] CGRectValue];
330 | self.scaleType = [aDecoder decodeIntegerForKey:AWRTImageAttachScaleType];
331 | }
332 | return self;
333 | }
334 |
335 | -(void)encodeWithCoder:(NSCoder *)aCoder{
336 | [super encodeWithCoder:aCoder];
337 | [aCoder encodeInteger:self.alignment forKey:AWRTAttachmentCompAlignment];
338 | [aCoder encodeInteger:self.boundsDepend forKey:AWRTAttachmentCompBoundsDepend];
339 | [aCoder encodeObject:[NSValue valueWithCGPoint:self.offset] forKey:AWRTAttachmentCompOffset];
340 | [aCoder encodeObject:[NSValue valueWithCGRect:self.bounds] forKey:AWRTAttachmentCompBounds];
341 | [aCoder encodeInteger:self.scaleType forKey:AWRTImageAttachScaleType];
342 | }
343 |
344 | #pragma mark - copy
345 | -(id)copyWithZone:(NSZone *)zone{
346 | return ((AWRTAttachmentComponent *)[super copyWithZone:zone])
347 | .AWAlignment(@(self.alignment))
348 | .AWBoundsDepend(@(self.boundsDepend))
349 | .AWOffsets([NSValue valueWithCGPoint:self.offset])
350 | .AWBounds([NSValue valueWithCGRect:self.bounds])
351 | .AWScaleType(@(self.scaleType))
352 | ;
353 | }
354 |
355 | @end
356 |
--------------------------------------------------------------------------------
/RichText/components/AWRTComponent.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 |
9 | typedef enum : NSUInteger {
10 | AWRTLabelTouchEventBegan,
11 | AWRTLabelTouchEventMoved,
12 | AWRTLabelTouchEventMovedIn,
13 | AWRTLabelTouchEventMovedOut,
14 | AWRTLabelTouchEventEndedIn,
15 | AWRTLabelTouchEventEndedOut,
16 | AWRTLabelTouchEventCancelled
17 | } AWRTLabelTouchEvent;
18 |
19 | static inline BOOL awIsTouchingIn(AWRTLabelTouchEvent touchEvent){
20 | return touchEvent == AWRTLabelTouchEventBegan || touchEvent == AWRTLabelTouchEventMoved || touchEvent == AWRTLabelTouchEventMovedIn;
21 | }
22 |
23 | static inline BOOL awIsTouchingOut(AWRTLabelTouchEvent touchEvent){
24 | return touchEvent == AWRTLabelTouchEventMovedOut;
25 | }
26 |
27 | static inline BOOL awIsTouching(AWRTLabelTouchEvent touchEvent){
28 | return awIsTouchingIn(touchEvent) || awIsTouchingOut(touchEvent);
29 | }
30 |
31 | static inline BOOL awIsTouchEnd(AWRTLabelTouchEvent touchEvent){
32 | return !awIsTouching(touchEvent);
33 | }
34 |
35 | @protocol AWRTComponentUpdateDelegate
36 | #pragma mark - build
37 | -(void) setNeedsBuild;
38 | -(void) updateIfNeed;
39 | -(BOOL) checkIfBuildingState;
40 | -(BOOL) checkIfInitingState;
41 | @end
42 |
43 | extern const NSString *AWRTComponentDefaultMode;
44 |
45 | typedef id (^AWRTComponentChain)(id);
46 |
47 | @class AWRTComponent;
48 | typedef void (^AWRTComponentAsyncArchiveBlock)(AWRTComponent *comp);
49 |
50 | @interface AWRTComponent : NSObject
51 |
52 | /// 一般是对应的AWRichText
53 | @property (nonatomic, weak) id parent;
54 |
55 | /// 标识符
56 | @property (nonatomic, copy) NSString *tag;
57 |
58 | #pragma mark - 属性
59 | /// 调试线
60 | @property (nonatomic, unsafe_unretained) BOOL debugFrame;
61 |
62 | /// 字体,所有的AWRTComponent及其子类对象都应该设置font,因为Attachment对象计算位置时仍然需要参考font。
63 | @property (nonatomic, strong) UIFont *font;
64 |
65 | /// 左右有几个空格,如需要给某个Component添加左右填充空白,单位是空格。
66 | /// 如paddingleft为1则表示此Component输出的富文本会在左侧添加1个空格。
67 | @property (nonatomic, unsafe_unretained) NSInteger paddingLeft;
68 | @property (nonatomic, unsafe_unretained) NSInteger paddingRight;
69 |
70 | #pragma mark - 更新及模式
71 | @property (nonatomic, copy) NSString *currentMode;
72 |
73 | -(void) beginUpdateMode:(NSString *)updateMode storeAttributesWhenBegin:(BOOL) storeAttributesWhenBegin restoreAttributesWhenFinished:(BOOL) restoreAttributesWhenFinished block:(void(^)(void))block;
74 | -(void) beginUpdateMode:(NSString *)updateMode block:(void (^)(void))block;
75 |
76 | -(void) storeAllAttributesToMode:(NSString *)mode replace:(BOOL) replace;
77 |
78 | -(NSArray *)allModes;
79 |
80 | ///清除当前设置的所有属性
81 | -(void) emptyComponentAttributes;
82 |
83 | #pragma mark - touchable
84 | @property (nonatomic, unsafe_unretained) BOOL touchable;
85 | @property (nonatomic, unsafe_unretained) NSRange range;
86 | @property (nonatomic, strong) void (^touchCallback)(AWRTComponent *comp, AWRTLabelTouchEvent touchEvent);
87 | @property (nonatomic, strong) NSMutableArray *touchRects;
88 |
89 | #pragma mark - 获取attributedString
90 | //生成attributedString,调用会触发build
91 | @property (nonatomic, readonly, strong) NSAttributedString *attributedString;
92 | //清空生成的attributedString,一般用于重建组件
93 | -(void) clearAttributedString;
94 |
95 | //异步构造
96 | @property (nonatomic, strong) AWRTComponentAsyncArchiveBlock asyncArchiveBlock;
97 |
98 | #pragma mark - 子类需重载的函数
99 | -(void) onInit;
100 | -(NSSet *) editableAttributes;
101 | -(NSAttributedString *) build;
102 |
103 | #pragma mark - 链式操作
104 | -(AWRTComponentChain) AWAsyncArchiveBlock;
105 | -(AWRTComponentChain) AWPaddingRight;
106 | -(AWRTComponentChain) AWPaddingLeft;
107 | -(AWRTComponentChain) AWDebugFrame;
108 | -(AWRTComponentChain) AWFont;
109 | @end
110 |
111 |
--------------------------------------------------------------------------------
/RichText/components/AWRTComponent.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTComponent.h"
8 |
9 | #import "AWSimpleKVO.h"
10 |
11 | NSString *AWRTComponentDefaultMode = @"aw_rt_default_mode";
12 |
13 | #define AWRTCompFont @"AWRTCompFont"
14 | #define AWRTCompPaddingLeft @"AWRTCompPaddingLeft"
15 | #define AWRTCompPaddingRight @"AWRTCompPaddingRight"
16 | #define AWRTCompAttributesForMode @"AWRTCompAttributesForMode"
17 | #define AWRTCompCurrentMode @"AWRTCompCurrentMode"
18 | #define AWRTCompDebugFrame @"AWRTCompDebugFrame"
19 | #define AWRTCompTouchable @"AWRTCompTouchable"
20 |
21 | @interface AWRTComponent()
22 | @property (nonatomic, strong) NSMutableDictionary *attributesForMode;
23 | @property (nonatomic, strong) NSAttributedString *attributedString;
24 |
25 | @property (nonatomic, unsafe_unretained) BOOL ignoreKvoObserve;
26 | @end
27 |
28 | @implementation AWRTComponent
29 |
30 | #pragma mark - init
31 | - (instancetype)init {
32 | self = [super init];
33 | if (self) {
34 | [self onInit];
35 | }
36 | return self;
37 | }
38 |
39 | -(void)dealloc{
40 | [self _removeUpdateableObservers];
41 | }
42 |
43 | -(instancetype)initWithCoder:(NSCoder *)aDecoder{
44 | if(self){
45 | [self _addUpdateableObservers];
46 |
47 | self.font = [aDecoder decodeObjectForKey:AWRTCompFont];
48 | self.paddingLeft = [aDecoder decodeIntegerForKey:AWRTCompPaddingLeft];
49 | self.paddingRight = [aDecoder decodeIntegerForKey:AWRTCompPaddingRight];
50 | self.attributesForMode = [aDecoder decodeObjectForKey:AWRTCompAttributesForMode];
51 | self.currentMode = [aDecoder decodeObjectForKey:AWRTCompCurrentMode];
52 | self.debugFrame = [aDecoder decodeBoolForKey:AWRTCompDebugFrame];
53 | self.touchable = [aDecoder decodeBoolForKey:AWRTCompTouchable];
54 | }
55 | return self;
56 | }
57 |
58 | -(void)encodeWithCoder:(NSCoder *)aCoder{
59 | [aCoder encodeObject:self.font forKey:AWRTCompFont];
60 | [aCoder encodeInteger:self.paddingLeft forKey:AWRTCompPaddingLeft];
61 | [aCoder encodeInteger:self.paddingRight forKey:AWRTCompPaddingRight];
62 | [aCoder encodeObject:self.attributesForMode forKey:AWRTCompAttributesForMode];
63 | [aCoder encodeObject:self.currentMode forKey:AWRTCompCurrentMode];
64 | [aCoder encodeBool:self.debugFrame forKey:AWRTCompDebugFrame];
65 | [aCoder encodeBool:self.touchable forKey:AWRTCompTouchable];
66 | }
67 |
68 | -(id)copyWithZone:(NSZone *)zone{
69 | AWRTComponent *newComp = [[self.class alloc] init];
70 | newComp.font = [self.font copyWithZone:zone];
71 | newComp.paddingLeft = self.paddingLeft;
72 | newComp.paddingRight = self.paddingRight;
73 | newComp.attributesForMode = [self.attributesForMode mutableCopyWithZone:zone];
74 | newComp.currentMode = self.currentMode;
75 | newComp.debugFrame = self.debugFrame;
76 | newComp.touchable = self.touchable;
77 |
78 | return newComp;
79 | }
80 |
81 | #pragma mark - kvo
82 | -(void)_addUpdateableObservers{
83 | NSSet *editableAttributes = self.editableAttributes;
84 | for (NSString *key in editableAttributes) {
85 | __weak typeof(self) weakSelf = self;
86 | [self awAddObserverForKeyPath:key options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil block:^(NSObject *observer, NSString *keyPath, NSDictionary *change, void *context) {
87 | [weakSelf observeValueForKeyPath:key ofObject:weakSelf change:change context:context];
88 | }];
89 | }
90 | }
91 |
92 | -(void)_removeUpdateableObservers{
93 | NSSet *editableAttributes = self.editableAttributes;
94 | for (NSString *key in editableAttributes) {
95 | [self awRemoveObserverForKeyPath:key context:nil];
96 | }
97 | }
98 |
99 | -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
100 | if (self.ignoreKvoObserve) {
101 | return;
102 | }
103 | if ([change[@"old"] isEqual:change[@"new"]]) {
104 | return;
105 | }
106 |
107 | [self setNeedsBuild];
108 | }
109 |
110 | #pragma mark - update delegate
111 | -(void)setNeedsBuild{
112 | [self.parent setNeedsBuild];
113 | }
114 |
115 | -(void)updateIfNeed{
116 | [self.parent updateIfNeed];
117 | }
118 |
119 | -(BOOL)checkIfBuildingState{
120 | return [self.parent checkIfBuildingState];
121 | }
122 |
123 | -(BOOL)checkIfInitingState{
124 | return [self.parent checkIfInitingState];
125 | }
126 |
127 | #pragma mark - attributes
128 |
129 | -(void)setAsyncArchiveBlock:(AWRTComponentAsyncArchiveBlock)asyncArchiveBlock{
130 | _asyncArchiveBlock = asyncArchiveBlock;
131 | if (_asyncArchiveBlock) {
132 | _asyncArchiveBlock(self);
133 | }
134 | }
135 |
136 | -(AWRTComponentChain)AWAsyncArchiveBlock{
137 | return ^(id block){
138 | self.asyncArchiveBlock = block;
139 | return self;
140 | };
141 | }
142 |
143 | -(AWRTComponentChain) AWPaddingLeft{
144 | return ^(id paddingLeft){
145 | self.paddingLeft = [paddingLeft integerValue];
146 | return self;
147 | };
148 | }
149 |
150 | -(AWRTComponentChain) AWPaddingRight{
151 | return ^(id paddingRight){
152 | self.paddingRight = [paddingRight integerValue];
153 | return self;
154 | };
155 | }
156 |
157 | -(AWRTComponentChain)AWDebugFrame{
158 | return ^(id debugFrame){
159 | if ([debugFrame respondsToSelector:@selector(integerValue)]) {
160 | self.debugFrame = [debugFrame integerValue];
161 | }
162 | return self;
163 | };
164 | }
165 |
166 | -(AWRTComponentChain) AWFont{
167 | return ^(id font){
168 | if ([font isKindOfClass:[UIFont class]]) {
169 | self.font = font;
170 | }
171 | return self;
172 | };
173 | }
174 |
175 | #pragma mark - pading help methods
176 |
177 | -(NSString *)paddingStringWithCount:(NSInteger) count{
178 | if (count > 0) {
179 | NSMutableString *tempString = [NSMutableString new];
180 | for (int i = 0; i < count; i++) {
181 | [tempString appendString:@" "];
182 | }
183 | return tempString;
184 | }
185 | return nil;
186 | }
187 |
188 | -(NSAttributedString *) applyPaddingForAttributedString:(NSAttributedString *)as{
189 | if (!self.paddingLeft && !self.paddingRight) {
190 | return as;
191 | }
192 |
193 | NSMutableAttributedString * attributedString = [as mutableCopy];
194 |
195 | NSDictionary *paddingAttributes = nil;
196 | if (self.font) {
197 | paddingAttributes = @{NSFontAttributeName:self.font};
198 | }
199 | NSString *paddingLeft = [self paddingStringWithCount:self.paddingLeft];
200 | if (paddingLeft) {
201 | NSMutableAttributedString *newAttributedString = [[NSMutableAttributedString alloc] initWithString:paddingLeft attributes:paddingAttributes];
202 | [newAttributedString appendAttributedString:attributedString];
203 | attributedString = newAttributedString;
204 | }
205 |
206 | NSString *paddingRight = [self paddingStringWithCount:self.paddingRight];
207 | if (paddingRight) {
208 | [attributedString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:paddingRight attributes:paddingAttributes]];
209 | }
210 | return attributedString;
211 | }
212 |
213 | #pragma mark - override
214 | -(void)onInit{
215 | self.attributesForMode = [[NSMutableDictionary alloc] init];
216 | self.currentMode = AWRTComponentDefaultMode;
217 | [self _addUpdateableObservers];
218 | }
219 |
220 | -(NSAttributedString *)attributedString{
221 | [self _build];
222 | return _attributedString;
223 | }
224 |
225 | -(void) clearAttributedString{
226 | _attributedString = nil;
227 | [self setNeedsBuild];
228 | }
229 |
230 | -(NSAttributedString *)build{
231 | return nil;
232 | }
233 |
234 | -(NSSet *)editableAttributes{
235 | return [NSSet setWithArray:@[@"paddingLeft", @"paddingRight", @"font", @"debugFrame"]];
236 | }
237 |
238 | -(void) _build{
239 | _attributedString = [self applyPaddingForAttributedString:[self build]];
240 | }
241 |
242 | #pragma mark - mode
243 | ///清除当前设置的所有属性
244 | -(NSDictionary *) allAttributesDict{
245 | NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
246 | for (NSString *key in self.editableAttributes) {
247 | dict[key] = [self valueForKey:key];
248 | }
249 | return dict;
250 | }
251 |
252 | -(void) emptyComponentAttributes{
253 | self.ignoreKvoObserve = YES;
254 | for (NSString *key in self.editableAttributes) {
255 | if([[self valueForKey:key] isKindOfClass:[NSValue class]]){
256 | [self setValue:@0 forKey:key];
257 | } else {
258 | [self setValue:nil forKey:key];
259 | }
260 | }
261 | self.ignoreKvoObserve = NO;
262 | }
263 |
264 | -(void) applyAttributesWithMode:(NSString *)mode{
265 | NSDictionary *currModeDict = self.attributesForMode[mode];
266 | if (![currModeDict isKindOfClass:[NSDictionary class]] || currModeDict.count <= 0) {
267 | return;
268 | }
269 |
270 | ///清除旧值
271 | [self emptyComponentAttributes];
272 |
273 | ///设置新值
274 | self.ignoreKvoObserve = YES;
275 | for (NSString *key in currModeDict.allKeys) {
276 | [self setValue:currModeDict[key] forKey:key];
277 | }
278 | self.ignoreKvoObserve = NO;
279 | }
280 |
281 | -(void)setCurrentMode:(NSString *)mode{
282 | if ([_currentMode isEqualToString:mode]) {
283 | return;
284 | }
285 |
286 | if (_currentMode && !self.attributesForMode[mode]) {
287 | return;
288 | }
289 |
290 | _currentMode = mode;
291 |
292 | [self applyAttributesWithMode:_currentMode];
293 |
294 | [self setNeedsBuild];
295 | }
296 |
297 | -(NSArray *)allModes{
298 | return self.attributesForMode.allKeys;
299 | }
300 |
301 | -(void) beginUpdateMode:(NSString *)updateMode block:(void (^)(void))block{
302 | [self beginUpdateMode:updateMode storeAttributesWhenBegin:YES restoreAttributesWhenFinished:YES block:block];
303 | }
304 |
305 | -(void) beginUpdateMode:(NSString *)updateMode storeAttributesWhenBegin:(BOOL) storeAttributesWhenBegin restoreAttributesWhenFinished:(BOOL) restoreAttributesWhenFinished block:(void(^)(void))block{
306 | if (!storeAttributesWhenBegin) {
307 | [self emptyComponentAttributes];
308 | }
309 |
310 | block();
311 |
312 | self.attributesForMode[updateMode] = self.allAttributesDict;
313 |
314 | if (restoreAttributesWhenFinished) {
315 | [self applyAttributesWithMode:self.currentMode];
316 | }
317 | }
318 |
319 | -(void)storeAllAttributesToMode:(NSString *)mode replace:(BOOL)replace{
320 | NSMutableDictionary *currDict = [self.attributesForMode[mode] mutableCopy];
321 | if ([currDict isKindOfClass:[NSDictionary class]] && currDict.count > 0) {
322 | if (replace) {
323 | self.attributesForMode[mode] = self.allAttributesDict;
324 | }else{
325 | NSDictionary *attributes = self.allAttributesDict;
326 | for (NSString *key in attributes.allKeys) {
327 | if (currDict[key] == nil) {
328 | currDict[key] = attributes[key];
329 | }
330 | }
331 | self.attributesForMode[mode] = currDict;
332 | }
333 | }else{
334 | self.attributesForMode[mode] = self.allAttributesDict;
335 | }
336 | }
337 |
338 | @end
339 |
340 |
--------------------------------------------------------------------------------
/RichText/components/AWRTImageComponent.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTAttachmentComponent.h"
8 |
9 | @class AWRTImageComponent;
10 | typedef AWRTImageComponent *(^AWRTImageComponentChain)(id);
11 |
12 | @interface AWRTImageComponent : AWRTAttachmentComponent
13 |
14 | //对图片进行等比缩放
15 | @property (nonatomic, unsafe_unretained) CGFloat imageScale;
16 |
17 | #pragma mark chain funcs
18 | -(AWRTImageComponentChain)AWImagePath;
19 | -(AWRTImageComponentChain)AWImage;
20 | -(AWRTImageComponentChain)AWImageScale;
21 |
22 | -(AWRTImageComponentChain)AWContent;
23 | -(AWRTImageComponentChain)AWBounds;
24 | -(AWRTImageComponentChain)AWAlignment;
25 | -(AWRTImageComponentChain)AWBoundsDepend;
26 | -(AWRTImageComponentChain)AWOffsets;
27 |
28 | -(AWRTImageComponentChain)AWFont;
29 | -(AWRTImageComponentChain)AWPaddingLeft;
30 | -(AWRTImageComponentChain)AWPaddingRight;
31 | -(AWRTImageComponentChain)AWDebugFrame;
32 |
33 | -(AWRTImageComponentChain) AWAsyncArchiveBlock;
34 |
35 | @end
36 |
--------------------------------------------------------------------------------
/RichText/components/AWRTImageComponent.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTImageComponent.h"
8 |
9 | #define AWRTImageCompImageScale @"AWRTImageCompImageScale"
10 | #define AWRTImageCompImage @"AWRTImageCompImage"
11 |
12 | @interface AWRTImageComponent()
13 | @property (nonatomic, strong) UIImage *image;
14 | @end
15 |
16 | @implementation AWRTImageComponent
17 |
18 | #pragma mark init
19 | -(void)onInit{
20 | [super onInit];
21 | self.imageScale = 1;
22 | }
23 |
24 | #pragma mark - override
25 | -(CGRect)contentPresetBounds{
26 | return CGRectMake(0, 0, _image.size.width, _image.size.height);
27 | }
28 |
29 | -(NSAttributedString *)build{
30 | if (!_image) {
31 | return nil;
32 | }
33 |
34 | return [super build];
35 | }
36 |
37 | -(NSSet *)editableAttributes{
38 | NSMutableSet *set = [NSMutableSet setWithArray:@[@"image", @"font", @"paddingLeft", @"paddingRight", @"imageScale"]];
39 | [set unionSet:[super editableAttributes]];
40 | return set;
41 | }
42 |
43 | #pragma mark - attributes
44 | -(void)setImage:(UIImage *)image{
45 | if (![image isKindOfClass:[UIImage class]]) {
46 | return;
47 | }
48 |
49 | _image = image;
50 |
51 | self.content = image;
52 | }
53 |
54 | -(void)_setImagePath:(NSString *)imagePath{
55 | if (![imagePath isKindOfClass:[NSString class]] || imagePath.length == 0) {
56 | return;
57 | }
58 | dispatch_async(dispatch_get_global_queue(0, 0), ^{
59 | UIImage *image = nil;
60 | if (imagePath.isAbsolutePath) {
61 | image = [UIImage imageWithContentsOfFile:imagePath];
62 | }else{
63 | image = [UIImage imageNamed:imagePath];
64 | }
65 | if (image) {
66 | dispatch_async(dispatch_get_main_queue(), ^{
67 | if (image) {
68 | self.image = image;
69 | }
70 | });
71 | }
72 | });
73 | }
74 |
75 | #pragma mark - chain
76 | -(AWRTImageComponentChain)AWImage{
77 | return ^(id image){
78 | if ([image isKindOfClass:[UIImage class]]) {
79 | self.image = image;
80 | }
81 | return self;
82 | };
83 | }
84 |
85 | -(AWRTImageComponentChain)AWImageScale{
86 | return ^(id scale){
87 | if ([scale respondsToSelector:@selector(floatValue)]) {
88 | self.imageScale = [scale floatValue];
89 | }
90 | return self;
91 | };
92 | }
93 |
94 | -(AWRTImageComponentChain)AWImagePath{
95 | return ^(id path){
96 | if ([path isKindOfClass:[NSString class]] && [path length] > 0) {
97 | [self _setImagePath:path];
98 | }
99 | return self;
100 | };
101 | }
102 |
103 | -(AWRTImageComponentChain)AWContent{
104 | return (AWRTImageComponentChain) [super AWContent];
105 | }
106 |
107 | -(AWRTImageComponentChain)AWBoundsDepend{
108 | return (AWRTImageComponentChain) [super AWBoundsDepend];
109 | }
110 |
111 | -(AWRTImageComponentChain)AWBounds{
112 | return (AWRTImageComponentChain) [super AWBounds];
113 | }
114 |
115 | -(AWRTImageComponentChain)AWAlignment{
116 | return (AWRTImageComponentChain) [super AWAlignment];
117 | }
118 |
119 | -(AWRTImageComponentChain)AWOffsets{
120 | return (AWRTImageComponentChain) [super AWOffsets];
121 | }
122 |
123 | -(AWRTImageComponentChain)AWFont{
124 | return (AWRTImageComponentChain) [super AWFont];
125 | }
126 |
127 | -(AWRTImageComponentChain)AWPaddingLeft{
128 | return (AWRTImageComponentChain) [super AWPaddingLeft];
129 | }
130 |
131 | -(AWRTImageComponentChain)AWPaddingRight{
132 | return (AWRTImageComponentChain) [super AWPaddingRight];
133 | }
134 |
135 | -(AWRTImageComponentChain)AWDebugFrame{
136 | return (AWRTImageComponentChain) [super AWDebugFrame];
137 | }
138 |
139 | -(AWRTImageComponentChain)AWAsyncArchiveBlock{
140 | return (AWRTImageComponentChain) [super AWAsyncArchiveBlock];
141 | }
142 |
143 | #pragma mark - coding
144 | -(instancetype)initWithCoder:(NSCoder *)aDecoder{
145 | if (self = [super initWithCoder:aDecoder]) {
146 | self.imageScale = [aDecoder decodeFloatForKey:AWRTImageCompImageScale];
147 | self.image = [aDecoder decodeObjectForKey:AWRTImageCompImage];
148 | }
149 | return self;
150 | }
151 |
152 | -(void)encodeWithCoder:(NSCoder *)aCoder{
153 | [super encodeWithCoder:aCoder];
154 | [aCoder encodeFloat:self.imageScale forKey:AWRTImageCompImageScale];
155 | [aCoder encodeObject:self.image forKey:AWRTImageCompImage];
156 | }
157 |
158 | #pragma mark - copy
159 | -(id)copyWithZone:(NSZone *)zone{
160 | return ((AWRTImageComponent *)[super copyWithZone:zone])
161 | .AWImage([self.image copy]).AWImageScale(@(self.imageScale));
162 | }
163 |
164 | @end
165 |
--------------------------------------------------------------------------------
/RichText/components/AWRTTextComponent.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTComponent.h"
8 |
9 | @class AWRTTextComponent;
10 | typedef AWRTTextComponent *(^AWTextComponentChain)(id);
11 |
12 | @interface AWRTTextComponent : AWRTComponent
13 | ///文字背景色
14 | @property (nonatomic, strong) UIColor *backgroundColor;
15 |
16 | ///文字颜色
17 | @property (nonatomic, strong) UIColor *color;
18 |
19 | ///文本
20 | @property (nonatomic, copy) NSString *text;
21 |
22 | ///左右分隔符,如引号,书名号等,例如 [我是文本]
23 | @property (nonatomic, copy) NSString *leftSeperateString;
24 | @property (nonatomic, copy) NSString *rightSeperateString;
25 |
26 | ///分隔符颜色
27 | @property (nonatomic, strong) UIColor *seperateStringColor;
28 |
29 | ///阴影
30 | @property (nonatomic, unsafe_unretained) CGFloat shadowBlurRadius;
31 | @property (nonatomic, strong) UIColor *shadowColor;
32 | @property (nonatomic, unsafe_unretained) CGSize shadowOffset;
33 |
34 | ///描边
35 | @property (nonatomic, strong) UIColor *strokeColor;
36 | @property (nonatomic, unsafe_unretained) CGFloat strokeWidth;
37 |
38 | ///下划线
39 | @property (nonatomic, strong) UIColor *underlineColor;
40 | @property (nonatomic, unsafe_unretained) NSUnderlineStyle underlineStyle;
41 |
42 | ///连字符:NSLigatureAttributeName
43 | @property (nonatomic, unsafe_unretained) NSInteger ligature;
44 |
45 | ///字符间距:NSKernAttributeName
46 | @property (nonatomic, unsafe_unretained) CGFloat kern;
47 |
48 | ///删除线:NSStrikethroughStyleAttributeName
49 | @property (nonatomic, unsafe_unretained) NSInteger strikethroughStyle;
50 | @property (nonatomic, strong) UIColor *strikethroughColor;
51 |
52 | ///链接:NSLinkAttributeName
53 | @property (nonatomic, copy) NSString *linkUrl;
54 |
55 | ///基线偏移:NSBaselineOffsetAttributeName
56 | @property (nonatomic, unsafe_unretained) CGFloat baselineOffset;
57 |
58 | ///未实现:
59 | ///NSParagraphStyleAttributeName(可在AWRichText中设置)
60 | ///NSVerticalGlyphFormAttributeName(横排/竖排)
61 | ///NSTextEffectAttributeName
62 | ///NSObliquenessAttributeName
63 | ///NSExpansionAttributeName
64 | ///NSWritingDirectionAttributeName(固定为左右方向)
65 |
66 | #pragma mark chain
67 | -(AWTextComponentChain) AWShadowBlurRadius;
68 | -(AWTextComponentChain) AWShadowColor;
69 | -(AWTextComponentChain) AWShadowOffset;
70 | -(AWTextComponentChain) AWStrokeColor;
71 | -(AWTextComponentChain) AWStrokeWidth;
72 | -(AWTextComponentChain) AWColor;
73 | -(AWTextComponentChain) AWBackgroundColor;
74 | -(AWTextComponentChain) AWText;
75 | -(AWTextComponentChain) AWLeftSeperateString;
76 | -(AWTextComponentChain) AWRightSeperateString;
77 | -(AWTextComponentChain) AWSeperateStringColor;
78 | -(AWTextComponentChain) AWUnderlineColor;
79 | -(AWTextComponentChain) AWUnderlineStyle;
80 |
81 | -(AWTextComponentChain) AWLigature;
82 | -(AWTextComponentChain) AWKern;
83 | -(AWTextComponentChain) AWStrikethroughStyle;
84 | -(AWTextComponentChain) AWStrikethroughColor;
85 | -(AWTextComponentChain) AWLinkUrl;
86 | -(AWTextComponentChain) AWBaselineOffset;
87 |
88 | -(AWTextComponentChain) AWFont;
89 | -(AWTextComponentChain) AWPaddingLeft;
90 | -(AWTextComponentChain) AWPaddingRight;
91 | -(AWTextComponentChain) AWDebugFrame;
92 |
93 | -(AWTextComponentChain) AWAsyncArchiveBlock;
94 |
95 | @end
96 |
--------------------------------------------------------------------------------
/RichText/components/AWRTTextComponent.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTTextComponent.h"
8 |
9 | #define AWRTTextCompShadowColor @"AWRTTextCompShadowColor"
10 | #define AWRTTextCompShadowOffset @"AWRTTextCompShadowOffset"
11 | #define AWRTTextCompShadowBlurRadius @"AWRTTextCompShadowBlurRadius"
12 | #define AWRTTextCompStrokeColor @"AWRTTextCompStrokeColor"
13 | #define AWRTTextCompStrokeWidth @"AWRTTextCompStrokeWidth"
14 | #define AWRTTextCompBgColor @"AWRTTextCompBgColor"
15 | #define AWRTTextCompColor @"AWRTTextCompColor"
16 | #define AWRTTextCompText @"AWRTTextCompText"
17 | #define AWRTTextCompLeftSeperateString @"AWRTTextCompLeftSeperateString"
18 | #define AWRTTextCompRightSeperateString @"AWRTTextCompRightSeperateString"
19 | #define AWRTTextCompSeperateStringColor @"AWRTTextCompSeperateStringColor"
20 | #define AWRTTextCompUnderlineColor @"AWRTTextCompUnderlineColor"
21 | #define AWRTTextCompUnderlineStyle @"AWRTTextCompUnderlineStyle"
22 |
23 | #define AWRTTextCompLigature @"AWRTTextCompLigature"
24 | #define AWRTTextCompKern @"AWRTTextCompKern"
25 | #define AWRTTextCompStrikethroughStyle @"AWRTTextCompStrikethroughStyle"
26 | #define AWRTTextCompStrikethroughColor @"AWRTTextCompStrikethroughColor"
27 | #define AWRTTextCompLinkUrl @"AWRTTextCompLinkUrl"
28 | #define AWRTTextCompBaselineOffset @"AWRTTextCompBaselineOffset"
29 |
30 | @interface AWRTTextComponent()
31 | @property (nonatomic, copy) NSString *showText;
32 | @end
33 |
34 | @implementation AWRTTextComponent
35 |
36 | #pragma mark - 生命周期
37 | -(void)onInit{
38 | [super onInit];
39 | self.backgroundColor = [UIColor clearColor];
40 | self.shadowOffset = CGSizeMake(1, 1);
41 | self.font = [UIFont systemFontOfSize:14];
42 | }
43 |
44 | #pragma mark - encode
45 | -(instancetype)initWithCoder:(NSCoder *)aDecoder{
46 | if (self = [super initWithCoder:aDecoder]) {
47 | self.shadowColor = [aDecoder decodeObjectForKey:AWRTTextCompShadowColor];
48 | self.shadowOffset = [[aDecoder decodeObjectForKey:AWRTTextCompShadowOffset] CGSizeValue];
49 | self.shadowBlurRadius = [aDecoder decodeFloatForKey:AWRTTextCompShadowBlurRadius];
50 |
51 | self.strokeColor = [aDecoder decodeObjectForKey:AWRTTextCompStrokeColor];
52 | self.strokeWidth = [aDecoder decodeFloatForKey:AWRTTextCompStrokeWidth];
53 |
54 | self.backgroundColor = [aDecoder decodeObjectForKey:AWRTTextCompBgColor];
55 | self.color = [aDecoder decodeObjectForKey:AWRTTextCompColor];
56 |
57 | self.text = [aDecoder decodeObjectForKey:AWRTTextCompText];
58 | self.leftSeperateString = [aDecoder decodeObjectForKey:AWRTTextCompLeftSeperateString];
59 | self.rightSeperateString = [aDecoder decodeObjectForKey:AWRTTextCompRightSeperateString];
60 | self.seperateStringColor = [aDecoder decodeObjectForKey:AWRTTextCompSeperateStringColor];
61 |
62 | self.underlineColor = [aDecoder decodeObjectForKey:AWRTTextCompUnderlineColor];
63 | self.underlineStyle = [aDecoder decodeIntegerForKey:AWRTTextCompUnderlineStyle];
64 |
65 | self.ligature = [aDecoder decodeIntegerForKey:AWRTTextCompLigature];
66 | self.kern = [aDecoder decodeFloatForKey:AWRTTextCompKern];
67 | self.strikethroughStyle = [aDecoder decodeIntegerForKey:AWRTTextCompStrikethroughStyle];
68 | self.strikethroughColor = [aDecoder decodeObjectForKey:AWRTTextCompStrikethroughColor];
69 | self.linkUrl = [aDecoder decodeObjectForKey:AWRTTextCompLinkUrl];
70 | self.baselineOffset = [aDecoder decodeFloatForKey:AWRTTextCompBaselineOffset];
71 | }
72 | return self;
73 | }
74 |
75 | -(void)encodeWithCoder:(NSCoder *)aCoder{
76 | [super encodeWithCoder:aCoder];
77 | [aCoder encodeObject:self.shadowColor forKey:AWRTTextCompShadowColor];
78 | [aCoder encodeObject:[NSValue valueWithCGSize:self.shadowOffset] forKey:AWRTTextCompShadowOffset];
79 | [aCoder encodeFloat:self.shadowBlurRadius forKey:AWRTTextCompShadowBlurRadius];
80 |
81 | [aCoder encodeObject:self.strokeColor forKey:AWRTTextCompStrokeColor];
82 | [aCoder encodeFloat:self.strokeWidth forKey:AWRTTextCompStrokeWidth];
83 |
84 | [aCoder encodeObject:self.color forKey:AWRTTextCompColor];
85 | [aCoder encodeObject:self.backgroundColor forKey:AWRTTextCompBgColor];
86 |
87 | [aCoder encodeObject:self.text forKey:AWRTTextCompText];
88 | [aCoder encodeObject:self.leftSeperateString forKey:AWRTTextCompLeftSeperateString];
89 | [aCoder encodeObject:self.rightSeperateString forKey:AWRTTextCompRightSeperateString];
90 | [aCoder encodeObject:self.seperateStringColor forKey:AWRTTextCompSeperateStringColor];
91 |
92 | [aCoder encodeObject:self.underlineColor forKey:AWRTTextCompUnderlineColor];
93 | [aCoder encodeInteger:self.underlineStyle forKey:AWRTTextCompUnderlineStyle];
94 |
95 | [aCoder encodeInteger:self.ligature forKey:AWRTTextCompLigature];
96 | [aCoder encodeFloat:self.kern forKey:AWRTTextCompKern];
97 | [aCoder encodeInteger:self.strikethroughStyle forKey:AWRTTextCompStrikethroughStyle];
98 | [aCoder encodeObject:self.strikethroughColor forKey:AWRTTextCompStrikethroughColor];
99 | [aCoder encodeObject:self.linkUrl forKey:AWRTTextCompLinkUrl];
100 | [aCoder encodeFloat:self.baselineOffset forKey:AWRTTextCompBaselineOffset];
101 | }
102 |
103 | #pragma mark - copy
104 | -(id)copyWithZone:(NSZone *)zone{
105 | return ((AWRTTextComponent *)[super copyWithZone:zone])
106 | .AWShadowColor([self.shadowColor copyWithZone:zone])
107 | .AWShadowOffset([NSValue valueWithCGSize:self.shadowOffset])
108 | .AWShadowBlurRadius(@(self.shadowBlurRadius))
109 | .AWStrokeColor([self.strokeColor copyWithZone:zone])
110 | .AWStrokeWidth(@(self.strokeWidth))
111 | .AWColor([self.color copyWithZone:zone])
112 | .AWBackgroundColor([self.backgroundColor copyWithZone:zone])
113 | .AWText([self.text copyWithZone:zone])
114 | .AWLeftSeperateString([self.leftSeperateString copyWithZone:zone])
115 | .AWRightSeperateString([self.rightSeperateString copyWithZone:zone])
116 | .AWSeperateStringColor([self.seperateStringColor copyWithZone:zone])
117 | .AWUnderlineStyle(@(self.underlineStyle))
118 | .AWUnderlineColor([self.underlineColor copyWithZone:zone])
119 | .AWLigature(@(self.ligature))
120 | .AWKern(@(self.kern))
121 | .AWStrikethroughStyle(@(self.strikethroughStyle))
122 | .AWStrikethroughColor([self.strikethroughColor copyWithZone:zone])
123 | .AWLinkUrl([self.linkUrl copyWithZone:zone])
124 | .AWBaselineOffset(@(self.baselineOffset))
125 | ;
126 | }
127 |
128 | #pragma mark - override
129 |
130 | -(NSAttributedString *)build{
131 | if (![self.text isKindOfClass:[NSString class]] || self.text.length == 0) {
132 | NSLog(@" build failed for self.text is invalid %@", @"");
133 | return nil;
134 | }
135 |
136 | ///使用showText
137 | self.showText = self.text;
138 |
139 | ///字体
140 | UIFont *font = self.font;
141 |
142 | NSMutableDictionary *defaultDict = [NSMutableDictionary dictionary];
143 | assert(font != nil);
144 | defaultDict[NSFontAttributeName] = font;
145 |
146 | ///文字颜色
147 | if (!self.color) {
148 | self.color = [UIColor blackColor];
149 | }
150 | defaultDict[NSForegroundColorAttributeName] = self.color;
151 |
152 | ///背景色
153 | if (self.backgroundColor) {
154 | defaultDict[NSBackgroundColorAttributeName] = self.backgroundColor;
155 | }
156 |
157 | ///shadow
158 | NSShadow *strShadow = nil;
159 | if (self.shadowBlurRadius != 0 && self.shadowColor) {
160 | strShadow = [[NSShadow alloc] init];
161 | strShadow.shadowBlurRadius = self.shadowBlurRadius;
162 | strShadow.shadowColor = self.shadowColor;
163 | strShadow.shadowOffset = self.shadowOffset;
164 | defaultDict[NSShadowAttributeName] = strShadow;
165 | }
166 |
167 | ///stroke
168 | if (self.strokeColor && self.strokeWidth != 0) {
169 | defaultDict[NSStrokeColorAttributeName] = self.strokeColor;
170 | defaultDict[NSStrokeWidthAttributeName] = @(self.strokeWidth);
171 | }
172 |
173 | ///underline
174 | if ([self.underlineColor isKindOfClass:[UIColor class]] && self.underlineStyle > 0) {
175 | defaultDict[NSUnderlineColorAttributeName] = self.underlineColor;
176 | defaultDict[NSUnderlineStyleAttributeName] = @(self.underlineStyle);
177 | }
178 |
179 | ///ligature
180 | if (self.ligature > 0) {
181 | defaultDict[NSLigatureAttributeName] = @(self.ligature);
182 | }
183 |
184 | ///kern
185 | if (self.kern > 0) {
186 | defaultDict[NSKernAttributeName] = @(self.kern);
187 | }
188 |
189 | ///strike
190 | if (self.strikethroughColor && self.strikethroughStyle > 0) {
191 | defaultDict[NSStrikethroughColorAttributeName] = self.strikethroughColor;
192 | defaultDict[NSStrikethroughStyleAttributeName] = @(self.strikethroughStyle);
193 | }
194 |
195 | ///linkUrl
196 | if (self.linkUrl) {
197 | defaultDict[NSLinkAttributeName] = self.linkUrl;
198 | }
199 |
200 | ///baseline offset
201 | if (self.baselineOffset) {
202 | defaultDict[NSBaselineOffsetAttributeName] = @(self.baselineOffset);
203 | }
204 |
205 | NSMutableAttributedString *textAttrString = [[NSMutableAttributedString alloc] initWithString:self.showText
206 | attributes:defaultDict];
207 |
208 | ///分隔符 left
209 | NSMutableAttributedString *mutableAttrString = nil;
210 | NSMutableDictionary *sepAttributes = nil;
211 |
212 | if (self.leftSeperateString || self.rightSeperateString) {
213 | mutableAttrString = [NSMutableAttributedString new];
214 |
215 | sepAttributes = [defaultDict mutableCopy];
216 | if (self.seperateStringColor) {
217 | sepAttributes[NSForegroundColorAttributeName] = self.seperateStringColor;
218 | }
219 | }
220 |
221 | if (self.leftSeperateString) {
222 | NSAttributedString *leftSepAttrStr = [[NSAttributedString alloc] initWithString:self.leftSeperateString
223 | attributes:sepAttributes];
224 |
225 | [mutableAttrString appendAttributedString:leftSepAttrStr];
226 | }
227 |
228 | ///分隔符 right
229 | if (self.rightSeperateString) {
230 | NSAttributedString *rightSepAttrStr = [[NSAttributedString alloc] initWithString:self.rightSeperateString
231 | attributes:sepAttributes];
232 | [mutableAttrString appendAttributedString:textAttrString];
233 | [mutableAttrString appendAttributedString:rightSepAttrStr];
234 | }else{
235 | [mutableAttrString appendAttributedString:textAttrString];
236 | }
237 |
238 | NSMutableAttributedString *retAttributedString = mutableAttrString ? mutableAttrString : textAttrString;
239 |
240 | ///防止属性类似的相邻component被解析成同一个CTRun影响触摸事件
241 | NSMutableAttributedString *markAttributedString = [[NSMutableAttributedString alloc] initWithString:@"\ufffc"
242 | attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:0.1]}];
243 | [retAttributedString appendAttributedString:markAttributedString];
244 |
245 | return retAttributedString;
246 | }
247 |
248 | -(NSSet *)editableAttributes{
249 | NSMutableSet *set = [NSMutableSet setWithArray:@[@"shadowBlurRadius", @"shadowColor", @"shadowOffset", @"strokeColor", @"strokeWidth", @"color", @"backgroundColor", @"text", @"leftSeperateString", @"rightSeperateString", @"seperateStringColor", @"font", @"paddingLeft", @"paddingRight", @"underlineColor", @"underlineStyle", @"ligature", @"kern", @"strikethroughStyle", @"strikethroughColor", @"linkUrl", @"baselineOffset"]];
250 | [set unionSet:[super editableAttributes]];
251 | return set;
252 | }
253 |
254 | #pragma mark - attributes
255 |
256 | -(void)setBackgroundColor:(UIColor *)backgroundColor{
257 | if ([_backgroundColor isEqual:backgroundColor]) {
258 | return;
259 | }
260 | _backgroundColor = backgroundColor;
261 | }
262 |
263 | -(void)setColor:(UIColor *)color{
264 | if ([_color isEqual:color]) {
265 | return;
266 | }
267 | _color = color;
268 | }
269 |
270 | -(void)setText:(NSString *)text{
271 | if ([_text isEqualToString:text]) {
272 | return;
273 | }
274 | _text = text;
275 | }
276 |
277 | -(void)setLeftSeperateString:(NSString *)leftSeperateString{
278 | if ([_leftSeperateString isEqualToString:leftSeperateString]) {
279 | return;
280 | }
281 | _leftSeperateString = leftSeperateString;
282 | }
283 |
284 | -(void)setRightSeperateString:(NSString *)rightSeperateString{
285 | if ([_rightSeperateString isEqualToString:rightSeperateString]) {
286 | return;
287 | }
288 | _rightSeperateString = rightSeperateString;
289 | }
290 |
291 | -(void)setSeperateStringColor:(UIColor *)seperateStringColor{
292 | if ([_seperateStringColor isEqual:seperateStringColor]) {
293 | return;
294 | }
295 | _seperateStringColor = seperateStringColor;
296 | }
297 |
298 | -(void)setShadowBlurRadius:(CGFloat)shadowBlurRadius{
299 | if (_shadowBlurRadius == shadowBlurRadius) {
300 | return;
301 | }
302 | _shadowBlurRadius = shadowBlurRadius;
303 | }
304 |
305 | -(void)setShadowColor:(UIColor *)shadowColor{
306 | if ([_shadowColor isEqual:shadowColor]) {
307 | return;
308 | }
309 | _shadowColor = shadowColor;
310 | }
311 |
312 | -(void)setShadowOffset:(CGSize )shadowOffset{
313 | if (CGSizeEqualToSize(shadowOffset, _shadowOffset)) {
314 | return;
315 | }
316 | _shadowOffset = shadowOffset;
317 | }
318 |
319 | -(void)setStrokeColor:(UIColor *)strokeColor{
320 | if ([_strokeColor isEqual:strokeColor]) {
321 | return;
322 | }
323 | _strokeColor = strokeColor;
324 | }
325 |
326 | -(void)setStrokeWidth:(CGFloat)strokeWidth{
327 | if (_strokeWidth == strokeWidth) {
328 | return;
329 | }
330 | _strokeWidth = strokeWidth;
331 | }
332 |
333 | -(void)setUnderlineColor:(UIColor *)underlineColor{
334 | if ([_underlineColor isEqual:underlineColor]) {
335 | return;
336 | }
337 | _underlineColor = underlineColor;
338 | }
339 |
340 | -(void)setUnderlineStyle:(NSUnderlineStyle)underlineStyle{
341 | if (_underlineStyle == underlineStyle) {
342 | return;
343 | }
344 | _underlineStyle = underlineStyle;
345 | }
346 |
347 | -(void)setLigature:(NSInteger)ligature{
348 | if (_ligature == ligature) {
349 | return;
350 | }
351 | _ligature = ligature;
352 | }
353 |
354 | -(void)setKern:(CGFloat)kern{
355 | if (_kern == kern) {
356 | return;
357 | }
358 | _kern = kern;
359 | }
360 |
361 | -(void)setStrikethroughStyle:(NSInteger)strikethroughStyle{
362 | if (_strikethroughStyle == strikethroughStyle) {
363 | return;
364 | }
365 | _strikethroughStyle = strikethroughStyle;
366 | }
367 |
368 | -(void)setStrikethroughColor:(UIColor *)strikethroughColor{
369 | if ([_strikethroughColor isEqual:strikethroughColor]) {
370 | return;
371 | }
372 | _strikethroughColor = strikethroughColor;
373 | }
374 |
375 | -(void)setLinkUrl:(NSString *)linkUrl{
376 | if ([_linkUrl isEqualToString:linkUrl]) {
377 | return;
378 | }
379 | _linkUrl = linkUrl;
380 | }
381 |
382 | -(void)setBaselineOffset:(CGFloat)baselineOffset{
383 | if (_baselineOffset == baselineOffset) {
384 | return;
385 | }
386 | _baselineOffset = baselineOffset;
387 | }
388 |
389 | #pragma mark chain funs
390 | -(AWTextComponentChain) AWShadowBlurRadius{
391 | return ^(id shadowBlurRadius){
392 | if ([shadowBlurRadius respondsToSelector:@selector(floatValue)]) {
393 | self.shadowBlurRadius = [shadowBlurRadius floatValue];
394 | }
395 | return self;
396 | };
397 | }
398 |
399 | -(AWTextComponentChain) AWShadowColor{
400 | return ^(id shadowColor){
401 | if ([shadowColor isKindOfClass:[UIColor class]]) {
402 | self.shadowColor = shadowColor;
403 | }
404 | return self;
405 | };
406 | }
407 |
408 | -(AWTextComponentChain) AWShadowOffset{
409 | return ^(id shadowOffset){
410 | if ([shadowOffset respondsToSelector:@selector(CGSizeValue)]) {
411 | self.shadowOffset = [shadowOffset CGSizeValue];
412 | }
413 | return self;
414 | };
415 | }
416 |
417 | -(AWTextComponentChain) AWStrokeColor{
418 | return ^(id strokeColor){
419 | if ([strokeColor isKindOfClass:[UIColor class]]) {
420 | self.strokeColor = strokeColor;
421 | }
422 | return self;
423 | };
424 | }
425 |
426 | -(AWTextComponentChain) AWStrokeWidth{
427 | return ^(id strokeWidth){
428 | if ([strokeWidth respondsToSelector:@selector(floatValue)]) {
429 | self.strokeWidth = [strokeWidth floatValue];
430 | }
431 | return self;
432 | };
433 | }
434 |
435 | -(AWTextComponentChain) AWColor{
436 | return ^(id color){
437 | if ([color isKindOfClass:[UIColor class]]) {
438 | self.color = color;
439 | }
440 | return self;
441 | };
442 | }
443 |
444 | -(AWTextComponentChain) AWBackgroundColor{
445 | return ^(id color){
446 | if ([color isKindOfClass:[UIColor class]]) {
447 | self.backgroundColor = color;
448 | }
449 | return self;
450 | };
451 | }
452 |
453 | -(AWTextComponentChain) AWText{
454 | return ^(id text){
455 | if ([text isKindOfClass:[NSString class]]) {
456 | self.text = text;
457 | }
458 | return self;
459 | };
460 | }
461 |
462 | -(AWTextComponentChain) AWLeftSeperateString{
463 | return ^(id leftSeperateString){
464 | if ([leftSeperateString isKindOfClass:[NSString class]]) {
465 | self.leftSeperateString = leftSeperateString;
466 | }
467 | return self;
468 | };
469 | }
470 |
471 | -(AWTextComponentChain) AWRightSeperateString{
472 | return ^(id rightSeperateString){
473 | if ([rightSeperateString isKindOfClass:[NSString class]]) {
474 | self.rightSeperateString = rightSeperateString;
475 | }
476 | return self;
477 | };
478 | }
479 |
480 | -(AWTextComponentChain) AWSeperateStringColor{
481 | return ^(id seperateStringColor){
482 | if ([seperateStringColor isKindOfClass:[UIColor class]]) {
483 | self.seperateStringColor = seperateStringColor;
484 | }
485 | return self;
486 | };
487 | }
488 |
489 | -(AWTextComponentChain) AWUnderlineColor{
490 | return ^(id color){
491 | if ([color isKindOfClass:[UIColor class]]) {
492 | self.underlineColor = color;
493 | }
494 | return self;
495 | };
496 | }
497 |
498 | -(AWTextComponentChain) AWUnderlineStyle{
499 | return ^(id style){
500 | if ([style respondsToSelector:@selector(integerValue)]) {
501 | self.underlineStyle = [style integerValue];
502 | }
503 | return self;
504 | };
505 | }
506 |
507 | -(AWTextComponentChain)AWPaddingLeft{
508 | return (AWTextComponentChain) [super AWPaddingLeft];
509 | }
510 |
511 | -(AWTextComponentChain)AWPaddingRight{
512 | return (AWTextComponentChain) [super AWPaddingRight];
513 | }
514 |
515 | -(AWTextComponentChain)AWFont{
516 | return (AWTextComponentChain)[super AWFont];
517 | }
518 |
519 | -(AWTextComponentChain)AWDebugFrame{
520 | return (AWTextComponentChain)[super AWDebugFrame];
521 | }
522 |
523 | -(AWTextComponentChain) AWLigature{
524 | return ^(id ligature){
525 | if ([ligature respondsToSelector:@selector(integerValue)]) {
526 | self.ligature = [ligature integerValue];
527 | }
528 | return self;
529 | };
530 | }
531 |
532 | -(AWTextComponentChain) AWKern{
533 | return ^(id kern){
534 | if ([kern respondsToSelector:@selector(floatValue)]) {
535 | self.kern = [kern integerValue];
536 | }
537 | return self;
538 | };
539 | }
540 |
541 | -(AWTextComponentChain) AWStrikethroughStyle{
542 | return ^(id strikethroughStyle){
543 | if ([strikethroughStyle respondsToSelector:@selector(integerValue)]) {
544 | self.strikethroughStyle = [strikethroughStyle integerValue];
545 | }
546 | return self;
547 | };
548 | }
549 |
550 | -(AWTextComponentChain) AWStrikethroughColor{
551 | return ^(id strikethroughColor){
552 | if ([strikethroughColor isKindOfClass:[UIColor class]]) {
553 | self.strikethroughColor = strikethroughColor;
554 | }
555 | return self;
556 | };
557 | }
558 |
559 | -(AWTextComponentChain) AWLinkUrl{
560 | return ^(id linkUrl){
561 | if ([linkUrl isKindOfClass:[NSString class]]) {
562 | self.linkUrl = linkUrl;
563 | }
564 | return self;
565 | };
566 | }
567 |
568 | -(AWTextComponentChain) AWBaselineOffset{
569 | return ^(id baselineOffset){
570 | if ([baselineOffset respondsToSelector:@selector(floatValue)]) {
571 | self.baselineOffset = [baselineOffset floatValue];
572 | }
573 | return self;
574 | };
575 | }
576 |
577 | -(AWTextComponentChain) AWAsyncArchiveBlock{
578 | return (AWTextComponentChain) [super AWAsyncArchiveBlock];
579 | }
580 |
581 | @end
582 |
--------------------------------------------------------------------------------
/RichText/components/AWRTViewComponent.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTAttachmentComponent.h"
8 |
9 | @class AWRTViewComponent;
10 | typedef AWRTViewComponent *(^AWRTViewComponentChain)(id);
11 |
12 | @interface AWRTViewComponent : AWRTAttachmentComponent
13 |
14 | ///view请提前设置好尺寸
15 | @property (nonatomic, strong) UIView *view;
16 |
17 | #pragma mark - chain
18 | -(AWRTViewComponentChain)AWView;
19 |
20 | -(AWRTViewComponentChain)AWContent;
21 | -(AWRTViewComponentChain)AWBounds;
22 | -(AWRTViewComponentChain)AWAlignment;
23 | -(AWRTViewComponentChain)AWBoundsDepend;
24 | -(AWRTViewComponentChain)AWOffsets;
25 | -(AWRTViewComponentChain)AWScaleType;
26 |
27 | -(AWRTViewComponentChain)AWFont;
28 | -(AWRTViewComponentChain)AWPaddingLeft;
29 | -(AWRTViewComponentChain)AWPaddingRight;
30 | -(AWRTViewComponentChain)AWDebugFrame;
31 |
32 | -(AWRTViewComponentChain)AWAsyncArchiveBlock;
33 | @end
34 |
--------------------------------------------------------------------------------
/RichText/components/AWRTViewComponent.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTViewComponent.h"
8 |
9 | #define AWRTViewCompView @"AWRTViewCompView"
10 |
11 | @implementation AWRTViewComponent
12 |
13 | #pragma mark - override
14 | -(void)onInit{
15 | [super onInit];
16 | }
17 |
18 | -(CGRect)contentPresetBounds{
19 | return _view.frame;
20 | }
21 |
22 | #pragma mark - attributes
23 | -(void)setView:(UIView *)view{
24 | if (![view isKindOfClass:[UIView class]]) {
25 | return;
26 | }
27 | if (_view == view) {
28 | return;
29 | }
30 | _view = view;
31 | self.content = view;
32 | }
33 |
34 | #pragma mark - chain
35 | -(AWRTViewComponentChain)AWView{
36 | return ^(id view){
37 | if ([view isKindOfClass:[UIView class]]) {
38 | self.view = view;
39 | }
40 | return self;
41 | };
42 | }
43 |
44 | -(AWRTViewComponentChain)AWContent{
45 | return (AWRTViewComponentChain) [super AWContent];
46 | }
47 |
48 | -(AWRTViewComponentChain)AWBoundsDepend{
49 | return (AWRTViewComponentChain) [super AWBoundsDepend];
50 | }
51 |
52 | -(AWRTViewComponentChain)AWBounds{
53 | return (AWRTViewComponentChain) [super AWBounds];
54 | }
55 |
56 | -(AWRTViewComponentChain)AWAlignment{
57 | return (AWRTViewComponentChain) [super AWAlignment];
58 | }
59 |
60 | -(AWRTViewComponentChain)AWOffsets{
61 | return (AWRTViewComponentChain) [super AWOffsets];
62 | }
63 |
64 | -(AWRTViewComponentChain)AWScaleType{
65 | return (AWRTViewComponentChain) [super AWScaleType];
66 | }
67 |
68 | -(AWRTViewComponentChain)AWFont{
69 | return (AWRTViewComponentChain) [super AWFont];
70 | }
71 |
72 | -(AWRTViewComponentChain)AWPaddingLeft{
73 | return (AWRTViewComponentChain) [super AWPaddingLeft];
74 | }
75 |
76 | -(AWRTViewComponentChain)AWPaddingRight{
77 | return (AWRTViewComponentChain) [super AWPaddingRight];
78 | }
79 |
80 | -(AWRTViewComponentChain)AWDebugFrame{
81 | return (AWRTViewComponentChain) [super AWDebugFrame];
82 | }
83 |
84 | -(AWRTViewComponentChain)AWAsyncArchiveBlock{
85 | return (AWRTViewComponentChain) [super AWAsyncArchiveBlock];
86 | }
87 |
88 |
89 | #pragma mark - coding
90 | -(instancetype)initWithCoder:(NSCoder *)aDecoder{
91 | if (self = [super initWithCoder:aDecoder]) {
92 | self.view = [aDecoder decodeObjectForKey:AWRTViewCompView];
93 | }
94 | return self;
95 | }
96 |
97 | -(void)encodeWithCoder:(NSCoder *)aCoder{
98 | [super encodeWithCoder:aCoder];
99 | [aCoder encodeObject:self.view forKey:AWRTViewCompView];
100 | }
101 |
102 | #pragma mark - copy
103 | -(id)copyWithZone:(NSZone *)zone{
104 | AWRTViewComponent *viewComp = ((AWRTViewComponent *)[super copyWithZone:zone]);
105 |
106 | NSData *viewData = nil;
107 | if (self.view) {
108 | viewData = [NSKeyedArchiver archivedDataWithRootObject:self.view];
109 | if (viewData) {
110 | viewComp.AWView([NSKeyedUnarchiver unarchiveObjectWithData:viewData]);
111 | }
112 | }
113 |
114 | return viewComp;
115 | }
116 |
117 | @end
118 |
--------------------------------------------------------------------------------
/RichText/tool/AWRTWeekRefrence.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 |
9 | ///弱引用实现
10 | @interface AWRTWeekRefrence : NSObject
11 |
12 | @property (nonatomic, readonly, weak) id ref;
13 |
14 | -(instancetype) initWithRef:(id)ref;
15 |
16 | +(void)test;
17 | @end
18 |
--------------------------------------------------------------------------------
/RichText/tool/AWRTWeekRefrence.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import "AWRTWeekRefrence.h"
8 |
9 | @interface TestAWWeakRef: NSObject
10 | @property (nonatomic, unsafe_unretained) int i;
11 | @end
12 |
13 | @implementation TestAWWeakRef
14 |
15 | +(instancetype) instanceWithI:(int) i{
16 | return [[self alloc] initWithI:i];
17 | }
18 |
19 | - (instancetype)initWithI:(int) i
20 | {
21 | self = [super init];
22 | if (self) {
23 | self.i = i;
24 | }
25 | return self;
26 | }
27 |
28 | @end
29 |
30 | @interface AWRTWeekRefrence()
31 | @property (nonatomic, weak) id ref;
32 | @end
33 |
34 | @implementation AWRTWeekRefrence
35 |
36 | -(instancetype)initWithRef:(id)ref{
37 | _ref = ref;
38 | return self;
39 | }
40 |
41 | -(void)forwardInvocation:(NSInvocation *)invocation{
42 | if (_ref) {
43 | [invocation invokeWithTarget:_ref];
44 | }else{
45 | void *ret = NULL;
46 | [invocation setReturnValue:&ret];
47 | }
48 | }
49 |
50 | -(NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
51 | if(_ref){
52 | return [_ref methodSignatureForSelector:sel];
53 | }else{
54 | return [NSObject methodSignatureForSelector:@selector(init)];
55 | }
56 | }
57 |
58 | -(BOOL)respondsToSelector:(SEL)aSelector{
59 | return [_ref respondsToSelector:aSelector];
60 | }
61 |
62 | - (BOOL)isEqual:(id)object{
63 | return [_ref isEqual:object];
64 | }
65 |
66 | -(NSUInteger)hash{
67 | return [_ref hash];
68 | }
69 |
70 | -(Class)superclass{
71 | return [_ref superclass];
72 | }
73 |
74 | -(Class)class{
75 | return [_ref class];
76 | }
77 |
78 | #pragma clang diagnostic push
79 | #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
80 | - (id)performSelector:(SEL)aSelector{
81 | if ([_ref respondsToSelector:aSelector]) {
82 | return [_ref performSelector:aSelector];
83 | }
84 | return nil;
85 | }
86 | - (id)performSelector:(SEL)aSelector withObject:(id)object{
87 | if ([_ref respondsToSelector:aSelector]) {
88 | return [_ref performSelector:aSelector withObject:object];
89 | }
90 | return nil;
91 | }
92 | - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2{
93 | if ([_ref respondsToSelector:aSelector]) {
94 | return [_ref performSelector:aSelector withObject:object1 withObject:object2];
95 | }
96 | return nil;
97 | }
98 | #pragma clang diagnostic pop
99 |
100 | - (BOOL)isProxy{
101 | return YES;
102 | }
103 |
104 | - (BOOL)isKindOfClass:(Class)aClass{
105 | return [_ref isKindOfClass:aClass];
106 | }
107 |
108 | - (BOOL)isMemberOfClass:(Class)aClass{
109 | return [_ref isMemberOfClass:aClass];
110 | }
111 |
112 | - (BOOL)conformsToProtocol:(Protocol *)aProtocol{
113 | return [_ref conformsToProtocol:aProtocol];
114 | }
115 |
116 | +(void) test{
117 | NSMutableArray *testObjArr = [[NSMutableArray alloc] init];
118 | for (int i = 0; i < 10; i++) {
119 | [testObjArr addObject:[TestAWWeakRef instanceWithI:i]];
120 | }
121 |
122 | NSMutableArray *testWeakArr = [[NSMutableArray alloc] init];
123 | for (int i = 0; i < 10; i++) {
124 | [testWeakArr addObject:[[AWRTWeekRefrence alloc]initWithRef:testObjArr[i]]];
125 | }
126 |
127 | for (int i = 0; i < 10; i++) {
128 | NSLog(@"weak[%d]=%d", i, [testWeakArr[i] i]);
129 | }
130 |
131 | NSLog(@" ---- ");
132 | testObjArr = nil;
133 |
134 | for (int i = 0; i < 10; i++) {
135 | NSLog(@"weak[%d]=%d", i, [testWeakArr[i] i]);
136 | }
137 |
138 | }
139 |
140 | @end
141 |
--------------------------------------------------------------------------------
/RichText/tool/AWSimpleKVO.h:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 | #import
8 |
9 | @interface AWSimpleKVO : NSObject
10 |
11 | -(instancetype) initWithObj:(NSObject *)obj;
12 |
13 | -(void)awAddObserverForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(void (^)(NSObject *observer, NSString *keyPath, NSDictionary *change, void *context)) block;
14 |
15 | -(void)awRemoveObserverForKeyPath:(NSString *)keyPath context:(void *)context;
16 |
17 | -(void)awSetValue:(id)value forKey:(NSString*)key;
18 |
19 | @end
20 |
21 | #define AWSIMPLEKVOPREFIX @"AWSimpleKVO_"
22 |
23 | @class AWSimpleKVO;
24 | @interface NSObject(AWSimpleKVO)
25 | -(AWSimpleKVO *)awSimpleKVO;
26 | -(void)awAddObserverForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(void (^)(NSObject *observer, NSString *keyPath, NSDictionary *change, void *context)) block;
27 | -(void)awRemoveObserverForKeyPath:(NSString *)keyPath context:(void *)context;
28 | -(BOOL)awIsObserving;
29 | -(void)awSetValue:(id) value forKey:(NSString *)key;
30 | @end
31 |
--------------------------------------------------------------------------------
/RichText/tool/AWSimpleKVO.m:
--------------------------------------------------------------------------------
1 | /*
2 | copyright 2018 wanghongyu.
3 | The project page:https://github.com/hardman/AWRichText
4 | My blog page: http://www.jianshu.com/u/1240d2400ca1
5 | */
6 |
7 |
8 | #import "AWSimpleKVO.h"
9 |
10 | #import
11 | #import
12 |
13 | #import
14 |
15 | @interface AWSimpleKVOCounterItem: NSObject
16 | @property (nonatomic, copy) NSString *className;
17 | @property (nonatomic, unsafe_unretained) NSInteger count;
18 | @end
19 |
20 | @implementation AWSimpleKVOCounterItem
21 | @end
22 |
23 | @interface AWSimpleKVOCounter: NSObject
24 | @property (nonatomic, strong) NSMutableDictionary * items;
25 | @end
26 |
27 | @implementation AWSimpleKVOCounter
28 |
29 | -(NSMutableDictionary *)items{
30 | if (!_items) {
31 | _items = [[NSMutableDictionary alloc] init];
32 | }
33 | return _items;
34 | }
35 |
36 | -(void) increaceForClassName:(NSString *)name{
37 | @synchronized(self){
38 | AWSimpleKVOCounterItem *item = self.items[name];
39 | if(!item){
40 | item = [[AWSimpleKVOCounterItem alloc] init];
41 | item.className = name;
42 | self.items[name] = item;
43 | }
44 | item.count++;
45 | }
46 | }
47 |
48 | -(void) reduceForClassName:(NSString *)name{
49 | @synchronized(self){
50 | AWSimpleKVOCounterItem *item = self.items[name];
51 | NSAssert(item != nil, @"错误");
52 | item.count --;
53 | if (item.count <= 0) {
54 | objc_disposeClassPair(NSClassFromString(name));
55 | self.items[name] = nil;
56 | }
57 | }
58 | }
59 |
60 | @end
61 |
62 | static AWSimpleKVOCounter *sSimpleKVOCounter = nil;
63 |
64 | static AWSimpleKVOCounter *awGetSimpleKVOCounter(){
65 | if (sSimpleKVOCounter == nil) {
66 | sSimpleKVOCounter = [[AWSimpleKVOCounter alloc] init];
67 | }
68 | return sSimpleKVOCounter;
69 | }
70 |
71 | typedef enum : NSUInteger {
72 | AWSimpleKVOSupporedIvarTypeUnknown,
73 | AWSimpleKVOSupporedIvarTypeChar,
74 | AWSimpleKVOSupporedIvarTypeInt,
75 | AWSimpleKVOSupporedIvarTypeShort,
76 | AWSimpleKVOSupporedIvarTypeLong,
77 | AWSimpleKVOSupporedIvarTypeLongLong,
78 | AWSimpleKVOSupporedIvarTypeUChar,
79 | AWSimpleKVOSupporedIvarTypeUInt,
80 | AWSimpleKVOSupporedIvarTypeUShort,
81 | AWSimpleKVOSupporedIvarTypeULong,
82 | AWSimpleKVOSupporedIvarTypeULongLong,
83 | AWSimpleKVOSupporedIvarTypeFloat,
84 | AWSimpleKVOSupporedIvarTypeDouble,
85 | AWSimpleKVOSupporedIvarTypeBool,
86 | AWSimpleKVOSupporedIvarTypeObject,
87 |
88 | AWSimpleKVOSupporedIvarTypeCGSize,
89 | AWSimpleKVOSupporedIvarTypeCGPoint,
90 | AWSimpleKVOSupporedIvarTypeCGRect,
91 | AWSimpleKVOSupporedIvarTypeCGVector,
92 | AWSimpleKVOSupporedIvarTypeCGAffineTransform,
93 | AWSimpleKVOSupporedIvarTypeUIEdgeInsets,
94 | AWSimpleKVOSupporedIvarTypeUIOffset,
95 | } AWSimpleKVOSupporedIvarType;
96 |
97 | @interface AWSimpleKVOItem: NSObject
98 | @property (nonatomic, copy) NSString *keyPath;
99 | @property (nonatomic, unsafe_unretained) void *context;
100 | @property (nonatomic, strong) void (^block)(NSObject *observer, NSString *keyPath, NSDictionary *change, void *context);
101 |
102 | @property (nonatomic, strong) id oldValue;
103 |
104 | @property (nonatomic, unsafe_unretained) AWSimpleKVOSupporedIvarType ivarType;
105 | @property (nonatomic, copy) NSString *ivarTypeCode;
106 |
107 | #pragma inner
108 | @property (nonatomic, unsafe_unretained) IMP _localMethod;
109 | @property (nonatomic, unsafe_unretained) IMP _superMethod;
110 | @property (nonatomic, unsafe_unretained) SEL _setSel;
111 | @property (nonatomic, copy) NSString *_localMethodTypeCoding;
112 | @end
113 |
114 | @implementation AWSimpleKVOItem
115 | -(BOOL) isValid {
116 | return self.block && [self.keyPath isKindOfClass:[NSString class]];
117 | }
118 |
119 | -(BOOL) invokeBlockWithChange:(NSDictionary *)change obj:(NSObject *)obj{
120 | if ([self isValid]){
121 | self.block(obj, self.keyPath, change, self.context);
122 | return YES;
123 | }
124 | ///执行失败
125 | return NO;
126 | }
127 | @end
128 |
129 | @interface AWSimpleKVO()
130 | @property (nonatomic, weak) NSObject *obj;
131 | @property (nonatomic, strong) NSMutableDictionary *observerDict;
132 | @property (nonatomic, copy) NSString *simpleKVOClassName;
133 | @property (nonatomic, weak) Class simpleKVOClass;
134 | @property (nonatomic, weak) Class simpleKVOOriClass;
135 |
136 | @property (nonatomic, unsafe_unretained) BOOL isCounted;
137 | @end
138 |
139 | static NSString *_getSelWithKeyPath(NSString *keyPath){
140 | if(keyPath.length == 0){
141 | return nil;
142 | }else{
143 | NSString *uppercase = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
144 | return [NSString stringWithFormat:@"set%@:",uppercase];
145 | }
146 | }
147 |
148 | static NSString *_getKeyPathWithSel(NSString *sel){
149 | if (sel.length <= 4) {
150 | return nil;
151 | }else{
152 | NSString *uppercase = [sel substringWithRange:NSMakeRange(3, sel.length - 4)];
153 | return [[[uppercase substringToIndex:1] lowercaseString] stringByAppendingString:[uppercase substringFromIndex:1]];
154 | }
155 | }
156 |
157 | static NSString *_getStructTypeWithTypeEncode(NSString *typeEncode){
158 | if (typeEncode.length < 3) {
159 | return nil;
160 | }
161 | NSRange locate = [typeEncode rangeOfString:@"="];
162 | if (locate.length == 0) {
163 | return nil;
164 | }
165 | return [typeEncode substringWithRange: NSMakeRange(1, locate.location - 1)];
166 | }
167 |
168 | static AWSimpleKVOItem * _localSetterReady(id obj, SEL sel) {
169 | NSString *str = NSStringFromSelector(sel);
170 | NSString *keyPath = _getKeyPathWithSel(str);
171 | AWSimpleKVO *simpleKVO = [obj awSimpleKVO];
172 | AWSimpleKVOItem *item = simpleKVO.observerDict[keyPath];
173 | return item;
174 | }
175 |
176 | static void _localSetterNotify(AWSimpleKVOItem *item, id obj, NSString *keyPath, id valueNew){
177 | if (item) {
178 | NSMutableDictionary *change = [[NSMutableDictionary alloc] init];
179 | change[@"old"] = item.oldValue;
180 | change[@"new"] = valueNew;
181 | item.oldValue = valueNew;
182 | item.block(obj, keyPath, change, nil);
183 | }
184 | }
185 |
186 | static void _localSetterObj(id obj, SEL sel, id v) {
187 | AWSimpleKVOItem *item = _localSetterReady(obj, sel);
188 | if(item.oldValue == v) {
189 | return;
190 | }
191 |
192 | ((void (*)(id, SEL, id))item._superMethod)(obj, sel, v);
193 |
194 | _localSetterNotify(item, obj, item.keyPath, v);
195 | }
196 |
197 | #define LOCAL_SETTER_NUMBER(type, TypeSet, typeGet) \
198 | static void _localSetter##TypeSet(id obj, SEL sel, type v){ \
199 | AWSimpleKVOItem *item = _localSetterReady(obj, sel); \
200 | if([item.oldValue typeGet##Value] == v) { \
201 | return; \
202 | } \
203 | ((void (*)(id, SEL, type))item._superMethod)(obj, sel, v); \
204 | _localSetterNotify(item, obj, item.keyPath, [NSNumber numberWith##TypeSet:v]); \
205 | }
206 |
207 | LOCAL_SETTER_NUMBER(char, Char, char)
208 | LOCAL_SETTER_NUMBER(int, Int, int)
209 | LOCAL_SETTER_NUMBER(short, Short, short)
210 | LOCAL_SETTER_NUMBER(long, Long, long)
211 | LOCAL_SETTER_NUMBER(long long, LongLong, longLong)
212 | LOCAL_SETTER_NUMBER(unsigned char, UnsignedChar, unsignedChar)
213 | LOCAL_SETTER_NUMBER(unsigned int, UnsignedInt, unsignedInt)
214 | LOCAL_SETTER_NUMBER(unsigned short, UnsignedShort, unsignedShort)
215 | LOCAL_SETTER_NUMBER(unsigned long, UnsignedLong, unsignedLong)
216 | LOCAL_SETTER_NUMBER(unsigned long long, UnsignedLongLong, unsignedLongLong)
217 | LOCAL_SETTER_NUMBER(float, Float, float)
218 | LOCAL_SETTER_NUMBER(double, Double, double)
219 | LOCAL_SETTER_NUMBER(bool, Bool, bool)
220 |
221 | #define LOCAL_SETTER_STRUCTURE(type, equalMethod) \
222 | static void _localSetter##type(id obj, SEL sel, type v) { \
223 | AWSimpleKVOItem *item = _localSetterReady(obj, sel); \
224 | if(equalMethod([item.oldValue type##Value], v)){ \
225 | return; \
226 | } \
227 | ((void (*)(id, SEL, type))item._superMethod)(obj, sel, v); \
228 | _localSetterNotify(item, obj, item.keyPath, [NSValue valueWith##type: v]); \
229 | }
230 |
231 | LOCAL_SETTER_STRUCTURE(CGPoint, CGPointEqualToPoint)
232 | LOCAL_SETTER_STRUCTURE(CGSize, CGSizeEqualToSize)
233 | LOCAL_SETTER_STRUCTURE(CGRect, CGRectEqualToRect)
234 |
235 | static BOOL _CGVectorIsEqualToVector(CGVector vector, CGVector vector1) {
236 | return vector.dx == vector1.dx && vector.dy == vector1.dy;
237 | }
238 | LOCAL_SETTER_STRUCTURE(CGVector, _CGVectorIsEqualToVector)
239 | LOCAL_SETTER_STRUCTURE(CGAffineTransform, CGAffineTransformEqualToTransform)
240 | LOCAL_SETTER_STRUCTURE(UIEdgeInsets, UIEdgeInsetsEqualToEdgeInsets)
241 | LOCAL_SETTER_STRUCTURE(UIOffset, UIOffsetEqualToOffset)
242 |
243 | @implementation AWSimpleKVO
244 |
245 | -(instancetype)initWithObj:(NSObject *)obj{
246 | if (!obj) {
247 | return nil;
248 | }
249 |
250 | if(self = [super init]) {
251 | @synchronized(self) {
252 | self.obj = obj;
253 | self.observerDict = [[NSMutableDictionary alloc] init];
254 | if (obj.awIsObserving) {
255 | self.simpleKVOClassName = NSStringFromClass(obj.class);
256 | }else{
257 | self.simpleKVOClassName = [AWSIMPLEKVOPREFIX stringByAppendingString:NSStringFromClass(obj.class)];
258 | }
259 | }
260 | }
261 | return self;
262 | }
263 |
264 | -(void)awSetValue:(id)value forKey:(NSString*)key{
265 | if ([self.obj awIsObserving]) {
266 | object_setClass(self.obj, self.simpleKVOOriClass);
267 | [self.obj setValue:value forKey:key];
268 | object_setClass(self.obj, self.simpleKVOClass);
269 | }else{
270 | [self.obj setValue:value forKey:key];
271 | }
272 | }
273 |
274 | -(void)awAddObserverForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(void (^)(NSObject *observer, NSString *keyPath, NSDictionary *change, void *context)) block{
275 | NSAssert(self.obj != nil, @"observer is nil");
276 | NSAssert([keyPath isKindOfClass:[NSString class]], @"keyPath is invalid");
277 | NSAssert(block != nil, @"block is invalid");
278 |
279 | AWSimpleKVOSupporedIvarType ivarType = AWSimpleKVOSupporedIvarTypeUnknown;
280 | const char * ivTypeCode = method_getTypeEncoding(class_getInstanceMethod([self.obj class], NSSelectorFromString(keyPath)));
281 | if (!ivTypeCode) {
282 | //NSAssert(NO, @"不支持的ivar类型");
283 | return;
284 | }
285 |
286 | SEL setSel = NSSelectorFromString(_getSelWithKeyPath(keyPath));
287 | IMP localMethod = NULL;
288 | IMP superMethod = class_getMethodImplementation(self.obj.awIsObserving ? self.simpleKVOOriClass: self.obj.class, setSel);
289 | NSString *localMethodTypeCoding = nil;
290 |
291 | switch (*ivTypeCode) {
292 | case 'c':
293 | ivarType = AWSimpleKVOSupporedIvarTypeChar;
294 | localMethod = (IMP)_localSetterChar;
295 | localMethodTypeCoding = @"v@:c";
296 | break;
297 | case 'i':
298 | ivarType = AWSimpleKVOSupporedIvarTypeInt;
299 | localMethod = (IMP)_localSetterInt;
300 | localMethodTypeCoding = @"v@:i";
301 | break;
302 | case 's':
303 | ivarType = AWSimpleKVOSupporedIvarTypeShort;
304 | localMethod = (IMP)_localSetterShort;
305 | localMethodTypeCoding = @"v@:s";
306 | break;
307 | case 'l':
308 | ivarType = AWSimpleKVOSupporedIvarTypeLong;
309 | localMethod = (IMP)_localSetterLong;
310 | localMethodTypeCoding = @"v@:l";
311 | break;
312 | case 'q':
313 | ivarType = AWSimpleKVOSupporedIvarTypeLongLong;
314 | localMethod = (IMP)_localSetterLongLong;
315 | localMethodTypeCoding = @"v@:q";
316 | break;
317 | case 'C':
318 | ivarType = AWSimpleKVOSupporedIvarTypeUChar;
319 | localMethod = (IMP)_localSetterUnsignedChar;
320 | localMethodTypeCoding = @"v@:C";
321 | break;
322 | case 'I':
323 | ivarType = AWSimpleKVOSupporedIvarTypeUInt;
324 | localMethod = (IMP)_localSetterUnsignedInt;
325 | localMethodTypeCoding = @"v@:I";
326 | break;
327 | case 'S':
328 | ivarType = AWSimpleKVOSupporedIvarTypeUShort;
329 | localMethod = (IMP)_localSetterUnsignedShort;
330 | localMethodTypeCoding = @"v@:S";
331 | break;
332 | case 'L':
333 | ivarType = AWSimpleKVOSupporedIvarTypeULong;
334 | localMethod = (IMP)_localSetterUnsignedLong;
335 | localMethodTypeCoding = @"v@:L";
336 | break;
337 | case 'Q':
338 | ivarType = AWSimpleKVOSupporedIvarTypeULongLong;
339 | localMethod = (IMP)_localSetterUnsignedLongLong;
340 | localMethodTypeCoding = @"v@:Q";
341 | break;
342 | case 'f':
343 | ivarType = AWSimpleKVOSupporedIvarTypeFloat;
344 | localMethod = (IMP)_localSetterFloat;
345 | localMethodTypeCoding = @"v@:f";
346 | break;
347 | case 'd':
348 | ivarType = AWSimpleKVOSupporedIvarTypeDouble;
349 | localMethod = (IMP)_localSetterDouble;
350 | localMethodTypeCoding = @"v@:d";
351 | break;
352 | case 'B':
353 | ivarType = AWSimpleKVOSupporedIvarTypeBool;
354 | localMethod = (IMP)_localSetterBool;
355 | localMethodTypeCoding = @"v@:B";
356 | break;
357 | case '@':
358 | ivarType = AWSimpleKVOSupporedIvarTypeObject;
359 | localMethod = (IMP)_localSetterObj;
360 | localMethodTypeCoding = @"v@:@";
361 | break;
362 | case '{':{
363 | NSString *typeEncode = [NSString stringWithUTF8String:ivTypeCode];
364 | NSString *structType = _getStructTypeWithTypeEncode(typeEncode);
365 | if ([structType isEqualToString: @"CGSize"]) {
366 | ivarType = AWSimpleKVOSupporedIvarTypeCGSize;
367 | localMethod = (IMP)_localSetterCGSize;
368 | localMethodTypeCoding = @"v@:{CGSize=dd}";
369 | }else if([structType isEqualToString: @"CGPoint" ]) {
370 | ivarType = AWSimpleKVOSupporedIvarTypeCGPoint;
371 | localMethod = (IMP)_localSetterCGPoint;
372 | localMethodTypeCoding = @"v@:{CGPoint=dd}";
373 | }else if([structType isEqualToString: @"CGRect" ]) {
374 | ivarType = AWSimpleKVOSupporedIvarTypeCGRect;
375 | localMethod = (IMP)_localSetterCGRect;
376 | localMethodTypeCoding = @"v@:{CGRect={CGPoint=dd}{CGSize=dd}}";
377 | }else if([structType isEqualToString: @"CGVector"]) {
378 | ivarType = AWSimpleKVOSupporedIvarTypeCGVector;
379 | localMethod = (IMP)_localSetterCGVector;
380 | localMethodTypeCoding = @"v@:{CGVector=dd}";
381 | }else if([structType isEqualToString: @"CGAffineTransform"]) {
382 | ivarType = AWSimpleKVOSupporedIvarTypeCGAffineTransform;
383 | localMethod = (IMP)_localSetterCGAffineTransform;
384 | localMethodTypeCoding = @"v@:{CGAffineTransform=dddddd}";
385 | }else if([structType isEqualToString: @"UIEdgeInsets"]) {
386 | ivarType = AWSimpleKVOSupporedIvarTypeUIEdgeInsets;
387 | localMethod = (IMP)_localSetterUIEdgeInsets;
388 | localMethodTypeCoding = @"v@:{UIEdgeInsets=dddd}";
389 | }else if([structType isEqualToString: @"UIOffset"]) {
390 | ivarType = AWSimpleKVOSupporedIvarTypeUIOffset;
391 | localMethod = (IMP)_localSetterUIOffset;
392 | localMethodTypeCoding = @"v@:{UIOffset=dd}";
393 | }
394 | }
395 | break;
396 | default:
397 | break;
398 | }
399 |
400 | if (ivarType == AWSimpleKVOSupporedIvarTypeUnknown){
401 | //NSAssert(NO, @"不支持的ivar类型");
402 | return;
403 | }
404 |
405 | AWSimpleKVOItem *item = nil;
406 | @synchronized(self){
407 | item = self.observerDict[keyPath];
408 | }
409 | if (item) {
410 | NSAssert(NO, @"重复监听");
411 | return;
412 | }
413 | item = [[AWSimpleKVOItem alloc] init];
414 | item.keyPath = keyPath;
415 | item.context = context;
416 | item.block = block;
417 | item.ivarType = ivarType;
418 | item.ivarTypeCode = [[NSString stringWithFormat:@"%s", ivTypeCode] substringToIndex: 1];
419 |
420 | item._setSel = setSel;
421 | item._localMethod = localMethod;
422 | item._superMethod = superMethod;
423 | item._localMethodTypeCoding = localMethodTypeCoding;
424 |
425 | @synchronized(self){
426 | self.observerDict[keyPath] = item;
427 | }
428 |
429 | Class classNew = [self addNewClassObserverClass:self.obj.class keyPath:keyPath item:item];
430 | NSAssert(classNew != nil, @"replce method failed");
431 |
432 | object_setClass(self.obj, classNew);
433 | }
434 |
435 | -(Class) simpleKVOClass{
436 | if (!_simpleKVOClass) {
437 | @synchronized(self) {
438 | if (!_simpleKVOClass) {
439 | _simpleKVOClass = NSClassFromString(self.simpleKVOClassName);
440 | }
441 | }
442 | }
443 | return _simpleKVOClass;
444 | }
445 |
446 | -(Class) simpleKVOOriClass{
447 | if (!_simpleKVOOriClass) {
448 | @synchronized(self) {
449 | if (!_simpleKVOOriClass) {
450 | _simpleKVOOriClass = class_getSuperclass(self.simpleKVOClass);
451 | }
452 | }
453 | }
454 | return _simpleKVOOriClass;
455 | }
456 |
457 | -(Class) currObjClass{
458 | if ([self.obj awIsObserving]) {
459 | return self.simpleKVOClass;
460 | }else{
461 | return self.obj.class;
462 | }
463 | }
464 |
465 | -(Class) addNewClassObserverClass:(Class) c keyPath:(NSString *)keyPath item:(AWSimpleKVOItem *)item {
466 | Class classNew = self.simpleKVOClass;
467 | if (!classNew) {
468 | @synchronized(self.class) {
469 | classNew = self.simpleKVOClass;
470 | if(!classNew) {
471 | NSString *classNewName = self.simpleKVOClassName;
472 | classNew = objc_allocateClassPair(c, classNewName.UTF8String, 0);
473 | objc_registerClassPair(classNew);
474 | self.simpleKVOClass = classNew;
475 | self.simpleKVOOriClass = c;
476 | }
477 | }
478 | }
479 |
480 | class_addMethod(classNew, item._setSel, item._localMethod, item._localMethodTypeCoding.UTF8String);
481 |
482 | if (!self.isCounted) {
483 | [awGetSimpleKVOCounter() increaceForClassName: self.simpleKVOClassName];
484 | self.isCounted = YES;
485 | }
486 | return classNew;
487 | }
488 |
489 | -(void)awRemoveObserverForKeyPath:(NSString *)keyPath context:(void *)context{
490 | @synchronized(self){
491 | self.observerDict[keyPath] = nil;
492 | }
493 | }
494 |
495 | -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
496 | AWSimpleKVOItem *item = nil;
497 | @synchronized(self){
498 | item = self.observerDict[keyPath];
499 | }
500 | if(![item invokeBlockWithChange:change obj:self.obj]){
501 | [self awRemoveObserverForKeyPath:item.keyPath context:item.context];
502 | }
503 | }
504 |
505 | -(void) removeAllObservers {
506 | @synchronized(self){
507 | [self.observerDict removeAllObjects];
508 | }
509 | }
510 |
511 | -(void)dealloc{
512 | [self removeAllObservers];
513 | if (_simpleKVOClassName) {
514 | [awGetSimpleKVOCounter() reduceForClassName: _simpleKVOClassName];
515 | }
516 | }
517 |
518 | @end
519 |
520 | static char awAWSimpleKVOKey = 0;
521 |
522 | @implementation NSObject(AWSimpleKVO)
523 |
524 | ///属性
525 | -(void)setAwSimpleKVO:(AWSimpleKVO *)awSimpleKVO{
526 | objc_setAssociatedObject(self, &awAWSimpleKVOKey, awSimpleKVO, OBJC_ASSOCIATION_RETAIN);
527 | }
528 |
529 | -(AWSimpleKVO *)awSimpleKVO{
530 | AWSimpleKVO *simpleKVO = objc_getAssociatedObject(self, &awAWSimpleKVOKey);
531 | if (!simpleKVO) {
532 | simpleKVO = [[AWSimpleKVO alloc] initWithObj:self];
533 | self.awSimpleKVO = simpleKVO;
534 | }
535 | return simpleKVO;
536 | }
537 |
538 | -(void)awAddObserverForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(void (^)(NSObject *observer, NSString *keyPath, NSDictionary *change, void *context)) block{
539 | [self.awSimpleKVO awAddObserverForKeyPath:keyPath options:options context:context block:block];
540 | }
541 |
542 | -(void)awRemoveObserverForKeyPath:(NSString *)keyPath context:(void *)context{
543 | [self.awSimpleKVO awRemoveObserverForKeyPath:keyPath context:context];
544 | }
545 |
546 | -(void) awSetValue:(id) value forKey:(NSString *)key{
547 | if([self awIsObserving]){
548 | [self.awSimpleKVO awSetValue:value forKey:key];
549 | }else{
550 | [self setValue:value forKey:key];
551 | }
552 | }
553 |
554 | -(BOOL)awIsObserving{
555 | return [NSStringFromClass(self.class) hasPrefix:AWSIMPLEKVOPREFIX];
556 | }
557 | @end
558 |
--------------------------------------------------------------------------------