├── .gitignore ├── MFSpringView ├── MFShaderHelper.h ├── MFShaderHelper.m ├── MFSpringView.h ├── MFSpringView.m ├── MFVertexAttribArrayBuffer.h ├── MFVertexAttribArrayBuffer.m ├── spring.fsh └── spring.vsh ├── MFSpringViewDemo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── MFSpringViewDemo ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── ViewController.h ├── ViewController.m ├── girl.jpg └── main.m ├── README.md └── image ├── image.gif └── image1.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | # CocoaPods 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | # Pods/ 38 | 39 | # Carthage 40 | # 41 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 42 | # Carthage/Checkouts 43 | 44 | Carthage/Build 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 52 | 53 | fastlane/report.xml 54 | fastlane/Preview.html 55 | fastlane/screenshots 56 | fastlane/test_output 57 | 58 | # Code Injection 59 | # 60 | # After new code Injection tools there's a generated folder /iOSInjectionProject 61 | # https://github.com/johnno1962/injectionforxcode 62 | 63 | iOSInjectionProject/ 64 | 65 | xcshareddata/ -------------------------------------------------------------------------------- /MFSpringView/MFShaderHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // MFShaderHelper.h 3 | // MFSpringViewDemo 4 | // 5 | // Created by Lyman on 2019/2/28. 6 | // Copyright © 2019 Lyman Li. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface MFShaderHelper : NSObject 13 | 14 | /** 15 | 将一个顶点着色器和一个片段着色器挂载到一个着色器程序上,并返回程序的 id 16 | 17 | @param shaderName 着色器名称,顶点着色器应该命名为 shaderName.vsh ,片段着色器应该命名为 shaderName.fsh 18 | @return 着色器程序的 ID 19 | */ 20 | + (GLuint)programWithShaderName:(NSString *)shaderName; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /MFSpringView/MFShaderHelper.m: -------------------------------------------------------------------------------- 1 | // 2 | // MFShaderHelper.m 3 | // MFSpringViewDemo 4 | // 5 | // Created by Lyman on 2019/2/28. 6 | // Copyright © 2019 Lyman Li. All rights reserved. 7 | // 8 | 9 | #import "MFShaderHelper.h" 10 | 11 | @implementation MFShaderHelper 12 | 13 | // 将一个顶点着色器和一个片段着色器挂载到一个着色器程序上,并返回程序的 id 14 | + (GLuint)programWithShaderName:(NSString *)shaderName { 15 | // 编译两个着色器 16 | GLuint vertexShader = [self compileShaderWithName:shaderName type:GL_VERTEX_SHADER]; 17 | GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER]; 18 | 19 | // 挂载 shader 到 program 上 20 | GLuint program = glCreateProgram(); 21 | glAttachShader(program, vertexShader); 22 | glAttachShader(program, fragmentShader); 23 | 24 | // 链接 program 25 | glLinkProgram(program); 26 | 27 | // 检查链接是否成功 28 | GLint linkSuccess; 29 | glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess); 30 | if (linkSuccess == GL_FALSE) { 31 | GLchar messages[256]; 32 | glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]); 33 | NSString *messageString = [NSString stringWithUTF8String:messages]; 34 | NSAssert(NO, @"program链接失败:%@", messageString); 35 | exit(1); 36 | } 37 | return program; 38 | } 39 | 40 | // 编译一个 shader,并返回 shader 的 id 41 | + (GLuint)compileShaderWithName:(NSString *)name type:(GLenum)shaderType { 42 | // 查找 shader 文件 43 | NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"]; // 根据不同的类型确定后缀名 44 | NSError *error; 45 | NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error]; 46 | if (!shaderString) { 47 | NSAssert(NO, @"读取shader失败"); 48 | exit(1); 49 | } 50 | 51 | // 创建一个 shader 对象 52 | GLuint shader = glCreateShader(shaderType); 53 | 54 | // 获取 shader 的内容 55 | const char *shaderStringUTF8 = [shaderString UTF8String]; 56 | int shaderStringLength = (int)[shaderString length]; 57 | glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength); 58 | 59 | // 编译shader 60 | glCompileShader(shader); 61 | 62 | // 查询 shader 是否编译成功 63 | GLint compileSuccess; 64 | glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess); 65 | if (compileSuccess == GL_FALSE) { 66 | GLchar messages[256]; 67 | glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]); 68 | NSString *messageString = [NSString stringWithUTF8String:messages]; 69 | NSAssert(NO, @"shader编译失败:%@", messageString); 70 | exit(1); 71 | } 72 | 73 | return shader; 74 | } 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /MFSpringView/MFSpringView.h: -------------------------------------------------------------------------------- 1 | // 2 | // MFSpringView.h 3 | // MFSpringView 4 | // 5 | // Created by Lyman Li on 2018/11/25. 6 | // Copyright © 2018年 Lyman Li. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class MFSpringView; 12 | 13 | @protocol MFSpringViewDelegate 14 | 15 | - (void)springViewStretchAreaDidChanged:(MFSpringView *)springView; 16 | 17 | @end 18 | 19 | @interface MFSpringView : GLKView 20 | 21 | @property (nonatomic, weak) id springDelegate; 22 | @property (nonatomic, assign, readonly) BOOL hasChange; // 拉伸区域是否被拉伸 23 | 24 | /** 25 | 将区域拉伸或压缩为某个高度 26 | 27 | @param startY 开始的纵坐标位置(相对于纹理) 28 | @param endY 结束的纵坐标位置(相对于纹理) 29 | @param newHeight 新的高度(相对于纹理) 30 | */ 31 | - (void)stretchingFromStartY:(CGFloat)startY 32 | toEndY:(CGFloat)endY 33 | withNewHeight:(CGFloat)newHeight; 34 | 35 | /** 36 | 纹理顶部的纵坐标 0~1 37 | 38 | @return 纹理顶部的纵坐标(相对于 View) 39 | */ 40 | - (CGFloat)textureTopY; 41 | 42 | /** 43 | 纹理底部的纵坐标 0~1 44 | 45 | @return 纹理底部的纵坐标(相对于 View) 46 | */ 47 | - (CGFloat)textureBottomY; 48 | 49 | /** 50 | 可伸缩区域顶部的纵坐标 0~1 51 | 52 | @return 可伸缩区域顶部的纵坐标(相对于 View) 53 | */ 54 | - (CGFloat)stretchAreaTopY; 55 | 56 | /** 57 | 可伸缩区域底部的纵坐标 0~1 58 | 59 | @return 可伸缩区域底部的纵坐标(相对于 View) 60 | */ 61 | - (CGFloat)stretchAreaBottomY; 62 | 63 | /** 64 | 纹理高度 0~1 65 | 66 | @return 纹理高度(相对于 View) 67 | */ 68 | - (CGFloat)textureHeight; 69 | 70 | /** 71 | 获取当前的渲染结果 72 | */ 73 | - (UIImage *)createResult; 74 | 75 | /** 76 | 根据当前的拉伸结果来重新生成纹理 77 | */ 78 | - (void)updateTexture; 79 | 80 | /** 81 | 更新图片 82 | */ 83 | - (void)updateImage:(UIImage *)image; 84 | 85 | @end 86 | -------------------------------------------------------------------------------- /MFSpringView/MFSpringView.m: -------------------------------------------------------------------------------- 1 | // 2 | // MFSpringView.m 3 | // MFSpringView 4 | // 5 | // Created by Lyman Li on 2018/11/25. 6 | // Copyright © 2018年 Lyman Li. All rights reserved. 7 | // 8 | 9 | #import "MFShaderHelper.h" 10 | #import "MFVertexAttribArrayBuffer.h" 11 | 12 | #import "MFSpringView.h" 13 | 14 | static CGFloat const kDefaultOriginTextureHeight = 0.7f; // 初始纹理高度占控件高度的比例 15 | static NSInteger const kVerticesCount = 8; // 顶点数量 16 | 17 | typedef struct { 18 | GLKVector3 positionCoord; 19 | GLKVector2 textureCoord; 20 | } SenceVertex; 21 | 22 | @interface MFSpringView () 23 | 24 | @property (nonatomic, strong) GLKBaseEffect *baseEffect; 25 | @property (nonatomic, assign) SenceVertex *vertices; 26 | 27 | @property (nonatomic, strong) MFVertexAttribArrayBuffer *vertexAttribArrayBuffer; 28 | @property (nonatomic, assign) CGSize currentImageSize; 29 | 30 | @property (nonatomic, assign, readwrite) BOOL hasChange; 31 | @property (nonatomic, assign) CGFloat currentTextureWidth; 32 | 33 | // 临时创建的帧缓存和纹理缓存 34 | @property (nonatomic, assign) GLuint tmpFrameBuffer; 35 | @property (nonatomic, assign) GLuint tmpTexture; 36 | 37 | // 用于重新绘制纹理 38 | @property (nonatomic, assign) CGFloat currentTextureStartY; 39 | @property (nonatomic, assign) CGFloat currentTextureEndY; 40 | @property (nonatomic, assign) CGFloat currentNewHeight; 41 | 42 | @end 43 | 44 | @implementation MFSpringView 45 | 46 | - (void)dealloc { 47 | if ([EAGLContext currentContext] == self.context) { 48 | [EAGLContext setCurrentContext:nil]; 49 | } 50 | if (_vertices) { 51 | free(_vertices); 52 | _vertices = nil; 53 | } 54 | if (_tmpFrameBuffer) { 55 | glDeleteFramebuffers(1, &_tmpFrameBuffer); 56 | _tmpFrameBuffer = 0; 57 | } 58 | if (_tmpTexture) { 59 | glDeleteTextures(1, &_tmpTexture); 60 | _tmpTexture = 0; 61 | } 62 | } 63 | 64 | - (instancetype)initWithFrame:(CGRect)frame { 65 | self = [super initWithFrame:frame]; 66 | if (self) { 67 | [self commonInit]; 68 | } 69 | return self; 70 | } 71 | 72 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 73 | self = [super initWithCoder:aDecoder]; 74 | if (self) { 75 | [self commonInit]; 76 | } 77 | return self; 78 | } 79 | 80 | #pragma mark - Public 81 | 82 | - (void)stretchingFromStartY:(CGFloat)startY 83 | toEndY:(CGFloat)endY 84 | withNewHeight:(CGFloat)newHeight { 85 | self.hasChange = YES; 86 | 87 | [self calculateOriginTextureCoordWithTextureSize:self.currentImageSize 88 | startY:startY 89 | endY:endY 90 | newHeight:newHeight]; 91 | [self.vertexAttribArrayBuffer updateDataWithAttribStride:sizeof(SenceVertex) 92 | numberOfVertices:kVerticesCount 93 | data:self.vertices 94 | usage:GL_STATIC_DRAW]; 95 | [self display]; 96 | 97 | if (self.springDelegate && 98 | [self.springDelegate respondsToSelector:@selector(springViewStretchAreaDidChanged:)]) { 99 | [self.springDelegate springViewStretchAreaDidChanged:self]; 100 | } 101 | } 102 | 103 | - (CGFloat)textureTopY { 104 | return (1 - self.vertices[0].positionCoord.y) / 2; 105 | } 106 | 107 | - (CGFloat)textureBottomY { 108 | return (1 - self.vertices[7].positionCoord.y) / 2; 109 | } 110 | 111 | - (CGFloat)stretchAreaTopY { 112 | return (1 - self.vertices[2].positionCoord.y) / 2; 113 | } 114 | 115 | - (CGFloat)stretchAreaBottomY { 116 | return (1 - self.vertices[5].positionCoord.y) / 2; 117 | } 118 | 119 | - (CGFloat)textureHeight { 120 | return self.textureBottomY - self.textureTopY; 121 | } 122 | 123 | - (UIImage *)createResult { 124 | [self resetTextureWithOriginWidth:self.currentImageSize.width 125 | originHeight:self.currentImageSize.height 126 | topY:self.currentTextureStartY 127 | bottomY:self.currentTextureEndY 128 | newHeight:self.currentNewHeight]; 129 | 130 | glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer); 131 | CGSize imageSize = [self newImageSize]; 132 | UIImage *image = [self imageFromTextureWithWidth:imageSize.width height:imageSize.height]; 133 | glBindFramebuffer(GL_FRAMEBUFFER, 0); 134 | 135 | return image; 136 | } 137 | 138 | - (void)updateTexture { 139 | [self resetTextureWithOriginWidth:self.currentImageSize.width 140 | originHeight:self.currentImageSize.height 141 | topY:self.currentTextureStartY 142 | bottomY:self.currentTextureEndY 143 | newHeight:self.currentNewHeight]; 144 | // 设置新的纹理 145 | if (self.baseEffect.texture2d0.name != 0) { 146 | GLuint textureName = self.baseEffect.texture2d0.name; 147 | glDeleteTextures(1, &textureName); 148 | } 149 | self.baseEffect.texture2d0.name = self.tmpTexture; 150 | 151 | // 重置图片的尺寸 152 | self.currentImageSize = [self newImageSize]; 153 | 154 | self.hasChange = NO; 155 | 156 | [self calculateOriginTextureCoordWithTextureSize:self.currentImageSize 157 | startY:0 158 | endY:0 159 | newHeight:0]; 160 | [self.vertexAttribArrayBuffer updateDataWithAttribStride:sizeof(SenceVertex) 161 | numberOfVertices:kVerticesCount 162 | data:self.vertices 163 | usage:GL_STATIC_DRAW]; 164 | [self display]; 165 | } 166 | 167 | - (void)updateImage:(UIImage *)image { 168 | self.hasChange = NO; 169 | 170 | NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft : @(YES)}; 171 | GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:[image CGImage] 172 | options:options 173 | error:NULL]; 174 | 175 | self.baseEffect = [[GLKBaseEffect alloc] init]; 176 | self.baseEffect.texture2d0.name = textureInfo.name; 177 | 178 | self.currentImageSize = image.size; 179 | 180 | CGFloat ratio = (self.currentImageSize.height / self.currentImageSize.width) * 181 | (self.bounds.size.width / self.bounds.size.height); 182 | CGFloat textureHeight = MIN(ratio, kDefaultOriginTextureHeight); 183 | self.currentTextureWidth = textureHeight / ratio; 184 | 185 | [self calculateOriginTextureCoordWithTextureSize:self.currentImageSize 186 | startY:0 187 | endY:0 188 | newHeight:0]; 189 | [self.vertexAttribArrayBuffer updateDataWithAttribStride:sizeof(SenceVertex) 190 | numberOfVertices:kVerticesCount 191 | data:self.vertices 192 | usage:GL_STATIC_DRAW]; 193 | [self display]; 194 | } 195 | 196 | #pragma mark - Private 197 | 198 | - (void)commonInit { 199 | self.vertices = malloc(sizeof(SenceVertex) * kVerticesCount); 200 | 201 | self.backgroundColor = [UIColor clearColor]; 202 | self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; 203 | self.delegate = self; 204 | [EAGLContext setCurrentContext:self.context]; 205 | glClearColor(0, 0, 0, 0); 206 | 207 | self.vertexAttribArrayBuffer = [[MFVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SenceVertex) numberOfVertices:kVerticesCount data:self.vertices usage:GL_STATIC_DRAW]; 208 | } 209 | 210 | /** 211 | 根据当前控件的尺寸和纹理的尺寸,计算初始纹理坐标 212 | 213 | @param size 原始纹理尺寸 214 | @param startY 中间区域的开始纵坐标位置 0~1 215 | @param endY 中间区域的结束纵坐标位置 0~1 216 | @param newHeight 新的中间区域的高度 217 | */ 218 | - (void)calculateOriginTextureCoordWithTextureSize:(CGSize)size 219 | startY:(CGFloat)startY 220 | endY:(CGFloat)endY 221 | newHeight:(CGFloat)newHeight { 222 | CGFloat ratio = (size.height / size.width) * 223 | (self.bounds.size.width / self.bounds.size.height); 224 | CGFloat textureWidth = self.currentTextureWidth; 225 | CGFloat textureHeight = textureWidth * ratio; 226 | 227 | // 拉伸量 228 | CGFloat delta = (newHeight - (endY - startY)) * textureHeight; 229 | 230 | // 判断是否超出最大值 231 | if (textureHeight + delta >= 1) { 232 | delta = 1 - textureHeight; 233 | newHeight = delta / textureHeight + (endY - startY); 234 | } 235 | 236 | // 纹理的顶点 237 | GLKVector3 pointLT = {-textureWidth, textureHeight + delta, 0}; // 左上角 238 | GLKVector3 pointRT = {textureWidth, textureHeight + delta, 0}; // 右上角 239 | GLKVector3 pointLB = {-textureWidth, -textureHeight - delta, 0}; // 左下角 240 | GLKVector3 pointRB = {textureWidth, -textureHeight - delta, 0}; // 右下角 241 | 242 | // 中间矩形区域的顶点 243 | CGFloat startYCoord = MIN(-2 * textureHeight * startY + textureHeight, textureHeight); 244 | CGFloat endYCoord = MAX(-2 * textureHeight * endY + textureHeight, -textureHeight); 245 | GLKVector3 centerPointLT = {-textureWidth, startYCoord + delta, 0}; // 左上角 246 | GLKVector3 centerPointRT = {textureWidth, startYCoord + delta, 0}; // 右上角 247 | GLKVector3 centerPointLB = {-textureWidth, endYCoord - delta, 0}; // 左下角 248 | GLKVector3 centerPointRB = {textureWidth, endYCoord - delta, 0}; // 右下角 249 | 250 | // 纹理的上面两个顶点 251 | self.vertices[0].positionCoord = pointLT; 252 | self.vertices[0].textureCoord = GLKVector2Make(0, 1); 253 | self.vertices[1].positionCoord = pointRT; 254 | self.vertices[1].textureCoord = GLKVector2Make(1, 1); 255 | // 中间区域的4个顶点 256 | self.vertices[2].positionCoord = centerPointLT; 257 | self.vertices[2].textureCoord = GLKVector2Make(0, 1 - startY); 258 | self.vertices[3].positionCoord = centerPointRT; 259 | self.vertices[3].textureCoord = GLKVector2Make(1, 1 - startY); 260 | self.vertices[4].positionCoord = centerPointLB; 261 | self.vertices[4].textureCoord = GLKVector2Make(0, 1 - endY); 262 | self.vertices[5].positionCoord = centerPointRB; 263 | self.vertices[5].textureCoord = GLKVector2Make(1, 1 - endY); 264 | // 纹理的下面两个顶点 265 | self.vertices[6].positionCoord = pointLB; 266 | self.vertices[6].textureCoord = GLKVector2Make(0, 0); 267 | self.vertices[7].positionCoord = pointRB; 268 | self.vertices[7].textureCoord = GLKVector2Make(1, 0); 269 | 270 | // 保存临时值 271 | self.currentTextureStartY = startY; 272 | self.currentTextureEndY = endY; 273 | self.currentNewHeight = newHeight; 274 | } 275 | 276 | 277 | /** 278 | 根据当前屏幕上的显示,来重新创建纹理 279 | 280 | @param originWidth 纹理的原始实际宽度 281 | @param originHeight 纹理的原始实际高度 282 | @param topY 0 ~ 1,拉伸区域的顶边的纵坐标 283 | @param bottomY 0 ~ 1,拉伸区域的底边的纵坐标 284 | @param newHeight 0 ~ 1,拉伸区域的新高度 285 | */ 286 | - (void)resetTextureWithOriginWidth:(CGFloat)originWidth 287 | originHeight:(CGFloat)originHeight 288 | topY:(CGFloat)topY 289 | bottomY:(CGFloat)bottomY 290 | newHeight:(CGFloat)newHeight { 291 | // 新的纹理尺寸 292 | GLsizei newTextureWidth = originWidth; 293 | GLsizei newTextureHeight = originHeight * (newHeight - (bottomY - topY)) + originHeight; 294 | 295 | // 高度变化百分比 296 | CGFloat heightScale = newTextureHeight / originHeight; 297 | 298 | // 在新的纹理坐标下,重新计算topY、bottomY 299 | CGFloat newTopY = topY / heightScale; 300 | CGFloat newBottomY = (topY + newHeight) / heightScale; 301 | 302 | // 创建顶点数组 303 | SenceVertex *tmpVertices = malloc(sizeof(SenceVertex) * kVerticesCount); 304 | tmpVertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}}; 305 | tmpVertices[1] = (SenceVertex){{1, 1, 0}, {1, 1}}; 306 | tmpVertices[2] = (SenceVertex){{-1, -2 * newTopY + 1, 0}, {0, 1 - topY}}; 307 | tmpVertices[3] = (SenceVertex){{1, -2 * newTopY + 1, 0}, {1, 1 - topY}}; 308 | tmpVertices[4] = (SenceVertex){{-1, -2 * newBottomY + 1, 0}, {0, 1 - bottomY}}; 309 | tmpVertices[5] = (SenceVertex){{1, -2 * newBottomY + 1, 0}, {1, 1 - bottomY}}; 310 | tmpVertices[6] = (SenceVertex){{-1, -1, 0}, {0, 0}}; 311 | tmpVertices[7] = (SenceVertex){{1, -1, 0}, {1, 0}}; 312 | 313 | 314 | /// 下面开始渲染到纹理的流程 315 | // 生成帧缓存,挂载渲染缓存 316 | GLuint frameBuffer; 317 | GLuint texture; 318 | 319 | glGenFramebuffers(1, &frameBuffer); 320 | glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer); 321 | 322 | glGenTextures(1, &texture); 323 | glBindTexture(GL_TEXTURE_2D, texture); 324 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); 325 | 326 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 327 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 328 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 329 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 330 | 331 | glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); 332 | 333 | // 设置视口尺寸 334 | glViewport(0, 0, newTextureWidth, newTextureHeight); 335 | 336 | // 获取着色器程序 337 | GLuint program = [MFShaderHelper programWithShaderName:@"spring"]; 338 | glUseProgram(program); 339 | 340 | // 获取参数 341 | GLuint positionSlot = glGetAttribLocation(program, "Position"); 342 | GLuint textureSlot = glGetUniformLocation(program, "Texture"); 343 | GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords"); 344 | 345 | // 传值 346 | glActiveTexture(GL_TEXTURE0); 347 | glBindTexture(GL_TEXTURE_2D, self.baseEffect.texture2d0.name); 348 | glUniform1i(textureSlot, 0); 349 | 350 | MFVertexAttribArrayBuffer *vbo = [[MFVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SenceVertex) numberOfVertices:kVerticesCount data:tmpVertices usage:GL_STATIC_DRAW]; 351 | 352 | [vbo prepareToDrawWithAttrib:positionSlot numberOfCoordinates:3 attribOffset:offsetof(SenceVertex, positionCoord) shouldEnable:YES]; 353 | [vbo prepareToDrawWithAttrib:textureCoordsSlot numberOfCoordinates:2 attribOffset:offsetof(SenceVertex, textureCoord) shouldEnable:YES]; 354 | 355 | // 绘制 356 | [vbo drawArrayWithMode:GL_TRIANGLE_STRIP startVertexIndex:0 numberOfVertices:kVerticesCount]; 357 | 358 | // 解绑缓存 359 | glBindFramebuffer(GL_FRAMEBUFFER, 0); 360 | // 释放顶点数组 361 | free(tmpVertices); 362 | 363 | self.tmpTexture = texture; 364 | self.tmpFrameBuffer = frameBuffer; 365 | } 366 | 367 | // 返回某个纹理对应的 UIImage,调用前先绑定对应的帧缓存 368 | - (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height { 369 | int size = width * height * 4; 370 | GLubyte *buffer = malloc(size); 371 | glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer); 372 | CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL); 373 | int bitsPerComponent = 8; 374 | int bitsPerPixel = 32; 375 | int bytesPerRow = 4 * width; 376 | CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); 377 | CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; 378 | CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; 379 | CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); 380 | 381 | // 此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,刚好翻转过来 382 | UIGraphicsBeginImageContext(CGSizeMake(width, height)); 383 | CGContextRef context = UIGraphicsGetCurrentContext(); 384 | CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); 385 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 386 | UIGraphicsEndImageContext(); 387 | 388 | free(buffer); 389 | return image; 390 | } 391 | 392 | // 根据当前屏幕的尺寸,返回新的图片尺寸 393 | - (CGSize)newImageSize { 394 | CGFloat newImageHeight = self.currentImageSize.height * ((self.currentNewHeight - (self.currentTextureEndY - self.currentTextureStartY)) + 1); 395 | return CGSizeMake(self.currentImageSize.width, newImageHeight); 396 | } 397 | 398 | #pragma mark - Custom Accessor 399 | - (void)setTmpFrameBuffer:(GLuint)tmpFrameBuffer { 400 | if (_tmpFrameBuffer) { 401 | glDeleteFramebuffers(1, &_tmpFrameBuffer); 402 | } 403 | _tmpFrameBuffer = tmpFrameBuffer; 404 | } 405 | 406 | #pragma mark - GLKViewDelegate 407 | 408 | - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { 409 | [self.baseEffect prepareToDraw]; 410 | 411 | glClear(GL_COLOR_BUFFER_BIT); 412 | 413 | [self.vertexAttribArrayBuffer prepareToDrawWithAttrib:GLKVertexAttribPosition 414 | numberOfCoordinates:3 415 | attribOffset:offsetof(SenceVertex, positionCoord) 416 | shouldEnable:YES]; 417 | 418 | [self.vertexAttribArrayBuffer prepareToDrawWithAttrib:GLKVertexAttribTexCoord0 419 | numberOfCoordinates:2 420 | attribOffset:offsetof(SenceVertex, textureCoord) 421 | shouldEnable:YES]; 422 | 423 | [self.vertexAttribArrayBuffer drawArrayWithMode:GL_TRIANGLE_STRIP 424 | startVertexIndex:0 425 | numberOfVertices:kVerticesCount]; 426 | } 427 | 428 | @end 429 | -------------------------------------------------------------------------------- /MFSpringView/MFVertexAttribArrayBuffer.h: -------------------------------------------------------------------------------- 1 | // 2 | // MFVertexAttribArrayBuffer.h 3 | // MFSpringView 4 | // 5 | // Created by Lyman on 2018/11/26. 6 | // Copyright © 2018 Lyman Li. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MFVertexAttribArrayBuffer : NSObject 12 | 13 | - (id)initWithAttribStride:(GLsizei)stride 14 | numberOfVertices:(GLsizei)count 15 | data:(const GLvoid *)data 16 | usage:(GLenum)usage; 17 | 18 | - (void)prepareToDrawWithAttrib:(GLuint)index 19 | numberOfCoordinates:(GLint)count 20 | attribOffset:(GLsizeiptr)offset 21 | shouldEnable:(BOOL)shouldEnable; 22 | 23 | - (void)drawArrayWithMode:(GLenum)mode 24 | startVertexIndex:(GLint)first 25 | numberOfVertices:(GLsizei)count; 26 | 27 | - (void)updateDataWithAttribStride:(GLsizei)stride 28 | numberOfVertices:(GLsizei)count 29 | data:(const GLvoid *)data 30 | usage:(GLenum)usage; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /MFSpringView/MFVertexAttribArrayBuffer.m: -------------------------------------------------------------------------------- 1 | // 2 | // MFVertexAttribArrayBuffer.m 3 | // MFSpringView 4 | // 5 | // Created by Lyman on 2018/11/26. 6 | // Copyright © 2018 Lyman Li. All rights reserved. 7 | // 8 | 9 | #import "MFVertexAttribArrayBuffer.h" 10 | 11 | @interface MFVertexAttribArrayBuffer () 12 | 13 | @property (nonatomic, assign) GLuint glName; 14 | @property (nonatomic, assign) GLsizeiptr bufferSizeBytes; 15 | @property (nonatomic, assign) GLsizei stride; 16 | 17 | @end 18 | 19 | @implementation MFVertexAttribArrayBuffer 20 | 21 | - (void)dealloc { 22 | if (_glName != 0) { 23 | glDeleteBuffers(1, &_glName); 24 | _glName = 0; 25 | } 26 | } 27 | 28 | - (id)initWithAttribStride:(GLsizei)stride 29 | numberOfVertices:(GLsizei)count 30 | data:(const GLvoid *)data 31 | usage:(GLenum)usage { 32 | self = [super init]; 33 | if (self) { 34 | _stride = stride; 35 | _bufferSizeBytes = stride * count; 36 | glGenBuffers(1, &_glName); 37 | glBindBuffer(GL_ARRAY_BUFFER, _glName); 38 | glBufferData(GL_ARRAY_BUFFER, _bufferSizeBytes, data, usage); 39 | } 40 | return self; 41 | } 42 | 43 | - (void)prepareToDrawWithAttrib:(GLuint)index 44 | numberOfCoordinates:(GLint)count 45 | attribOffset:(GLsizeiptr)offset 46 | shouldEnable:(BOOL)shouldEnable { 47 | glBindBuffer(GL_ARRAY_BUFFER, self.glName); 48 | if (shouldEnable) { 49 | glEnableVertexAttribArray(index); 50 | } 51 | glVertexAttribPointer(index, count, GL_FLOAT, GL_FALSE, self.stride, NULL + offset); 52 | } 53 | 54 | - (void)drawArrayWithMode:(GLenum)mode 55 | startVertexIndex:(GLint)first 56 | numberOfVertices:(GLsizei)count { 57 | glDrawArrays(mode, first, count); 58 | } 59 | 60 | - (void)updateDataWithAttribStride:(GLsizei)stride 61 | numberOfVertices:(GLsizei)count 62 | data:(const GLvoid *)data 63 | usage:(GLenum)usage { 64 | self.stride = stride; 65 | self.bufferSizeBytes = stride * count; 66 | glBindBuffer(GL_ARRAY_BUFFER, self.glName); 67 | glBufferData(GL_ARRAY_BUFFER, self.bufferSizeBytes, data, usage); 68 | } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /MFSpringView/spring.fsh: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D Texture; 4 | varying vec2 TextureCoordsVarying; 5 | 6 | void main (void) { 7 | vec4 mask = texture2D(Texture, TextureCoordsVarying); 8 | gl_FragColor = vec4(mask.rgb, 1.0); 9 | } 10 | -------------------------------------------------------------------------------- /MFSpringView/spring.vsh: -------------------------------------------------------------------------------- 1 | attribute vec4 Position; 2 | attribute vec2 TextureCoords; 3 | varying vec2 TextureCoordsVarying; 4 | 5 | void main (void) { 6 | gl_Position = Position; 7 | TextureCoordsVarying = TextureCoords; 8 | } 9 | -------------------------------------------------------------------------------- /MFSpringViewDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E8144C5E2227805700C45F65 /* spring.vsh in Resources */ = {isa = PBXBuildFile; fileRef = E8144C5C2227805700C45F65 /* spring.vsh */; }; 11 | E8144C5F2227805700C45F65 /* spring.fsh in Resources */ = {isa = PBXBuildFile; fileRef = E8144C5D2227805700C45F65 /* spring.fsh */; }; 12 | E8144C62222780C900C45F65 /* MFShaderHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = E8144C61222780C900C45F65 /* MFShaderHelper.m */; }; 13 | E82B890C21AAB2F500F096CB /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E82B890B21AAB2F500F096CB /* AppDelegate.m */; }; 14 | E82B890F21AAB2F500F096CB /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E82B890E21AAB2F500F096CB /* ViewController.m */; }; 15 | E82B891221AAB2F500F096CB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E82B891021AAB2F500F096CB /* Main.storyboard */; }; 16 | E82B891421AAB2F500F096CB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E82B891321AAB2F500F096CB /* Assets.xcassets */; }; 17 | E82B891721AAB2F500F096CB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E82B891521AAB2F500F096CB /* LaunchScreen.storyboard */; }; 18 | E82B891A21AAB2F500F096CB /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E82B891921AAB2F500F096CB /* main.m */; }; 19 | E82B892321AAB94500F096CB /* MFSpringView.m in Sources */ = {isa = PBXBuildFile; fileRef = E82B892221AAB94500F096CB /* MFSpringView.m */; }; 20 | E8C31E8721ABC1E700472CC0 /* MFVertexAttribArrayBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = E8C31E8621ABC1E700472CC0 /* MFVertexAttribArrayBuffer.m */; }; 21 | E8C31E8921ABD2A800472CC0 /* girl.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E8C31E8821ABD2A800472CC0 /* girl.jpg */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | E8144C5C2227805700C45F65 /* spring.vsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = spring.vsh; sourceTree = ""; }; 26 | E8144C5D2227805700C45F65 /* spring.fsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = spring.fsh; sourceTree = ""; }; 27 | E8144C60222780C800C45F65 /* MFShaderHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MFShaderHelper.h; sourceTree = ""; }; 28 | E8144C61222780C900C45F65 /* MFShaderHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MFShaderHelper.m; sourceTree = ""; }; 29 | E82B890721AAB2F500F096CB /* MFSpringViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MFSpringViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | E82B890A21AAB2F500F096CB /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 31 | E82B890B21AAB2F500F096CB /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 32 | E82B890D21AAB2F500F096CB /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 33 | E82B890E21AAB2F500F096CB /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 34 | E82B891121AAB2F500F096CB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 35 | E82B891321AAB2F500F096CB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 36 | E82B891621AAB2F500F096CB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 37 | E82B891821AAB2F500F096CB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38 | E82B891921AAB2F500F096CB /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 39 | E82B892121AAB94500F096CB /* MFSpringView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MFSpringView.h; sourceTree = ""; }; 40 | E82B892221AAB94500F096CB /* MFSpringView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MFSpringView.m; sourceTree = ""; }; 41 | E8C31E8521ABC1E700472CC0 /* MFVertexAttribArrayBuffer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MFVertexAttribArrayBuffer.h; sourceTree = ""; }; 42 | E8C31E8621ABC1E700472CC0 /* MFVertexAttribArrayBuffer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MFVertexAttribArrayBuffer.m; sourceTree = ""; }; 43 | E8C31E8821ABD2A800472CC0 /* girl.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = girl.jpg; sourceTree = ""; }; 44 | /* End PBXFileReference section */ 45 | 46 | /* Begin PBXFrameworksBuildPhase section */ 47 | E82B890421AAB2F500F096CB /* Frameworks */ = { 48 | isa = PBXFrameworksBuildPhase; 49 | buildActionMask = 2147483647; 50 | files = ( 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | E82B88FE21AAB2F500F096CB = { 58 | isa = PBXGroup; 59 | children = ( 60 | E82B892021AAB92C00F096CB /* MFSpringView */, 61 | E82B890921AAB2F500F096CB /* MFSpringViewDemo */, 62 | E82B890821AAB2F500F096CB /* Products */, 63 | ); 64 | sourceTree = ""; 65 | }; 66 | E82B890821AAB2F500F096CB /* Products */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | E82B890721AAB2F500F096CB /* MFSpringViewDemo.app */, 70 | ); 71 | name = Products; 72 | sourceTree = ""; 73 | }; 74 | E82B890921AAB2F500F096CB /* MFSpringViewDemo */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | E8C31E8821ABD2A800472CC0 /* girl.jpg */, 78 | E82B890A21AAB2F500F096CB /* AppDelegate.h */, 79 | E82B890B21AAB2F500F096CB /* AppDelegate.m */, 80 | E82B890D21AAB2F500F096CB /* ViewController.h */, 81 | E82B890E21AAB2F500F096CB /* ViewController.m */, 82 | E82B891021AAB2F500F096CB /* Main.storyboard */, 83 | E82B891321AAB2F500F096CB /* Assets.xcassets */, 84 | E82B891521AAB2F500F096CB /* LaunchScreen.storyboard */, 85 | E82B891821AAB2F500F096CB /* Info.plist */, 86 | E82B891921AAB2F500F096CB /* main.m */, 87 | ); 88 | path = MFSpringViewDemo; 89 | sourceTree = ""; 90 | }; 91 | E82B892021AAB92C00F096CB /* MFSpringView */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | E8144C5D2227805700C45F65 /* spring.fsh */, 95 | E8144C5C2227805700C45F65 /* spring.vsh */, 96 | E8144C60222780C800C45F65 /* MFShaderHelper.h */, 97 | E8144C61222780C900C45F65 /* MFShaderHelper.m */, 98 | E82B892121AAB94500F096CB /* MFSpringView.h */, 99 | E82B892221AAB94500F096CB /* MFSpringView.m */, 100 | E8C31E8521ABC1E700472CC0 /* MFVertexAttribArrayBuffer.h */, 101 | E8C31E8621ABC1E700472CC0 /* MFVertexAttribArrayBuffer.m */, 102 | ); 103 | path = MFSpringView; 104 | sourceTree = ""; 105 | }; 106 | /* End PBXGroup section */ 107 | 108 | /* Begin PBXNativeTarget section */ 109 | E82B890621AAB2F500F096CB /* MFSpringViewDemo */ = { 110 | isa = PBXNativeTarget; 111 | buildConfigurationList = E82B891D21AAB2F500F096CB /* Build configuration list for PBXNativeTarget "MFSpringViewDemo" */; 112 | buildPhases = ( 113 | E82B890321AAB2F500F096CB /* Sources */, 114 | E82B890421AAB2F500F096CB /* Frameworks */, 115 | E82B890521AAB2F500F096CB /* Resources */, 116 | ); 117 | buildRules = ( 118 | ); 119 | dependencies = ( 120 | ); 121 | name = MFSpringViewDemo; 122 | productName = MFSpringViewDemo; 123 | productReference = E82B890721AAB2F500F096CB /* MFSpringViewDemo.app */; 124 | productType = "com.apple.product-type.application"; 125 | }; 126 | /* End PBXNativeTarget section */ 127 | 128 | /* Begin PBXProject section */ 129 | E82B88FF21AAB2F500F096CB /* Project object */ = { 130 | isa = PBXProject; 131 | attributes = { 132 | LastUpgradeCheck = 0900; 133 | ORGANIZATIONNAME = "Lyman Li"; 134 | TargetAttributes = { 135 | E82B890621AAB2F500F096CB = { 136 | CreatedOnToolsVersion = 9.0.1; 137 | ProvisioningStyle = Automatic; 138 | }; 139 | }; 140 | }; 141 | buildConfigurationList = E82B890221AAB2F500F096CB /* Build configuration list for PBXProject "MFSpringViewDemo" */; 142 | compatibilityVersion = "Xcode 8.0"; 143 | developmentRegion = en; 144 | hasScannedForEncodings = 0; 145 | knownRegions = ( 146 | en, 147 | Base, 148 | ); 149 | mainGroup = E82B88FE21AAB2F500F096CB; 150 | productRefGroup = E82B890821AAB2F500F096CB /* Products */; 151 | projectDirPath = ""; 152 | projectRoot = ""; 153 | targets = ( 154 | E82B890621AAB2F500F096CB /* MFSpringViewDemo */, 155 | ); 156 | }; 157 | /* End PBXProject section */ 158 | 159 | /* Begin PBXResourcesBuildPhase section */ 160 | E82B890521AAB2F500F096CB /* Resources */ = { 161 | isa = PBXResourcesBuildPhase; 162 | buildActionMask = 2147483647; 163 | files = ( 164 | E82B891721AAB2F500F096CB /* LaunchScreen.storyboard in Resources */, 165 | E8C31E8921ABD2A800472CC0 /* girl.jpg in Resources */, 166 | E8144C5F2227805700C45F65 /* spring.fsh in Resources */, 167 | E82B891421AAB2F500F096CB /* Assets.xcassets in Resources */, 168 | E82B891221AAB2F500F096CB /* Main.storyboard in Resources */, 169 | E8144C5E2227805700C45F65 /* spring.vsh in Resources */, 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | }; 173 | /* End PBXResourcesBuildPhase section */ 174 | 175 | /* Begin PBXSourcesBuildPhase section */ 176 | E82B890321AAB2F500F096CB /* Sources */ = { 177 | isa = PBXSourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | E82B890F21AAB2F500F096CB /* ViewController.m in Sources */, 181 | E82B891A21AAB2F500F096CB /* main.m in Sources */, 182 | E82B892321AAB94500F096CB /* MFSpringView.m in Sources */, 183 | E82B890C21AAB2F500F096CB /* AppDelegate.m in Sources */, 184 | E8C31E8721ABC1E700472CC0 /* MFVertexAttribArrayBuffer.m in Sources */, 185 | E8144C62222780C900C45F65 /* MFShaderHelper.m in Sources */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | /* End PBXSourcesBuildPhase section */ 190 | 191 | /* Begin PBXVariantGroup section */ 192 | E82B891021AAB2F500F096CB /* Main.storyboard */ = { 193 | isa = PBXVariantGroup; 194 | children = ( 195 | E82B891121AAB2F500F096CB /* Base */, 196 | ); 197 | name = Main.storyboard; 198 | sourceTree = ""; 199 | }; 200 | E82B891521AAB2F500F096CB /* LaunchScreen.storyboard */ = { 201 | isa = PBXVariantGroup; 202 | children = ( 203 | E82B891621AAB2F500F096CB /* Base */, 204 | ); 205 | name = LaunchScreen.storyboard; 206 | sourceTree = ""; 207 | }; 208 | /* End PBXVariantGroup section */ 209 | 210 | /* Begin XCBuildConfiguration section */ 211 | E82B891B21AAB2F500F096CB /* Debug */ = { 212 | isa = XCBuildConfiguration; 213 | buildSettings = { 214 | ALWAYS_SEARCH_USER_PATHS = NO; 215 | CLANG_ANALYZER_NONNULL = YES; 216 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 217 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 218 | CLANG_CXX_LIBRARY = "libc++"; 219 | CLANG_ENABLE_MODULES = YES; 220 | CLANG_ENABLE_OBJC_ARC = YES; 221 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 222 | CLANG_WARN_BOOL_CONVERSION = YES; 223 | CLANG_WARN_COMMA = YES; 224 | CLANG_WARN_CONSTANT_CONVERSION = YES; 225 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 226 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 227 | CLANG_WARN_EMPTY_BODY = YES; 228 | CLANG_WARN_ENUM_CONVERSION = YES; 229 | CLANG_WARN_INFINITE_RECURSION = YES; 230 | CLANG_WARN_INT_CONVERSION = YES; 231 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 232 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 233 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 234 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 235 | CLANG_WARN_STRICT_PROTOTYPES = YES; 236 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 237 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 238 | CLANG_WARN_UNREACHABLE_CODE = YES; 239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 240 | CODE_SIGN_IDENTITY = "iPhone Developer"; 241 | COPY_PHASE_STRIP = NO; 242 | DEBUG_INFORMATION_FORMAT = dwarf; 243 | ENABLE_STRICT_OBJC_MSGSEND = YES; 244 | ENABLE_TESTABILITY = YES; 245 | GCC_C_LANGUAGE_STANDARD = gnu11; 246 | GCC_DYNAMIC_NO_PIC = NO; 247 | GCC_NO_COMMON_BLOCKS = YES; 248 | GCC_OPTIMIZATION_LEVEL = 0; 249 | GCC_PREPROCESSOR_DEFINITIONS = ( 250 | "DEBUG=1", 251 | "$(inherited)", 252 | ); 253 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 255 | GCC_WARN_UNDECLARED_SELECTOR = YES; 256 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 257 | GCC_WARN_UNUSED_FUNCTION = YES; 258 | GCC_WARN_UNUSED_VARIABLE = YES; 259 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 260 | MTL_ENABLE_DEBUG_INFO = YES; 261 | ONLY_ACTIVE_ARCH = YES; 262 | SDKROOT = iphoneos; 263 | }; 264 | name = Debug; 265 | }; 266 | E82B891C21AAB2F500F096CB /* Release */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ALWAYS_SEARCH_USER_PATHS = NO; 270 | CLANG_ANALYZER_NONNULL = YES; 271 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 272 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 273 | CLANG_CXX_LIBRARY = "libc++"; 274 | CLANG_ENABLE_MODULES = YES; 275 | CLANG_ENABLE_OBJC_ARC = YES; 276 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 277 | CLANG_WARN_BOOL_CONVERSION = YES; 278 | CLANG_WARN_COMMA = YES; 279 | CLANG_WARN_CONSTANT_CONVERSION = YES; 280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 281 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 282 | CLANG_WARN_EMPTY_BODY = YES; 283 | CLANG_WARN_ENUM_CONVERSION = YES; 284 | CLANG_WARN_INFINITE_RECURSION = YES; 285 | CLANG_WARN_INT_CONVERSION = YES; 286 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 288 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 289 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 290 | CLANG_WARN_STRICT_PROTOTYPES = YES; 291 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 292 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 293 | CLANG_WARN_UNREACHABLE_CODE = YES; 294 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 295 | CODE_SIGN_IDENTITY = "iPhone Developer"; 296 | COPY_PHASE_STRIP = NO; 297 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 298 | ENABLE_NS_ASSERTIONS = NO; 299 | ENABLE_STRICT_OBJC_MSGSEND = YES; 300 | GCC_C_LANGUAGE_STANDARD = gnu11; 301 | GCC_NO_COMMON_BLOCKS = YES; 302 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 303 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 304 | GCC_WARN_UNDECLARED_SELECTOR = YES; 305 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 306 | GCC_WARN_UNUSED_FUNCTION = YES; 307 | GCC_WARN_UNUSED_VARIABLE = YES; 308 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 309 | MTL_ENABLE_DEBUG_INFO = NO; 310 | SDKROOT = iphoneos; 311 | VALIDATE_PRODUCT = YES; 312 | }; 313 | name = Release; 314 | }; 315 | E82B891E21AAB2F500F096CB /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 | CODE_SIGN_STYLE = Automatic; 320 | DEVELOPMENT_TEAM = 7P79J85545; 321 | INFOPLIST_FILE = MFSpringViewDemo/Info.plist; 322 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 323 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 324 | PRODUCT_BUNDLE_IDENTIFIER = com.lyman.MFSpringViewDemo; 325 | PRODUCT_NAME = "$(TARGET_NAME)"; 326 | TARGETED_DEVICE_FAMILY = "1,2"; 327 | }; 328 | name = Debug; 329 | }; 330 | E82B891F21AAB2F500F096CB /* Release */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 334 | CODE_SIGN_STYLE = Automatic; 335 | DEVELOPMENT_TEAM = 7P79J85545; 336 | INFOPLIST_FILE = MFSpringViewDemo/Info.plist; 337 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 338 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 339 | PRODUCT_BUNDLE_IDENTIFIER = com.lyman.MFSpringViewDemo; 340 | PRODUCT_NAME = "$(TARGET_NAME)"; 341 | TARGETED_DEVICE_FAMILY = "1,2"; 342 | }; 343 | name = Release; 344 | }; 345 | /* End XCBuildConfiguration section */ 346 | 347 | /* Begin XCConfigurationList section */ 348 | E82B890221AAB2F500F096CB /* Build configuration list for PBXProject "MFSpringViewDemo" */ = { 349 | isa = XCConfigurationList; 350 | buildConfigurations = ( 351 | E82B891B21AAB2F500F096CB /* Debug */, 352 | E82B891C21AAB2F500F096CB /* Release */, 353 | ); 354 | defaultConfigurationIsVisible = 0; 355 | defaultConfigurationName = Release; 356 | }; 357 | E82B891D21AAB2F500F096CB /* Build configuration list for PBXNativeTarget "MFSpringViewDemo" */ = { 358 | isa = XCConfigurationList; 359 | buildConfigurations = ( 360 | E82B891E21AAB2F500F096CB /* Debug */, 361 | E82B891F21AAB2F500F096CB /* Release */, 362 | ); 363 | defaultConfigurationIsVisible = 0; 364 | defaultConfigurationName = Release; 365 | }; 366 | /* End XCConfigurationList section */ 367 | }; 368 | rootObject = E82B88FF21AAB2F500F096CB /* Project object */; 369 | } 370 | -------------------------------------------------------------------------------- /MFSpringViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MFSpringViewDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // MFSpringViewDemo 4 | // 5 | // Created by Lyman Li on 2018/11/25. 6 | // Copyright © 2018年 Lyman Li. 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 | -------------------------------------------------------------------------------- /MFSpringViewDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // MFSpringViewDemo 4 | // 5 | // Created by Lyman Li on 2018/11/25. 6 | // Copyright © 2018年 Lyman Li. 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 | -------------------------------------------------------------------------------- /MFSpringViewDemo/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 | } -------------------------------------------------------------------------------- /MFSpringViewDemo/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 | -------------------------------------------------------------------------------- /MFSpringViewDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 76 | 90 | 101 | 102 | 103 | 104 | 105 | 106 | 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 | -------------------------------------------------------------------------------- /MFSpringViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPhotoLibraryUsageDescription 6 | 请允许访问相册 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /MFSpringViewDemo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // MFSpringViewDemo 4 | // 5 | // Created by Lyman Li on 2018/11/25. 6 | // Copyright © 2018年 Lyman Li. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /MFSpringViewDemo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // MFSpringViewDemo 4 | // 5 | // Created by Lyman Li on 2018/11/25. 6 | // Copyright © 2018年 Lyman Li. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "MFSpringView.h" 12 | 13 | #import "ViewController.h" 14 | 15 | @interface ViewController () 16 | 17 | @property (weak, nonatomic) IBOutlet MFSpringView *springView; 18 | @property (weak, nonatomic) IBOutlet NSLayoutConstraint *topLineSpace; 19 | @property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomLineSpace; 20 | @property (weak, nonatomic) IBOutlet UIButton *topButton; 21 | @property (weak, nonatomic) IBOutlet UIButton *bottomButton; 22 | @property (weak, nonatomic) IBOutlet UISlider *slider; 23 | @property (weak, nonatomic) IBOutlet UIView *topLine; 24 | @property (weak, nonatomic) IBOutlet UIView *bottomLine; 25 | @property (weak, nonatomic) IBOutlet UIView *mask; 26 | 27 | @property (nonatomic, assign) CGFloat currentTop; // 上方横线距离纹理顶部的高度 28 | @property (nonatomic, assign) CGFloat currentBottom; // 下方横线距离纹理顶部的高度 29 | 30 | @end 31 | 32 | @implementation ViewController 33 | 34 | - (void)viewDidLoad { 35 | [super viewDidLoad]; 36 | 37 | [self setupButtons]; 38 | self.springView.springDelegate = self; 39 | [self.springView updateImage:[UIImage imageNamed:@"girl.jpg"]]; 40 | 41 | [self setupStretchArea]; 42 | } 43 | 44 | - (void)viewDidAppear:(BOOL)animated { 45 | static dispatch_once_t onceToken; 46 | dispatch_once(&onceToken, ^{ 47 | [self setupStretchArea]; // 这里的计算要用到view的size,所以等待AutoLayout把尺寸计算出来后再调用 48 | }); 49 | } 50 | 51 | #pragma mark - Private 52 | 53 | - (void)setupButtons { 54 | self.topButton.layer.borderWidth = 1; 55 | self.topButton.layer.borderColor = [[UIColor whiteColor] CGColor]; 56 | [self.topButton addGestureRecognizer:[[UIPanGestureRecognizer alloc] 57 | initWithTarget:self 58 | action:@selector(actionPanTop:)]]; 59 | 60 | self.bottomButton.layer.borderWidth = 1; 61 | self.bottomButton.layer.borderColor = [[UIColor whiteColor] CGColor]; 62 | [self.bottomButton addGestureRecognizer:[[UIPanGestureRecognizer alloc] 63 | initWithTarget:self 64 | action:@selector(actionPanBottom:)]]; 65 | } 66 | 67 | - (CGFloat)stretchAreaYWithLineSpace:(CGFloat)lineSpace { 68 | return (lineSpace / self.springView.bounds.size.height - self.springView.textureTopY) / self.springView.textureHeight; 69 | } 70 | 71 | // 设置初始的拉伸区域位置 72 | - (void)setupStretchArea { 73 | self.currentTop = 0.25f; 74 | self.currentBottom = 0.75f; 75 | CGFloat textureOriginHeight = 0.7f; // 初始纹理占 View 的比例 76 | self.topLineSpace.constant = ((self.currentTop * textureOriginHeight) + (1 - textureOriginHeight) / 2) * self.springView.bounds.size.height; 77 | self.bottomLineSpace.constant = ((self.currentBottom * textureOriginHeight) + (1 - textureOriginHeight) / 2) * self.springView.bounds.size.height; 78 | } 79 | 80 | - (void)setViewsHidden:(BOOL)hidden { 81 | self.topLine.hidden = hidden; 82 | self.bottomLine.hidden = hidden; 83 | self.topButton.hidden = hidden; 84 | self.bottomButton.hidden = hidden; 85 | self.mask.hidden = hidden; 86 | } 87 | 88 | // 保存图片到相册 89 | - (void)saveImage:(UIImage *)image { 90 | [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ 91 | [PHAssetChangeRequest creationRequestForAssetFromImage:image]; 92 | } completionHandler:^(BOOL success, NSError * _Nullable error) { 93 | NSLog(@"success = %d, error = %@", success, error); 94 | }]; 95 | } 96 | 97 | #pragma mark - Action 98 | 99 | - (void)actionPanTop:(UIPanGestureRecognizer *)pan { 100 | if ([self.springView hasChange]) { 101 | [self.springView updateTexture]; 102 | self.slider.value = 0.5f; // 重置滑杆位置 103 | } 104 | 105 | CGPoint translation = [pan translationInView:self.view]; 106 | self.topLineSpace.constant = MIN(self.topLineSpace.constant + translation.y, 107 | self.bottomLineSpace.constant); 108 | CGFloat textureTop = self.springView.bounds.size.height * self.springView.textureTopY; 109 | self.topLineSpace.constant = MAX(self.topLineSpace.constant, textureTop); 110 | [pan setTranslation:CGPointZero inView:self.view]; 111 | 112 | self.currentTop = [self stretchAreaYWithLineSpace:self.topLineSpace.constant]; 113 | self.currentBottom = [self stretchAreaYWithLineSpace:self.bottomLineSpace.constant]; 114 | } 115 | 116 | - (void)actionPanBottom:(UIPanGestureRecognizer *)pan { 117 | if ([self.springView hasChange]) { 118 | [self.springView updateTexture]; 119 | self.slider.value = 0.5f; // 重置滑杆位置 120 | } 121 | 122 | CGPoint translation = [pan translationInView:self.view]; 123 | self.bottomLineSpace.constant = MAX(self.bottomLineSpace.constant + translation.y, 124 | self.topLineSpace.constant); 125 | CGFloat textureBottom = self.springView.bounds.size.height * self.springView.textureBottomY; 126 | self.bottomLineSpace.constant = MIN(self.bottomLineSpace.constant, textureBottom); 127 | [pan setTranslation:CGPointZero inView:self.view]; 128 | 129 | self.currentTop = [self stretchAreaYWithLineSpace:self.topLineSpace.constant]; 130 | self.currentBottom = [self stretchAreaYWithLineSpace:self.bottomLineSpace.constant]; 131 | } 132 | 133 | #pragma mark - IBAction 134 | 135 | - (IBAction)sliderValueDidChanged:(UISlider *)sender { 136 | CGFloat newHeight = (self.currentBottom - self.currentTop) * ((sender.value) + 0.5); 137 | [self.springView stretchingFromStartY:self.currentTop 138 | toEndY:self.currentBottom 139 | withNewHeight:newHeight]; 140 | } 141 | 142 | - (IBAction)sliderDidTouchDown:(id)sender { 143 | [self setViewsHidden:YES]; 144 | } 145 | 146 | - (IBAction)sliderDidTouchUp:(id)sender { 147 | [self setViewsHidden:NO]; 148 | } 149 | 150 | - (IBAction)saveAction:(id)sender { 151 | UIImage *image = [self.springView createResult]; 152 | [self saveImage:image]; 153 | } 154 | 155 | #pragma mark - MFSpringViewDelegate 156 | 157 | - (void)springViewStretchAreaDidChanged:(MFSpringView *)springView { 158 | CGFloat topY = self.springView.bounds.size.height * self.springView.stretchAreaTopY; 159 | CGFloat bottomY = self.springView.bounds.size.height * self.springView.stretchAreaBottomY; 160 | self.topLineSpace.constant = topY; 161 | self.bottomLineSpace.constant = bottomY; 162 | } 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /MFSpringViewDemo/girl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmf12/MFSpringView/82802b3e26c46d0f168bff473baa69fb3928bff8/MFSpringViewDemo/girl.jpg -------------------------------------------------------------------------------- /MFSpringViewDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MFSpringViewDemo 4 | // 5 | // Created by Lyman Li on 2018/11/25. 6 | // Copyright © 2018年 Lyman Li. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MFSpringView 2 | 3 | 基于 OpenGL ES 实现图片局部拉伸功能的控件,可以用来实现长腿功能。 4 | 5 | ## 效果展示 6 | 7 | ![](https://github.com/lmf12/ImageHost/blob/master/MFSpringView/image.gif) 8 | 9 | ## 原理 10 | 11 | 将图片分割为 6 个三角形,如下图所示,然后对中间矩形(V2~V5)进行拉伸或压缩处理。 12 | 13 | ![](https://github.com/lmf12/ImageHost/blob/master/MFSpringView/image1.jpg) 14 | 15 | ## 渲染到纹理 16 | 17 | 当单次图片编辑结束之后,需要重新生成纹理,即读取当前屏幕呈现的结果。 18 | 19 | 出于对结果分辨率的考虑,我们不会直接读取当前屏幕渲染结果对应的帧缓存,而是采取「渲染到纹理」的方式,重新生成一个宽度与原图一致的纹理。 20 | 21 | ## 为什么使用 OpenGL ES 22 | 23 | 实现图片局部拉伸功能的逻辑并不复杂,理论上也可以通过 CoreGraphics 的绘图功能来实现。 24 | 25 | 但是由于 CoreGraphics 的绘图功能依赖于 CPU ,不断地重绘图像会引起卡顿。 26 | 27 | 因此,从性能的角度考虑,使用 OpenGL ES 更佳。 28 | 29 | ## 更多介绍 30 | 31 | [使用 iOS OpenGL ES 实现长腿功能](http://www.lymanli.com/2019/03/04/ios-opengles-spring/) 32 | -------------------------------------------------------------------------------- /image/image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmf12/MFSpringView/82802b3e26c46d0f168bff473baa69fb3928bff8/image/image.gif -------------------------------------------------------------------------------- /image/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmf12/MFSpringView/82802b3e26c46d0f168bff473baa69fb3928bff8/image/image1.jpg --------------------------------------------------------------------------------