├── TouchVisualizer.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcuserdata
│ │ └── zacharygibson.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
├── xcuserdata
│ └── zacharygibson.xcuserdatad
│ │ └── xcschemes
│ │ ├── xcschememanagement.plist
│ │ └── COSTouchVisualizerWindow.xcscheme
└── project.pbxproj
├── package.json
├── TouchVisualizer.h
├── README.md
└── TouchVisualizer.m
/TouchVisualizer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TouchVisualizer.xcodeproj/project.xcworkspace/xcuserdata/zacharygibson.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zachgibson/react-native-touch-visualizer/HEAD/TouchVisualizer.xcodeproj/project.xcworkspace/xcuserdata/zacharygibson.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-touch-visualizer",
3 | "version": "0.0.1",
4 | "keywords": [
5 | "react-native",
6 | "react",
7 | "native",
8 | "touch",
9 | "react-component",
10 | "react-native-component",
11 | "touch-visualizer",
12 | "mobile"
13 | ],
14 | "description": "Visualize touches and drags in React Native apps.",
15 | "main": "TouchVisualizer.xcodeproj",
16 | "scripts": {
17 | "test": "echo \"Error: no test specified\" && exit 1"
18 | },
19 | "author": "Zach Gibson (http://zachgibsondesign.com/)",
20 | "license": "MIT"
21 | }
22 |
--------------------------------------------------------------------------------
/TouchVisualizer.xcodeproj/xcuserdata/zacharygibson.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | TouchVisualizer.xcscheme
8 |
9 | orderHint
10 | 20
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | 58B511DA1A9E6C8500147676
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/TouchVisualizer.h:
--------------------------------------------------------------------------------
1 | #import
2 | #include
3 |
4 | @protocol TouchVisualizerDelegate;
5 |
6 | @interface TouchVisualizer : UIWindow
7 |
8 | @property (nonatomic, weak) id touchVisualizerWindowDelegate;
9 |
10 | // Touch effects
11 | @property (nonatomic) UIImage *touchImage;
12 | @property (nonatomic) CGFloat touchAlpha;
13 | @property (nonatomic) NSTimeInterval fadeDuration;
14 | @property (nonatomic) UIColor *strokeColor;
15 | @property (nonatomic) UIColor *fillColor;
16 |
17 | // Ripple Effects
18 | @property (nonatomic) UIImage *rippleImage;
19 | @property (nonatomic) CGFloat rippleAlpha;
20 | @property (nonatomic) NSTimeInterval rippleFadeDuration;
21 | @property (nonatomic) UIColor *rippleStrokeColor;
22 | @property (nonatomic) UIColor *rippleFillColor;
23 |
24 | @end
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Touch Visualizer
2 | Customizable touch visualizer for app demos.
3 | Supports multiple presses and works on native iOS components; e.g. Action Sheets.
4 |
5 | 
6 |
7 | ## Install via npm
8 | ```bash
9 | $ npm install react-native-touch-visualizer --save
10 | ```
11 |
12 | ## Link it to your project
13 | ```bash
14 | $ react-native link react-native-touch-visualizer
15 | ```
16 |
17 | ## Edit AppDelegate.m
18 | ```objc
19 | #import "TouchVisualizer.h"
20 | ```
21 |
22 | Change
23 | ```objc
24 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
25 | ```
26 | to
27 | ```objc
28 | self.window = [[TouchVisualizer alloc] initWithFrame:[UIScreen mainScreen].bounds];
29 | ```
30 |
31 | ## Copyright
32 | Copyright (c) 2017 Zachary Gibson Licensed under the MIT license.
33 |
--------------------------------------------------------------------------------
/TouchVisualizer.xcodeproj/xcuserdata/zacharygibson.xcuserdatad/xcschemes/COSTouchVisualizerWindow.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/TouchVisualizer.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 13BE3DEE1AC21097009241FE /* TouchVisualizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* TouchVisualizer.m */; };
11 | /* End PBXBuildFile section */
12 |
13 | /* Begin PBXCopyFilesBuildPhase section */
14 | 58B511D91A9E6C8500147676 /* CopyFiles */ = {
15 | isa = PBXCopyFilesBuildPhase;
16 | buildActionMask = 2147483647;
17 | dstPath = "include/$(PRODUCT_NAME)";
18 | dstSubfolderSpec = 16;
19 | files = (
20 | );
21 | runOnlyForDeploymentPostprocessing = 0;
22 | };
23 | /* End PBXCopyFilesBuildPhase section */
24 |
25 | /* Begin PBXFileReference section */
26 | 134814201AA4EA6300B7C361 /* libTouchVisualizer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTouchVisualizer.a; sourceTree = BUILT_PRODUCTS_DIR; };
27 | 13BE3DEC1AC21097009241FE /* TouchVisualizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TouchVisualizer.h; sourceTree = ""; };
28 | 13BE3DED1AC21097009241FE /* TouchVisualizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TouchVisualizer.m; sourceTree = ""; };
29 | /* End PBXFileReference section */
30 |
31 | /* Begin PBXFrameworksBuildPhase section */
32 | 58B511D81A9E6C8500147676 /* Frameworks */ = {
33 | isa = PBXFrameworksBuildPhase;
34 | buildActionMask = 2147483647;
35 | files = (
36 | );
37 | runOnlyForDeploymentPostprocessing = 0;
38 | };
39 | /* End PBXFrameworksBuildPhase section */
40 |
41 | /* Begin PBXGroup section */
42 | 134814211AA4EA7D00B7C361 /* Products */ = {
43 | isa = PBXGroup;
44 | children = (
45 | 134814201AA4EA6300B7C361 /* libTouchVisualizer.a */,
46 | );
47 | name = Products;
48 | sourceTree = "";
49 | };
50 | 58B511D21A9E6C8500147676 = {
51 | isa = PBXGroup;
52 | children = (
53 | 13BE3DEC1AC21097009241FE /* TouchVisualizer.h */,
54 | 13BE3DED1AC21097009241FE /* TouchVisualizer.m */,
55 | 134814211AA4EA7D00B7C361 /* Products */,
56 | );
57 | sourceTree = "";
58 | };
59 | /* End PBXGroup section */
60 |
61 | /* Begin PBXNativeTarget section */
62 | 58B511DA1A9E6C8500147676 /* TouchVisualizer */ = {
63 | isa = PBXNativeTarget;
64 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "TouchVisualizer" */;
65 | buildPhases = (
66 | 58B511D71A9E6C8500147676 /* Sources */,
67 | 58B511D81A9E6C8500147676 /* Frameworks */,
68 | 58B511D91A9E6C8500147676 /* CopyFiles */,
69 | );
70 | buildRules = (
71 | );
72 | dependencies = (
73 | );
74 | name = TouchVisualizer;
75 | productName = RCTDataManager;
76 | productReference = 134814201AA4EA6300B7C361 /* libTouchVisualizer.a */;
77 | productType = "com.apple.product-type.library.static";
78 | };
79 | /* End PBXNativeTarget section */
80 |
81 | /* Begin PBXProject section */
82 | 58B511D31A9E6C8500147676 /* Project object */ = {
83 | isa = PBXProject;
84 | attributes = {
85 | LastUpgradeCheck = 0610;
86 | ORGANIZATIONNAME = Facebook;
87 | TargetAttributes = {
88 | 58B511DA1A9E6C8500147676 = {
89 | CreatedOnToolsVersion = 6.1.1;
90 | };
91 | };
92 | };
93 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "TouchVisualizer" */;
94 | compatibilityVersion = "Xcode 3.2";
95 | developmentRegion = English;
96 | hasScannedForEncodings = 0;
97 | knownRegions = (
98 | en,
99 | );
100 | mainGroup = 58B511D21A9E6C8500147676;
101 | productRefGroup = 58B511D21A9E6C8500147676;
102 | projectDirPath = "";
103 | projectRoot = "";
104 | targets = (
105 | 58B511DA1A9E6C8500147676 /* TouchVisualizer */,
106 | );
107 | };
108 | /* End PBXProject section */
109 |
110 | /* Begin PBXSourcesBuildPhase section */
111 | 58B511D71A9E6C8500147676 /* Sources */ = {
112 | isa = PBXSourcesBuildPhase;
113 | buildActionMask = 2147483647;
114 | files = (
115 | 13BE3DEE1AC21097009241FE /* TouchVisualizer.m in Sources */,
116 | );
117 | runOnlyForDeploymentPostprocessing = 0;
118 | };
119 | /* End PBXSourcesBuildPhase section */
120 |
121 | /* Begin XCBuildConfiguration section */
122 | 58B511ED1A9E6C8500147676 /* Debug */ = {
123 | isa = XCBuildConfiguration;
124 | buildSettings = {
125 | ALWAYS_SEARCH_USER_PATHS = NO;
126 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
127 | CLANG_CXX_LIBRARY = "libc++";
128 | CLANG_ENABLE_MODULES = YES;
129 | CLANG_ENABLE_OBJC_ARC = YES;
130 | CLANG_WARN_BOOL_CONVERSION = YES;
131 | CLANG_WARN_CONSTANT_CONVERSION = YES;
132 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
133 | CLANG_WARN_EMPTY_BODY = YES;
134 | CLANG_WARN_ENUM_CONVERSION = YES;
135 | CLANG_WARN_INT_CONVERSION = YES;
136 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
137 | CLANG_WARN_UNREACHABLE_CODE = YES;
138 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
139 | COPY_PHASE_STRIP = NO;
140 | ENABLE_STRICT_OBJC_MSGSEND = YES;
141 | GCC_C_LANGUAGE_STANDARD = gnu99;
142 | GCC_DYNAMIC_NO_PIC = NO;
143 | GCC_OPTIMIZATION_LEVEL = 0;
144 | GCC_PREPROCESSOR_DEFINITIONS = (
145 | "DEBUG=1",
146 | "$(inherited)",
147 | );
148 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
149 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
150 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
151 | GCC_WARN_UNDECLARED_SELECTOR = YES;
152 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
153 | GCC_WARN_UNUSED_FUNCTION = YES;
154 | GCC_WARN_UNUSED_VARIABLE = YES;
155 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
156 | MTL_ENABLE_DEBUG_INFO = YES;
157 | ONLY_ACTIVE_ARCH = YES;
158 | SDKROOT = iphoneos;
159 | };
160 | name = Debug;
161 | };
162 | 58B511EE1A9E6C8500147676 /* Release */ = {
163 | isa = XCBuildConfiguration;
164 | buildSettings = {
165 | ALWAYS_SEARCH_USER_PATHS = NO;
166 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
167 | CLANG_CXX_LIBRARY = "libc++";
168 | CLANG_ENABLE_MODULES = YES;
169 | CLANG_ENABLE_OBJC_ARC = YES;
170 | CLANG_WARN_BOOL_CONVERSION = YES;
171 | CLANG_WARN_CONSTANT_CONVERSION = YES;
172 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
173 | CLANG_WARN_EMPTY_BODY = YES;
174 | CLANG_WARN_ENUM_CONVERSION = YES;
175 | CLANG_WARN_INT_CONVERSION = YES;
176 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
177 | CLANG_WARN_UNREACHABLE_CODE = YES;
178 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
179 | COPY_PHASE_STRIP = YES;
180 | ENABLE_NS_ASSERTIONS = NO;
181 | ENABLE_STRICT_OBJC_MSGSEND = YES;
182 | GCC_C_LANGUAGE_STANDARD = gnu99;
183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
184 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
185 | GCC_WARN_UNDECLARED_SELECTOR = YES;
186 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
187 | GCC_WARN_UNUSED_FUNCTION = YES;
188 | GCC_WARN_UNUSED_VARIABLE = YES;
189 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
190 | MTL_ENABLE_DEBUG_INFO = NO;
191 | SDKROOT = iphoneos;
192 | VALIDATE_PRODUCT = YES;
193 | };
194 | name = Release;
195 | };
196 | 58B511F01A9E6C8500147676 /* Debug */ = {
197 | isa = XCBuildConfiguration;
198 | buildSettings = {
199 | HEADER_SEARCH_PATHS = (
200 | "$(inherited)",
201 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
202 | "$(SRCROOT)/../../React/**",
203 | "$(SRCROOT)/../../node_modules/react-native/React/**",
204 | );
205 | LIBRARY_SEARCH_PATHS = "$(inherited)";
206 | OTHER_LDFLAGS = "-ObjC";
207 | PRODUCT_NAME = TouchVisualizer;
208 | SKIP_INSTALL = YES;
209 | };
210 | name = Debug;
211 | };
212 | 58B511F11A9E6C8500147676 /* Release */ = {
213 | isa = XCBuildConfiguration;
214 | buildSettings = {
215 | HEADER_SEARCH_PATHS = (
216 | "$(inherited)",
217 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
218 | "$(SRCROOT)/../../React/**",
219 | );
220 | LIBRARY_SEARCH_PATHS = "$(inherited)";
221 | OTHER_LDFLAGS = "-ObjC";
222 | PRODUCT_NAME = TouchVisualizer;
223 | SKIP_INSTALL = YES;
224 | };
225 | name = Release;
226 | };
227 | /* End XCBuildConfiguration section */
228 |
229 | /* Begin XCConfigurationList section */
230 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "TouchVisualizer" */ = {
231 | isa = XCConfigurationList;
232 | buildConfigurations = (
233 | 58B511ED1A9E6C8500147676 /* Debug */,
234 | 58B511EE1A9E6C8500147676 /* Release */,
235 | );
236 | defaultConfigurationIsVisible = 0;
237 | defaultConfigurationName = Release;
238 | };
239 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "TouchVisualizer" */ = {
240 | isa = XCConfigurationList;
241 | buildConfigurations = (
242 | 58B511F01A9E6C8500147676 /* Debug */,
243 | 58B511F11A9E6C8500147676 /* Release */,
244 | );
245 | defaultConfigurationIsVisible = 0;
246 | defaultConfigurationName = Release;
247 | };
248 | /* End XCConfigurationList section */
249 | };
250 | rootObject = 58B511D31A9E6C8500147676 /* Project object */;
251 | }
252 |
--------------------------------------------------------------------------------
/TouchVisualizer.m:
--------------------------------------------------------------------------------
1 | #import "TouchVisualizer.h"
2 |
3 | #pragma mark - Touch Visualizer Window
4 |
5 | @interface TouchVisualizerWindow : UIWindow
6 | @end
7 |
8 | @implementation TouchVisualizerWindow
9 |
10 | // UIKit tries to get the rootViewController from the overlay window.
11 | // Instead, try to find the rootViewController on some other
12 | // application window.
13 | // Fixes problems with status bar hiding, because it considers the
14 | // overlay window a candidate for controlling the status bar.
15 | - (UIViewController *)rootViewController {
16 | for (UIWindow *window in [[UIApplication sharedApplication] windows]) {
17 | if (self == window)
18 | continue;
19 | UIViewController *realRootViewController = window.rootViewController;
20 | if (realRootViewController != nil)
21 | return realRootViewController;
22 | }
23 | return [super rootViewController];
24 | }
25 |
26 | @end
27 |
28 | #pragma mark - Conopsys Touch Spot View
29 |
30 | @interface COSTouchSpotView : UIImageView
31 |
32 | @property (nonatomic) NSTimeInterval timestamp;
33 | @property (nonatomic) BOOL shouldAutomaticallyRemoveAfterTimeout;
34 | @property (nonatomic, getter=isFadingOut) BOOL fadingOut;
35 |
36 | @end
37 |
38 | @implementation COSTouchSpotView
39 | @end
40 |
41 | #pragma mark - Conopsys Touch Visualizer Window
42 |
43 | @interface TouchVisualizer ()
44 |
45 | @property (nonatomic) UIWindow *overlayWindow;
46 | @property (nonatomic) UIViewController *overlayWindowViewController;
47 | @property (nonatomic) BOOL fingerTipRemovalScheduled;
48 | @property (nonatomic) NSTimer *timer;
49 | @property (nonatomic) NSSet *allTouches;
50 |
51 | - (void)TouchVisualizer_commonInit;
52 | - (void)scheduleFingerTipRemoval;
53 | - (void)cancelScheduledFingerTipRemoval;
54 | - (void)removeInactiveFingerTips;
55 | - (void)removeFingerTipWithHash:(NSUInteger)hash animated:(BOOL)animated;
56 | - (BOOL)shouldAutomaticallyRemoveFingerTipForTouch:(UITouch *)touch;
57 |
58 | @end
59 |
60 | @implementation TouchVisualizer
61 |
62 | - (id)initWithCoder:(NSCoder *)decoder {
63 | // This covers NIB-loaded windows.
64 | if (self = [super initWithCoder:decoder])
65 | [self TouchVisualizer_commonInit];
66 | return self;
67 | }
68 |
69 | - (id)initWithFrame:(CGRect)rect {
70 | // This covers programmatically-created windows.
71 | if (self = [super initWithFrame:rect])
72 | [self TouchVisualizer_commonInit];
73 | return self;
74 | }
75 |
76 | - (void)TouchVisualizer_commonInit {
77 | self.strokeColor = [UIColor lightGrayColor];
78 | self.fillColor = [UIColor whiteColor];
79 | self.rippleStrokeColor = [UIColor whiteColor];
80 | self.rippleFillColor = [UIColor blueColor];
81 | self.touchAlpha = 0.5;
82 | self.fadeDuration = 0.3;
83 | self.rippleAlpha = 0.0;
84 | self.rippleFadeDuration = 0.0;
85 | }
86 |
87 | #pragma mark - Touch / Ripple and Images
88 |
89 | - (UIImage *)touchImage {
90 | if (!_touchImage) {
91 | UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 50.0, 50.0)];
92 |
93 | UIGraphicsBeginImageContextWithOptions(clipPath.bounds.size, NO, 0);
94 |
95 | UIBezierPath *drawPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(25.0, 25.0)
96 | radius:22.0
97 | startAngle:0
98 | endAngle:2 * M_PI
99 | clockwise:YES];
100 |
101 | drawPath.lineWidth = 1.0;
102 |
103 | [self.strokeColor setStroke];
104 | [self.fillColor setFill];
105 |
106 | [drawPath stroke];
107 | [drawPath fill];
108 |
109 | [clipPath addClip];
110 |
111 | _touchImage = UIGraphicsGetImageFromCurrentImageContext();
112 | UIGraphicsEndImageContext();
113 | }
114 | return _touchImage;
115 | }
116 |
117 | - (UIImage *)rippleImage {
118 | if (!_rippleImage) {
119 | UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 50.0, 50.0)];
120 |
121 | UIGraphicsBeginImageContextWithOptions(clipPath.bounds.size, NO, 0);
122 |
123 | UIBezierPath *drawPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(25.0, 25.0)
124 | radius:22.0
125 | startAngle:0
126 | endAngle:2 * M_PI
127 | clockwise:YES];
128 |
129 | drawPath.lineWidth = 2.0;
130 |
131 | [self.rippleStrokeColor setStroke];
132 | [self.rippleFillColor setFill];
133 |
134 | [drawPath stroke];
135 | [drawPath fill];
136 |
137 | [clipPath addClip];
138 |
139 | _rippleImage = UIGraphicsGetImageFromCurrentImageContext();
140 | UIGraphicsEndImageContext();
141 | }
142 | return _rippleImage;
143 | }
144 |
145 | #pragma mark - Active
146 |
147 | - (BOOL)anyScreenIsMirrored {
148 | if (![UIScreen instancesRespondToSelector:@selector(mirroredScreen)])
149 | return NO;
150 |
151 | for (UIScreen *screen in [UIScreen screens]) {
152 | if ([screen mirroredScreen] != nil) {
153 | return YES;
154 | }
155 | }
156 | return NO;
157 | }
158 |
159 | #pragma mark - UIWindow overrides
160 |
161 | - (void)sendEvent:(UIEvent *)event {
162 | self.allTouches = [event allTouches];
163 | for (UITouch *touch in [self.allTouches allObjects]) {
164 | switch (touch.phase) {
165 | case UITouchPhaseBegan:
166 | case UITouchPhaseMoved: {
167 | // Generate ripples
168 | COSTouchSpotView *rippleView = [[COSTouchSpotView alloc] initWithImage:self.rippleImage];
169 | [self.overlayWindow addSubview:rippleView];
170 |
171 | rippleView.alpha = self.rippleAlpha;
172 | rippleView.center = [touch locationInView:self.overlayWindow];
173 |
174 | [UIView animateWithDuration:self.rippleFadeDuration
175 | delay:0.0
176 | options:UIViewAnimationOptionCurveEaseIn // See other
177 | // options
178 | animations:^{
179 | [rippleView setAlpha:0.0];
180 | [rippleView setFrame:CGRectInset(rippleView.frame, 25, 25)];
181 | } completion:^(BOOL finished) {
182 | [rippleView removeFromSuperview];
183 | }];
184 | }
185 | case UITouchPhaseStationary: {
186 | COSTouchSpotView *touchView = (COSTouchSpotView *)[self.overlayWindow viewWithTag:touch.hash];
187 |
188 | if (touch.phase != UITouchPhaseStationary && touchView != nil && [touchView isFadingOut]) {
189 | [self.timer invalidate];
190 | [touchView removeFromSuperview];
191 | touchView = nil;
192 | }
193 |
194 | if (touchView == nil && touch.phase != UITouchPhaseStationary) {
195 | touchView = [[COSTouchSpotView alloc] initWithImage:self.touchImage];
196 | [self.overlayWindow addSubview:touchView];
197 | }
198 | if (![touchView isFadingOut]) {
199 | touchView.alpha = self.touchAlpha;
200 | touchView.center = [touch locationInView:self.overlayWindow];
201 | touchView.tag = touch.hash;
202 | touchView.timestamp = touch.timestamp;
203 | touchView.shouldAutomaticallyRemoveAfterTimeout = [self shouldAutomaticallyRemoveFingerTipForTouch:touch];
204 | }
205 | break;
206 | }
207 | case UITouchPhaseEnded:
208 | case UITouchPhaseCancelled: {
209 | [self removeFingerTipWithHash:touch.hash animated:YES];
210 | break;
211 | }
212 | }
213 | }
214 |
215 | [super sendEvent:event];
216 | [self scheduleFingerTipRemoval]; // We may not see all UITouchPhaseEnded/UITouchPhaseCancelled events.
217 | }
218 |
219 | #pragma mark - Private
220 |
221 | - (UIWindow *)overlayWindow {
222 | if (!_overlayWindow) {
223 | _overlayWindow = [[TouchVisualizerWindow alloc] initWithFrame:self.frame];
224 | _overlayWindow.userInteractionEnabled = NO;
225 | _overlayWindow.windowLevel = UIWindowLevelStatusBar;
226 | _overlayWindow.backgroundColor = [UIColor clearColor];
227 | _overlayWindow.hidden = NO;
228 | }
229 | return _overlayWindow;
230 | }
231 |
232 | - (void)scheduleFingerTipRemoval {
233 |
234 | if (self.fingerTipRemovalScheduled)
235 | return;
236 | self.fingerTipRemovalScheduled = YES;
237 | [self performSelector:@selector(removeInactiveFingerTips)
238 | withObject:nil
239 | afterDelay:0.1];
240 | }
241 |
242 | - (void)cancelScheduledFingerTipRemoval {
243 | self.fingerTipRemovalScheduled = YES;
244 | [NSObject cancelPreviousPerformRequestsWithTarget:self
245 | selector:@selector(removeInactiveFingerTips)
246 | object:nil];
247 | }
248 |
249 | - (void)removeInactiveFingerTips {
250 | self.fingerTipRemovalScheduled = NO;
251 |
252 | NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime];
253 | const CGFloat REMOVAL_DELAY = 0.2;
254 | for (COSTouchSpotView *touchView in [self.overlayWindow subviews]) {
255 | if (![touchView isKindOfClass:[COSTouchSpotView class]])
256 | continue;
257 |
258 | if (touchView.shouldAutomaticallyRemoveAfterTimeout && now > touchView.timestamp + REMOVAL_DELAY)
259 | [self removeFingerTipWithHash:touchView.tag animated:YES];
260 | }
261 |
262 | if ([[self.overlayWindow subviews] count])
263 | [self scheduleFingerTipRemoval];
264 | }
265 |
266 | - (void)removeFingerTipWithHash:(NSUInteger)hash animated:(BOOL)animated {
267 | COSTouchSpotView *touchView = (COSTouchSpotView *)[self.overlayWindow viewWithTag:hash];
268 | if (touchView == nil)
269 | return;
270 |
271 | if ([touchView isFadingOut])
272 | return;
273 |
274 | BOOL animationsWereEnabled = [UIView areAnimationsEnabled];
275 |
276 | if (animated) {
277 | [UIView setAnimationsEnabled:YES];
278 | [UIView beginAnimations:nil context:nil];
279 | [UIView setAnimationDuration:self.fadeDuration];
280 | }
281 |
282 | touchView.frame = CGRectMake(touchView.center.x - touchView.frame.size.width,
283 | touchView.center.y - touchView.frame.size.height,
284 | touchView.frame.size.width * 2, touchView.frame.size.height * 2);
285 |
286 | touchView.alpha = 0.0;
287 |
288 | if (animated) {
289 | [UIView commitAnimations];
290 | [UIView setAnimationsEnabled:animationsWereEnabled];
291 | }
292 |
293 | touchView.fadingOut = YES;
294 | [touchView performSelector:@selector(removeFromSuperview)
295 | withObject:nil
296 | afterDelay:self.fadeDuration];
297 | }
298 |
299 | - (BOOL)shouldAutomaticallyRemoveFingerTipForTouch:(UITouch *)touch;
300 | {
301 | // We don't reliably get UITouchPhaseEnded or UITouchPhaseCancelled
302 | // events via -sendEvent: for certain touch events. Known cases
303 | // include swipe-to-delete on a table view row, and tap-to-cancel
304 | // swipe to delete. We automatically remove their associated
305 | // fingertips after a suitable timeout.
306 | //
307 | // It would be much nicer if we could remove all touch events after
308 | // a suitable time out, but then we'll prematurely remove touch and
309 | // hold events that are picked up by gesture recognizers (since we
310 | // don't use UITouchPhaseStationary touches for those. *sigh*). So we
311 | // end up with this more complicated setup.
312 |
313 | UIView *view = [touch view];
314 | view = [view hitTest:[touch locationInView:view] withEvent:nil];
315 |
316 | while (view != nil) {
317 | if ([view isKindOfClass:[UITableViewCell class]]) {
318 | for (UIGestureRecognizer *recognizer in [touch gestureRecognizers]) {
319 | if ([recognizer isKindOfClass:[UISwipeGestureRecognizer class]])
320 | return YES;
321 | }
322 | }
323 |
324 | if ([view isKindOfClass:[UITableView class]]) {
325 | if ([[touch gestureRecognizers] count] == 0)
326 | return YES;
327 | }
328 | view = view.superview;
329 | }
330 |
331 | return NO;
332 | }
333 |
334 | RCT_EXPORT_MODULE()
335 |
336 | @end
337 |
--------------------------------------------------------------------------------