├── PhotoTweaks
├── Images.xcassets
│ └── AppIcon.appiconset
│ │ ├── PhotoTweak.png
│ │ └── Contents.json
├── en.lproj
│ └── PhotoTweaks.strings
├── zh-Hans.lproj
│ └── PhotoTweaks.strings
├── AppDelegate.h
├── main.m
├── PhotoTweaks
│ ├── UIColor+Tweak.h
│ ├── PhotoTweakView.h
│ ├── UIColor+Tweak.m
│ ├── PhotoTweaksViewController.h
│ ├── PhotoTweaksViewController.m
│ └── PhotoTweakView.m
├── Base.lproj
│ ├── Main.storyboard
│ └── LaunchScreen.xib
├── Info.plist
└── AppDelegate.m
├── .gitignore
├── PhotoTweaks.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ ├── tuyou.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ │ └── Chandler.xcuserdatad
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── PhotoTweaks.xccheckout
├── xcshareddata
│ └── xcschemes
│ │ └── PhotoTweaks.xcscheme
└── project.pbxproj
├── .travis.yml
├── PhotoTweaks.podspec
├── PhotoTweaksTests
├── Info.plist
└── PhotoTweaksTests.m
├── LICENSE
└── README.md
/PhotoTweaks/Images.xcassets/AppIcon.appiconset/PhotoTweak.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itouch2/PhotoTweaks/HEAD/PhotoTweaks/Images.xcassets/AppIcon.appiconset/PhotoTweak.png
--------------------------------------------------------------------------------
/PhotoTweaks/en.lproj/PhotoTweaks.strings:
--------------------------------------------------------------------------------
1 | /*
2 | PhotoTweaks.strings
3 | PhotoTweaks
4 |
5 | Created by BruceZCQ on 3/31/15.
6 | Copyright (c) 2015 Tu You. All rights reserved.
7 | */
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | *.xcbkptlist
3 |
4 | *.xcuserstate
5 |
6 | *.xcuserstate
7 |
8 | *.xcuserstate
9 |
10 | PhotoTweaks.xcodeproj/xcuserdata/Chandler.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
11 |
--------------------------------------------------------------------------------
/PhotoTweaks.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/PhotoTweaks.xcodeproj/project.xcworkspace/xcuserdata/tuyou.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itouch2/PhotoTweaks/HEAD/PhotoTweaks.xcodeproj/project.xcworkspace/xcuserdata/tuyou.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/PhotoTweaks/zh-Hans.lproj/PhotoTweaks.strings:
--------------------------------------------------------------------------------
1 | /*
2 | PhotoTweaks.strings
3 | PhotoTweaks
4 |
5 | Created by BruceZCQ on 3/31/15.
6 | Copyright (c) 2015 Tu You. All rights reserved.
7 | */
8 |
9 | "Cancel"="取消";
10 | "Done"="完成";
11 | "RESET"="重置";
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 | xcode_project: PhotoTweaks.xcodeproj
3 | xcode_scheme: PhotoTweaks
4 | script: xcodebuild build -sdk iphonesimulator11 -project PhotoTweaks.xcodeproj -scheme PhotoTweaks CODE_SIGNING_REQUIRED=NO
5 | xcode_sdk:
6 | - iphonesimulator11
7 |
--------------------------------------------------------------------------------
/PhotoTweaks/AppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.h
3 | // PhotoTweaks
4 | //
5 | // Created by Tu You on 14/11/28.
6 | // Copyright (c) 2014年 Tu You. 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 |
--------------------------------------------------------------------------------
/PhotoTweaks/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // PhotoTweaks
4 | //
5 | // Created by Tu You on 14/11/28.
6 | // Copyright (c) 2014年 Tu You. 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 |
--------------------------------------------------------------------------------
/PhotoTweaks.xcodeproj/project.xcworkspace/xcuserdata/Chandler.xcuserdatad/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges
6 |
7 | SnapshotAutomaticallyBeforeSignificantChanges
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/PhotoTweaks/PhotoTweaks/UIColor+Tweak.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+Tweak.h
3 | // PhotoTweaks
4 | //
5 | // Created by TuYou on 14/12/6.
6 | // Copyright (c) 2014年 Tu You. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface UIColor (Tweak)
12 |
13 | + (UIColor *)cancelButtonColor;
14 | + (UIColor *)cancelButtonHighlightedColor;
15 |
16 | + (UIColor *)saveButtonColor;
17 | + (UIColor *)saveButtonHighlightedColor;
18 |
19 | + (UIColor *)resetButtonColor;
20 | + (UIColor *)resetButtonHighlightedColor;
21 |
22 | + (UIColor *)cropLineColor;
23 | + (UIColor *)gridLineColor;
24 | + (UIColor *)maskColor;
25 | + (UIColor *)photoTweakCanvasBackgroundColor;
26 |
27 | @end
28 |
--------------------------------------------------------------------------------
/PhotoTweaks.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = 'PhotoTweaks'
3 | spec.version = '1.0.4'
4 | spec.license = 'MIT'
5 | spec.homepage = 'https://github.com/itouch2/PhotoTweaks'
6 | spec.authors = {'Tu You' => 'yoututouch@gmail.com'}
7 | spec.summary = 'Drag, Rotate, Scale and Crop.'
8 | spec.source = {:git => 'https://github.com/itouch2/PhotoTweaks.git', :tag => '1.0.4'}
9 | spec.source_files = 'PhotoTweaks/PhotoTweaks/*.{h,m}'
10 | spec.framework = 'Foundation', 'CoreGraphics', 'UIKit'
11 | spec.requires_arc = true
12 | spec.platform = :ios, '7.0'
13 | spec.screenshot = 'https://cloud.githubusercontent.com/assets/4316898/6525485/ce2d65ae-c440-11e4-8a73-c461a3f31b5f.png'
14 | end
15 |
--------------------------------------------------------------------------------
/PhotoTweaksTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | tuyou.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/PhotoTweaksTests/PhotoTweaksTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoTweaksTests.m
3 | // PhotoTweaksTests
4 | //
5 | // Created by Tu You on 14/11/28.
6 | // Copyright (c) 2014年 Tu You. All rights reserved.
7 | //
8 |
9 | #import
10 | #import
11 |
12 | @interface PhotoTweaksTests : XCTestCase
13 |
14 | @end
15 |
16 | @implementation PhotoTweaksTests
17 |
18 | - (void)setUp {
19 | [super setUp];
20 | // Put setup code here. This method is called before the invocation of each test method in the class.
21 | }
22 |
23 | - (void)tearDown {
24 | // Put teardown code here. This method is called after the invocation of each test method in the class.
25 | [super tearDown];
26 | }
27 |
28 | - (void)testExample {
29 | // This is an example of a functional test case.
30 | XCTAssert(YES, @"Pass");
31 | }
32 |
33 | - (void)testPerformanceExample {
34 | // This is an example of a performance test case.
35 | [self measureBlock:^{
36 | // Put the code you want to measure the time of here.
37 | }];
38 | }
39 |
40 | @end
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 tuyou
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/PhotoTweaks/PhotoTweaks/PhotoTweakView.h:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoView.h
3 | // PhotoTweaks
4 | //
5 | // Created by Tu You on 14/12/2.
6 | // Copyright (c) 2014年 Tu You. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | extern const CGFloat kMaxRotationAngle;
12 |
13 | @class CropView;
14 |
15 | @interface PhotoContentView : UIView
16 |
17 | @property (strong, nonatomic) UIImageView *imageView;
18 | @property (strong, nonatomic) UIImage *image;
19 |
20 | @end
21 |
22 | @protocol CropViewDelegate
23 |
24 | - (void)cropEnded:(CropView *)cropView;
25 | - (void)cropMoved:(CropView *)cropView;
26 |
27 | @end
28 |
29 | @interface CropView : UIView
30 | @end
31 |
32 | @interface PhotoTweakView : UIView
33 |
34 | @property (nonatomic, assign, readonly) CGFloat angle;
35 | @property (nonatomic, assign, readonly) CGPoint photoContentOffset;
36 |
37 | @property (nonatomic, strong, readonly) CropView *cropView;
38 | @property (nonatomic, strong, readonly) PhotoContentView *photoContentView;
39 | @property (nonatomic, strong, readonly) UISlider *slider;
40 | @property (nonatomic, strong, readonly) UIButton *resetBtn;
41 |
42 |
43 | - (instancetype)initWithFrame:(CGRect)frame
44 | image:(UIImage *)image
45 | maxRotationAngle:(CGFloat)maxRotationAngle;
46 |
47 | - (instancetype)initWithFrame:(CGRect)frame
48 | image:(UIImage *)image;
49 |
50 | - (CGPoint)photoTranslation;
51 |
52 | @end
53 |
--------------------------------------------------------------------------------
/PhotoTweaks/PhotoTweaks/UIColor+Tweak.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+Tweak.m
3 | // PhotoTweaks
4 | //
5 | // Created by TuYou on 14/12/6.
6 | // Copyright (c) 2014年 Tu You. All rights reserved.
7 | //
8 |
9 | #import "UIColor+Tweak.h"
10 |
11 | @implementation UIColor (Tweak)
12 |
13 | + (UIColor *)cancelButtonColor
14 | {
15 | return [UIColor colorWithRed:0.09 green:0.49 blue:1 alpha:1];
16 | }
17 |
18 | + (UIColor *)cancelButtonHighlightedColor
19 | {
20 | return [UIColor colorWithRed:0.11 green:0.17 blue:0.26 alpha:1];
21 | }
22 |
23 | + (UIColor *)saveButtonColor
24 | {
25 | return [UIColor colorWithRed:1 green:0.8 blue:0 alpha:1];
26 | }
27 |
28 | + (UIColor *)saveButtonHighlightedColor
29 | {
30 | return [UIColor colorWithRed:0.26 green:0.23 blue:0.13 alpha:1];
31 | }
32 |
33 | + (UIColor *)resetButtonColor
34 | {
35 | return [UIColor colorWithRed:0.09 green:0.49 blue:1 alpha:1];
36 | }
37 |
38 | + (UIColor *)resetButtonHighlightedColor
39 | {
40 | return [UIColor colorWithRed:0.11 green:0.17 blue:0.26 alpha:1];
41 | }
42 |
43 | + (UIColor *)maskColor
44 | {
45 | return [UIColor colorWithWhite:0.0 alpha:0.6];
46 | }
47 |
48 | + (UIColor *)cropLineColor
49 | {
50 | return [UIColor colorWithWhite:1.0 alpha:1.0];
51 | }
52 |
53 | + (UIColor *)gridLineColor
54 | {
55 | return [UIColor colorWithRed:0.52 green:0.48 blue:0.47 alpha:0.8];
56 | }
57 |
58 | + (UIColor *)photoTweakCanvasBackgroundColor
59 | {
60 | return [UIColor colorWithWhite:0.0 alpha:1.0];
61 | }
62 |
63 | @end
64 |
--------------------------------------------------------------------------------
/PhotoTweaks/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 |
--------------------------------------------------------------------------------
/PhotoTweaks.xcodeproj/project.xcworkspace/xcshareddata/PhotoTweaks.xccheckout:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDESourceControlProjectFavoriteDictionaryKey
6 |
7 | IDESourceControlProjectIdentifier
8 | F6B6C3E5-ABF7-4307-97CD-5615215C3360
9 | IDESourceControlProjectName
10 | PhotoTweaks
11 | IDESourceControlProjectOriginsDictionary
12 |
13 | 69DED6FD9E7FD7F98E45C3020DA22B3A6748FD94
14 | https://github.com/itouch2/PhotoTweaks.git
15 |
16 | IDESourceControlProjectPath
17 | PhotoTweaks.xcodeproj
18 | IDESourceControlProjectRelativeInstallPathDictionary
19 |
20 | 69DED6FD9E7FD7F98E45C3020DA22B3A6748FD94
21 | ../..
22 |
23 | IDESourceControlProjectURL
24 | https://github.com/itouch2/PhotoTweaks.git
25 | IDESourceControlProjectVersion
26 | 111
27 | IDESourceControlProjectWCCIdentifier
28 | 69DED6FD9E7FD7F98E45C3020DA22B3A6748FD94
29 | IDESourceControlProjectWCConfigurations
30 |
31 |
32 | IDESourceControlRepositoryExtensionIdentifierKey
33 | public.vcs.git
34 | IDESourceControlWCCIdentifierKey
35 | 69DED6FD9E7FD7F98E45C3020DA22B3A6748FD94
36 | IDESourceControlWCCName
37 | PhotoTweaks
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/PhotoTweaks/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | tuyou.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
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 | NSPhotoLibraryUsageDescription
47 | PhotoTweaks needs to use Photo Library
48 | NSPhotoLibraryAddUsageDescription
49 | PhotoTweaks needs to use Photo Library
50 |
51 |
52 |
--------------------------------------------------------------------------------
/PhotoTweaks/Images.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 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "PhotoTweak.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "idiom" : "iphone",
41 | "size" : "60x60",
42 | "scale" : "3x"
43 | },
44 | {
45 | "idiom" : "ipad",
46 | "size" : "20x20",
47 | "scale" : "1x"
48 | },
49 | {
50 | "idiom" : "ipad",
51 | "size" : "20x20",
52 | "scale" : "2x"
53 | },
54 | {
55 | "idiom" : "ipad",
56 | "size" : "29x29",
57 | "scale" : "1x"
58 | },
59 | {
60 | "idiom" : "ipad",
61 | "size" : "29x29",
62 | "scale" : "2x"
63 | },
64 | {
65 | "idiom" : "ipad",
66 | "size" : "40x40",
67 | "scale" : "1x"
68 | },
69 | {
70 | "idiom" : "ipad",
71 | "size" : "40x40",
72 | "scale" : "2x"
73 | },
74 | {
75 | "idiom" : "ipad",
76 | "size" : "76x76",
77 | "scale" : "1x"
78 | },
79 | {
80 | "idiom" : "ipad",
81 | "size" : "76x76",
82 | "scale" : "2x"
83 | },
84 | {
85 | "idiom" : "ipad",
86 | "size" : "83.5x83.5",
87 | "scale" : "2x"
88 | },
89 | {
90 | "idiom" : "ios-marketing",
91 | "size" : "1024x1024",
92 | "scale" : "1x"
93 | }
94 | ],
95 | "info" : {
96 | "version" : 1,
97 | "author" : "xcode"
98 | }
99 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PhotoTweaks
5 |
6 | PhotoTweaks is an interface to crop photos. It can let user drag, rotate, scale the image, and crop it. You will find it mimics the interaction of Photos.app on iOS 8. :]
7 |
8 | [](http://cocoapods.org/?q=PhotoTweaks)
9 | [](http://cocoapods.org/?q=PhotoTweaks)
10 | [](https://github.com/itouch2/PhotoTweaks/blob/master/LICENSE)
11 |
12 | ## Usage
13 |
14 | PhotoTweaksViewController offers all the operations to crop the photo, which includes translation, rotate and scale.
15 |
16 | To use it,
17 |
18 | ```objective-c
19 | UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
20 | PhotoTweaksViewController *photoTweaksViewController = [[PhotoTweaksViewController alloc] initWithImage:image];
21 | photoTweaksViewController.delegate = self;
22 | photoTweaksViewController.autoSaveToLibray = YES;
23 | photoTweaksViewController.maxRotationAngle = M_PI_4;
24 | [picker pushViewController:photoTweaksViewController animated:YES];
25 | ```
26 |
27 | ```maxRotationAngle``` is the property to set the maximum supported rotation angle.
28 |
29 | Get the cropped image
30 | ```objective-c
31 | - (void)photoTweaksController:(PhotoTweaksViewController *)controller didFinishWithCroppedImage:(UIImage *)croppedImage
32 | {
33 | [controller.navigationController dismissViewControllerAnimated:YES completion:nil];
34 | // cropped image
35 | }
36 | ```
37 | ## Installation
38 | PhotoTweaks is available on [CocoaPods](http://cocoapods.org). Add the follwing to your Podfile:
39 | ```ruby
40 | pod 'PhotoTweaks', '~> 1.0.4'
41 | ```
42 | Alternatively, you can manually drag the ```PhotoTweaks``` folder into your Xcode project.
43 |
44 |
45 | ## A Quick Peek
46 |
47 | 
48 |
49 | ## Protip
50 | If using with an existing UIImagePickerController, be sure to set ```allowsEditing = NO``` otherwise you may force the user to crop with the native editing tool before showing PhotoTweaksViewController.
51 |
--------------------------------------------------------------------------------
/PhotoTweaks/PhotoTweaks/PhotoTweaksViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoTweaksViewController.h
3 | // PhotoTweaks
4 | //
5 | // Created by Tu You on 14/12/5.
6 | // Copyright (c) 2014年 Tu You. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @protocol PhotoTweaksViewControllerDelegate;
12 |
13 | /**
14 | The photo tweaks controller.
15 | */
16 | @interface PhotoTweaksViewController : UIViewController
17 |
18 | /**
19 | Image to process.
20 | */
21 | @property (nonatomic, strong, readonly) UIImage *image;
22 |
23 | /**
24 | Flag indicating whether the image cropped will be saved to photo library automatically. Defaults to YES.
25 | */
26 | @property (nonatomic, assign) BOOL autoSaveToLibray;
27 |
28 | /**
29 | Max rotation angle
30 | */
31 | @property (nonatomic, assign) CGFloat maxRotationAngle;
32 |
33 | /**
34 | The optional photo tweaks controller delegate.
35 | */
36 | @property (nonatomic, weak) id delegate;
37 |
38 | /**
39 | Save action button's default title color
40 | */
41 | @property (nonatomic, strong) UIColor *saveButtonTitleColor;
42 |
43 | /**
44 | Save action button's highlight title color
45 | */
46 | @property (nonatomic, strong) UIColor *saveButtonHighlightTitleColor;
47 |
48 | /**
49 | Cancel action button's default title color
50 | */
51 | @property (nonatomic, strong) UIColor *cancelButtonTitleColor;
52 |
53 | /**
54 | Cancel action button's highlight title color
55 | */
56 | @property (nonatomic, strong) UIColor *cancelButtonHighlightTitleColor;
57 |
58 | /**
59 | Reset action button's default title color
60 | */
61 | @property (nonatomic, strong) UIColor *resetButtonTitleColor;
62 |
63 | /**
64 | Reset action button's highlight title color
65 | */
66 | @property (nonatomic, strong) UIColor *resetButtonHighlightTitleColor;
67 |
68 | /**
69 | Slider tint color
70 | */
71 | @property (nonatomic, strong) UIColor *sliderTintColor;
72 |
73 | /**
74 | Creates a photo tweaks view controller with the image to process.
75 | */
76 | - (instancetype)initWithImage:(UIImage *)image;
77 |
78 | @end
79 |
80 | /**
81 | The photo tweaks controller delegate
82 | */
83 | @protocol PhotoTweaksViewControllerDelegate
84 |
85 | /**
86 | Called on image cropped.
87 | */
88 | - (void)photoTweaksController:(PhotoTweaksViewController *)controller didFinishWithCroppedImage:(UIImage *)croppedImage;
89 |
90 | /**
91 | Called on cropping image canceled
92 | */
93 | - (void)photoTweaksControllerDidCancel:(PhotoTweaksViewController *)controller;
94 |
95 | @end
96 |
--------------------------------------------------------------------------------
/PhotoTweaks/AppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.m
3 | // PhotoTweaks
4 | //
5 | // Created by Tu You on 14/11/28.
6 | // Copyright (c) 2014年 Tu You. All rights reserved.
7 | //
8 |
9 | #import "AppDelegate.h"
10 | #import "PhotoTweaksViewController.h"
11 |
12 | @interface AppDelegate ()
13 |
14 | @end
15 |
16 | @implementation AppDelegate
17 |
18 |
19 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
20 | {
21 | // Override point for customization after application launch.
22 |
23 | UIImagePickerController *picker = [[UIImagePickerController alloc] init];
24 | picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
25 | picker.delegate = self;
26 | picker.allowsEditing = NO;
27 | picker.navigationBarHidden = YES;
28 | self.window.rootViewController = picker;
29 |
30 | return YES;
31 | }
32 |
33 | - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
34 | {
35 | UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
36 | PhotoTweaksViewController *photoTweaksViewController = [[PhotoTweaksViewController alloc] initWithImage:image];
37 | photoTweaksViewController.delegate = self;
38 | photoTweaksViewController.autoSaveToLibray = YES;
39 | photoTweaksViewController.maxRotationAngle = M_PI_4;
40 | [picker pushViewController:photoTweaksViewController animated:YES];
41 | }
42 |
43 | - (void)photoTweaksController:(PhotoTweaksViewController *)controller didFinishWithCroppedImage:(UIImage *)croppedImage
44 | {
45 | [controller.navigationController popViewControllerAnimated:YES];
46 | }
47 |
48 | - (void)photoTweaksControllerDidCancel:(PhotoTweaksViewController *)controller
49 | {
50 | [controller.navigationController popViewControllerAnimated:YES];
51 | }
52 |
53 | - (void)applicationWillResignActive:(UIApplication *)application {
54 | // 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.
55 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
56 | }
57 |
58 | - (void)applicationDidEnterBackground:(UIApplication *)application {
59 | // 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.
60 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
61 | }
62 |
63 | - (void)applicationWillEnterForeground:(UIApplication *)application {
64 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
65 | }
66 |
67 | - (void)applicationDidBecomeActive:(UIApplication *)application {
68 | // 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.
69 | }
70 |
71 | - (void)applicationWillTerminate:(UIApplication *)application {
72 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
73 | }
74 |
75 | @end
76 |
--------------------------------------------------------------------------------
/PhotoTweaks/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/PhotoTweaks.xcodeproj/xcshareddata/xcschemes/PhotoTweaks.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
75 |
76 |
82 |
83 |
84 |
85 |
86 |
87 |
93 |
94 |
100 |
101 |
102 |
103 |
105 |
106 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/PhotoTweaks/PhotoTweaks/PhotoTweaksViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoTweaksViewController.m
3 | // PhotoTweaks
4 | //
5 | // Created by Tu You on 14/12/5.
6 | // Copyright (c) 2014年 Tu You. All rights reserved.
7 | //
8 |
9 | #import "PhotoTweaksViewController.h"
10 | #import "PhotoTweakView.h"
11 | #import "UIColor+Tweak.h"
12 | #import
13 |
14 | @interface PhotoTweaksViewController ()
15 |
16 | @property (strong, nonatomic) PhotoTweakView *photoView;
17 |
18 | @end
19 |
20 | @implementation PhotoTweaksViewController
21 |
22 | - (instancetype)initWithImage:(UIImage *)image
23 | {
24 | if (self = [super init]) {
25 | _image = image;
26 | _autoSaveToLibray = YES;
27 | _maxRotationAngle = kMaxRotationAngle;
28 | }
29 | return self;
30 | }
31 |
32 | - (void)viewDidLoad
33 | {
34 | [super viewDidLoad];
35 |
36 | self.navigationController.navigationBarHidden = YES;
37 |
38 | if ([self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)]) {
39 | self.automaticallyAdjustsScrollViewInsets = NO;
40 | }
41 |
42 | self.view.clipsToBounds = YES;
43 | self.view.backgroundColor = [UIColor photoTweakCanvasBackgroundColor];
44 |
45 | [self setupSubviews];
46 | }
47 |
48 | - (void)setupSubviews
49 | {
50 | self.photoView = [[PhotoTweakView alloc] initWithFrame:self.view.bounds image:self.image maxRotationAngle:self.maxRotationAngle];
51 | self.photoView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
52 | [self.view addSubview:self.photoView];
53 |
54 | UIButton *cancelBtn = [UIButton buttonWithType:UIButtonTypeCustom];
55 | cancelBtn.frame = CGRectMake(8, CGRectGetHeight(self.view.frame) - 40, 60, 40);
56 | cancelBtn.titleLabel.textAlignment = NSTextAlignmentLeft;
57 | [cancelBtn setTitle:NSLocalizedStringFromTable(@"Cancel", @"PhotoTweaks", nil) forState:UIControlStateNormal];
58 | UIColor *cancelTitleColor = !self.cancelButtonTitleColor ?
59 | [UIColor cancelButtonColor] : self.cancelButtonTitleColor;
60 | [cancelBtn setTitleColor:cancelTitleColor forState:UIControlStateNormal];
61 | UIColor *cancelHighlightTitleColor = !self.cancelButtonHighlightTitleColor ?
62 | [UIColor cancelButtonHighlightedColor] : self.cancelButtonHighlightTitleColor;
63 | [cancelBtn setTitleColor:cancelHighlightTitleColor forState:UIControlStateHighlighted];
64 | cancelBtn.titleLabel.font = [UIFont systemFontOfSize:17];
65 | [cancelBtn addTarget:self action:@selector(cancelBtnTapped) forControlEvents:UIControlEventTouchUpInside];
66 | [self.view addSubview:cancelBtn];
67 |
68 | UIButton *cropBtn = [UIButton buttonWithType:UIButtonTypeCustom];
69 | cropBtn.titleLabel.textAlignment = NSTextAlignmentRight;
70 | cropBtn.frame = CGRectMake(CGRectGetWidth(self.view.frame) - 60, CGRectGetHeight(self.view.frame) - 40, 60, 40);
71 | [cropBtn setTitle:NSLocalizedStringFromTable(@"Done", @"PhotoTweaks", nil) forState:UIControlStateNormal];
72 | UIColor *saveButtonTitleColor = !self.saveButtonTitleColor ?
73 | [UIColor saveButtonColor] : self.saveButtonTitleColor;
74 | [cropBtn setTitleColor:saveButtonTitleColor forState:UIControlStateNormal];
75 |
76 | UIColor *saveButtonHighlightTitleColor = !self.saveButtonHighlightTitleColor ?
77 | [UIColor saveButtonHighlightedColor] : self.saveButtonHighlightTitleColor;
78 | [cropBtn setTitleColor:saveButtonHighlightTitleColor forState:UIControlStateHighlighted];
79 | cropBtn.titleLabel.font = [UIFont systemFontOfSize:17];
80 | [cropBtn addTarget:self action:@selector(saveBtnTapped) forControlEvents:UIControlEventTouchUpInside];
81 | [self.view addSubview:cropBtn];
82 | }
83 |
84 | - (void)cancelBtnTapped
85 | {
86 | [self.delegate photoTweaksControllerDidCancel:self];
87 | }
88 |
89 | - (void)saveBtnTapped
90 | {
91 | CGAffineTransform transform = CGAffineTransformIdentity;
92 |
93 | // translate
94 | CGPoint translation = [self.photoView photoTranslation];
95 | transform = CGAffineTransformTranslate(transform, translation.x, translation.y);
96 |
97 | // rotate
98 | transform = CGAffineTransformRotate(transform, self.photoView.angle);
99 |
100 | // scale
101 | CGAffineTransform t = self.photoView.photoContentView.transform;
102 | CGFloat xScale = sqrt(t.a * t.a + t.c * t.c);
103 | CGFloat yScale = sqrt(t.b * t.b + t.d * t.d);
104 | transform = CGAffineTransformScale(transform, xScale, yScale);
105 |
106 | CGImageRef imageRef = [self newTransformedImage:transform
107 | sourceImage:self.image.CGImage
108 | sourceSize:self.image.size
109 | sourceOrientation:self.image.imageOrientation
110 | outputWidth:self.image.size.width
111 | cropSize:self.photoView.cropView.frame.size
112 | imageViewSize:self.photoView.photoContentView.bounds.size];
113 |
114 | UIImage *image = [UIImage imageWithCGImage:imageRef];
115 | CGImageRelease(imageRef);
116 |
117 | if (self.autoSaveToLibray) {
118 | UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:finishedSavingWithError:contextInfo:), nil);
119 | }
120 |
121 | [self.delegate photoTweaksController:self didFinishWithCroppedImage:image];
122 | }
123 |
124 | - (void)image:(UIImage *)image finishedSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
125 | if(error != nil) {
126 | NSLog(@"ERROR: %@",[error localizedDescription]);
127 | }
128 | }
129 |
130 | - (CGImageRef)newScaledImage:(CGImageRef)source withOrientation:(UIImageOrientation)orientation toSize:(CGSize)size withQuality:(CGInterpolationQuality)quality
131 | {
132 | CGSize srcSize = size;
133 | CGFloat rotation = 0.0;
134 |
135 | switch(orientation)
136 | {
137 | case UIImageOrientationUp: {
138 | rotation = 0;
139 | } break;
140 | case UIImageOrientationDown: {
141 | rotation = M_PI;
142 | } break;
143 | case UIImageOrientationLeft:{
144 | rotation = M_PI_2;
145 | srcSize = CGSizeMake(size.height, size.width);
146 | } break;
147 | case UIImageOrientationRight: {
148 | rotation = -M_PI_2;
149 | srcSize = CGSizeMake(size.height, size.width);
150 | } break;
151 | default:
152 | break;
153 | }
154 |
155 | CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
156 |
157 | CGContextRef context = CGBitmapContextCreate(NULL,
158 | size.width,
159 | size.height,
160 | 8, //CGImageGetBitsPerComponent(source),
161 | 0,
162 | rgbColorSpace,//CGImageGetColorSpace(source),
163 | kCGImageAlphaPremultipliedLast|kCGBitmapByteOrder32Big//(CGBitmapInfo)kCGImageAlphaNoneSkipFirst //CGImageGetBitmapInfo(source)
164 | );
165 | CGColorSpaceRelease(rgbColorSpace);
166 |
167 | CGContextSetInterpolationQuality(context, quality);
168 | CGContextTranslateCTM(context, size.width/2, size.height/2);
169 | CGContextRotateCTM(context,rotation);
170 |
171 | CGContextDrawImage(context, CGRectMake(-srcSize.width/2 ,
172 | -srcSize.height/2,
173 | srcSize.width,
174 | srcSize.height),
175 | source);
176 |
177 | CGImageRef resultRef = CGBitmapContextCreateImage(context);
178 | CGContextRelease(context);
179 |
180 | return resultRef;
181 | }
182 |
183 | - (CGImageRef)newTransformedImage:(CGAffineTransform)transform
184 | sourceImage:(CGImageRef)sourceImage
185 | sourceSize:(CGSize)sourceSize
186 | sourceOrientation:(UIImageOrientation)sourceOrientation
187 | outputWidth:(CGFloat)outputWidth
188 | cropSize:(CGSize)cropSize
189 | imageViewSize:(CGSize)imageViewSize
190 | {
191 | CGImageRef source = [self newScaledImage:sourceImage
192 | withOrientation:sourceOrientation
193 | toSize:sourceSize
194 | withQuality:kCGInterpolationNone];
195 |
196 | CGFloat aspect = cropSize.height/cropSize.width;
197 | CGSize outputSize = CGSizeMake(outputWidth, outputWidth*aspect);
198 |
199 | CGContextRef context = CGBitmapContextCreate(NULL,
200 | outputSize.width,
201 | outputSize.height,
202 | CGImageGetBitsPerComponent(source),
203 | 0,
204 | CGImageGetColorSpace(source),
205 | CGImageGetBitmapInfo(source));
206 | CGContextSetFillColorWithColor(context, [[UIColor clearColor] CGColor]);
207 | CGContextFillRect(context, CGRectMake(0, 0, outputSize.width, outputSize.height));
208 |
209 | CGAffineTransform uiCoords = CGAffineTransformMakeScale(outputSize.width / cropSize.width,
210 | outputSize.height / cropSize.height);
211 | uiCoords = CGAffineTransformTranslate(uiCoords, cropSize.width/2.0, cropSize.height / 2.0);
212 | uiCoords = CGAffineTransformScale(uiCoords, 1.0, -1.0);
213 | CGContextConcatCTM(context, uiCoords);
214 |
215 | CGContextConcatCTM(context, transform);
216 | CGContextScaleCTM(context, 1.0, -1.0);
217 |
218 | CGContextDrawImage(context, CGRectMake(-imageViewSize.width/2.0,
219 | -imageViewSize.height/2.0,
220 | imageViewSize.width,
221 | imageViewSize.height)
222 | , source);
223 |
224 | CGImageRef resultRef = CGBitmapContextCreateImage(context);
225 | CGContextRelease(context);
226 | CGImageRelease(source);
227 | return resultRef;
228 | }
229 |
230 | - (UIStatusBarStyle)preferredStatusBarStyle
231 | {
232 | return UIStatusBarStyleLightContent;
233 | }
234 |
235 | - (void)didReceiveMemoryWarning
236 | {
237 | [super didReceiveMemoryWarning];
238 | }
239 |
240 | @end
241 |
--------------------------------------------------------------------------------
/PhotoTweaks.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 00B1B9281ACAA90100A8E285 /* PhotoTweaks.strings in Resources */ = {isa = PBXBuildFile; fileRef = 00B1B92A1ACAA90100A8E285 /* PhotoTweaks.strings */; };
11 | B71706151A32A18D00A6CC10 /* UIColor+Tweak.m in Sources */ = {isa = PBXBuildFile; fileRef = B71706141A32A18D00A6CC10 /* UIColor+Tweak.m */; };
12 | CB3496FC1A3207E800C7EB0E /* PhotoTweaksViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB3496FB1A3207E800C7EB0E /* PhotoTweaksViewController.m */; };
13 | CBD5FA591A28425300D4D520 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD5FA581A28425300D4D520 /* main.m */; };
14 | CBD5FA5C1A28425300D4D520 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD5FA5B1A28425300D4D520 /* AppDelegate.m */; };
15 | CBD5FA621A28425300D4D520 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CBD5FA601A28425300D4D520 /* Main.storyboard */; };
16 | CBD5FA641A28425300D4D520 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CBD5FA631A28425300D4D520 /* Images.xcassets */; };
17 | CBD5FA671A28425300D4D520 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = CBD5FA651A28425300D4D520 /* LaunchScreen.xib */; };
18 | CBD5FA731A28425300D4D520 /* PhotoTweaksTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD5FA721A28425300D4D520 /* PhotoTweaksTests.m */; };
19 | CBDE57B71A2CAA0700285CFF /* PhotoTweakView.m in Sources */ = {isa = PBXBuildFile; fileRef = CBDE57B61A2CAA0600285CFF /* PhotoTweakView.m */; };
20 | /* End PBXBuildFile section */
21 |
22 | /* Begin PBXContainerItemProxy section */
23 | CBD5FA6D1A28425300D4D520 /* PBXContainerItemProxy */ = {
24 | isa = PBXContainerItemProxy;
25 | containerPortal = CBD5FA4B1A28425300D4D520 /* Project object */;
26 | proxyType = 1;
27 | remoteGlobalIDString = CBD5FA521A28425300D4D520;
28 | remoteInfo = PhotoTweaks;
29 | };
30 | /* End PBXContainerItemProxy section */
31 |
32 | /* Begin PBXFileReference section */
33 | 00B1B9291ACAA90100A8E285 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/PhotoTweaks.strings; sourceTree = ""; };
34 | 00B1B9361ACAA98400A8E285 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/PhotoTweaks.strings"; sourceTree = ""; };
35 | B71706131A32A18D00A6CC10 /* UIColor+Tweak.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+Tweak.h"; sourceTree = ""; };
36 | B71706141A32A18D00A6CC10 /* UIColor+Tweak.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+Tweak.m"; sourceTree = ""; };
37 | CB3496FA1A3207E800C7EB0E /* PhotoTweaksViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoTweaksViewController.h; sourceTree = ""; };
38 | CB3496FB1A3207E800C7EB0E /* PhotoTweaksViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoTweaksViewController.m; sourceTree = ""; };
39 | CBD5FA531A28425300D4D520 /* PhotoTweaks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PhotoTweaks.app; sourceTree = BUILT_PRODUCTS_DIR; };
40 | CBD5FA571A28425300D4D520 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
41 | CBD5FA581A28425300D4D520 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
42 | CBD5FA5A1A28425300D4D520 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
43 | CBD5FA5B1A28425300D4D520 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
44 | CBD5FA611A28425300D4D520 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
45 | CBD5FA631A28425300D4D520 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; };
46 | CBD5FA661A28425300D4D520 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; };
47 | CBD5FA6C1A28425300D4D520 /* PhotoTweaksTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PhotoTweaksTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
48 | CBD5FA711A28425300D4D520 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
49 | CBD5FA721A28425300D4D520 /* PhotoTweaksTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PhotoTweaksTests.m; sourceTree = ""; };
50 | CBDE57B51A2CAA0600285CFF /* PhotoTweakView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoTweakView.h; sourceTree = ""; };
51 | CBDE57B61A2CAA0600285CFF /* PhotoTweakView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoTweakView.m; sourceTree = ""; };
52 | /* End PBXFileReference section */
53 |
54 | /* Begin PBXFrameworksBuildPhase section */
55 | CBD5FA501A28425300D4D520 /* Frameworks */ = {
56 | isa = PBXFrameworksBuildPhase;
57 | buildActionMask = 2147483647;
58 | files = (
59 | );
60 | runOnlyForDeploymentPostprocessing = 0;
61 | };
62 | CBD5FA691A28425300D4D520 /* Frameworks */ = {
63 | isa = PBXFrameworksBuildPhase;
64 | buildActionMask = 2147483647;
65 | files = (
66 | );
67 | runOnlyForDeploymentPostprocessing = 0;
68 | };
69 | /* End PBXFrameworksBuildPhase section */
70 |
71 | /* Begin PBXGroup section */
72 | CBD5FA4A1A28425300D4D520 = {
73 | isa = PBXGroup;
74 | children = (
75 | CBD5FA551A28425300D4D520 /* PhotoTweaks */,
76 | CBD5FA6F1A28425300D4D520 /* PhotoTweaksTests */,
77 | CBD5FA541A28425300D4D520 /* Products */,
78 | );
79 | sourceTree = "";
80 | };
81 | CBD5FA541A28425300D4D520 /* Products */ = {
82 | isa = PBXGroup;
83 | children = (
84 | CBD5FA531A28425300D4D520 /* PhotoTweaks.app */,
85 | CBD5FA6C1A28425300D4D520 /* PhotoTweaksTests.xctest */,
86 | );
87 | name = Products;
88 | sourceTree = "";
89 | };
90 | CBD5FA551A28425300D4D520 /* PhotoTweaks */ = {
91 | isa = PBXGroup;
92 | children = (
93 | CBDE57B41A2CA9EE00285CFF /* PhotoTweaks */,
94 | CBD5FA5A1A28425300D4D520 /* AppDelegate.h */,
95 | CBD5FA5B1A28425300D4D520 /* AppDelegate.m */,
96 | CBD5FA601A28425300D4D520 /* Main.storyboard */,
97 | CBD5FA631A28425300D4D520 /* Images.xcassets */,
98 | CBD5FA651A28425300D4D520 /* LaunchScreen.xib */,
99 | CBD5FA561A28425300D4D520 /* Supporting Files */,
100 | );
101 | path = PhotoTweaks;
102 | sourceTree = "";
103 | };
104 | CBD5FA561A28425300D4D520 /* Supporting Files */ = {
105 | isa = PBXGroup;
106 | children = (
107 | CBD5FA571A28425300D4D520 /* Info.plist */,
108 | CBD5FA581A28425300D4D520 /* main.m */,
109 | 00B1B92A1ACAA90100A8E285 /* PhotoTweaks.strings */,
110 | );
111 | name = "Supporting Files";
112 | sourceTree = "";
113 | };
114 | CBD5FA6F1A28425300D4D520 /* PhotoTweaksTests */ = {
115 | isa = PBXGroup;
116 | children = (
117 | CBD5FA721A28425300D4D520 /* PhotoTweaksTests.m */,
118 | CBD5FA701A28425300D4D520 /* Supporting Files */,
119 | );
120 | path = PhotoTweaksTests;
121 | sourceTree = "";
122 | };
123 | CBD5FA701A28425300D4D520 /* Supporting Files */ = {
124 | isa = PBXGroup;
125 | children = (
126 | CBD5FA711A28425300D4D520 /* Info.plist */,
127 | );
128 | name = "Supporting Files";
129 | sourceTree = "";
130 | };
131 | CBDE57B41A2CA9EE00285CFF /* PhotoTweaks */ = {
132 | isa = PBXGroup;
133 | children = (
134 | CB3496FA1A3207E800C7EB0E /* PhotoTweaksViewController.h */,
135 | CB3496FB1A3207E800C7EB0E /* PhotoTweaksViewController.m */,
136 | CBDE57B51A2CAA0600285CFF /* PhotoTweakView.h */,
137 | CBDE57B61A2CAA0600285CFF /* PhotoTweakView.m */,
138 | B71706131A32A18D00A6CC10 /* UIColor+Tweak.h */,
139 | B71706141A32A18D00A6CC10 /* UIColor+Tweak.m */,
140 | );
141 | path = PhotoTweaks;
142 | sourceTree = "";
143 | };
144 | /* End PBXGroup section */
145 |
146 | /* Begin PBXNativeTarget section */
147 | CBD5FA521A28425300D4D520 /* PhotoTweaks */ = {
148 | isa = PBXNativeTarget;
149 | buildConfigurationList = CBD5FA761A28425300D4D520 /* Build configuration list for PBXNativeTarget "PhotoTweaks" */;
150 | buildPhases = (
151 | CBD5FA4F1A28425300D4D520 /* Sources */,
152 | CBD5FA501A28425300D4D520 /* Frameworks */,
153 | CBD5FA511A28425300D4D520 /* Resources */,
154 | );
155 | buildRules = (
156 | );
157 | dependencies = (
158 | );
159 | name = PhotoTweaks;
160 | productName = PhotoTweaks;
161 | productReference = CBD5FA531A28425300D4D520 /* PhotoTweaks.app */;
162 | productType = "com.apple.product-type.application";
163 | };
164 | CBD5FA6B1A28425300D4D520 /* PhotoTweaksTests */ = {
165 | isa = PBXNativeTarget;
166 | buildConfigurationList = CBD5FA791A28425300D4D520 /* Build configuration list for PBXNativeTarget "PhotoTweaksTests" */;
167 | buildPhases = (
168 | CBD5FA681A28425300D4D520 /* Sources */,
169 | CBD5FA691A28425300D4D520 /* Frameworks */,
170 | CBD5FA6A1A28425300D4D520 /* Resources */,
171 | );
172 | buildRules = (
173 | );
174 | dependencies = (
175 | CBD5FA6E1A28425300D4D520 /* PBXTargetDependency */,
176 | );
177 | name = PhotoTweaksTests;
178 | productName = PhotoTweaksTests;
179 | productReference = CBD5FA6C1A28425300D4D520 /* PhotoTweaksTests.xctest */;
180 | productType = "com.apple.product-type.bundle.unit-test";
181 | };
182 | /* End PBXNativeTarget section */
183 |
184 | /* Begin PBXProject section */
185 | CBD5FA4B1A28425300D4D520 /* Project object */ = {
186 | isa = PBXProject;
187 | attributes = {
188 | LastUpgradeCheck = 0610;
189 | ORGANIZATIONNAME = "Tu You";
190 | TargetAttributes = {
191 | CBD5FA521A28425300D4D520 = {
192 | CreatedOnToolsVersion = 6.1;
193 | };
194 | CBD5FA6B1A28425300D4D520 = {
195 | CreatedOnToolsVersion = 6.1;
196 | TestTargetID = CBD5FA521A28425300D4D520;
197 | };
198 | };
199 | };
200 | buildConfigurationList = CBD5FA4E1A28425300D4D520 /* Build configuration list for PBXProject "PhotoTweaks" */;
201 | compatibilityVersion = "Xcode 3.2";
202 | developmentRegion = English;
203 | hasScannedForEncodings = 0;
204 | knownRegions = (
205 | en,
206 | Base,
207 | "zh-Hans",
208 | );
209 | mainGroup = CBD5FA4A1A28425300D4D520;
210 | productRefGroup = CBD5FA541A28425300D4D520 /* Products */;
211 | projectDirPath = "";
212 | projectRoot = "";
213 | targets = (
214 | CBD5FA521A28425300D4D520 /* PhotoTweaks */,
215 | CBD5FA6B1A28425300D4D520 /* PhotoTweaksTests */,
216 | );
217 | };
218 | /* End PBXProject section */
219 |
220 | /* Begin PBXResourcesBuildPhase section */
221 | CBD5FA511A28425300D4D520 /* Resources */ = {
222 | isa = PBXResourcesBuildPhase;
223 | buildActionMask = 2147483647;
224 | files = (
225 | 00B1B9281ACAA90100A8E285 /* PhotoTweaks.strings in Resources */,
226 | CBD5FA621A28425300D4D520 /* Main.storyboard in Resources */,
227 | CBD5FA671A28425300D4D520 /* LaunchScreen.xib in Resources */,
228 | CBD5FA641A28425300D4D520 /* Images.xcassets in Resources */,
229 | );
230 | runOnlyForDeploymentPostprocessing = 0;
231 | };
232 | CBD5FA6A1A28425300D4D520 /* Resources */ = {
233 | isa = PBXResourcesBuildPhase;
234 | buildActionMask = 2147483647;
235 | files = (
236 | );
237 | runOnlyForDeploymentPostprocessing = 0;
238 | };
239 | /* End PBXResourcesBuildPhase section */
240 |
241 | /* Begin PBXSourcesBuildPhase section */
242 | CBD5FA4F1A28425300D4D520 /* Sources */ = {
243 | isa = PBXSourcesBuildPhase;
244 | buildActionMask = 2147483647;
245 | files = (
246 | CBD5FA5C1A28425300D4D520 /* AppDelegate.m in Sources */,
247 | B71706151A32A18D00A6CC10 /* UIColor+Tweak.m in Sources */,
248 | CBD5FA591A28425300D4D520 /* main.m in Sources */,
249 | CB3496FC1A3207E800C7EB0E /* PhotoTweaksViewController.m in Sources */,
250 | CBDE57B71A2CAA0700285CFF /* PhotoTweakView.m in Sources */,
251 | );
252 | runOnlyForDeploymentPostprocessing = 0;
253 | };
254 | CBD5FA681A28425300D4D520 /* Sources */ = {
255 | isa = PBXSourcesBuildPhase;
256 | buildActionMask = 2147483647;
257 | files = (
258 | CBD5FA731A28425300D4D520 /* PhotoTweaksTests.m in Sources */,
259 | );
260 | runOnlyForDeploymentPostprocessing = 0;
261 | };
262 | /* End PBXSourcesBuildPhase section */
263 |
264 | /* Begin PBXTargetDependency section */
265 | CBD5FA6E1A28425300D4D520 /* PBXTargetDependency */ = {
266 | isa = PBXTargetDependency;
267 | target = CBD5FA521A28425300D4D520 /* PhotoTweaks */;
268 | targetProxy = CBD5FA6D1A28425300D4D520 /* PBXContainerItemProxy */;
269 | };
270 | /* End PBXTargetDependency section */
271 |
272 | /* Begin PBXVariantGroup section */
273 | 00B1B92A1ACAA90100A8E285 /* PhotoTweaks.strings */ = {
274 | isa = PBXVariantGroup;
275 | children = (
276 | 00B1B9291ACAA90100A8E285 /* en */,
277 | 00B1B9361ACAA98400A8E285 /* zh-Hans */,
278 | );
279 | name = PhotoTweaks.strings;
280 | sourceTree = "";
281 | };
282 | CBD5FA601A28425300D4D520 /* Main.storyboard */ = {
283 | isa = PBXVariantGroup;
284 | children = (
285 | CBD5FA611A28425300D4D520 /* Base */,
286 | );
287 | name = Main.storyboard;
288 | sourceTree = "";
289 | };
290 | CBD5FA651A28425300D4D520 /* LaunchScreen.xib */ = {
291 | isa = PBXVariantGroup;
292 | children = (
293 | CBD5FA661A28425300D4D520 /* Base */,
294 | );
295 | name = LaunchScreen.xib;
296 | sourceTree = "";
297 | };
298 | /* End PBXVariantGroup section */
299 |
300 | /* Begin XCBuildConfiguration section */
301 | CBD5FA741A28425300D4D520 /* Debug */ = {
302 | isa = XCBuildConfiguration;
303 | buildSettings = {
304 | ALWAYS_SEARCH_USER_PATHS = NO;
305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
306 | CLANG_CXX_LIBRARY = "libc++";
307 | CLANG_ENABLE_MODULES = YES;
308 | CLANG_ENABLE_OBJC_ARC = YES;
309 | CLANG_WARN_BOOL_CONVERSION = YES;
310 | CLANG_WARN_CONSTANT_CONVERSION = YES;
311 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
312 | CLANG_WARN_EMPTY_BODY = YES;
313 | CLANG_WARN_ENUM_CONVERSION = YES;
314 | CLANG_WARN_INT_CONVERSION = YES;
315 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
316 | CLANG_WARN_UNREACHABLE_CODE = YES;
317 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
318 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
319 | COPY_PHASE_STRIP = NO;
320 | ENABLE_STRICT_OBJC_MSGSEND = YES;
321 | GCC_C_LANGUAGE_STANDARD = gnu99;
322 | GCC_DYNAMIC_NO_PIC = NO;
323 | GCC_OPTIMIZATION_LEVEL = 0;
324 | GCC_PREPROCESSOR_DEFINITIONS = (
325 | "DEBUG=1",
326 | "$(inherited)",
327 | );
328 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
329 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
330 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
331 | GCC_WARN_UNDECLARED_SELECTOR = YES;
332 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
333 | GCC_WARN_UNUSED_FUNCTION = YES;
334 | GCC_WARN_UNUSED_VARIABLE = YES;
335 | IPHONEOS_DEPLOYMENT_TARGET = 8.1;
336 | MTL_ENABLE_DEBUG_INFO = YES;
337 | ONLY_ACTIVE_ARCH = YES;
338 | SDKROOT = iphoneos;
339 | TARGETED_DEVICE_FAMILY = "1,2";
340 | };
341 | name = Debug;
342 | };
343 | CBD5FA751A28425300D4D520 /* Release */ = {
344 | isa = XCBuildConfiguration;
345 | buildSettings = {
346 | ALWAYS_SEARCH_USER_PATHS = NO;
347 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
348 | CLANG_CXX_LIBRARY = "libc++";
349 | CLANG_ENABLE_MODULES = YES;
350 | CLANG_ENABLE_OBJC_ARC = YES;
351 | CLANG_WARN_BOOL_CONVERSION = YES;
352 | CLANG_WARN_CONSTANT_CONVERSION = YES;
353 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
354 | CLANG_WARN_EMPTY_BODY = YES;
355 | CLANG_WARN_ENUM_CONVERSION = YES;
356 | CLANG_WARN_INT_CONVERSION = YES;
357 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
358 | CLANG_WARN_UNREACHABLE_CODE = YES;
359 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
360 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
361 | COPY_PHASE_STRIP = YES;
362 | ENABLE_NS_ASSERTIONS = NO;
363 | ENABLE_STRICT_OBJC_MSGSEND = YES;
364 | GCC_C_LANGUAGE_STANDARD = gnu99;
365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
366 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
367 | GCC_WARN_UNDECLARED_SELECTOR = YES;
368 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
369 | GCC_WARN_UNUSED_FUNCTION = YES;
370 | GCC_WARN_UNUSED_VARIABLE = YES;
371 | IPHONEOS_DEPLOYMENT_TARGET = 8.1;
372 | MTL_ENABLE_DEBUG_INFO = NO;
373 | SDKROOT = iphoneos;
374 | TARGETED_DEVICE_FAMILY = "1,2";
375 | VALIDATE_PRODUCT = YES;
376 | };
377 | name = Release;
378 | };
379 | CBD5FA771A28425300D4D520 /* Debug */ = {
380 | isa = XCBuildConfiguration;
381 | buildSettings = {
382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
383 | INFOPLIST_FILE = PhotoTweaks/Info.plist;
384 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
385 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
386 | PRODUCT_NAME = "$(TARGET_NAME)";
387 | TARGETED_DEVICE_FAMILY = 1;
388 | };
389 | name = Debug;
390 | };
391 | CBD5FA781A28425300D4D520 /* Release */ = {
392 | isa = XCBuildConfiguration;
393 | buildSettings = {
394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
395 | INFOPLIST_FILE = PhotoTweaks/Info.plist;
396 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
397 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
398 | PRODUCT_NAME = "$(TARGET_NAME)";
399 | TARGETED_DEVICE_FAMILY = 1;
400 | };
401 | name = Release;
402 | };
403 | CBD5FA7A1A28425300D4D520 /* Debug */ = {
404 | isa = XCBuildConfiguration;
405 | buildSettings = {
406 | BUNDLE_LOADER = "$(TEST_HOST)";
407 | FRAMEWORK_SEARCH_PATHS = (
408 | "$(SDKROOT)/Developer/Library/Frameworks",
409 | "$(inherited)",
410 | );
411 | GCC_PREPROCESSOR_DEFINITIONS = (
412 | "DEBUG=1",
413 | "$(inherited)",
414 | );
415 | INFOPLIST_FILE = PhotoTweaksTests/Info.plist;
416 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
417 | PRODUCT_NAME = "$(TARGET_NAME)";
418 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PhotoTweaks.app/PhotoTweaks";
419 | };
420 | name = Debug;
421 | };
422 | CBD5FA7B1A28425300D4D520 /* Release */ = {
423 | isa = XCBuildConfiguration;
424 | buildSettings = {
425 | BUNDLE_LOADER = "$(TEST_HOST)";
426 | FRAMEWORK_SEARCH_PATHS = (
427 | "$(SDKROOT)/Developer/Library/Frameworks",
428 | "$(inherited)",
429 | );
430 | INFOPLIST_FILE = PhotoTweaksTests/Info.plist;
431 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
432 | PRODUCT_NAME = "$(TARGET_NAME)";
433 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PhotoTweaks.app/PhotoTweaks";
434 | };
435 | name = Release;
436 | };
437 | /* End XCBuildConfiguration section */
438 |
439 | /* Begin XCConfigurationList section */
440 | CBD5FA4E1A28425300D4D520 /* Build configuration list for PBXProject "PhotoTweaks" */ = {
441 | isa = XCConfigurationList;
442 | buildConfigurations = (
443 | CBD5FA741A28425300D4D520 /* Debug */,
444 | CBD5FA751A28425300D4D520 /* Release */,
445 | );
446 | defaultConfigurationIsVisible = 0;
447 | defaultConfigurationName = Release;
448 | };
449 | CBD5FA761A28425300D4D520 /* Build configuration list for PBXNativeTarget "PhotoTweaks" */ = {
450 | isa = XCConfigurationList;
451 | buildConfigurations = (
452 | CBD5FA771A28425300D4D520 /* Debug */,
453 | CBD5FA781A28425300D4D520 /* Release */,
454 | );
455 | defaultConfigurationIsVisible = 0;
456 | defaultConfigurationName = Release;
457 | };
458 | CBD5FA791A28425300D4D520 /* Build configuration list for PBXNativeTarget "PhotoTweaksTests" */ = {
459 | isa = XCConfigurationList;
460 | buildConfigurations = (
461 | CBD5FA7A1A28425300D4D520 /* Debug */,
462 | CBD5FA7B1A28425300D4D520 /* Release */,
463 | );
464 | defaultConfigurationIsVisible = 0;
465 | defaultConfigurationName = Release;
466 | };
467 | /* End XCConfigurationList section */
468 | };
469 | rootObject = CBD5FA4B1A28425300D4D520 /* Project object */;
470 | }
471 |
--------------------------------------------------------------------------------
/PhotoTweaks/PhotoTweaks/PhotoTweakView.m:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoView.m
3 | // PhotoTweaks
4 | //
5 | // Created by Tu You on 14/12/2.
6 | // Copyright (c) 2014年 Tu You. All rights reserved.
7 | //
8 |
9 | #import "PhotoTweakView.h"
10 | #import "UIColor+Tweak.h"
11 | #import
12 |
13 | const CGFloat kMaxRotationAngle = 0.5;
14 | static const NSUInteger kCropLines = 2;
15 | static const NSUInteger kGridLines = 9;
16 |
17 | static const CGFloat kCropViewHotArea = 16;
18 | static const CGFloat kMinimumCropArea = 60;
19 | static const CGFloat kMaximumCanvasWidthRatio = 0.9;
20 | static const CGFloat kMaximumCanvasHeightRatio = 0.7;
21 | static const CGFloat kCanvasHeaderHeigth = 60;
22 | static const CGFloat kCropViewCornerLength = 22;
23 |
24 | static CGFloat distanceBetweenPoints(CGPoint point0, CGPoint point1)
25 | {
26 | return sqrt(pow(point1.x - point0.x, 2) + pow(point1.y - point0.y, 2));
27 | }
28 |
29 | //#define kInstruction
30 |
31 | @implementation PhotoContentView
32 |
33 | - (instancetype)initWithImage:(UIImage *)image
34 | {
35 | if (self = [super init]) {
36 | _image = image;
37 |
38 | self.frame = CGRectMake(0, 0, image.size.width, image.size.height);
39 |
40 | _imageView = [[UIImageView alloc] initWithFrame:self.bounds];
41 | _imageView.image = self.image;
42 | _imageView.userInteractionEnabled = YES;
43 | [self addSubview:_imageView];
44 | }
45 | return self;
46 | }
47 |
48 | - (void)layoutSubviews
49 | {
50 | [super layoutSubviews];
51 |
52 | self.imageView.frame = self.bounds;
53 | }
54 |
55 | @end
56 |
57 | @interface PhotoScrollView : UIScrollView
58 |
59 | @property (nonatomic, strong) PhotoContentView *photoContentView;
60 |
61 | @end
62 |
63 | @implementation PhotoScrollView
64 |
65 | - (void)setContentOffsetY:(CGFloat)offsetY
66 | {
67 | CGPoint contentOffset = self.contentOffset;
68 | contentOffset.y = offsetY;
69 | self.contentOffset = contentOffset;
70 | }
71 |
72 | - (void)setContentOffsetX:(CGFloat)offsetX
73 | {
74 | CGPoint contentOffset = self.contentOffset;
75 | contentOffset.x = offsetX;
76 | self.contentOffset = contentOffset;
77 | }
78 |
79 | - (CGFloat)zoomScaleToBound
80 | {
81 | CGFloat scaleW = self.bounds.size.width / self.photoContentView.bounds.size.width;
82 | CGFloat scaleH = self.bounds.size.height / self.photoContentView.bounds.size.height;
83 | CGFloat max = MAX(scaleW, scaleH);
84 |
85 | return max;
86 | }
87 |
88 | @end
89 |
90 | typedef NS_ENUM(NSInteger, CropCornerType) {
91 | CropCornerTypeUpperLeft,
92 | CropCornerTypeUpperRight,
93 | CropCornerTypeLowerRight,
94 | CropCornerTypeLowerLeft
95 | };
96 |
97 | @interface CropCornerView : UIView
98 |
99 | @end
100 |
101 | @implementation CropCornerView
102 |
103 | - (instancetype)initWithCornerType:(CropCornerType)type
104 | {
105 | if (self = [super init]) {
106 | self.frame = CGRectMake(0, 0, kCropViewCornerLength, kCropViewCornerLength);
107 | self.backgroundColor = [UIColor clearColor];
108 |
109 | CGFloat lineWidth = 2;
110 | UIView *horizontal = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kCropViewCornerLength, lineWidth)];
111 | horizontal.backgroundColor = [UIColor cropLineColor];
112 | [self addSubview:horizontal];
113 |
114 | UIView *vertical = [[UIView alloc] initWithFrame:CGRectMake(0, 0, lineWidth, kCropViewCornerLength)];
115 | vertical.backgroundColor = [UIColor cropLineColor];
116 | [self addSubview:vertical];
117 |
118 | if (type == CropCornerTypeUpperLeft) {
119 | horizontal.center = CGPointMake(kCropViewCornerLength / 2, lineWidth / 2);
120 | vertical.center = CGPointMake(lineWidth / 2, kCropViewCornerLength / 2);
121 | } else if (type == CropCornerTypeUpperRight) {
122 | horizontal.center = CGPointMake(kCropViewCornerLength / 2, lineWidth / 2);
123 | vertical.center = CGPointMake(kCropViewCornerLength - lineWidth / 2, kCropViewCornerLength / 2);
124 | } else if (type == CropCornerTypeLowerRight) {
125 | horizontal.center = CGPointMake(kCropViewCornerLength / 2, kCropViewCornerLength - lineWidth / 2);
126 | vertical.center = CGPointMake(kCropViewCornerLength - lineWidth / 2, kCropViewCornerLength / 2);
127 | } else if (type == CropCornerTypeLowerLeft) {
128 | horizontal.center = CGPointMake(kCropViewCornerLength / 2, kCropViewCornerLength - lineWidth / 2);
129 | vertical.center = CGPointMake(lineWidth / 2, kCropViewCornerLength / 2);
130 | }
131 | }
132 | return self;
133 | }
134 |
135 | @end
136 |
137 | @interface CropView ()
138 |
139 | @property (nonatomic, strong) CropCornerView *upperLeft;
140 | @property (nonatomic, strong) CropCornerView *upperRight;
141 | @property (nonatomic, strong) CropCornerView *lowerRight;
142 | @property (nonatomic, strong) CropCornerView *lowerLeft;
143 |
144 | @property (nonatomic, strong) NSMutableArray *horizontalCropLines;
145 | @property (nonatomic, strong) NSMutableArray *verticalCropLines;
146 |
147 | @property (nonatomic, strong) NSMutableArray *horizontalGridLines;
148 | @property (nonatomic, strong) NSMutableArray *verticalGridLines;
149 |
150 | @property (nonatomic, weak) id delegate;
151 |
152 | @property (nonatomic, assign) BOOL cropLinesDismissed;
153 | @property (nonatomic, assign) BOOL gridLinesDismissed;
154 |
155 | @end
156 |
157 | @implementation CropView
158 |
159 | - (instancetype)initWithFrame:(CGRect)frame
160 | {
161 | if (self = [super initWithFrame:frame]) {
162 | self.layer.borderColor = [UIColor cropLineColor].CGColor;
163 | self.layer.borderWidth = 1;
164 |
165 | _horizontalCropLines = [NSMutableArray array];
166 | for (int i = 0; i < kCropLines; i++) {
167 | UIView *line = [UIView new];
168 | line.backgroundColor = [UIColor cropLineColor];
169 | [_horizontalCropLines addObject:line];
170 | [self addSubview:line];
171 | }
172 |
173 | _verticalCropLines = [NSMutableArray array];
174 | for (int i = 0; i < kCropLines; i++) {
175 | UIView *line = [UIView new];
176 | line.backgroundColor = [UIColor cropLineColor];
177 | [_verticalCropLines addObject:line];
178 | [self addSubview:line];
179 | }
180 |
181 | _horizontalGridLines = [NSMutableArray array];
182 | for (int i = 0; i < kGridLines; i++) {
183 | UIView *line = [UIView new];
184 | line.backgroundColor = [UIColor gridLineColor];
185 | [_horizontalGridLines addObject:line];
186 | [self addSubview:line];
187 | }
188 |
189 | _verticalGridLines = [NSMutableArray array];
190 | for (int i = 0; i < kGridLines; i++) {
191 | UIView *line = [UIView new];
192 | line.backgroundColor = [UIColor gridLineColor];
193 | [_verticalGridLines addObject:line];
194 | [self addSubview:line];
195 | }
196 |
197 | _cropLinesDismissed = YES;
198 | _gridLinesDismissed = YES;
199 |
200 | _upperLeft = [[CropCornerView alloc] initWithCornerType:CropCornerTypeUpperLeft];
201 | _upperLeft.center = CGPointMake(kCropViewCornerLength / 2, kCropViewCornerLength / 2);
202 | _upperLeft.autoresizingMask = UIViewAutoresizingNone;
203 | [self addSubview:_upperLeft];
204 |
205 | _upperRight = [[CropCornerView alloc] initWithCornerType:CropCornerTypeUpperRight];
206 | _upperRight.center = CGPointMake(self.frame.size.width - kCropViewCornerLength / 2, kCropViewCornerLength / 2);
207 | _upperRight.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
208 | [self addSubview:_upperRight];
209 |
210 | _lowerRight = [[CropCornerView alloc] initWithCornerType:CropCornerTypeLowerRight];
211 | _lowerRight.center = CGPointMake(self.frame.size.width - kCropViewCornerLength / 2, self.frame.size.height - kCropViewCornerLength / 2);
212 | _lowerRight.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin;
213 | [self addSubview:_lowerRight];
214 |
215 | _lowerLeft = [[CropCornerView alloc] initWithCornerType:CropCornerTypeLowerLeft];
216 | _lowerLeft.center = CGPointMake(kCropViewCornerLength / 2, self.frame.size.height - kCropViewCornerLength / 2);
217 | _lowerLeft.autoresizingMask = UIViewAutoresizingFlexibleTopMargin;
218 | [self addSubview:_lowerLeft];
219 | }
220 | return self;
221 | }
222 |
223 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
224 | {
225 | if ([touches count] == 1) {
226 | [self updateCropLines:NO];
227 | }
228 | }
229 |
230 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
231 | {
232 | if ([touches count] == 1) {
233 | CGPoint location = [[touches anyObject] locationInView:self];
234 | CGRect frame = self.frame;
235 |
236 | CGPoint p0 = CGPointMake(0, 0);
237 | CGPoint p1 = CGPointMake(self.frame.size.width, 0);
238 | CGPoint p2 = CGPointMake(0, self.frame.size.height);
239 | CGPoint p3 = CGPointMake(self.frame.size.width, self.frame.size.height);
240 |
241 | BOOL canChangeWidth = frame.size.width > kMinimumCropArea;
242 | BOOL canChangeHeight = frame.size.height > kMinimumCropArea;
243 |
244 | if (distanceBetweenPoints(location, p0) < kCropViewHotArea) {
245 | if (canChangeWidth) {
246 | frame.origin.x += location.x;
247 | frame.size.width -= location.x;
248 | }
249 | if (canChangeHeight) {
250 | frame.origin.y += location.y;
251 | frame.size.height -= location.y;
252 | }
253 | } else if (distanceBetweenPoints(location, p1) < kCropViewHotArea) {
254 | if (canChangeWidth) {
255 | frame.size.width = location.x;
256 | }
257 | if (canChangeHeight) {
258 | frame.origin.y += location.y;
259 | frame.size.height -= location.y;
260 | }
261 | } else if (distanceBetweenPoints(location, p2) < kCropViewHotArea) {
262 | if (canChangeWidth) {
263 | frame.origin.x += location.x;
264 | frame.size.width -= location.x;
265 | }
266 | if (canChangeHeight) {
267 | frame.size.height = location.y;
268 | }
269 | } else if (distanceBetweenPoints(location, p3) < kCropViewHotArea) {
270 | if (canChangeWidth) {
271 | frame.size.width = location.x;
272 | }
273 | if (canChangeHeight) {
274 | frame.size.height = location.y;
275 | }
276 | } else if (fabs(location.x - p0.x) < kCropViewHotArea) {
277 | if (canChangeWidth) {
278 | frame.origin.x += location.x;
279 | frame.size.width -= location.x;
280 | }
281 | } else if (fabs(location.x - p1.x) < kCropViewHotArea) {
282 | if (canChangeWidth) {
283 | frame.size.width = location.x;
284 | }
285 | } else if (fabs(location.y - p0.y) < kCropViewHotArea) {
286 | if (canChangeHeight) {
287 | frame.origin.y += location.y;
288 | frame.size.height -= location.y;
289 | }
290 | } else if (fabs(location.y - p2.y) < kCropViewHotArea) {
291 | if (canChangeHeight) {
292 | frame.size.height = location.y;
293 | }
294 | }
295 |
296 | self.frame = frame;
297 |
298 | // update crop lines
299 | [self updateCropLines:NO];
300 |
301 | if ([self.delegate respondsToSelector:@selector(cropMoved:)]) {
302 | [self.delegate cropMoved:self];
303 | }
304 | }
305 | }
306 |
307 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
308 | {
309 | if ([self.delegate respondsToSelector:@selector(cropEnded:)]) {
310 | [self.delegate cropEnded:self];
311 | }
312 | }
313 |
314 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
315 | {
316 |
317 | }
318 |
319 | - (void)updateCropLines:(BOOL)animate
320 | {
321 | // show crop lines
322 | if (self.cropLinesDismissed) {
323 | [self showCropLines];
324 | }
325 |
326 | void (^animationBlock)(void) = ^(void) {
327 | [self updateLines:self.horizontalCropLines horizontal:YES];
328 | [self updateLines:self.verticalCropLines horizontal:NO];
329 | };
330 |
331 | if (animate) {
332 | [UIView animateWithDuration:0.25 animations:animationBlock];
333 | } else {
334 | animationBlock();
335 | }
336 | }
337 |
338 | - (void)updateGridLines:(BOOL)animate
339 | {
340 | // show grid lines
341 | if (self.gridLinesDismissed) {
342 | [self showGridLines];
343 | }
344 |
345 | void (^animationBlock)(void) = ^(void) {
346 |
347 | [self updateLines:self.horizontalGridLines horizontal:YES];
348 | [self updateLines:self.verticalGridLines horizontal:NO];
349 | };
350 |
351 | if (animate) {
352 | [UIView animateWithDuration:0.25 animations:animationBlock];
353 | } else {
354 | animationBlock();
355 | }
356 | }
357 |
358 | - (void)updateLines:(NSArray *)lines horizontal:(BOOL)horizontal
359 | {
360 | [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
361 | UIView *line = (UIView *)obj;
362 | if (horizontal) {
363 | line.frame = CGRectMake(0,
364 | (self.frame.size.height / (lines.count + 1)) * (idx + 1),
365 | self.frame.size.width,
366 | 1 / [UIScreen mainScreen].scale);
367 | } else {
368 | line.frame = CGRectMake((self.frame.size.width / (lines.count + 1)) * (idx + 1),
369 | 0,
370 | 1 / [UIScreen mainScreen].scale,
371 | self.frame.size.height);
372 | }
373 | }];
374 | }
375 |
376 | - (void)dismissCropLines
377 | {
378 | [UIView animateWithDuration:0.2 animations:^{
379 | [self dismissLines:self.horizontalCropLines];
380 | [self dismissLines:self.verticalCropLines];
381 | } completion:^(BOOL finished) {
382 | self.cropLinesDismissed = YES;
383 | }];
384 | }
385 |
386 | - (void)dismissGridLines
387 | {
388 | [UIView animateWithDuration:0.2 animations:^{
389 | [self dismissLines:self.horizontalGridLines];
390 | [self dismissLines:self.verticalGridLines];
391 | } completion:^(BOOL finished) {
392 | self.gridLinesDismissed = YES;
393 | }];
394 | }
395 |
396 | - (void)dismissLines:(NSArray *)lines
397 | {
398 | [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
399 | ((UIView *)obj).alpha = 0.0f;
400 | }];
401 | }
402 |
403 | - (void)showCropLines
404 | {
405 | self.cropLinesDismissed = NO;
406 | [UIView animateWithDuration:0.2 animations:^{
407 | [self showLines:self.horizontalCropLines];
408 | [self showLines:self.verticalCropLines];
409 | }];
410 | }
411 |
412 | - (void)showGridLines
413 | {
414 | self.gridLinesDismissed = NO;
415 | [UIView animateWithDuration:0.2 animations:^{
416 | [self showLines:self.horizontalGridLines];
417 | [self showLines:self.verticalGridLines];
418 | }];
419 | }
420 |
421 | - (void)showLines:(NSArray *)lines
422 | {
423 | [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
424 | ((UIView *)obj).alpha = 1.0f;
425 | }];
426 | }
427 |
428 | @end
429 |
430 | @interface PhotoTweakView ()
431 |
432 | @property (nonatomic, strong) PhotoScrollView *scrollView;
433 | @property (nonatomic, strong) CropView *cropView;
434 |
435 | @property (nonatomic, strong) UIImage *image;
436 | @property (nonatomic, strong) UISlider *slider;
437 | @property (nonatomic, strong) UIButton *resetBtn;
438 | @property (nonatomic, assign) CGSize originalSize;
439 | @property (nonatomic, assign) CGFloat angle;
440 |
441 | @property (nonatomic, assign) BOOL manualZoomed;
442 |
443 | // masks
444 | @property (nonatomic, strong) UIView *topMask;
445 | @property (nonatomic, strong) UIView *leftMask;
446 | @property (nonatomic, strong) UIView *bottomMask;
447 | @property (nonatomic, strong) UIView *rightMask;
448 |
449 | // constants
450 | @property (nonatomic, assign) CGSize maximumCanvasSize;
451 | @property (nonatomic, assign) CGFloat centerY;
452 | @property (nonatomic, assign) CGPoint originalPoint;
453 | @property (nonatomic, assign) CGFloat maxRotationAngle;
454 |
455 | @end
456 |
457 | @implementation PhotoTweakView
458 |
459 | - (instancetype)initWithFrame:(CGRect)frame
460 | image:(UIImage *)image
461 | maxRotationAngle:(CGFloat)maxRotationAngle
462 | {
463 | if (self = [super init]) {
464 |
465 | self.frame = frame;
466 |
467 | _image = image;
468 | _maxRotationAngle = maxRotationAngle;
469 |
470 | // scale the image
471 | _maximumCanvasSize = CGSizeMake(kMaximumCanvasWidthRatio * self.frame.size.width,
472 | kMaximumCanvasHeightRatio * self.frame.size.height - kCanvasHeaderHeigth);
473 |
474 | CGFloat scaleX = image.size.width / self.maximumCanvasSize.width;
475 | CGFloat scaleY = image.size.height / self.maximumCanvasSize.height;
476 | CGFloat scale = MAX(scaleX, scaleY);
477 | CGRect bounds = CGRectMake(0, 0, image.size.width / scale, image.size.height / scale);
478 | _originalSize = bounds.size;
479 |
480 | _centerY = self.maximumCanvasSize.height / 2 + kCanvasHeaderHeigth;
481 |
482 | _scrollView = [[PhotoScrollView alloc] initWithFrame:bounds];
483 | _scrollView.center = CGPointMake(CGRectGetWidth(self.frame) / 2, self.centerY);
484 | _scrollView.bounces = YES;
485 | _scrollView.layer.anchorPoint = CGPointMake(0.5, 0.5);
486 | _scrollView.alwaysBounceVertical = YES;
487 | _scrollView.alwaysBounceHorizontal = YES;
488 | _scrollView.delegate = self;
489 | _scrollView.minimumZoomScale = 1;
490 | _scrollView.maximumZoomScale = 10;
491 | _scrollView.showsVerticalScrollIndicator = NO;
492 | _scrollView.showsHorizontalScrollIndicator = NO;
493 | _scrollView.clipsToBounds = NO;
494 | _scrollView.contentSize = CGSizeMake(self.scrollView.bounds.size.width, self.scrollView.bounds.size.height);
495 | [self addSubview:_scrollView];
496 | #ifdef kInstruction
497 | _scrollView.layer.borderColor = [UIColor redColor].CGColor;
498 | _scrollView.layer.borderWidth = 1;
499 | _scrollView.showsVerticalScrollIndicator = YES;
500 | _scrollView.showsHorizontalScrollIndicator = YES;
501 | #endif
502 |
503 | _photoContentView = [[PhotoContentView alloc] initWithImage:image];
504 | _photoContentView.frame = self.scrollView.bounds;
505 | _photoContentView.backgroundColor = [UIColor clearColor];
506 | _photoContentView.userInteractionEnabled = YES;
507 | _scrollView.photoContentView = self.photoContentView;
508 | [self.scrollView addSubview:_photoContentView];
509 |
510 | _cropView = [[CropView alloc] initWithFrame:self.scrollView.frame];
511 | _cropView.center = self.scrollView.center;
512 | _cropView.delegate = self;
513 | [self addSubview:_cropView];
514 |
515 | UIColor *maskColor = [UIColor maskColor];
516 | _topMask = [UIView new];
517 | _topMask.backgroundColor = maskColor;
518 | [self addSubview:_topMask];
519 | _leftMask = [UIView new];
520 | _leftMask.backgroundColor = maskColor;
521 | [self addSubview:_leftMask];
522 | _bottomMask = [UIView new];
523 | _bottomMask.backgroundColor = maskColor;
524 | [self addSubview:_bottomMask];
525 | _rightMask = [UIView new];
526 | _rightMask.backgroundColor = maskColor;
527 | [self addSubview:_rightMask];
528 | [self updateMasks:NO];
529 |
530 | _slider = [[UISlider alloc] initWithFrame:CGRectMake(0, 0, 260, 20)];
531 | _slider.center = CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) - 135);
532 | _slider.minimumValue = -self.maxRotationAngle;
533 | _slider.maximumValue = self.maxRotationAngle;
534 | [_slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged];
535 | [_slider addTarget:self action:@selector(sliderTouchEnded:) forControlEvents:UIControlEventTouchUpInside];
536 | [self addSubview:_slider];
537 |
538 | _resetBtn = [UIButton buttonWithType:UIButtonTypeCustom];
539 | _resetBtn.frame = CGRectMake(0, 0, 60, 20);
540 | _resetBtn.center = CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) - 95);
541 | _resetBtn.titleLabel.font = [UIFont systemFontOfSize:14];
542 | [_resetBtn setTitleColor:[UIColor resetButtonColor] forState:UIControlStateNormal];
543 | [_resetBtn setTitleColor:[UIColor resetButtonHighlightedColor] forState:UIControlStateHighlighted];
544 | [_resetBtn setTitle:NSLocalizedStringFromTable(@"RESET", @"PhotoTweaks", nil) forState:UIControlStateNormal];
545 | [_resetBtn addTarget:self action:@selector(resetBtnTapped:) forControlEvents:UIControlEventTouchUpInside];
546 | [self addSubview:_resetBtn];
547 |
548 | _originalPoint = [self convertPoint:self.scrollView.center toView:self];
549 | }
550 | return self;
551 | }
552 |
553 | - (instancetype)initWithFrame:(CGRect)frame image:(UIImage *)image
554 | {
555 | return [self initWithFrame:frame image:image maxRotationAngle:kMaxRotationAngle];
556 | }
557 |
558 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
559 | {
560 | if (CGRectContainsPoint(self.slider.frame, point)) {
561 | return self.slider;
562 | } else if (CGRectContainsPoint(self.resetBtn.frame, point)) {
563 | return self.resetBtn;
564 | } else if (CGRectContainsPoint(CGRectInset(self.cropView.frame, -kCropViewHotArea, -kCropViewHotArea), point) && !CGRectContainsPoint(CGRectInset(self.cropView.frame, kCropViewHotArea, kCropViewHotArea), point)) {
565 | return self.cropView;
566 | }
567 | return self.scrollView;
568 | }
569 |
570 | - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
571 | {
572 | return self.photoContentView;
573 | }
574 |
575 | - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale
576 | {
577 | self.manualZoomed = YES;
578 | }
579 |
580 | #pragma mark - Crop View Delegate
581 |
582 | - (void)cropMoved:(CropView *)cropView
583 | {
584 | [self updateMasks:NO];
585 | }
586 |
587 | - (void)cropEnded:(CropView *)cropView
588 | {
589 | CGFloat scaleX = self.originalSize.width / cropView.bounds.size.width;
590 | CGFloat scaleY = self.originalSize.height / cropView.bounds.size.height;
591 | CGFloat scale = MIN(scaleX, scaleY);
592 |
593 | // calculate the new bounds of crop view
594 | CGRect newCropBounds = CGRectMake(0, 0, scale * cropView.frame.size.width, scale * cropView.frame.size.height);
595 |
596 | // calculate the new bounds of scroll view
597 | CGFloat width = fabs(cos(self.angle)) * newCropBounds.size.width + fabs(sin(self.angle)) * newCropBounds.size.height;
598 | CGFloat height = fabs(sin(self.angle)) * newCropBounds.size.width + fabs(cos(self.angle)) * newCropBounds.size.height;
599 |
600 | // calculate the zoom area of scroll view
601 | CGRect scaleFrame = cropView.frame;
602 | if (scaleFrame.size.width >= self.scrollView.bounds.size.width) {
603 | scaleFrame.size.width = self.scrollView.bounds.size.width - 1;
604 | }
605 | if (scaleFrame.size.height >= self.scrollView.bounds.size.height) {
606 | scaleFrame.size.height = self.scrollView.bounds.size.height - 1;
607 | }
608 |
609 | CGPoint contentOffset = self.scrollView.contentOffset;
610 | CGPoint contentOffsetCenter = CGPointMake(contentOffset.x + self.scrollView.bounds.size.width / 2, contentOffset.y + self.scrollView.bounds.size.height / 2);
611 | CGRect bounds = self.scrollView.bounds;
612 | bounds.size.width = width;
613 | bounds.size.height = height;
614 | self.scrollView.bounds = CGRectMake(0, 0, width, height);
615 | CGPoint newContentOffset = CGPointMake(contentOffsetCenter.x - self.scrollView.bounds.size.width / 2, contentOffsetCenter.y - self.scrollView.bounds.size.height / 2);
616 | self.scrollView.contentOffset = newContentOffset;
617 |
618 | [UIView animateWithDuration:0.25 animations:^{
619 | // animate crop view
620 | cropView.bounds = CGRectMake(0, 0, newCropBounds.size.width, newCropBounds.size.height);
621 | cropView.center = CGPointMake(CGRectGetWidth(self.frame) / 2, self.centerY);
622 |
623 | // zoom the specified area of scroll view
624 | CGRect zoomRect = [self convertRect:scaleFrame toView:self.scrollView.photoContentView];
625 | [self.scrollView zoomToRect:zoomRect animated:NO];
626 | }];
627 |
628 | self.manualZoomed = YES;
629 |
630 | // update masks
631 | [self updateMasks:YES];
632 |
633 | [self.cropView dismissCropLines];
634 |
635 | CGFloat scaleH = self.scrollView.bounds.size.height / self.scrollView.contentSize.height;
636 | CGFloat scaleW = self.scrollView.bounds.size.width / self.scrollView.contentSize.width;
637 | __block CGFloat scaleM = MAX(scaleH, scaleW);
638 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
639 | if (scaleM > 1) {
640 | scaleM = scaleM * self.scrollView.zoomScale;
641 | [self.scrollView setZoomScale:scaleM animated:NO];
642 | }
643 | [UIView animateWithDuration:0.2 animations:^{
644 | [self checkScrollViewContentOffset];
645 | }];
646 | });
647 | }
648 |
649 | - (void)updateMasks:(BOOL)animate
650 | {
651 | void (^animationBlock)(void) = ^(void) {
652 | self.topMask.frame = CGRectMake(0, 0, self.cropView.frame.origin.x + self.cropView.frame.size.width, self.cropView.frame.origin.y);
653 | self.leftMask.frame = CGRectMake(0, self.cropView.frame.origin.y, self.cropView.frame.origin.x, self.frame.size.height - self.cropView.frame.origin.y);
654 | self.bottomMask.frame = CGRectMake(self.cropView.frame.origin.x, self.cropView.frame.origin.y + self.cropView.frame.size.height, self.frame.size.width - self.cropView.frame.origin.x, self.frame.size.height - (self.cropView.frame.origin.y + self.cropView.frame.size.height));
655 | self.rightMask.frame = CGRectMake(self.cropView.frame.origin.x + self.cropView.frame.size.width, 0, self.frame.size.width - (self.cropView.frame.origin.x + self.cropView.frame.size.width), self.cropView.frame.origin.y + self.cropView.frame.size.height);
656 | };
657 |
658 | if (animate) {
659 | [UIView animateWithDuration:0.25 animations:animationBlock];
660 | } else {
661 | animationBlock();
662 | }
663 | }
664 |
665 | - (void)checkScrollViewContentOffset
666 | {
667 | self.scrollView.contentOffsetX = MAX(self.scrollView.contentOffset.x, 0);
668 | self.scrollView.contentOffsetY = MAX(self.scrollView.contentOffset.y, 0);
669 |
670 | if (self.scrollView.contentSize.height - self.scrollView.contentOffset.y <= self.scrollView.bounds.size.height) {
671 | self.scrollView.contentOffsetY = self.scrollView.contentSize.height - self.scrollView.bounds.size.height;
672 | }
673 |
674 | if (self.scrollView.contentSize.width - self.scrollView.contentOffset.x <= self.scrollView.bounds.size.width) {
675 | self.scrollView.contentOffsetX = self.scrollView.contentSize.width - self.scrollView.bounds.size.width;
676 | }
677 | }
678 |
679 | - (void)sliderValueChanged:(id)sender
680 | {
681 | // update masks
682 | [self updateMasks:NO];
683 |
684 | // update grids
685 | [self.cropView updateGridLines:NO];
686 |
687 | // rotate scroll view
688 | self.angle = self.slider.value;
689 | self.scrollView.transform = CGAffineTransformMakeRotation(self.angle);
690 |
691 | // position scroll view
692 | CGFloat width = fabs(cos(self.angle)) * self.cropView.frame.size.width + fabs(sin(self.angle)) * self.cropView.frame.size.height;
693 | CGFloat height = fabs(sin(self.angle)) * self.cropView.frame.size.width + fabs(cos(self.angle)) * self.cropView.frame.size.height;
694 | CGPoint center = self.scrollView.center;
695 |
696 | CGPoint contentOffset = self.scrollView.contentOffset;
697 | CGPoint contentOffsetCenter = CGPointMake(contentOffset.x + self.scrollView.bounds.size.width / 2, contentOffset.y + self.scrollView.bounds.size.height / 2);
698 | self.scrollView.bounds = CGRectMake(0, 0, width, height);
699 | CGPoint newContentOffset = CGPointMake(contentOffsetCenter.x - self.scrollView.bounds.size.width / 2, contentOffsetCenter.y - self.scrollView.bounds.size.height / 2);
700 | self.scrollView.contentOffset = newContentOffset;
701 | self.scrollView.center = center;
702 |
703 | // scale scroll view
704 | BOOL shouldScale = self.scrollView.contentSize.width / self.scrollView.bounds.size.width <= 1.0 || self.scrollView.contentSize.height / self.scrollView.bounds.size.height <= 1.0;
705 | if (!self.manualZoomed || shouldScale) {
706 | [self.scrollView setZoomScale:[self.scrollView zoomScaleToBound] animated:NO];
707 | self.scrollView.minimumZoomScale = [self.scrollView zoomScaleToBound];
708 |
709 | self.manualZoomed = NO;
710 | }
711 |
712 | [self checkScrollViewContentOffset];
713 | }
714 |
715 | - (void)sliderTouchEnded:(id)sender
716 | {
717 | [self.cropView dismissGridLines];
718 | }
719 |
720 | - (void)resetBtnTapped:(id)sender
721 | {
722 | [UIView animateWithDuration:0.25 animations:^{
723 | self.angle = 0;
724 |
725 | self.scrollView.transform = CGAffineTransformIdentity;
726 | self.scrollView.center = CGPointMake(CGRectGetWidth(self.frame) / 2, self.centerY);
727 | self.scrollView.bounds = CGRectMake(0, 0, self.originalSize.width, self.originalSize.height);
728 | self.scrollView.minimumZoomScale = 1;
729 | [self.scrollView setZoomScale:1 animated:NO];
730 |
731 | self.cropView.center = self.scrollView.center;
732 | [self updateMasks:NO];
733 |
734 | [self.slider setValue:0 animated:YES];
735 | }];
736 | }
737 |
738 | - (CGPoint)photoTranslation
739 | {
740 | CGRect rect = [self.photoContentView convertRect:self.photoContentView.bounds toView:self];
741 | CGPoint point = CGPointMake(rect.origin.x + rect.size.width / 2, rect.origin.y + rect.size.height / 2);
742 | CGPoint zeroPoint = CGPointMake(CGRectGetWidth(self.frame) / 2, self.centerY);
743 | return CGPointMake(point.x - zeroPoint.x, point.y - zeroPoint.y);
744 | }
745 |
746 | @end
747 |
--------------------------------------------------------------------------------