├── .gitignore ├── BubbleLayer ├── BubbleLayer.h └── BubbleLayer.m ├── Demo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── ihandle.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── ihandle.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── Demo.xcscheme │ └── xcschememanagement.plist ├── Demo ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Doraemon.jpeg ├── Info.plist ├── ViewController.h ├── ViewController.m └── main.m ├── DemoTests ├── DemoTests.m └── Info.plist ├── DemoUITests ├── DemoUITests.m └── Info.plist ├── README.md └── Swift └── BubbleLayer.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | 7 | # Thumbnails 8 | ._* 9 | -------------------------------------------------------------------------------- /BubbleLayer/BubbleLayer.h: -------------------------------------------------------------------------------- 1 | // BubbleLayer.h 2 | // VideoCoreTest 3 | 4 | #import 5 | #import 6 | 7 | // 一些在注释中使用的叫法 8 | // 箭头: 气泡形状突出那个三角形把它叫做【箭头】 9 | // 箭头的顶点和底点: 将箭头指向的那个点叫为【顶点】,其余两个点叫为【底点】 10 | // 箭头的高度和宽度: 箭头顶点到底点连线的距离叫为【箭头的高度】,两底点的距离叫为【箭头的宽度】 11 | // 矩形框: 气泡形状除了箭头,剩下的部分叫为【矩形框】 12 | // 箭头的相对位置: 如果箭头的方向是向右或者向左,0表达箭头在最上方,1表示箭头在最下方 13 | // 如果箭头的方向是向上或者向下,0表达箭头在最左边,1表示箭头在最右边 14 | // 默认是 0.5,即在中间 15 | 16 | // 箭头方向枚举 17 | typedef enum { 18 | ArrowDirectionRight = 0, //指向右边, 即在圆角矩形的右边 19 | ArrowDirectionBottom = 1, //指向下边 20 | ArrowDirectionLeft = 2, //指向左边 21 | ArrowDirectionTop = 3, //指向上边 22 | 23 | } ArrowDirection; 24 | 25 | 26 | @interface BubbleLayer : NSObject 27 | 28 | // 矩形的圆角的半径 29 | @property CGFloat cornerRadius; 30 | // 箭头位置的圆角半径 31 | @property CGFloat arrowRadius; 32 | // 箭头的高度 33 | @property CGFloat arrowHeight; 34 | // 箭头的宽度 35 | @property CGFloat arrowWidth; 36 | // 箭头方向 37 | @property ArrowDirection arrowDirection; 38 | // 箭头的相对位置 39 | @property CGFloat arrowPosition; 40 | 41 | 42 | // 这里的size是需要mask成气泡形状的view的size 43 | - (instancetype) initWithSize:(CGSize) originalSize; 44 | 45 | - (CAShapeLayer *) layer; //最终拿这个layer去设置mask 46 | 47 | @end 48 | 49 | -------------------------------------------------------------------------------- /BubbleLayer/BubbleLayer.m: -------------------------------------------------------------------------------- 1 | // BubbleLayer.m 2 | // VideoCoreTest 3 | // 4 | #import "BubbleLayer.h" 5 | 6 | 7 | @interface BubbleLayer() 8 | 9 | 10 | // 需要mask成气泡形状的view的size 11 | @property (nonatomic) CGSize size; 12 | 13 | 14 | @end 15 | 16 | @implementation BubbleLayer 17 | 18 | 19 | #pragma mark - preparation 20 | 21 | // 关键点: 绘制气泡形状前,需要计算箭头的三个点和矩形的四个角的点的坐标 22 | -(NSMutableArray *)keyPoints { 23 | 24 | NSMutableArray *points = [[NSMutableArray alloc]init]; 25 | 26 | // 先确定箭头的三个点 27 | CGPoint beginPoint; // 按顺时针画箭头时的第一个支点,例如箭头向上时的左边的支点 28 | CGPoint topPoint; // 顶点 29 | CGPoint endPoint; // 另外一个支点 30 | 31 | // 箭头顶点topPoint的X坐标(或Y坐标)的范围(用来计算arrowPosition) 32 | CGFloat tpXRange = _size.width - 2 * _cornerRadius - _arrowWidth; 33 | CGFloat tpYRange = _size.height - 2 * _cornerRadius - _arrowWidth; 34 | 35 | // 这几个参数用于确定矩形框的位置(就是给箭头腾出空间后剩下的区域) 36 | // 这些参数在下面会根据箭头的位置进行调整 37 | CGFloat x = 0, y = 0; // 矩形框左上角的坐标 38 | CGFloat width = _size.width, height = _size.height; //矩形框的大小 39 | 40 | // 计算箭头的位置,以及调整矩形框的位置和大小 41 | switch (_arrowDirection) { 42 | 43 | //箭头在右时 44 | case ArrowDirectionRight: 45 | 46 | topPoint = CGPointMake(_size.width , _size.height / 2 + tpYRange*(_arrowPosition - 0.5)); 47 | beginPoint = CGPointMake(topPoint.x - _arrowHeight, topPoint.y - _arrowWidth/2); 48 | endPoint = CGPointMake(beginPoint.x, beginPoint.y + _arrowWidth); 49 | 50 | width -= _arrowHeight; //矩形框右边的位置“腾出”给箭头 51 | break; 52 | 53 | //箭头在下时 54 | case ArrowDirectionBottom: 55 | topPoint = CGPointMake(_size.width / 2 + tpXRange*(_arrowPosition - 0.5), _size.height); 56 | beginPoint = CGPointMake(topPoint.x + _arrowWidth/2, topPoint.y - _arrowHeight); 57 | endPoint = CGPointMake(beginPoint.x - _arrowWidth, beginPoint.y); 58 | 59 | height -= _arrowHeight; 60 | break; 61 | 62 | //箭头在左时 63 | case ArrowDirectionLeft: 64 | topPoint = CGPointMake(0, _size.height / 2 + tpYRange*(_arrowPosition - 0.5)); 65 | beginPoint = CGPointMake(topPoint.x + _arrowHeight, topPoint.y + _arrowWidth/2); 66 | endPoint = CGPointMake(beginPoint.x, beginPoint.y - _arrowWidth); 67 | 68 | x = _arrowHeight; 69 | width -= _arrowHeight; 70 | break; 71 | 72 | //箭头在上时 73 | case ArrowDirectionTop: 74 | topPoint = CGPointMake(_size.width / 2 + tpXRange*(_arrowPosition - 0.5), 0); 75 | beginPoint = CGPointMake(topPoint.x - _arrowWidth/2, topPoint.y + _arrowHeight); 76 | endPoint = CGPointMake(beginPoint.x + _arrowWidth, beginPoint.y); 77 | 78 | y = _arrowHeight; 79 | height -= _arrowHeight; 80 | break; 81 | } 82 | 83 | // 先把箭头的三个点放进关键点数组中 84 | points = [NSMutableArray arrayWithObjects:[NSValue valueWithCGPoint:beginPoint], 85 | [NSValue valueWithCGPoint:topPoint], 86 | [NSValue valueWithCGPoint:endPoint], nil]; 87 | 88 | 89 | 90 | //确定圆角矩形的四个点 91 | CGPoint bottomRight = CGPointMake(x+width, y+height); //右下角的点 92 | CGPoint bottomLeft = CGPointMake(x, y+height); 93 | CGPoint topLeft = CGPointMake(x, y); 94 | CGPoint topRight = CGPointMake(x+width, y); 95 | 96 | 97 | //先把圆角矩形的四个点放在一个临时数组, 放置顺序跟下面紧接着的操作有关 98 | NSMutableArray *rectPoints = [NSMutableArray arrayWithObjects: [NSValue valueWithCGPoint:bottomRight], 99 | [NSValue valueWithCGPoint:bottomLeft], 100 | [NSValue valueWithCGPoint:topLeft], 101 | [NSValue valueWithCGPoint:topRight], nil]; 102 | 103 | 104 | // 绘制气泡形状的时候,从箭头开始,顺时针地进行 105 | // 假设箭头是向右的,那么画完箭头之后会先画到矩形框的右下角 106 | // 所以此时先把矩形框右下角的点放进关键点数组,其他三个点按顺时针方向添加 107 | // 箭头在其他方向时,以此类推 108 | int rectPointIndex = (int)_arrowDirection; 109 | for(int i=0; i<4; i++) { 110 | [points addObject:[rectPoints objectAtIndex:rectPointIndex]]; 111 | rectPointIndex = (rectPointIndex+1)%4; 112 | } 113 | 114 | return points; 115 | } 116 | 117 | 118 | #pragma mark - draw 119 | 120 | // 绘制气泡形状,获取path 121 | - (CGPathRef )bubblePath { 122 | 123 | UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0); 124 | CGContextRef ctx = UIGraphicsGetCurrentContext(); 125 | 126 | // 获取绘图所需要的关键点 127 | NSMutableArray *points = [self keyPoints]; 128 | 129 | // 第一步是要画箭头的“第一个支点”所在的那个角,所以要把“笔”放在这个支点顺时针顺序的上一个点 130 | // 所以把“笔”放在最后才画的矩形框的角的位置, 准备开始画箭头 131 | CGPoint currentPoint = [[points objectAtIndex:6] CGPointValue]; 132 | CGContextMoveToPoint(ctx, currentPoint.x, currentPoint.y); 133 | 134 | CGPoint pointA, pointB; //用于 CGContextAddArcToPoint函数 135 | CGFloat radius; 136 | int count = 0; 137 | 138 | while(count < 7) { 139 | // 整个过程需要画七个圆角(矩形框的四个角和箭头处的三个角),所以分为七个步骤 140 | 141 | // 箭头处的三个圆角和矩形框的四个圆角不一样 142 | radius = count < 3 ? _arrowRadius : _cornerRadius; 143 | 144 | pointA = [[points objectAtIndex:count] CGPointValue]; 145 | pointB = [[points objectAtIndex:(count+1)%7] CGPointValue]; 146 | // 画矩形框最后一个角的时候,pointB就是points[0] 147 | 148 | CGContextAddArcToPoint(ctx, pointA.x, pointA.y, pointB.x, pointB.y, radius); 149 | 150 | count = count + 1; 151 | } 152 | 153 | // 获取path 154 | CGContextClosePath(ctx); 155 | CGPathRef path = CGContextCopyPath(ctx); 156 | 157 | UIGraphicsEndImageContext(); 158 | 159 | return path; 160 | } 161 | 162 | 163 | 164 | // 生成用于mask的layer 165 | - (CAShapeLayer *) layer{ 166 | CAShapeLayer *layer = [CAShapeLayer layer]; 167 | layer.path = [self bubblePath]; 168 | return layer; 169 | } 170 | 171 | 172 | #pragma mark - setup 173 | 174 | // 设置默认参数 175 | - (void)setDefaultProperty { 176 | 177 | _cornerRadius = 8; 178 | _arrowWidth = 30; 179 | _arrowHeight = 12; 180 | _arrowDirection = 1; 181 | _arrowPosition = 0.5; 182 | _arrowRadius = 3; 183 | 184 | } 185 | 186 | #pragma mark - init 187 | 188 | -(instancetype)initWithSize:(CGSize) originalSize { 189 | if(self = [super init]) { 190 | [self setDefaultProperty]; 191 | _size = originalSize; 192 | } 193 | return self; 194 | } 195 | 196 | @end 197 | -------------------------------------------------------------------------------- /Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4956C8B91EB109C30021E6E8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4956C8B81EB109C30021E6E8 /* main.m */; }; 11 | 4956C8BC1EB109C30021E6E8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4956C8BB1EB109C30021E6E8 /* AppDelegate.m */; }; 12 | 4956C8BF1EB109C30021E6E8 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4956C8BE1EB109C30021E6E8 /* ViewController.m */; }; 13 | 4956C8C21EB109C30021E6E8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4956C8C01EB109C30021E6E8 /* Main.storyboard */; }; 14 | 4956C8C41EB109C30021E6E8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4956C8C31EB109C30021E6E8 /* Assets.xcassets */; }; 15 | 4956C8C71EB109C30021E6E8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4956C8C51EB109C30021E6E8 /* LaunchScreen.storyboard */; }; 16 | 4956C8D21EB109C30021E6E8 /* DemoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4956C8D11EB109C30021E6E8 /* DemoTests.m */; }; 17 | 4956C8DD1EB109C30021E6E8 /* DemoUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4956C8DC1EB109C30021E6E8 /* DemoUITests.m */; }; 18 | 4956C8ED1EB10A6B0021E6E8 /* BubbleLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4956C8EC1EB10A6B0021E6E8 /* BubbleLayer.m */; }; 19 | 497E6063216DA2510041B32E /* Doraemon.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 497E6062216DA2510041B32E /* Doraemon.jpeg */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | 4956C8CE1EB109C30021E6E8 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 4956C8AC1EB109C30021E6E8 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 4956C8B31EB109C30021E6E8; 28 | remoteInfo = Demo; 29 | }; 30 | 4956C8D91EB109C30021E6E8 /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 4956C8AC1EB109C30021E6E8 /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 4956C8B31EB109C30021E6E8; 35 | remoteInfo = Demo; 36 | }; 37 | /* End PBXContainerItemProxy section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 4956C8B41EB109C30021E6E8 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 4956C8B81EB109C30021E6E8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 42 | 4956C8BA1EB109C30021E6E8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 43 | 4956C8BB1EB109C30021E6E8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 44 | 4956C8BD1EB109C30021E6E8 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 45 | 4956C8BE1EB109C30021E6E8 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 46 | 4956C8C11EB109C30021E6E8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 47 | 4956C8C31EB109C30021E6E8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 4956C8C61EB109C30021E6E8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | 4956C8C81EB109C30021E6E8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | 4956C8CD1EB109C30021E6E8 /* DemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 4956C8D11EB109C30021E6E8 /* DemoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DemoTests.m; sourceTree = ""; }; 52 | 4956C8D31EB109C30021E6E8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | 4956C8D81EB109C30021E6E8 /* DemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 4956C8DC1EB109C30021E6E8 /* DemoUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DemoUITests.m; sourceTree = ""; }; 55 | 4956C8DE1EB109C30021E6E8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 4956C8EB1EB10A6B0021E6E8 /* BubbleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BubbleLayer.h; sourceTree = ""; }; 57 | 4956C8EC1EB10A6B0021E6E8 /* BubbleLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BubbleLayer.m; sourceTree = ""; }; 58 | 497E6062216DA2510041B32E /* Doraemon.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Doraemon.jpeg; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | 4956C8B11EB109C30021E6E8 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | 4956C8CA1EB109C30021E6E8 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | 4956C8D51EB109C30021E6E8 /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 2147483647; 79 | files = ( 80 | ); 81 | runOnlyForDeploymentPostprocessing = 0; 82 | }; 83 | /* End PBXFrameworksBuildPhase section */ 84 | 85 | /* Begin PBXGroup section */ 86 | 4956C8AB1EB109C30021E6E8 = { 87 | isa = PBXGroup; 88 | children = ( 89 | 4956C8EA1EB10A6B0021E6E8 /* BubbleLayer */, 90 | 4956C8B61EB109C30021E6E8 /* Demo */, 91 | 4956C8D01EB109C30021E6E8 /* DemoTests */, 92 | 4956C8DB1EB109C30021E6E8 /* DemoUITests */, 93 | 4956C8B51EB109C30021E6E8 /* Products */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | 4956C8B51EB109C30021E6E8 /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 4956C8B41EB109C30021E6E8 /* Demo.app */, 101 | 4956C8CD1EB109C30021E6E8 /* DemoTests.xctest */, 102 | 4956C8D81EB109C30021E6E8 /* DemoUITests.xctest */, 103 | ); 104 | name = Products; 105 | sourceTree = ""; 106 | }; 107 | 4956C8B61EB109C30021E6E8 /* Demo */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 497E6062216DA2510041B32E /* Doraemon.jpeg */, 111 | 4956C8BA1EB109C30021E6E8 /* AppDelegate.h */, 112 | 4956C8BB1EB109C30021E6E8 /* AppDelegate.m */, 113 | 4956C8BD1EB109C30021E6E8 /* ViewController.h */, 114 | 4956C8BE1EB109C30021E6E8 /* ViewController.m */, 115 | 4956C8C01EB109C30021E6E8 /* Main.storyboard */, 116 | 4956C8C31EB109C30021E6E8 /* Assets.xcassets */, 117 | 4956C8C51EB109C30021E6E8 /* LaunchScreen.storyboard */, 118 | 4956C8C81EB109C30021E6E8 /* Info.plist */, 119 | 4956C8B71EB109C30021E6E8 /* Supporting Files */, 120 | ); 121 | path = Demo; 122 | sourceTree = ""; 123 | }; 124 | 4956C8B71EB109C30021E6E8 /* Supporting Files */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 4956C8B81EB109C30021E6E8 /* main.m */, 128 | ); 129 | name = "Supporting Files"; 130 | sourceTree = ""; 131 | }; 132 | 4956C8D01EB109C30021E6E8 /* DemoTests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 4956C8D11EB109C30021E6E8 /* DemoTests.m */, 136 | 4956C8D31EB109C30021E6E8 /* Info.plist */, 137 | ); 138 | path = DemoTests; 139 | sourceTree = ""; 140 | }; 141 | 4956C8DB1EB109C30021E6E8 /* DemoUITests */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 4956C8DC1EB109C30021E6E8 /* DemoUITests.m */, 145 | 4956C8DE1EB109C30021E6E8 /* Info.plist */, 146 | ); 147 | path = DemoUITests; 148 | sourceTree = ""; 149 | }; 150 | 4956C8EA1EB10A6B0021E6E8 /* BubbleLayer */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 4956C8EB1EB10A6B0021E6E8 /* BubbleLayer.h */, 154 | 4956C8EC1EB10A6B0021E6E8 /* BubbleLayer.m */, 155 | ); 156 | path = BubbleLayer; 157 | sourceTree = SOURCE_ROOT; 158 | }; 159 | /* End PBXGroup section */ 160 | 161 | /* Begin PBXNativeTarget section */ 162 | 4956C8B31EB109C30021E6E8 /* Demo */ = { 163 | isa = PBXNativeTarget; 164 | buildConfigurationList = 4956C8E11EB109C30021E6E8 /* Build configuration list for PBXNativeTarget "Demo" */; 165 | buildPhases = ( 166 | 4956C8B01EB109C30021E6E8 /* Sources */, 167 | 4956C8B11EB109C30021E6E8 /* Frameworks */, 168 | 4956C8B21EB109C30021E6E8 /* Resources */, 169 | ); 170 | buildRules = ( 171 | ); 172 | dependencies = ( 173 | ); 174 | name = Demo; 175 | productName = Demo; 176 | productReference = 4956C8B41EB109C30021E6E8 /* Demo.app */; 177 | productType = "com.apple.product-type.application"; 178 | }; 179 | 4956C8CC1EB109C30021E6E8 /* DemoTests */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = 4956C8E41EB109C30021E6E8 /* Build configuration list for PBXNativeTarget "DemoTests" */; 182 | buildPhases = ( 183 | 4956C8C91EB109C30021E6E8 /* Sources */, 184 | 4956C8CA1EB109C30021E6E8 /* Frameworks */, 185 | 4956C8CB1EB109C30021E6E8 /* Resources */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | 4956C8CF1EB109C30021E6E8 /* PBXTargetDependency */, 191 | ); 192 | name = DemoTests; 193 | productName = DemoTests; 194 | productReference = 4956C8CD1EB109C30021E6E8 /* DemoTests.xctest */; 195 | productType = "com.apple.product-type.bundle.unit-test"; 196 | }; 197 | 4956C8D71EB109C30021E6E8 /* DemoUITests */ = { 198 | isa = PBXNativeTarget; 199 | buildConfigurationList = 4956C8E71EB109C30021E6E8 /* Build configuration list for PBXNativeTarget "DemoUITests" */; 200 | buildPhases = ( 201 | 4956C8D41EB109C30021E6E8 /* Sources */, 202 | 4956C8D51EB109C30021E6E8 /* Frameworks */, 203 | 4956C8D61EB109C30021E6E8 /* Resources */, 204 | ); 205 | buildRules = ( 206 | ); 207 | dependencies = ( 208 | 4956C8DA1EB109C30021E6E8 /* PBXTargetDependency */, 209 | ); 210 | name = DemoUITests; 211 | productName = DemoUITests; 212 | productReference = 4956C8D81EB109C30021E6E8 /* DemoUITests.xctest */; 213 | productType = "com.apple.product-type.bundle.ui-testing"; 214 | }; 215 | /* End PBXNativeTarget section */ 216 | 217 | /* Begin PBXProject section */ 218 | 4956C8AC1EB109C30021E6E8 /* Project object */ = { 219 | isa = PBXProject; 220 | attributes = { 221 | LastUpgradeCheck = 1020; 222 | ORGANIZATIONNAME = iHandle; 223 | TargetAttributes = { 224 | 4956C8B31EB109C30021E6E8 = { 225 | CreatedOnToolsVersion = 8.3; 226 | ProvisioningStyle = Automatic; 227 | }; 228 | 4956C8CC1EB109C30021E6E8 = { 229 | CreatedOnToolsVersion = 8.3; 230 | DevelopmentTeam = N56D5J55Q8; 231 | ProvisioningStyle = Automatic; 232 | TestTargetID = 4956C8B31EB109C30021E6E8; 233 | }; 234 | 4956C8D71EB109C30021E6E8 = { 235 | CreatedOnToolsVersion = 8.3; 236 | DevelopmentTeam = N56D5J55Q8; 237 | ProvisioningStyle = Automatic; 238 | TestTargetID = 4956C8B31EB109C30021E6E8; 239 | }; 240 | }; 241 | }; 242 | buildConfigurationList = 4956C8AF1EB109C30021E6E8 /* Build configuration list for PBXProject "Demo" */; 243 | compatibilityVersion = "Xcode 3.2"; 244 | developmentRegion = en; 245 | hasScannedForEncodings = 0; 246 | knownRegions = ( 247 | en, 248 | Base, 249 | ); 250 | mainGroup = 4956C8AB1EB109C30021E6E8; 251 | productRefGroup = 4956C8B51EB109C30021E6E8 /* Products */; 252 | projectDirPath = ""; 253 | projectRoot = ""; 254 | targets = ( 255 | 4956C8B31EB109C30021E6E8 /* Demo */, 256 | 4956C8CC1EB109C30021E6E8 /* DemoTests */, 257 | 4956C8D71EB109C30021E6E8 /* DemoUITests */, 258 | ); 259 | }; 260 | /* End PBXProject section */ 261 | 262 | /* Begin PBXResourcesBuildPhase section */ 263 | 4956C8B21EB109C30021E6E8 /* Resources */ = { 264 | isa = PBXResourcesBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | 4956C8C71EB109C30021E6E8 /* LaunchScreen.storyboard in Resources */, 268 | 4956C8C41EB109C30021E6E8 /* Assets.xcassets in Resources */, 269 | 497E6063216DA2510041B32E /* Doraemon.jpeg in Resources */, 270 | 4956C8C21EB109C30021E6E8 /* Main.storyboard in Resources */, 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | }; 274 | 4956C8CB1EB109C30021E6E8 /* Resources */ = { 275 | isa = PBXResourcesBuildPhase; 276 | buildActionMask = 2147483647; 277 | files = ( 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | 4956C8D61EB109C30021E6E8 /* Resources */ = { 282 | isa = PBXResourcesBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | ); 286 | runOnlyForDeploymentPostprocessing = 0; 287 | }; 288 | /* End PBXResourcesBuildPhase section */ 289 | 290 | /* Begin PBXSourcesBuildPhase section */ 291 | 4956C8B01EB109C30021E6E8 /* Sources */ = { 292 | isa = PBXSourcesBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | 4956C8BF1EB109C30021E6E8 /* ViewController.m in Sources */, 296 | 4956C8ED1EB10A6B0021E6E8 /* BubbleLayer.m in Sources */, 297 | 4956C8BC1EB109C30021E6E8 /* AppDelegate.m in Sources */, 298 | 4956C8B91EB109C30021E6E8 /* main.m in Sources */, 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | 4956C8C91EB109C30021E6E8 /* Sources */ = { 303 | isa = PBXSourcesBuildPhase; 304 | buildActionMask = 2147483647; 305 | files = ( 306 | 4956C8D21EB109C30021E6E8 /* DemoTests.m in Sources */, 307 | ); 308 | runOnlyForDeploymentPostprocessing = 0; 309 | }; 310 | 4956C8D41EB109C30021E6E8 /* Sources */ = { 311 | isa = PBXSourcesBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | 4956C8DD1EB109C30021E6E8 /* DemoUITests.m in Sources */, 315 | ); 316 | runOnlyForDeploymentPostprocessing = 0; 317 | }; 318 | /* End PBXSourcesBuildPhase section */ 319 | 320 | /* Begin PBXTargetDependency section */ 321 | 4956C8CF1EB109C30021E6E8 /* PBXTargetDependency */ = { 322 | isa = PBXTargetDependency; 323 | target = 4956C8B31EB109C30021E6E8 /* Demo */; 324 | targetProxy = 4956C8CE1EB109C30021E6E8 /* PBXContainerItemProxy */; 325 | }; 326 | 4956C8DA1EB109C30021E6E8 /* PBXTargetDependency */ = { 327 | isa = PBXTargetDependency; 328 | target = 4956C8B31EB109C30021E6E8 /* Demo */; 329 | targetProxy = 4956C8D91EB109C30021E6E8 /* PBXContainerItemProxy */; 330 | }; 331 | /* End PBXTargetDependency section */ 332 | 333 | /* Begin PBXVariantGroup section */ 334 | 4956C8C01EB109C30021E6E8 /* Main.storyboard */ = { 335 | isa = PBXVariantGroup; 336 | children = ( 337 | 4956C8C11EB109C30021E6E8 /* Base */, 338 | ); 339 | name = Main.storyboard; 340 | sourceTree = ""; 341 | }; 342 | 4956C8C51EB109C30021E6E8 /* LaunchScreen.storyboard */ = { 343 | isa = PBXVariantGroup; 344 | children = ( 345 | 4956C8C61EB109C30021E6E8 /* Base */, 346 | ); 347 | name = LaunchScreen.storyboard; 348 | sourceTree = ""; 349 | }; 350 | /* End PBXVariantGroup section */ 351 | 352 | /* Begin XCBuildConfiguration section */ 353 | 4956C8DF1EB109C30021E6E8 /* Debug */ = { 354 | isa = XCBuildConfiguration; 355 | buildSettings = { 356 | ALWAYS_SEARCH_USER_PATHS = NO; 357 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 358 | CLANG_ANALYZER_NONNULL = YES; 359 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 360 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 361 | CLANG_CXX_LIBRARY = "libc++"; 362 | CLANG_ENABLE_MODULES = YES; 363 | CLANG_ENABLE_OBJC_ARC = YES; 364 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 365 | CLANG_WARN_BOOL_CONVERSION = YES; 366 | CLANG_WARN_COMMA = YES; 367 | CLANG_WARN_CONSTANT_CONVERSION = YES; 368 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 369 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 370 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 371 | CLANG_WARN_EMPTY_BODY = YES; 372 | CLANG_WARN_ENUM_CONVERSION = YES; 373 | CLANG_WARN_INFINITE_RECURSION = YES; 374 | CLANG_WARN_INT_CONVERSION = YES; 375 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 376 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 377 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 379 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 380 | CLANG_WARN_STRICT_PROTOTYPES = YES; 381 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 382 | CLANG_WARN_UNREACHABLE_CODE = YES; 383 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 384 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 385 | COPY_PHASE_STRIP = NO; 386 | DEBUG_INFORMATION_FORMAT = dwarf; 387 | ENABLE_STRICT_OBJC_MSGSEND = YES; 388 | ENABLE_TESTABILITY = YES; 389 | GCC_C_LANGUAGE_STANDARD = gnu99; 390 | GCC_DYNAMIC_NO_PIC = NO; 391 | GCC_NO_COMMON_BLOCKS = YES; 392 | GCC_OPTIMIZATION_LEVEL = 0; 393 | GCC_PREPROCESSOR_DEFINITIONS = ( 394 | "DEBUG=1", 395 | "$(inherited)", 396 | ); 397 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 398 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 399 | GCC_WARN_UNDECLARED_SELECTOR = YES; 400 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 401 | GCC_WARN_UNUSED_FUNCTION = YES; 402 | GCC_WARN_UNUSED_VARIABLE = YES; 403 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 404 | MTL_ENABLE_DEBUG_INFO = YES; 405 | ONLY_ACTIVE_ARCH = YES; 406 | SDKROOT = iphoneos; 407 | TARGETED_DEVICE_FAMILY = "1,2"; 408 | }; 409 | name = Debug; 410 | }; 411 | 4956C8E01EB109C30021E6E8 /* Release */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ALWAYS_SEARCH_USER_PATHS = NO; 415 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 416 | CLANG_ANALYZER_NONNULL = YES; 417 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 418 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 419 | CLANG_CXX_LIBRARY = "libc++"; 420 | CLANG_ENABLE_MODULES = YES; 421 | CLANG_ENABLE_OBJC_ARC = YES; 422 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 423 | CLANG_WARN_BOOL_CONVERSION = YES; 424 | CLANG_WARN_COMMA = YES; 425 | CLANG_WARN_CONSTANT_CONVERSION = YES; 426 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 427 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 428 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 429 | CLANG_WARN_EMPTY_BODY = YES; 430 | CLANG_WARN_ENUM_CONVERSION = YES; 431 | CLANG_WARN_INFINITE_RECURSION = YES; 432 | CLANG_WARN_INT_CONVERSION = YES; 433 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 434 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 435 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 436 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 437 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 438 | CLANG_WARN_STRICT_PROTOTYPES = YES; 439 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 440 | CLANG_WARN_UNREACHABLE_CODE = YES; 441 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 442 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 443 | COPY_PHASE_STRIP = NO; 444 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 445 | ENABLE_NS_ASSERTIONS = NO; 446 | ENABLE_STRICT_OBJC_MSGSEND = YES; 447 | GCC_C_LANGUAGE_STANDARD = gnu99; 448 | GCC_NO_COMMON_BLOCKS = YES; 449 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 450 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 451 | GCC_WARN_UNDECLARED_SELECTOR = YES; 452 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 453 | GCC_WARN_UNUSED_FUNCTION = YES; 454 | GCC_WARN_UNUSED_VARIABLE = YES; 455 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 456 | MTL_ENABLE_DEBUG_INFO = NO; 457 | SDKROOT = iphoneos; 458 | TARGETED_DEVICE_FAMILY = "1,2"; 459 | VALIDATE_PRODUCT = YES; 460 | }; 461 | name = Release; 462 | }; 463 | 4956C8E21EB109C30021E6E8 /* Debug */ = { 464 | isa = XCBuildConfiguration; 465 | buildSettings = { 466 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 467 | CODE_SIGN_IDENTITY = "iPhone Developer"; 468 | CODE_SIGN_STYLE = Automatic; 469 | DEVELOPMENT_TEAM = ""; 470 | INFOPLIST_FILE = Demo/Info.plist; 471 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 472 | PRODUCT_BUNDLE_IDENTIFIER = demo.bubbleLayer.ihandle; 473 | PRODUCT_NAME = "$(TARGET_NAME)"; 474 | PROVISIONING_PROFILE_SPECIFIER = ""; 475 | }; 476 | name = Debug; 477 | }; 478 | 4956C8E31EB109C30021E6E8 /* Release */ = { 479 | isa = XCBuildConfiguration; 480 | buildSettings = { 481 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 482 | CODE_SIGN_IDENTITY = "iPhone Developer"; 483 | CODE_SIGN_STYLE = Automatic; 484 | DEVELOPMENT_TEAM = ""; 485 | INFOPLIST_FILE = Demo/Info.plist; 486 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 487 | PRODUCT_BUNDLE_IDENTIFIER = demo.bubbleLayer.ihandle; 488 | PRODUCT_NAME = "$(TARGET_NAME)"; 489 | PROVISIONING_PROFILE_SPECIFIER = ""; 490 | }; 491 | name = Release; 492 | }; 493 | 4956C8E51EB109C30021E6E8 /* Debug */ = { 494 | isa = XCBuildConfiguration; 495 | buildSettings = { 496 | BUNDLE_LOADER = "$(TEST_HOST)"; 497 | DEVELOPMENT_TEAM = N56D5J55Q8; 498 | INFOPLIST_FILE = DemoTests/Info.plist; 499 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 500 | PRODUCT_BUNDLE_IDENTIFIER = ihandle.DemoTests; 501 | PRODUCT_NAME = "$(TARGET_NAME)"; 502 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Demo.app/Demo"; 503 | }; 504 | name = Debug; 505 | }; 506 | 4956C8E61EB109C30021E6E8 /* Release */ = { 507 | isa = XCBuildConfiguration; 508 | buildSettings = { 509 | BUNDLE_LOADER = "$(TEST_HOST)"; 510 | DEVELOPMENT_TEAM = N56D5J55Q8; 511 | INFOPLIST_FILE = DemoTests/Info.plist; 512 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 513 | PRODUCT_BUNDLE_IDENTIFIER = ihandle.DemoTests; 514 | PRODUCT_NAME = "$(TARGET_NAME)"; 515 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Demo.app/Demo"; 516 | }; 517 | name = Release; 518 | }; 519 | 4956C8E81EB109C30021E6E8 /* Debug */ = { 520 | isa = XCBuildConfiguration; 521 | buildSettings = { 522 | DEVELOPMENT_TEAM = N56D5J55Q8; 523 | INFOPLIST_FILE = DemoUITests/Info.plist; 524 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 525 | PRODUCT_BUNDLE_IDENTIFIER = ihandle.DemoUITests; 526 | PRODUCT_NAME = "$(TARGET_NAME)"; 527 | TEST_TARGET_NAME = Demo; 528 | }; 529 | name = Debug; 530 | }; 531 | 4956C8E91EB109C30021E6E8 /* Release */ = { 532 | isa = XCBuildConfiguration; 533 | buildSettings = { 534 | DEVELOPMENT_TEAM = N56D5J55Q8; 535 | INFOPLIST_FILE = DemoUITests/Info.plist; 536 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 537 | PRODUCT_BUNDLE_IDENTIFIER = ihandle.DemoUITests; 538 | PRODUCT_NAME = "$(TARGET_NAME)"; 539 | TEST_TARGET_NAME = Demo; 540 | }; 541 | name = Release; 542 | }; 543 | /* End XCBuildConfiguration section */ 544 | 545 | /* Begin XCConfigurationList section */ 546 | 4956C8AF1EB109C30021E6E8 /* Build configuration list for PBXProject "Demo" */ = { 547 | isa = XCConfigurationList; 548 | buildConfigurations = ( 549 | 4956C8DF1EB109C30021E6E8 /* Debug */, 550 | 4956C8E01EB109C30021E6E8 /* Release */, 551 | ); 552 | defaultConfigurationIsVisible = 0; 553 | defaultConfigurationName = Release; 554 | }; 555 | 4956C8E11EB109C30021E6E8 /* Build configuration list for PBXNativeTarget "Demo" */ = { 556 | isa = XCConfigurationList; 557 | buildConfigurations = ( 558 | 4956C8E21EB109C30021E6E8 /* Debug */, 559 | 4956C8E31EB109C30021E6E8 /* Release */, 560 | ); 561 | defaultConfigurationIsVisible = 0; 562 | defaultConfigurationName = Release; 563 | }; 564 | 4956C8E41EB109C30021E6E8 /* Build configuration list for PBXNativeTarget "DemoTests" */ = { 565 | isa = XCConfigurationList; 566 | buildConfigurations = ( 567 | 4956C8E51EB109C30021E6E8 /* Debug */, 568 | 4956C8E61EB109C30021E6E8 /* Release */, 569 | ); 570 | defaultConfigurationIsVisible = 0; 571 | defaultConfigurationName = Release; 572 | }; 573 | 4956C8E71EB109C30021E6E8 /* Build configuration list for PBXNativeTarget "DemoUITests" */ = { 574 | isa = XCConfigurationList; 575 | buildConfigurations = ( 576 | 4956C8E81EB109C30021E6E8 /* Debug */, 577 | 4956C8E91EB109C30021E6E8 /* Release */, 578 | ); 579 | defaultConfigurationIsVisible = 0; 580 | defaultConfigurationName = Release; 581 | }; 582 | /* End XCConfigurationList section */ 583 | }; 584 | rootObject = 4956C8AC1EB109C30021E6E8 /* Project object */; 585 | } 586 | -------------------------------------------------------------------------------- /Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo.xcodeproj/project.xcworkspace/xcuserdata/ihandle.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iHandle/BubbleLayer/9730c54ced5166d8c36627a89e9c72ecc7456750/Demo.xcodeproj/project.xcworkspace/xcuserdata/ihandle.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Demo.xcodeproj/xcuserdata/ihandle.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Demo.xcodeproj/xcuserdata/ihandle.xcuserdatad/xcschemes/Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Demo.xcodeproj/xcuserdata/ihandle.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Demo.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 4956C8B31EB109C30021E6E8 16 | 17 | primary 18 | 19 | 20 | 4956C8CC1EB109C30021E6E8 21 | 22 | primary 23 | 24 | 25 | 4956C8D71EB109C30021E6E8 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Demo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // Demo 4 | // 5 | // 6 | 7 | #import 8 | 9 | @interface AppDelegate : UIResponder 10 | 11 | @property (strong, nonatomic) UIWindow *window; 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /Demo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // Demo 4 | // 5 | 6 | // 7 | 8 | #import "AppDelegate.h" 9 | 10 | @interface AppDelegate () 11 | 12 | @end 13 | 14 | @implementation AppDelegate 15 | 16 | 17 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 18 | // Override point for customization after application launch. 19 | return YES; 20 | } 21 | 22 | 23 | - (void)applicationWillResignActive:(UIApplication *)application { 24 | // 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. 25 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 26 | } 27 | 28 | 29 | - (void)applicationDidEnterBackground:(UIApplication *)application { 30 | // 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. 31 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 32 | } 33 | 34 | 35 | - (void)applicationWillEnterForeground:(UIApplication *)application { 36 | // 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. 37 | } 38 | 39 | 40 | - (void)applicationDidBecomeActive:(UIApplication *)application { 41 | // 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. 42 | } 43 | 44 | 45 | - (void)applicationWillTerminate:(UIApplication *)application { 46 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 47 | } 48 | 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /Demo/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 | } -------------------------------------------------------------------------------- /Demo/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 | -------------------------------------------------------------------------------- /Demo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 72 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /Demo/Doraemon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iHandle/BubbleLayer/9730c54ced5166d8c36627a89e9c72ecc7456750/Demo/Doraemon.jpeg -------------------------------------------------------------------------------- /Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Demo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // Bubble 4 | // 5 | // 6 | 7 | #import 8 | #import "BubbleLayer.h" 9 | 10 | @interface ViewController : UIViewController 11 | 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /Demo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // Demo 4 | // 5 | 6 | #import "ViewController.h" 7 | 8 | @interface ViewController () 9 | 10 | // 这些控件的一些参数在storyboard设置了 11 | @property (weak, nonatomic) IBOutlet UIImageView *imageView; 12 | @property (weak, nonatomic) IBOutlet UISegmentedControl *segCtrl; 13 | @property (weak, nonatomic) IBOutlet UISlider *sdArrowHeight; 14 | @property (weak, nonatomic) IBOutlet UISlider *sdArrowWidth; 15 | @property (weak, nonatomic) IBOutlet UISlider *sdArrowRadius; 16 | @property (weak, nonatomic) IBOutlet UISlider *sdCornerRadius; 17 | @property (weak, nonatomic) IBOutlet UISlider *sdArrowPosition; 18 | @property (weak, nonatomic) IBOutlet UISwitch *bbSwitch; 19 | 20 | @property (strong, nonatomic) BubbleLayer *bbLayer; 21 | 22 | 23 | @end 24 | 25 | @implementation ViewController 26 | 27 | - (void)viewDidLoad { 28 | [super viewDidLoad]; 29 | 30 | 31 | UIImage *image = [UIImage imageNamed:(@"Doraemon.jpeg")]; 32 | _imageView.image = image; 33 | 34 | 35 | 36 | [_segCtrl addTarget:self action:@selector(segCtrlAction) forControlEvents:UIControlEventValueChanged]; 37 | 38 | [_sdArrowHeight addTarget:self action:@selector(sliderAction:) forControlEvents:UIControlEventValueChanged]; 39 | [_sdArrowWidth addTarget:self action:@selector(sliderAction:) forControlEvents:UIControlEventValueChanged]; 40 | [_sdArrowRadius addTarget:self action:@selector(sliderAction:) forControlEvents:UIControlEventValueChanged]; 41 | [_sdCornerRadius addTarget:self action:@selector(sliderAction:) forControlEvents:UIControlEventValueChanged]; 42 | [_sdArrowPosition addTarget:self action:@selector(sliderAction:) forControlEvents:UIControlEventValueChanged]; 43 | 44 | [_bbSwitch addTarget:self action:@selector(switchAction) forControlEvents:UIControlEventValueChanged]; 45 | 46 | 47 | 48 | } 49 | 50 | 51 | - (void) segCtrlAction { 52 | 53 | NSInteger index = _segCtrl.selectedSegmentIndex; 54 | ArrowDirection direction = (ArrowDirection)index; 55 | _bbLayer.arrowDirection = direction; 56 | _imageView.layer.mask = _bbLayer.layer; 57 | 58 | } 59 | 60 | 61 | - (void) sliderAction: (UISlider*)sender { 62 | 63 | if ([sender isEqual:_sdArrowHeight]) { 64 | _bbLayer.arrowHeight = _sdArrowHeight.value; 65 | } else if ([sender isEqual:_sdArrowWidth]) { 66 | _bbLayer.arrowWidth = _sdArrowWidth.value; 67 | } else if ([sender isEqual:_sdArrowRadius]) { 68 | _bbLayer.arrowRadius = _sdArrowRadius.value; 69 | } else if ([sender isEqual:_sdCornerRadius]) { 70 | _bbLayer.cornerRadius = _sdCornerRadius.value; 71 | } else if ([sender isEqual:_sdArrowPosition]) { 72 | _bbLayer.arrowPosition = _sdArrowPosition.value; 73 | } 74 | 75 | _imageView.layer.mask = _bbLayer.layer; 76 | 77 | } 78 | 79 | 80 | // 气泡模式的开关 81 | - (void) switchAction { 82 | 83 | BOOL enabled = _bbSwitch.isOn; 84 | _segCtrl.enabled = enabled; 85 | _sdArrowHeight.enabled = enabled; 86 | _sdArrowWidth.enabled = enabled; 87 | _sdArrowRadius.enabled = enabled; 88 | _sdCornerRadius.enabled = enabled; 89 | _sdArrowPosition.enabled = enabled; 90 | 91 | if (enabled) { 92 | 93 | if(_bbLayer == nil) { 94 | _bbLayer = [[BubbleLayer alloc]initWithSize:_imageView.frame.size]; 95 | _bbLayer.arrowDirection = (ArrowDirection)_segCtrl.selectedSegmentIndex; 96 | _bbLayer.arrowHeight = _sdArrowHeight.value; 97 | _bbLayer.arrowWidth = _sdArrowWidth.value; 98 | _bbLayer.arrowRadius = _sdArrowRadius.value; 99 | _bbLayer.cornerRadius = _sdCornerRadius.value; 100 | _bbLayer.arrowPosition = _sdArrowPosition.value; 101 | 102 | } 103 | 104 | _imageView.layer.mask = _bbLayer.layer; 105 | } 106 | else 107 | _imageView.layer.mask = nil; 108 | 109 | 110 | } 111 | 112 | 113 | - (void)didReceiveMemoryWarning { 114 | [super didReceiveMemoryWarning]; 115 | // Dispose of any resources that can be recreated. 116 | } 117 | 118 | 119 | @end 120 | -------------------------------------------------------------------------------- /Demo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Demo 4 | // 5 | // 6 | 7 | #import 8 | #import "AppDelegate.h" 9 | 10 | int main(int argc, char * argv[]) { 11 | @autoreleasepool { 12 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DemoTests/DemoTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // DemoTests.m 3 | // DemoTests 4 | // 5 | // Created by iHandle on 2017/4/27. 6 | // Copyright © 2017年 iHandle. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DemoTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation DemoTests 16 | 17 | - (void)setUp { 18 | [super setUp]; 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | - (void)tearDown { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | [super tearDown]; 25 | } 26 | 27 | - (void)testExample { 28 | // This is an example of a functional test case. 29 | // Use XCTAssert and related functions to verify your tests produce the correct results. 30 | } 31 | 32 | - (void)testPerformanceExample { 33 | // This is an example of a performance test case. 34 | [self measureBlock:^{ 35 | // Put the code you want to measure the time of here. 36 | }]; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /DemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /DemoUITests/DemoUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // DemoUITests.m 3 | // DemoUITests 4 | // 5 | // Created by iHandle on 2017/4/27. 6 | // Copyright © 2017年 iHandle. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface DemoUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation DemoUITests 16 | 17 | - (void)setUp { 18 | [super setUp]; 19 | 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | 22 | // In UI tests it is usually best to stop immediately when a failure occurs. 23 | self.continueAfterFailure = NO; 24 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 25 | [[[XCUIApplication alloc] init] launch]; 26 | 27 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 28 | } 29 | 30 | - (void)tearDown { 31 | // Put teardown code here. This method is called after the invocation of each test method in the class. 32 | [super tearDown]; 33 | } 34 | 35 | - (void)testExample { 36 | // Use recording to get started writing UI tests. 37 | // Use XCTAssert and related functions to verify your tests produce the correct results. 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /DemoUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Bubble Layer 2 | 3 | 用于将一个`view`做成“气泡”的样式,效果如下图所示: 4 | 5 | 6 | 7 | #### 使用方法 8 | 将`BubbleLayer.h`和`BubbleLayer.m`两个文件导入你的工程,然后在使用的地方`import`头文件。如果是`Swift`工程,可以直接导入`BubbleLayer.swift`。 9 | 10 | 下面是一个使用`Objective-C`的例子: 11 | 12 | ```objc 13 | BubbleLayer *bbLayer = [[BubbleLayer alloc]initWithSize:myView.bounds.size]; 14 | 15 | // 矩形框的圆角半径 16 | bbLayer.cornerRadius = 20; 17 | 18 | // 凸起那部分暂且称之为“箭头”,下面的参数设置它的形状 19 | bbLayer.arrowDirection = ArrowDirectionLeft; 20 | bbLayer.arrowHeight = 22; // 箭头的高度(长度) 21 | bbLayer.arrowWidth = 30; // 箭头的宽度 22 | bbLayer.arrowPosition = 0.5;// 箭头的相对位置 23 | bbLayer.arrowRadius = 3; // 箭头处的圆角半径 24 | 25 | [myView.layer setMask:[bbLayer layer]]; 26 | 27 | ``` 28 | 29 | #### Demo 30 | 如果不太清楚的参数的含义,可以通过使用Demo理解。 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Swift/BubbleLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BubbleLayer.swift 3 | // BubbleLayer-Swift 4 | // 5 | // 6 | 7 | import UIKit 8 | 9 | // 一些在注释中使用的叫法 10 | // 箭头: 气泡形状突出那个三角形把它叫做【箭头】 11 | // 箭头的顶点和底点: 将箭头指向的那个点叫为【顶点】,其余两个点叫为【底点】 12 | // 箭头的高度和宽度: 箭头顶点到底点连线的距离叫为【箭头的高度】,两底点的距离叫为【箭头的宽度】 13 | // 矩形框: 气泡形状除了箭头,剩下的部分叫为【矩形框】 14 | // 箭头的相对位置: 如果箭头的方向是向右或者向左,0表达箭头在最上方,1表示箭头在最下方 15 | // 如果箭头的方向是向上或者向下,0表达箭头在最左边,1表示箭头在最右边 16 | // 默认是 0.5,即在中间 17 | 18 | // 箭头方向枚举 19 | enum ArrowDirection: Int { 20 | case right = 0 //指向右边, 即在圆角矩形的右边 21 | case bottom = 1 //指向下边 22 | case left = 2 //指向左边 23 | case top = 3 //指向上边 24 | } 25 | 26 | class BubbleLayer: NSObject { 27 | 28 | // 矩形的圆角的半径 29 | var cornerRadius: CGFloat = 8 30 | // 箭头位置的圆角半径 31 | var arrowRadius: CGFloat = 3 32 | // 箭头的高度 33 | var arrowHeight: CGFloat = 12 34 | // 箭头的宽度 35 | var arrowWidth: CGFloat = 30 36 | // 箭头方向 37 | var arrowDirection: Int = 1 38 | // 箭头的相对位置 39 | var arrowPosition: CGFloat = 0.5 40 | // 这里的size是需要mask成气泡形状的view的size 41 | var size: CGSize = CGSize.zero 42 | 43 | 44 | init(originalSize: CGSize) { 45 | size = originalSize 46 | } 47 | 48 | //最终拿这个layer去设置mask 49 | func layer() -> CAShapeLayer { 50 | let layer = CAShapeLayer() 51 | layer.path = self.bubblePath() 52 | return layer 53 | } 54 | 55 | 56 | // 绘制气泡形状,获取path 57 | func bubblePath() -> CGPath? { 58 | 59 | UIGraphicsBeginImageContextWithOptions(size, false, 0.0) 60 | let ctx = UIGraphicsGetCurrentContext() 61 | 62 | // 获取绘图所需要的关键点 63 | let points = self.keyPoints() 64 | 65 | // 第一步是要画箭头的“第一个支点”所在的那个角,所以要把“笔”放在这个支点顺时针顺序的上一个点 66 | // 所以把“笔”放在最后才画的矩形框的角的位置, 准备开始画箭头 67 | let currentPoint = points[6] 68 | ctx?.move(to: currentPoint) 69 | 70 | // 用于 CGContextAddArcToPoint函数的变量 71 | var pointA = CGPoint.zero 72 | var pointB = CGPoint.zero 73 | var radius: CGFloat = 0 74 | var count: Int = 0 75 | 76 | while count < 7 { 77 | // 整个过程需要画七个圆角(矩形框的四个角和箭头处的三个角),所以分为七个步骤 78 | 79 | // 箭头处的三个圆角和矩形框的四个圆角不一样 80 | radius = count < 3 ? arrowRadius : cornerRadius 81 | 82 | pointA = points[count] 83 | pointB = points[(count + 1) % 7] 84 | // 画矩形框最后一个角的时候,pointB就是points[0] 85 | 86 | ctx?.addArc(tangent1End: pointA, tangent2End: pointB, radius: radius) 87 | 88 | count = count + 1 89 | 90 | } 91 | 92 | ctx?.closePath() 93 | UIGraphicsEndImageContext() 94 | 95 | return ctx?.path?.copy() 96 | } 97 | 98 | 99 | // 关键点: 绘制气泡形状前,需要计算箭头的三个点和矩形的四个角的点的坐标 100 | func keyPoints() -> Array { 101 | 102 | // 先确定箭头的三个点 103 | var beginPoint = CGPoint.zero // 按顺时针画箭头时的第一个支点,例如箭头向上时的左边的支点 104 | var topPoint = CGPoint.zero // 顶点 105 | var endPoint = CGPoint.zero // 另外一个支点 106 | 107 | // 箭头顶点topPoint的X坐标(或Y坐标)的范围(用来计算arrowPosition) 108 | let tpXRange = size.width - 2 * cornerRadius - arrowWidth 109 | let tpYRange = size.height - 2 * cornerRadius - arrowWidth 110 | 111 | // 用于表示矩形框的位置和大小 112 | var rX: CGFloat = 0 113 | var rY: CGFloat = 0 114 | var rWidth = size.width 115 | var rHeight = size.height 116 | 117 | // 计算箭头的位置,以及调整矩形框的位置和大小 118 | switch arrowDirection { 119 | 120 | case 0: //箭头在右时 121 | topPoint = CGPoint(x: size.width, y: size.height / 2 + tpYRange * (arrowPosition - 0.5)) 122 | beginPoint = CGPoint(x: topPoint.x - arrowHeight, y:topPoint.y - arrowWidth / 2 ) 123 | endPoint = CGPoint(x: beginPoint.x, y: beginPoint.y + arrowWidth) 124 | 125 | rWidth = rWidth - arrowHeight //矩形框右边的位置“腾出”给箭头 126 | 127 | case 1: //箭头在下时 128 | topPoint = CGPoint(x: size.width / 2 + tpXRange * (arrowPosition - 0.5), y: size.height) 129 | beginPoint = CGPoint(x: topPoint.x + arrowWidth / 2, y:topPoint.y - arrowHeight ) 130 | endPoint = CGPoint(x: beginPoint.x - arrowWidth, y: beginPoint.y) 131 | 132 | rHeight = rHeight - arrowHeight 133 | 134 | case 2: //箭头在左时 135 | topPoint = CGPoint(x: 0, y: size.height / 2 + tpYRange * (arrowPosition - 0.5)) 136 | beginPoint = CGPoint(x: topPoint.x + arrowHeight, y: topPoint.y + arrowWidth / 2) 137 | endPoint = CGPoint(x: beginPoint.x, y: beginPoint.y - arrowWidth) 138 | 139 | rX = arrowHeight 140 | rWidth = rWidth - arrowHeight 141 | 142 | case 3: //箭头在上时 143 | topPoint = CGPoint(x: size.width / 2 + tpXRange * (arrowPosition - 0.5), y: 0) 144 | beginPoint = CGPoint(x: topPoint.x - arrowWidth / 2, y: topPoint.y + arrowHeight) 145 | endPoint = CGPoint(x: beginPoint.x + arrowWidth, y: beginPoint.y) 146 | 147 | rY = arrowHeight 148 | rHeight = rHeight - arrowHeight 149 | 150 | default: 151 | () 152 | } 153 | 154 | // 先把箭头的三个点放进关键点数组中 155 | var points = [beginPoint, topPoint, endPoint] 156 | 157 | //确定圆角矩形的四个点 158 | let bottomRight = CGPoint(x: rX + rWidth, y: rY + rHeight); //右下角的点 159 | let bottomLeft = CGPoint(x: rX, y: rY + rHeight); 160 | let topLeft = CGPoint(x: rX, y: rY); 161 | let topRight = CGPoint(x: rX + rWidth, y: rY); 162 | 163 | //先放在一个临时数组, 放置顺序跟下面紧接着的操作有关 164 | let rectPoints = [bottomRight, bottomLeft, topLeft, topRight] 165 | 166 | // 绘制气泡形状的时候,从箭头开始,顺时针地进行 167 | // 箭头向右时,画完箭头之后会先画到矩形框的右下角 168 | // 所以此时先把矩形框右下角的点放进关键点数组,其他三个点按顺时针方向添加 169 | // 箭头在其他方向时,以此类推 170 | 171 | var rectPointIndex: Int = arrowDirection 172 | for _ in 0...3 { 173 | points.append(rectPoints[rectPointIndex]) 174 | rectPointIndex = (rectPointIndex + 1) % 4 175 | } 176 | 177 | return points 178 | } 179 | 180 | 181 | } 182 | --------------------------------------------------------------------------------