├── README.md ├── WeChatFloatBall.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── TIM.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist └── WeChatFloatBall ├── AppDelegate.h ├── AppDelegate.m ├── BaseNavigationController.h ├── BaseNavigationController.m ├── FloatBallManager ├── CancelFloatView.h ├── CancelFloatView.m ├── FloatBallDefine.h ├── FloatBallManager.h ├── FloatBallManager.m ├── FloatBallResource.bundle │ ├── demo_cancel_float@2x.png │ ├── demo_cancel_float@3x.png │ ├── demo_cancel_float_default2@2x.png │ ├── demo_cancel_float_default2@3x.png │ ├── demo_cancel_float_default@2x.png │ ├── demo_cancel_float_default@3x.png │ ├── demo_link@2x.png │ └── demo_link@3x.png ├── FloatTransitionAnimator.h ├── FloatTransitionAnimator.m ├── NSObject+ViewController.h ├── NSObject+ViewController.m ├── UINavigationController+FloatBall.h ├── UINavigationController+FloatBall.m ├── UIView+FloatFrame.h └── UIView+FloatFrame.m ├── SecondViewController.h ├── SecondViewController.m ├── Supporting Files ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist └── main.m ├── ViewController.h └── ViewController.m /README.md: -------------------------------------------------------------------------------- 1 | # WeChatFloatBall 2 | 一行代码实现高仿微信悬浮球 3 | 4 | 首先来看下微信上的效果: 5 | 6 | ![微信](https://upload-images.jianshu.io/upload_images/5306625-6efbc87feeda1e69.GIF?imageMogr2/auto-orient/strip) 7 | 8 | 再来看下我们的实现效果: 9 | 10 | ![悬浮球](https://upload-images.jianshu.io/upload_images/5306625-37c8d8efe60dac55.GIF?imageMogr2/auto-orient/strip) 11 | 12 | ## 使用方式 13 | 1.如果你的项目没有类似如下代码: 14 | `_navigationController.delegate`和`_navigationController.interactivePopGestureRecognizer.delegate` 15 | 也就是没有对`UINavigationController`和`UINavigationController`的右滑返回手势设置代理。 16 | 那么你只需要添加一行代码就能集成... 17 | 一行代码,真的只有一行: 18 | ``` 19 | //添加要监控的类名 20 | [[FloatBallManager shared] addFloatMonitorVCClasses:@[@"SecondViewController"]]; 21 | ``` 22 | 最好在`- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions`里添加。 23 | 24 | 2.如果不巧的是,你的项目设置了上述两个代理(当然,大部分情况下都会设置)。不方,只要添加如下配置就好了: 25 | ``` 26 | #pragma mark - UINavigationControllerDelegate 27 | #pragma mark 自定义转场动画 28 | - (id )navigationController:(UINavigationController *)navigationController 29 | animationControllerForOperation:(UINavigationControllerOperation)operation 30 | fromViewController:(UIViewController *)fromVC 31 | toViewController:(UIViewController *)toVC 32 | { 33 | return [[FloatBallManager shared] floatBallAnimationWithOperation:operation fromViewController:fromVC toViewController:toVC]; 34 | } 35 | 36 | #pragma mark 交互式转场 37 | - (id )navigationController:(UINavigationController *)navigationController 38 | interactionControllerForAnimationController:(id ) animationController 39 | { 40 | return [[FloatBallManager shared] floatInteractionControllerForAnimationController:animationController]; 41 | } 42 | 43 | - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated 44 | { 45 | [[FloatBallManager shared] didShowViewController:viewController navigationController:navigationController]; 46 | } 47 | ``` 48 | 49 | 更加详细的技术实现过程介绍,见[简书传送门](https://www.jianshu.com/p/d2145413db76) 50 | -------------------------------------------------------------------------------- /WeChatFloatBall.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 223F2F86213D8B7000C4973D /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F61213D8B6A00C4973D /* AppDelegate.m */; }; 11 | 223F2F88213D8B7000C4973D /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F65213D8B6B00C4973D /* ViewController.m */; }; 12 | 223F2F89213D8B7000C4973D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 223F2F68213D8B6B00C4973D /* Assets.xcassets */; }; 13 | 223F2F8A213D8B7000C4973D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 223F2F69213D8B6B00C4973D /* LaunchScreen.storyboard */; }; 14 | 223F2F8B213D8B7000C4973D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F6B213D8B6B00C4973D /* main.m */; }; 15 | 223F2F8D213D8B7000C4973D /* BaseNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F6F213D8B6C00C4973D /* BaseNavigationController.m */; }; 16 | 223F2F8E213D8B7000C4973D /* SecondViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F72213D8B6C00C4973D /* SecondViewController.m */; }; 17 | 223F2F8F213D8B7000C4973D /* FloatTransitionAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F74213D8B6C00C4973D /* FloatTransitionAnimator.m */; }; 18 | 223F2F91213D8B7000C4973D /* CancelFloatView.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F7C213D8B6C00C4973D /* CancelFloatView.m */; }; 19 | 223F2F92213D8B7000C4973D /* UIView+FloatFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F7D213D8B6C00C4973D /* UIView+FloatFrame.m */; }; 20 | 223F2F93213D8B7000C4973D /* NSObject+ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F7F213D8B6C00C4973D /* NSObject+ViewController.m */; }; 21 | 223F2F94213D8B7000C4973D /* FloatBallManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F2F80213D8B6C00C4973D /* FloatBallManager.m */; }; 22 | 223F2F96213D8B7000C4973D /* FloatBallResource.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 223F2F83213D8B6C00C4973D /* FloatBallResource.bundle */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 223F2F48213D8B0300C4973D /* WeChatFloatBall.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WeChatFloatBall.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 223F2F61213D8B6A00C4973D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 28 | 223F2F65213D8B6B00C4973D /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 29 | 223F2F68213D8B6B00C4973D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 223F2F6A213D8B6B00C4973D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | 223F2F6B213D8B6B00C4973D /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 32 | 223F2F6C213D8B6B00C4973D /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 33 | 223F2F6D213D8B6B00C4973D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 34 | 223F2F6E213D8B6B00C4973D /* BaseNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BaseNavigationController.h; sourceTree = ""; }; 35 | 223F2F6F213D8B6C00C4973D /* BaseNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BaseNavigationController.m; sourceTree = ""; }; 36 | 223F2F71213D8B6C00C4973D /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 37 | 223F2F72213D8B6C00C4973D /* SecondViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SecondViewController.m; sourceTree = ""; }; 38 | 223F2F74213D8B6C00C4973D /* FloatTransitionAnimator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FloatTransitionAnimator.m; sourceTree = ""; }; 39 | 223F2F75213D8B6C00C4973D /* UIView+FloatFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+FloatFrame.h"; sourceTree = ""; }; 40 | 223F2F76213D8B6C00C4973D /* CancelFloatView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CancelFloatView.h; sourceTree = ""; }; 41 | 223F2F77213D8B6C00C4973D /* FloatBallManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FloatBallManager.h; sourceTree = ""; }; 42 | 223F2F78213D8B6C00C4973D /* NSObject+ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+ViewController.h"; sourceTree = ""; }; 43 | 223F2F7B213D8B6C00C4973D /* FloatBallDefine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FloatBallDefine.h; sourceTree = ""; }; 44 | 223F2F7C213D8B6C00C4973D /* CancelFloatView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CancelFloatView.m; sourceTree = ""; }; 45 | 223F2F7D213D8B6C00C4973D /* UIView+FloatFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+FloatFrame.m"; sourceTree = ""; }; 46 | 223F2F7E213D8B6C00C4973D /* FloatTransitionAnimator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FloatTransitionAnimator.h; sourceTree = ""; }; 47 | 223F2F7F213D8B6C00C4973D /* NSObject+ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+ViewController.m"; sourceTree = ""; }; 48 | 223F2F80213D8B6C00C4973D /* FloatBallManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FloatBallManager.m; sourceTree = ""; }; 49 | 223F2F83213D8B6C00C4973D /* FloatBallResource.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = FloatBallResource.bundle; sourceTree = ""; }; 50 | 223F2F84213D8B6D00C4973D /* SecondViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SecondViewController.h; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 223F2F45213D8B0300C4973D /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 223F2F3F213D8B0300C4973D = { 65 | isa = PBXGroup; 66 | children = ( 67 | 223F2F4A213D8B0300C4973D /* WeChatFloatBall */, 68 | 223F2F49213D8B0300C4973D /* Products */, 69 | ); 70 | sourceTree = ""; 71 | }; 72 | 223F2F49213D8B0300C4973D /* Products */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 223F2F48213D8B0300C4973D /* WeChatFloatBall.app */, 76 | ); 77 | name = Products; 78 | sourceTree = ""; 79 | }; 80 | 223F2F4A213D8B0300C4973D /* WeChatFloatBall */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 223F2F6D213D8B6B00C4973D /* AppDelegate.h */, 84 | 223F2F61213D8B6A00C4973D /* AppDelegate.m */, 85 | 223F2F6E213D8B6B00C4973D /* BaseNavigationController.h */, 86 | 223F2F6F213D8B6C00C4973D /* BaseNavigationController.m */, 87 | 223F2F84213D8B6D00C4973D /* SecondViewController.h */, 88 | 223F2F72213D8B6C00C4973D /* SecondViewController.m */, 89 | 223F2F71213D8B6C00C4973D /* ViewController.h */, 90 | 223F2F65213D8B6B00C4973D /* ViewController.m */, 91 | 223F2F73213D8B6C00C4973D /* FloatBallManager */, 92 | 223F2F67213D8B6B00C4973D /* Supporting Files */, 93 | ); 94 | path = WeChatFloatBall; 95 | sourceTree = ""; 96 | }; 97 | 223F2F67213D8B6B00C4973D /* Supporting Files */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 223F2F68213D8B6B00C4973D /* Assets.xcassets */, 101 | 223F2F69213D8B6B00C4973D /* LaunchScreen.storyboard */, 102 | 223F2F6B213D8B6B00C4973D /* main.m */, 103 | 223F2F6C213D8B6B00C4973D /* Info.plist */, 104 | ); 105 | path = "Supporting Files"; 106 | sourceTree = ""; 107 | }; 108 | 223F2F73213D8B6C00C4973D /* FloatBallManager */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 223F2F76213D8B6C00C4973D /* CancelFloatView.h */, 112 | 223F2F7C213D8B6C00C4973D /* CancelFloatView.m */, 113 | 223F2F77213D8B6C00C4973D /* FloatBallManager.h */, 114 | 223F2F80213D8B6C00C4973D /* FloatBallManager.m */, 115 | 223F2F7E213D8B6C00C4973D /* FloatTransitionAnimator.h */, 116 | 223F2F74213D8B6C00C4973D /* FloatTransitionAnimator.m */, 117 | 223F2F7B213D8B6C00C4973D /* FloatBallDefine.h */, 118 | 223F2F78213D8B6C00C4973D /* NSObject+ViewController.h */, 119 | 223F2F7F213D8B6C00C4973D /* NSObject+ViewController.m */, 120 | 223F2F75213D8B6C00C4973D /* UIView+FloatFrame.h */, 121 | 223F2F7D213D8B6C00C4973D /* UIView+FloatFrame.m */, 122 | 223F2F83213D8B6C00C4973D /* FloatBallResource.bundle */, 123 | ); 124 | path = FloatBallManager; 125 | sourceTree = ""; 126 | }; 127 | /* End PBXGroup section */ 128 | 129 | /* Begin PBXNativeTarget section */ 130 | 223F2F47213D8B0300C4973D /* WeChatFloatBall */ = { 131 | isa = PBXNativeTarget; 132 | buildConfigurationList = 223F2F5E213D8B0600C4973D /* Build configuration list for PBXNativeTarget "WeChatFloatBall" */; 133 | buildPhases = ( 134 | 223F2F44213D8B0300C4973D /* Sources */, 135 | 223F2F45213D8B0300C4973D /* Frameworks */, 136 | 223F2F46213D8B0300C4973D /* Resources */, 137 | ); 138 | buildRules = ( 139 | ); 140 | dependencies = ( 141 | ); 142 | name = WeChatFloatBall; 143 | productName = WeChatFloatBall; 144 | productReference = 223F2F48213D8B0300C4973D /* WeChatFloatBall.app */; 145 | productType = "com.apple.product-type.application"; 146 | }; 147 | /* End PBXNativeTarget section */ 148 | 149 | /* Begin PBXProject section */ 150 | 223F2F40213D8B0300C4973D /* Project object */ = { 151 | isa = PBXProject; 152 | attributes = { 153 | LastUpgradeCheck = 0940; 154 | ORGANIZATIONNAME = TIM; 155 | TargetAttributes = { 156 | 223F2F47213D8B0300C4973D = { 157 | CreatedOnToolsVersion = 9.4.1; 158 | }; 159 | }; 160 | }; 161 | buildConfigurationList = 223F2F43213D8B0300C4973D /* Build configuration list for PBXProject "WeChatFloatBall" */; 162 | compatibilityVersion = "Xcode 9.3"; 163 | developmentRegion = en; 164 | hasScannedForEncodings = 0; 165 | knownRegions = ( 166 | en, 167 | Base, 168 | ); 169 | mainGroup = 223F2F3F213D8B0300C4973D; 170 | productRefGroup = 223F2F49213D8B0300C4973D /* Products */; 171 | projectDirPath = ""; 172 | projectRoot = ""; 173 | targets = ( 174 | 223F2F47213D8B0300C4973D /* WeChatFloatBall */, 175 | ); 176 | }; 177 | /* End PBXProject section */ 178 | 179 | /* Begin PBXResourcesBuildPhase section */ 180 | 223F2F46213D8B0300C4973D /* Resources */ = { 181 | isa = PBXResourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | 223F2F89213D8B7000C4973D /* Assets.xcassets in Resources */, 185 | 223F2F96213D8B7000C4973D /* FloatBallResource.bundle in Resources */, 186 | 223F2F8A213D8B7000C4973D /* LaunchScreen.storyboard in Resources */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | /* End PBXResourcesBuildPhase section */ 191 | 192 | /* Begin PBXSourcesBuildPhase section */ 193 | 223F2F44213D8B0300C4973D /* Sources */ = { 194 | isa = PBXSourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | 223F2F93213D8B7000C4973D /* NSObject+ViewController.m in Sources */, 198 | 223F2F8E213D8B7000C4973D /* SecondViewController.m in Sources */, 199 | 223F2F88213D8B7000C4973D /* ViewController.m in Sources */, 200 | 223F2F86213D8B7000C4973D /* AppDelegate.m in Sources */, 201 | 223F2F8B213D8B7000C4973D /* main.m in Sources */, 202 | 223F2F94213D8B7000C4973D /* FloatBallManager.m in Sources */, 203 | 223F2F8D213D8B7000C4973D /* BaseNavigationController.m in Sources */, 204 | 223F2F8F213D8B7000C4973D /* FloatTransitionAnimator.m in Sources */, 205 | 223F2F91213D8B7000C4973D /* CancelFloatView.m in Sources */, 206 | 223F2F92213D8B7000C4973D /* UIView+FloatFrame.m in Sources */, 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | }; 210 | /* End PBXSourcesBuildPhase section */ 211 | 212 | /* Begin PBXVariantGroup section */ 213 | 223F2F69213D8B6B00C4973D /* LaunchScreen.storyboard */ = { 214 | isa = PBXVariantGroup; 215 | children = ( 216 | 223F2F6A213D8B6B00C4973D /* Base */, 217 | ); 218 | name = LaunchScreen.storyboard; 219 | sourceTree = ""; 220 | }; 221 | /* End PBXVariantGroup section */ 222 | 223 | /* Begin XCBuildConfiguration section */ 224 | 223F2F5C213D8B0600C4973D /* Debug */ = { 225 | isa = XCBuildConfiguration; 226 | buildSettings = { 227 | ALWAYS_SEARCH_USER_PATHS = NO; 228 | CLANG_ANALYZER_NONNULL = YES; 229 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 231 | CLANG_CXX_LIBRARY = "libc++"; 232 | CLANG_ENABLE_MODULES = YES; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_ENABLE_OBJC_WEAK = YES; 235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_COMMA = YES; 238 | CLANG_WARN_CONSTANT_CONVERSION = YES; 239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 242 | CLANG_WARN_EMPTY_BODY = YES; 243 | CLANG_WARN_ENUM_CONVERSION = YES; 244 | CLANG_WARN_INFINITE_RECURSION = YES; 245 | CLANG_WARN_INT_CONVERSION = YES; 246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 251 | CLANG_WARN_STRICT_PROTOTYPES = YES; 252 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 254 | CLANG_WARN_UNREACHABLE_CODE = YES; 255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 256 | CODE_SIGN_IDENTITY = "iPhone Developer"; 257 | COPY_PHASE_STRIP = NO; 258 | DEBUG_INFORMATION_FORMAT = dwarf; 259 | ENABLE_STRICT_OBJC_MSGSEND = YES; 260 | ENABLE_TESTABILITY = YES; 261 | GCC_C_LANGUAGE_STANDARD = gnu11; 262 | GCC_DYNAMIC_NO_PIC = NO; 263 | GCC_NO_COMMON_BLOCKS = YES; 264 | GCC_OPTIMIZATION_LEVEL = 0; 265 | GCC_PREPROCESSOR_DEFINITIONS = ( 266 | "DEBUG=1", 267 | "$(inherited)", 268 | ); 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 276 | MTL_ENABLE_DEBUG_INFO = YES; 277 | ONLY_ACTIVE_ARCH = YES; 278 | SDKROOT = iphoneos; 279 | }; 280 | name = Debug; 281 | }; 282 | 223F2F5D213D8B0600C4973D /* Release */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ALWAYS_SEARCH_USER_PATHS = NO; 286 | CLANG_ANALYZER_NONNULL = YES; 287 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 289 | CLANG_CXX_LIBRARY = "libc++"; 290 | CLANG_ENABLE_MODULES = YES; 291 | CLANG_ENABLE_OBJC_ARC = YES; 292 | CLANG_ENABLE_OBJC_WEAK = YES; 293 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 294 | CLANG_WARN_BOOL_CONVERSION = YES; 295 | CLANG_WARN_COMMA = YES; 296 | CLANG_WARN_CONSTANT_CONVERSION = YES; 297 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 298 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 299 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 300 | CLANG_WARN_EMPTY_BODY = YES; 301 | CLANG_WARN_ENUM_CONVERSION = YES; 302 | CLANG_WARN_INFINITE_RECURSION = YES; 303 | CLANG_WARN_INT_CONVERSION = YES; 304 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 305 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 306 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 307 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 308 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 309 | CLANG_WARN_STRICT_PROTOTYPES = YES; 310 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 311 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | CODE_SIGN_IDENTITY = "iPhone Developer"; 315 | COPY_PHASE_STRIP = NO; 316 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 317 | ENABLE_NS_ASSERTIONS = NO; 318 | ENABLE_STRICT_OBJC_MSGSEND = YES; 319 | GCC_C_LANGUAGE_STANDARD = gnu11; 320 | GCC_NO_COMMON_BLOCKS = YES; 321 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 322 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 323 | GCC_WARN_UNDECLARED_SELECTOR = YES; 324 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 325 | GCC_WARN_UNUSED_FUNCTION = YES; 326 | GCC_WARN_UNUSED_VARIABLE = YES; 327 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 328 | MTL_ENABLE_DEBUG_INFO = NO; 329 | SDKROOT = iphoneos; 330 | VALIDATE_PRODUCT = YES; 331 | }; 332 | name = Release; 333 | }; 334 | 223F2F5F213D8B0600C4973D /* Debug */ = { 335 | isa = XCBuildConfiguration; 336 | buildSettings = { 337 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 338 | CODE_SIGN_STYLE = Automatic; 339 | DEVELOPMENT_TEAM = QCTFNMNKBD; 340 | INFOPLIST_FILE = "$(SRCROOT)/WeChatFloatBall/Supporting Files/Info.plist"; 341 | LD_RUNPATH_SEARCH_PATHS = ( 342 | "$(inherited)", 343 | "@executable_path/Frameworks", 344 | ); 345 | PRODUCT_BUNDLE_IDENTIFIER = com.tim.WeChatFloatBall; 346 | PRODUCT_NAME = "$(TARGET_NAME)"; 347 | TARGETED_DEVICE_FAMILY = "1,2"; 348 | }; 349 | name = Debug; 350 | }; 351 | 223F2F60213D8B0600C4973D /* Release */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 355 | CODE_SIGN_STYLE = Automatic; 356 | DEVELOPMENT_TEAM = QCTFNMNKBD; 357 | INFOPLIST_FILE = "$(SRCROOT)/WeChatFloatBall/Supporting Files/Info.plist"; 358 | LD_RUNPATH_SEARCH_PATHS = ( 359 | "$(inherited)", 360 | "@executable_path/Frameworks", 361 | ); 362 | PRODUCT_BUNDLE_IDENTIFIER = com.tim.WeChatFloatBall; 363 | PRODUCT_NAME = "$(TARGET_NAME)"; 364 | TARGETED_DEVICE_FAMILY = "1,2"; 365 | }; 366 | name = Release; 367 | }; 368 | /* End XCBuildConfiguration section */ 369 | 370 | /* Begin XCConfigurationList section */ 371 | 223F2F43213D8B0300C4973D /* Build configuration list for PBXProject "WeChatFloatBall" */ = { 372 | isa = XCConfigurationList; 373 | buildConfigurations = ( 374 | 223F2F5C213D8B0600C4973D /* Debug */, 375 | 223F2F5D213D8B0600C4973D /* Release */, 376 | ); 377 | defaultConfigurationIsVisible = 0; 378 | defaultConfigurationName = Release; 379 | }; 380 | 223F2F5E213D8B0600C4973D /* Build configuration list for PBXNativeTarget "WeChatFloatBall" */ = { 381 | isa = XCConfigurationList; 382 | buildConfigurations = ( 383 | 223F2F5F213D8B0600C4973D /* Debug */, 384 | 223F2F60213D8B0600C4973D /* Release */, 385 | ); 386 | defaultConfigurationIsVisible = 0; 387 | defaultConfigurationName = Release; 388 | }; 389 | /* End XCConfigurationList section */ 390 | }; 391 | rootObject = 223F2F40213D8B0300C4973D /* Project object */; 392 | } 393 | -------------------------------------------------------------------------------- /WeChatFloatBall.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WeChatFloatBall.xcodeproj/xcuserdata/TIM.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /WeChatFloatBall.xcodeproj/xcuserdata/TIM.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | WeChatFloatBall.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /WeChatFloatBall/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/29. 6 | // Copyright © 2018年 TIM. 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 | -------------------------------------------------------------------------------- /WeChatFloatBall/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/29. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "ViewController.h" 11 | #import "FloatBallManager.h" 12 | #import "BaseNavigationController.h" 13 | 14 | @interface AppDelegate () 15 | 16 | @end 17 | 18 | @implementation AppDelegate 19 | 20 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 21 | { 22 | ViewController *vc = [ViewController new]; 23 | UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc]; 24 | // BaseNavigationController *nav = [[BaseNavigationController alloc] initWithRootViewController:vc]; 25 | nav.navigationBar.tintColor = [UIColor whiteColor]; 26 | nav.navigationBar.barStyle = UIBarStyleBlack; 27 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 28 | self.window.rootViewController = nav; 29 | [self.window makeKeyAndVisible]; 30 | //添加要监控的类名 31 | [[FloatBallManager shared] addFloatMonitorVCClasses:@[@"SecondViewController"]]; 32 | return YES; 33 | } 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /WeChatFloatBall/BaseNavigationController.h: -------------------------------------------------------------------------------- 1 | // 2 | // BaseNavigationController.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/30. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface BaseNavigationController : UINavigationController 12 | @end 13 | -------------------------------------------------------------------------------- /WeChatFloatBall/BaseNavigationController.m: -------------------------------------------------------------------------------- 1 | // 2 | // BaseNavigationController.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/30. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "BaseNavigationController.h" 10 | #import "FloatBallManager.h" 11 | 12 | @interface BaseNavigationController () 13 | 14 | @end 15 | 16 | @implementation BaseNavigationController 17 | 18 | - (void)viewDidLoad 19 | { 20 | [super viewDidLoad]; 21 | self.delegate = self; 22 | self.interactivePopGestureRecognizer.delegate = self; 23 | } 24 | 25 | #pragma mark - UIGestureRecognizerDelegate 26 | 27 | - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer 28 | { 29 | if (self.viewControllers.count <= 1) { 30 | return NO; 31 | } 32 | return YES; 33 | } 34 | 35 | #pragma mark - UINavigationControllerDelegate 36 | #pragma mark 实现自定义转场动画 37 | - (id )navigationController:(UINavigationController *)navigationController 38 | animationControllerForOperation:(UINavigationControllerOperation)operation 39 | fromViewController:(UIViewController *)fromVC 40 | toViewController:(UIViewController *)toVC 41 | { 42 | return [[FloatBallManager shared] floatBallAnimationWithOperation:operation fromViewController:fromVC toViewController:toVC]; 43 | } 44 | 45 | #pragma mark 交互式转场 46 | - (id )navigationController:(UINavigationController *)navigationController 47 | interactionControllerForAnimationController:(id ) animationController 48 | { 49 | return [[FloatBallManager shared] floatInteractionControllerForAnimationController:animationController]; 50 | } 51 | 52 | #pragma mark 添加滑动手势 53 | - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated 54 | { 55 | [[FloatBallManager shared] didShowViewController:viewController navigationController:navigationController]; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/CancelFloatView.h: -------------------------------------------------------------------------------- 1 | // 2 | // CancelFloatView.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/31. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CancelFloatView : UIView 12 | 13 | /** 是否正在显示,显示的时候背景为红包,否则为灰色 */ 14 | - (void)setCancelFloatViewShowing:(BOOL)showing; 15 | 16 | /** 悬浮球是否进入右下角视图 */ 17 | - (void)moveWithTouchInRound:(BOOL)touchInRound; 18 | 19 | /** 右滑手势拖动改变显示百分比 */ 20 | - (void)showCancelFloatViewWithProress:(CGFloat)progress completion:(void (^)(void))completion; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/CancelFloatView.m: -------------------------------------------------------------------------------- 1 | // 2 | // CancelFloatView.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/31. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "CancelFloatView.h" 10 | #import "UIView+FloatFrame.h" 11 | #import "FloatBallDefine.h" 12 | 13 | @interface CancelFloatView () 14 | 15 | @property (nonatomic, strong) UIImageView *imageView; 16 | @property (nonatomic, strong) UILabel *label; 17 | /** 模糊板 */ 18 | @property (nonatomic, strong) UIToolbar *toolbar; 19 | 20 | /** 悬浮球是否拖动到圆内 */ 21 | @property (nonatomic, assign) BOOL touchInRound; 22 | /** 是否正在显示,显示的时候背景为红包,否则为灰色 */ 23 | @property (nonatomic, assign) BOOL showing; 24 | 25 | @end 26 | 27 | @implementation CancelFloatView 28 | 29 | - (instancetype)initWithFrame:(CGRect)frame 30 | { 31 | if (self = [super initWithFrame:frame]) { 32 | self.backgroundColor = [UIColor clearColor]; 33 | [self setupView]; 34 | } 35 | return self; 36 | } 37 | 38 | - (void)setupView 39 | { 40 | _toolbar = [UIToolbar new]; 41 | _toolbar.barStyle = UIBarStyleBlack; 42 | [self addSubview:_toolbar]; 43 | _imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"FloatBallResource.bundle/demo_cancel_float_default"]]; 44 | _imageView.contentMode = UIViewContentModeCenter; 45 | [self addSubview:_imageView]; 46 | _label = [UILabel new]; 47 | _label.font = [UIFont systemFontOfSize:10]; 48 | _label.textColor = [UIColor colorWithHue:0 saturation:0 brightness:0.9 alpha:1.0]; 49 | _label.textAlignment = NSTextAlignmentCenter; 50 | _label.text = @"浮窗"; 51 | [self addSubview:_label]; 52 | } 53 | 54 | - (void)layoutSubviews 55 | { 56 | [super layoutSubviews]; 57 | _toolbar.frame = self.bounds; 58 | _imageView.size = CGSizeMake(40, 40); 59 | _imageView.centerX = self.width / 2.0 + 26; 60 | _imageView.centerY = self.height / 2.0; 61 | _label.size = CGSizeMake(self.width, 20); 62 | _label.centerX = _imageView.centerX; 63 | _label.y = CGRectGetMaxY(_imageView.frame) + 14; 64 | } 65 | 66 | - (void)drawRect:(CGRect)rect 67 | { 68 | [super drawRect:rect]; 69 | CGFloat radius = _touchInRound ? RoundViewRadius : RoundViewRadius - RoundViewOffset; 70 | UIBezierPath *maskPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(RoundViewRadius, RoundViewRadius) radius:radius startAngle:M_PI endAngle:M_PI * 1.5 clockwise:1]; 71 | [maskPath addLineToPoint:CGPointMake(RoundViewRadius, RoundViewRadius)]; 72 | [maskPath addLineToPoint:CGPointMake(0, RoundViewRadius)]; 73 | CAShapeLayer *maskLayer = [CAShapeLayer layer]; 74 | maskLayer.frame = self.bounds; 75 | maskLayer.path = maskPath.CGPath; 76 | self.layer.mask = maskLayer; 77 | } 78 | 79 | #pragma mark - Public 80 | /** 悬浮球是否进入右下角视图 */ 81 | - (void)moveWithTouchInRound:(BOOL)touchInRound 82 | { 83 | if (_touchInRound != touchInRound) { 84 | _touchInRound = touchInRound; 85 | [self setNeedsDisplay]; 86 | if (touchInRound) { 87 | _imageView.image = [UIImage imageNamed:_showing ? @"FloatBallResource.bundle/demo_cancel_float" : @"FloatBallResource.bundle/demo_cancel_float_default2"]; 88 | } 89 | else { 90 | _imageView.image = [UIImage imageNamed:_showing ? @"FloatBallResource.bundle/demo_cancel_float" : @"FloatBallResource.bundle/demo_cancel_float_default"]; 91 | } 92 | } 93 | } 94 | 95 | - (void)showCancelFloatViewWithProress:(CGFloat)progress completion:(void (^)(void))completion 96 | { 97 | if (progress == 0) { 98 | //位移重置 99 | [UIView animateWithDuration:FloatTranslationOutDuration animations:^{ 100 | self.transform = CGAffineTransformIdentity; 101 | } completion:^(BOOL finished) { 102 | FloatBlockExec(completion); 103 | }]; 104 | } 105 | else if (progress == 1) { 106 | [UIView animateWithDuration:FloatTranslationInDuration animations:^{ 107 | //位移动画 108 | self.transform = CGAffineTransformMakeTranslation(-RoundViewRadius, -RoundViewRadius); 109 | }]; 110 | } 111 | else { 112 | //位移动画 113 | self.transform = CGAffineTransformMakeTranslation(-RoundViewRadius * progress, -RoundViewRadius * progress); 114 | } 115 | } 116 | 117 | - (void)setCancelFloatViewShowing:(BOOL)showing 118 | { 119 | if (_showing != showing) { 120 | _showing = showing; 121 | self.backgroundColor = showing ? [UIColor colorWithRed:0.9 green:0.3 blue:0.3 alpha:1] : [UIColor clearColor]; 122 | _toolbar.hidden = showing; 123 | _imageView.image = [UIImage imageNamed:showing ? @"FloatBallResource.bundle/demo_cancel_float" : @"FloatBallResource.bundle/demo_cancel_float_default"]; 124 | _label.text = showing ? @"取消浮窗" : @"浮窗"; 125 | } 126 | } 127 | 128 | @end 129 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallDefine.h: -------------------------------------------------------------------------------- 1 | // 2 | // FloatBallDefine.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/29. 6 | // Copyright © 2018年 tim. All rights reserved. 7 | // 8 | 9 | #ifndef FloatBallDefine_h 10 | #define FloatBallDefine_h 11 | 12 | #define FloatScreenWidth [UIScreen mainScreen].bounds.size.width 13 | #define FloatScreenHeight [UIScreen mainScreen].bounds.size.height 14 | 15 | //悬浮球宽高 16 | #define FloatWidth 60 17 | //悬浮球靠边停留边距 18 | #define FloatMargin 15 19 | //右下角1/4圆半径 20 | #define RoundViewRadius 170 21 | //圆执行动画的半径变化 22 | #define RoundViewOffset 10 23 | 24 | //转场动画时间 25 | #define AnimationDuration 0.35 26 | //动画切换后的执行时间/快速滑动的pop动画时间 27 | #define ContinueAnimationDuration 0.2 28 | //悬浮、右下角圆出现动画时间 29 | #define FloatTranslationInDuration 0.3 30 | //悬浮、右下角圆消失动画时间 31 | #define FloatTranslationOutDuration 0.2 32 | 33 | #define AnimationWillBeginKey @"AnimationWillBeginKey" 34 | #define AnimationWillEndKey @"AnimationWillEndKey" 35 | #define AnimationDidEndKey @"AnimationDidEndKey" 36 | 37 | //定义了一个__weak的self_weak_变量 38 | #define FloatWeakSelf __weak __typeof(&*self)weakSelf = self; 39 | //局域定义了一个__strong的self指针指向self_weak 40 | #define FloatStrongSelf __strong __typeof(&*weakSelf)self = weakSelf; 41 | 42 | #define FloatWeakObject(__object) __weak typeof(__object) weak_##__object = __object; 43 | 44 | /** 检测block是否可用并执行 */ 45 | #define FloatBlockExec(block, ...) if (block) { block(__VA_ARGS__); }; 46 | 47 | #endif /* CommonDefine_h */ 48 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // FloatBallManager.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/31. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | extern NSString *const kPopInteractiveKey; 13 | extern NSString *const kAnimatorKey; 14 | //该pop为右滑手势 15 | extern NSString *const kPopWithPanGes; 16 | 17 | @interface FloatBallManager : NSObject 18 | 19 | + (instancetype)shared; 20 | /** 添加需要监控添加悬浮球的控制器类名 */ 21 | - (void)addFloatMonitorVCClasses:(NSArray *)VCClasses; 22 | 23 | /** 24 | 如果自身设置了interactivePopGestureRecognizer.delegate的代理,并且系统自身的右滑手势无法识别。 25 | 请添加类似如下代码 26 | - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer 27 | { 28 | if (self.viewControllers.count <= 1) { 29 | return NO; 30 | } 31 | return YES; 32 | } 33 | */ 34 | 35 | /** 36 | 如果自身设置了UINavigationControllerDelegate的代理 37 | 需要添加类似如下代码 38 | - (id )navigationController:(UINavigationController *)navigationController 39 | animationControllerForOperation:(UINavigationControllerOperation)operation 40 | fromViewController:(UIViewController *)fromVC 41 | toViewController:(UIViewController *)toVC 42 | { 43 | return [[FloatBallManager shared] floatBallAnimationWithOperation:operation fromViewController:fromVC toViewController:toVC]; 44 | } 45 | */ 46 | - (id )floatBallAnimationWithOperation:(UINavigationControllerOperation)operation 47 | fromViewController:(UIViewController *)fromVC 48 | toViewController:(UIViewController *)toVC; 49 | 50 | /** 51 | 如果自身设置了UINavigationControllerDelegate的代理 52 | 需要添加类似如下代码 53 | - (id )navigationController:(UINavigationController *)navigationController 54 | interactionControllerForAnimationController:(id ) animationController 55 | { 56 | return [[FloatBallManager shared] floatInteractionControllerForAnimationController:animationController]; 57 | } 58 | */ 59 | - (id )floatInteractionControllerForAnimationController:(id ) animationController; 60 | 61 | /** 62 | 如果自身设置了UINavigationControllerDelegate的代理 63 | 需要添加类似如下代码 64 | - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated 65 | { 66 | [[FloatBallManager shared] didShowViewController:viewController navigationController:navigationController]; 67 | } 68 | */ 69 | - (void)didShowViewController:(UIViewController *)viewController navigationController:(UINavigationController *)navigationController; 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // FloatBallManager.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/31. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "FloatBallManager.h" 10 | #import "FloatBallDefine.h" 11 | #import "FloatTransitionAnimator.h" 12 | #import "CancelFloatView.h" 13 | #import "NSObject+ViewController.h" 14 | #import "UIView+FloatFrame.h" 15 | #import 16 | 17 | /** 悬浮球显示状态 */ 18 | typedef NS_ENUM(NSInteger, FloatShowStyle) { 19 | FloatShowStyleNormal, //悬浮球隐藏,正常状态 20 | FloatShowStyleShow, //悬浮球显示,有缩小的控制器 21 | FloatShowStyleShowContent //正在显示悬浮球里的内容,悬浮球alpha变成0 22 | }; 23 | 24 | @interface FloatBallManager () 25 | 26 | /** 需要监控的类名 */ 27 | @property (nonatomic, strong) NSMutableArray *monitorVCClasses; 28 | /** 悬浮球显示模式 */ 29 | @property (nonatomic, assign) FloatShowStyle showStyle; 30 | /** 手势是否在右下角圆内 */ 31 | @property (nonatomic, assign) BOOL touchInRound; 32 | /** 栈顶即将要pop的控制器 */ 33 | @property (nonatomic, strong) UIViewController *lastPopViewController; 34 | /** 悬浮球加载的控制器 */ 35 | @property (nonatomic, strong) UIViewController *floatViewController; 36 | /** 悬浮球 */ 37 | @property (nonatomic, strong) UIImageView *floatView; 38 | /** 右下角1/4圆 */ 39 | @property (nonatomic, strong) CancelFloatView *cancelFloatView; 40 | 41 | @end 42 | 43 | NSString *const kPopInteractiveKey = @"kPopInteractiveKey"; 44 | NSString *const kAnimatorKey = @"kAnimatorKey"; 45 | //是否为右滑手势引起的pop操作 46 | NSString *const kPopWithPanGes = @"kPopWithPanGes"; 47 | 48 | @implementation FloatBallManager 49 | 50 | - (void)dealloc 51 | { 52 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 53 | } 54 | 55 | + (instancetype)shared { 56 | static FloatBallManager *floatManager = nil; 57 | static dispatch_once_t onceToken; 58 | dispatch_once(&onceToken, ^{ 59 | floatManager = [[super allocWithZone:nil] init]; 60 | floatManager.monitorVCClasses = [NSMutableArray array]; 61 | [NSObject currentNavigationController].interactivePopGestureRecognizer.delegate = floatManager; 62 | [NSObject currentNavigationController].delegate = floatManager; 63 | [[NSNotificationCenter defaultCenter] addObserver:floatManager selector:@selector(p_animationWillBegin) name:AnimationWillBeginKey object:nil]; 64 | [[NSNotificationCenter defaultCenter] addObserver:floatManager selector:@selector(p_animationWillEnd) name:AnimationWillEndKey object:nil]; 65 | [[NSNotificationCenter defaultCenter] addObserver:floatManager selector:@selector(p_animationDidEnd:) name:AnimationDidEndKey object:nil]; 66 | }); 67 | return floatManager; 68 | } 69 | 70 | + (id)allocWithZone:(NSZone *)zone { 71 | return [FloatBallManager shared]; 72 | } 73 | 74 | - (id)copyWithZone:(NSZone *)zone { 75 | return [FloatBallManager shared]; 76 | } 77 | 78 | - (id)mutableCopyWithZone:(NSZone *)zone { 79 | return [FloatBallManager shared]; 80 | } 81 | 82 | #pragma mark - Public 83 | 84 | - (void)addFloatMonitorVCClasses:(NSArray *)VCClasses 85 | { 86 | for (NSString *classString in VCClasses) { 87 | if (![self.monitorVCClasses containsObject:classString]) { 88 | [self.monitorVCClasses addObject:classString]; 89 | } 90 | } 91 | } 92 | 93 | - (id )floatBallAnimationWithOperation:(UINavigationControllerOperation)operation 94 | fromViewController:(UIViewController *)fromVC 95 | toViewController:(UIViewController *)toVC 96 | { 97 | FloatTransitionAnimator *animator; 98 | UIViewController *vc; 99 | if (operation == UINavigationControllerOperationPush) { 100 | if (toVC == self.floatViewController) { 101 | animator = [self createAnimatorWithOperation:operation]; 102 | vc = toVC; 103 | } 104 | } 105 | else if (operation == UINavigationControllerOperationPop) { 106 | if (fromVC == self.lastPopViewController) { 107 | animator = [self createAnimatorWithOperation:operation]; 108 | vc = fromVC; 109 | } 110 | else { 111 | NSNumber *popWithPanGesNumber = objc_getAssociatedObject([NSObject currentNavigationController], &kPopWithPanGes); 112 | BOOL popWithPanGes = [popWithPanGesNumber boolValue]; 113 | //直接点击左上角返回 114 | if (!popWithPanGes && 115 | [self.monitorVCClasses containsObject:NSStringFromClass([fromVC class])] && 116 | self.showStyle == FloatShowStyleShowContent) { 117 | animator = [self createAnimatorWithOperation:operation]; 118 | vc = fromVC; 119 | } 120 | } 121 | } 122 | if (animator && vc) { 123 | objc_setAssociatedObject(vc, &kAnimatorKey, animator, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 124 | } 125 | return animator; 126 | } 127 | 128 | - (FloatTransitionAnimator *)createAnimatorWithOperation:(UINavigationControllerOperation)operation 129 | { 130 | return [FloatTransitionAnimator animatorWithStartCenter:self.floatView.center radius:FloatWidth / 2.0 operation:operation]; 131 | } 132 | 133 | - (id )floatInteractionControllerForAnimationController:(id ) animationController 134 | { 135 | NSNumber *popWithPanGesNumber = objc_getAssociatedObject([NSObject currentNavigationController], &kPopWithPanGes); 136 | BOOL popWithPanGes = [popWithPanGesNumber boolValue]; 137 | if (self.lastPopViewController && popWithPanGes) { 138 | FloatTransitionAnimator *animator = objc_getAssociatedObject(self.lastPopViewController, &kAnimatorKey); 139 | if ([animationController isEqual:animator] && animator.operation == UINavigationControllerOperationPop) { 140 | UIPercentDrivenInteractiveTransition *interactive = [UIPercentDrivenInteractiveTransition new]; 141 | objc_setAssociatedObject(self.lastPopViewController, &kPopInteractiveKey, interactive, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 142 | return interactive; 143 | } 144 | } 145 | return nil; 146 | } 147 | 148 | - (void)didShowViewController:(UIViewController *)viewController navigationController:(UINavigationController *)navigationController 149 | { 150 | if ([self.monitorVCClasses containsObject:NSStringFromClass([viewController class])]) { 151 | navigationController.interactivePopGestureRecognizer.enabled = NO; 152 | // 边缘手势 153 | UIScreenEdgePanGestureRecognizer *gesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleNavigationTransition:)]; 154 | gesture.edges = UIRectEdgeLeft; 155 | gesture.delegate = self; 156 | [viewController.view addGestureRecognizer:gesture]; 157 | } 158 | else { 159 | navigationController.interactivePopGestureRecognizer.enabled = YES; 160 | } 161 | } 162 | 163 | #pragma mark - 手势 164 | #pragma mark 自身添加的右滑返回手势 165 | - (void)handleNavigationTransition:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer 166 | { 167 | CGPoint point = [gestureRecognizer locationInView:[UIApplication sharedApplication].keyWindow]; 168 | //转场交互 169 | UIPercentDrivenInteractiveTransition *interactive = objc_getAssociatedObject(self.lastPopViewController, &kPopInteractiveKey); 170 | //转场动画 171 | FloatTransitionAnimator *animator = objc_getAssociatedObject(self.lastPopViewController, &kAnimatorKey); 172 | 173 | //未显示悬浮球内容 174 | BOOL notShowFloatContent = (self.showStyle == FloatShowStyleNormal || self.showStyle == FloatShowStyleShow); 175 | 176 | if (gestureRecognizer.state == UIGestureRecognizerStateBegan) { 177 | if (notShowFloatContent && self.showStyle == FloatShowStyleShow && self.floatViewController != self.lastPopViewController) { 178 | [self.cancelFloatView setCancelFloatViewShowing:NO]; 179 | } 180 | objc_setAssociatedObject([NSObject currentNavigationController], &kPopWithPanGes, [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_ASSIGN); 181 | [[NSObject currentNavigationController] popViewControllerAnimated:YES]; 182 | } 183 | else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) { 184 | CGFloat progress = point.x / FloatScreenWidth; 185 | //未显示悬浮球内容时,显示1/4圆 186 | if (notShowFloatContent) { 187 | // 屏幕滑动到1/6的时候右下角视图开始出现,滑动到一半,完全出现 188 | if (point.x >= FloatScreenWidth / 6.0 && point.x <= FloatScreenWidth / 2.0) { 189 | CGFloat progress = (point.x - FloatScreenWidth / 6.0) / (FloatScreenWidth / 2.0 - FloatScreenWidth / 6.0); 190 | [self.cancelFloatView showCancelFloatViewWithProress:progress completion:nil]; 191 | } 192 | [self p_floatMoveWithPoint:point]; 193 | } 194 | else { //显示悬浮球内容时,改变悬浮球透明度 195 | self.floatView.alpha = progress; 196 | } 197 | //更新转场动画进度 198 | [animator updateInteractiveTransition:progress]; 199 | [interactive updateInteractiveTransition:progress]; 200 | } 201 | else if (gestureRecognizer.state == UIGestureRecognizerStateEnded || 202 | gestureRecognizer.state == UIGestureRecognizerStateCancelled) { 203 | objc_setAssociatedObject([NSObject currentNavigationController], &kPopWithPanGes, [NSNumber numberWithBool:NO], OBJC_ASSOCIATION_ASSIGN); 204 | if (notShowFloatContent) { 205 | [self.cancelFloatView showCancelFloatViewWithProress:0 completion:nil]; 206 | } 207 | //快速滑动时,通过手势加速度算出动画执行时间可移动距离,模拟系统快速拖动时可pop操作 208 | CGPoint velocityPoint = [gestureRecognizer velocityInView:[UIApplication sharedApplication].keyWindow]; 209 | CGFloat velocityX = velocityPoint.x * AnimationDuration; 210 | //滑动超过屏幕一半,完成转场 211 | if (fmax(velocityX, point.x) > FloatScreenWidth / 2.0) { 212 | if (notShowFloatContent) { 213 | //右滑手势,滑动至右下角1/4圆内则显示悬浮球 214 | if ([self p_checkTouchPointInRound:point]) { 215 | [self p_animationWillEnd]; 216 | self.floatViewController = self.lastPopViewController; 217 | //更新转场动画,从当前触摸点开始缩小到悬浮点位置 218 | [animator replaceAnimation]; 219 | } 220 | else { 221 | [animator continueAnimationWithFastSliding:velocityX > FloatScreenWidth / 2.0]; 222 | } 223 | } 224 | else { //正在显示悬浮球内容 225 | //右滑手势拖动超过一半,手指离开屏幕,也会从当前触摸位置缩小到悬浮球 226 | [animator replaceAnimation]; 227 | [self p_animationWillEnd]; 228 | } 229 | [interactive finishInteractiveTransition]; 230 | } 231 | else { //未触发pop,取消转场操作,动画回归 232 | [animator cancelInteractiveTransition]; 233 | if (!notShowFloatContent) { 234 | self.floatView.alpha = 0; 235 | } 236 | [interactive cancelInteractiveTransition]; 237 | } 238 | self.lastPopViewController = nil; 239 | } 240 | } 241 | 242 | #pragma mark 点击悬浮球,push到缩小的控制器 243 | - (void)tapFloatView:(UITapGestureRecognizer *)ges 244 | { 245 | if (!self.floatViewController) { 246 | return; 247 | } 248 | if ([[[NSObject currentNavigationController].childViewControllers lastObject] isEqual:self.floatView]) { 249 | return; 250 | } 251 | [self.cancelFloatView setCancelFloatViewShowing:YES]; 252 | [[NSObject currentNavigationController] pushViewController:self.floatViewController animated:YES]; 253 | } 254 | 255 | - (void)dragFloatView:(UIPanGestureRecognizer *)ges 256 | { 257 | CGPoint inSuperViewPoint = [ges locationInView:ges.view.superview]; 258 | if (ges.state == UIGestureRecognizerStateBegan) { 259 | [self.cancelFloatView setCancelFloatViewShowing:YES]; 260 | [self.cancelFloatView showCancelFloatViewWithProress:1 completion:nil]; 261 | } 262 | else if (ges.state == UIGestureRecognizerStateChanged) { 263 | //translationInView 以在ges.view上拖动点为坐标原点的相对位移 264 | CGPoint transitionP = [ges translationInView:ges.view]; 265 | CGFloat transitionX = MAX(FloatWidth / 2.0, MIN(self.floatView.center.x + transitionP.x, FloatScreenWidth - FloatWidth / 2.0)); 266 | CGFloat transitionY = MAX(FloatWidth / 2.0, MIN(self.floatView.center.y + transitionP.y, FloatScreenHeight - FloatWidth / 2.0)); 267 | self.floatView.center = CGPointMake(transitionX, transitionY); 268 | [ges setTranslation:CGPointZero inView:ges.view]; 269 | //移动过程中监控坐标位置是否进入右下角圆内 270 | [self p_floatMoveWithPoint:inSuperViewPoint]; 271 | } 272 | else if (ges.state == UIGestureRecognizerStateEnded || ges.state == UIGestureRecognizerStateCancelled) { 273 | FloatWeakSelf 274 | //手势结束,处理右下角1/4圆 275 | [self.cancelFloatView showCancelFloatViewWithProress:0 completion:^{ 276 | //圆消失后再将状态重置,防止在执行消失过程中让用户看见 277 | FloatStrongSelf 278 | if ([self p_checkTouchPointInRound:inSuperViewPoint]) { 279 | [self.cancelFloatView setCancelFloatViewShowing:NO]; 280 | } 281 | }]; 282 | 283 | //手势结束,处理悬浮球 284 | if ([self p_checkTouchPointInRound:inSuperViewPoint]) { 285 | // 手势在右下角1/4圆内停留,则隐藏悬浮窗口并且将缓存的控制器释放 286 | [self p_clearControllerAnimatorAndInteractive:self.lastPopViewController]; 287 | self.floatViewController = nil; 288 | self.lastPopViewController = nil; 289 | //悬浮球跟圆同时往右下移出屏幕 290 | [UIView animateWithDuration:FloatTranslationOutDuration animations:^{ 291 | self.floatView.origin = CGPointMake(FloatScreenWidth, FloatScreenHeight); 292 | } completion:^(BOOL finished) { 293 | self.showStyle = FloatShowStyleNormal; 294 | }]; 295 | } 296 | else { 297 | //手势结束,悬浮球不在圆内,让悬浮球靠边 298 | [UIView animateWithDuration:FloatTranslationOutDuration animations:^{ 299 | CGFloat minX = FloatMargin; 300 | CGFloat maxX = FloatScreenWidth - self.floatView.width - FloatMargin; 301 | CGFloat minY = FloatMargin; 302 | CGFloat maxY = FloatScreenHeight - self.floatView.height - FloatMargin; 303 | CGPoint point = CGPointZero; 304 | if (self.floatView.centerX < FloatScreenWidth / 2.0) { 305 | point.x = minX; 306 | point.y = MIN(MAX(minY, self.floatView.y), maxY); 307 | } 308 | else { 309 | point.x = maxX; 310 | point.y = MIN(MAX(minY, self.floatView.y), maxY); 311 | } 312 | self.floatView.origin = point; 313 | }]; 314 | } 315 | } 316 | } 317 | 318 | #pragma mark - NSNotificationCenter 319 | 320 | - (void)p_animationWillBegin 321 | { 322 | self.showStyle = FloatShowStyleShowContent; 323 | } 324 | 325 | - (void)p_animationWillEnd 326 | { 327 | self.showStyle = FloatShowStyleShow; 328 | } 329 | 330 | - (void)p_animationDidEnd:(NSNotification *)notification 331 | { 332 | UIViewController *fromVC = notification.object; 333 | [self p_clearControllerAnimatorAndInteractive:fromVC]; 334 | } 335 | 336 | #pragma mark - Private 337 | #pragma mark 悬浮球移动到右下角1/4圆内则隐藏悬浮球 338 | - (void)p_floatMoveWithPoint:(CGPoint)point 339 | { 340 | BOOL inRound = [self p_checkTouchPointInRound:point]; 341 | if (_touchInRound != inRound) { 342 | _touchInRound = inRound; 343 | if (inRound) { 344 | //进入圆内,改变状态并震动手动 345 | [self.cancelFloatView moveWithTouchInRound:YES]; 346 | [self p_shockPhone]; 347 | } 348 | else { 349 | [self.cancelFloatView moveWithTouchInRound:NO]; 350 | } 351 | } 352 | } 353 | 354 | //判断手势触摸点是否在圆内 355 | - (BOOL)p_checkTouchPointInRound:(CGPoint)point 356 | { 357 | CGPoint center = CGPointMake(FloatScreenWidth, FloatScreenHeight); 358 | double dx = fabs(point.x - center.x); 359 | double dy = fabs(point.y - center.y); 360 | double distance = hypot(dx, dy); 361 | //触摸点到圆心的距离小于半径,则代表触摸点在圆内 362 | return distance < RoundViewRadius; 363 | } 364 | 365 | #pragma mark 手势清除controller绑定的转场动画与转场交互 366 | - (void)p_clearControllerAnimatorAndInteractive:(UIViewController *)vc 367 | { 368 | objc_setAssociatedObject(vc, &kPopInteractiveKey, nil, OBJC_ASSOCIATION_ASSIGN); 369 | objc_setAssociatedObject(vc, &kAnimatorKey, nil, OBJC_ASSOCIATION_ASSIGN); 370 | } 371 | 372 | #pragma mark - 手机震动 373 | - (void)p_shockPhone 374 | { 375 | static BOOL canShock = YES; 376 | if (@available(iOS 10.0, *)) { 377 | if (!canShock) { 378 | return; 379 | } 380 | canShock = NO; 381 | UIImpactFeedbackGenerator *impactFeedBack = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; 382 | [impactFeedBack prepare]; 383 | [impactFeedBack impactOccurred]; 384 | //防止同时触发几个震动 385 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 386 | canShock = YES; 387 | }); 388 | } 389 | } 390 | 391 | #pragma mark - UIGestureRecognizerDelegate 392 | 393 | - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer 394 | { 395 | if ([NSObject currentNavigationController].viewControllers.count > 1) { 396 | if ([self.monitorVCClasses containsObject:NSStringFromClass([[NSObject currentViewController] class])]) { 397 | [[UIApplication sharedApplication].keyWindow addSubview:self.floatView]; 398 | self.lastPopViewController = [NSObject currentViewController]; 399 | } 400 | else { 401 | self.lastPopViewController = nil; 402 | } 403 | return YES; 404 | } 405 | return NO; 406 | } 407 | 408 | #pragma mark - UINavigationControllerDelegate 409 | /** 转场动画 */ 410 | - (id )navigationController:(UINavigationController *)navigationController 411 | animationControllerForOperation:(UINavigationControllerOperation)operation 412 | fromViewController:(UIViewController *)fromVC 413 | toViewController:(UIViewController *)toVC 414 | { 415 | return [[FloatBallManager shared] floatBallAnimationWithOperation:operation fromViewController:fromVC toViewController:toVC]; 416 | } 417 | 418 | /** 转场交互 */ 419 | - (id )navigationController:(UINavigationController *)navigationController 420 | interactionControllerForAnimationController:(id ) animationController 421 | { 422 | return [[FloatBallManager shared] floatInteractionControllerForAnimationController:animationController]; 423 | } 424 | 425 | - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated 426 | { 427 | [[FloatBallManager shared] didShowViewController:viewController navigationController:navigationController]; 428 | } 429 | 430 | #pragma mark - setter 431 | 432 | - (void)setShowStyle:(FloatShowStyle)showStyle 433 | { 434 | _showStyle = showStyle; 435 | switch (showStyle) { 436 | case FloatShowStyleNormal: 437 | self.floatView.hidden = YES; 438 | //回到普通状态,重置悬浮球位置 439 | self.floatView.origin = CGPointMake(FloatScreenWidth - FloatWidth - FloatMargin, FloatScreenHeight - RoundViewRadius - FloatWidth); 440 | break; 441 | case FloatShowStyleShow: 442 | self.floatView.alpha = 1; 443 | self.floatView.hidden = NO; 444 | break; 445 | case FloatShowStyleShowContent: 446 | self.floatView.alpha = 0; 447 | self.floatView.hidden = NO; 448 | break; 449 | } 450 | } 451 | 452 | #pragma mark - getter 453 | 454 | - (UIImageView *)floatView 455 | { 456 | if (!_floatView) { 457 | _floatView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"FloatBallResource.bundle/demo_link"]]; 458 | _floatView.layer.cornerRadius = 30; 459 | _floatView.layer.masksToBounds = YES; 460 | _floatView.backgroundColor = [UIColor colorWithRed:0.3 green:0.3 blue:0.3 alpha:0.95]; 461 | _floatView.userInteractionEnabled = YES; 462 | _floatView.contentMode = UIViewContentModeCenter; 463 | _floatView.frame = CGRectMake(FloatScreenWidth - FloatWidth - FloatMargin, FloatScreenHeight - RoundViewRadius - FloatWidth, FloatWidth, FloatWidth); 464 | _floatView.hidden = YES; 465 | //添加拖动手势 466 | [_floatView addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragFloatView:)]]; 467 | //添加点击手势 468 | [_floatView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapFloatView:)]]; 469 | } 470 | return _floatView; 471 | } 472 | 473 | - (CancelFloatView *)cancelFloatView 474 | { 475 | if (!_cancelFloatView) { 476 | _cancelFloatView = [[CancelFloatView alloc] initWithFrame:CGRectMake(FloatScreenWidth, FloatScreenHeight , RoundViewRadius, RoundViewRadius)]; 477 | [[UIApplication sharedApplication].keyWindow addSubview:_cancelFloatView]; 478 | [[UIApplication sharedApplication].keyWindow bringSubviewToFront:_floatView]; 479 | } 480 | return _cancelFloatView; 481 | } 482 | 483 | @end 484 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TMWu/WeChatFloatBall/3ee4a97ce80bef3341b3d0d864a62ceae01df780/WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float@2x.png -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TMWu/WeChatFloatBall/3ee4a97ce80bef3341b3d0d864a62ceae01df780/WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float@3x.png -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float_default2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TMWu/WeChatFloatBall/3ee4a97ce80bef3341b3d0d864a62ceae01df780/WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float_default2@2x.png -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float_default2@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TMWu/WeChatFloatBall/3ee4a97ce80bef3341b3d0d864a62ceae01df780/WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float_default2@3x.png -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float_default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TMWu/WeChatFloatBall/3ee4a97ce80bef3341b3d0d864a62ceae01df780/WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float_default@2x.png -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float_default@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TMWu/WeChatFloatBall/3ee4a97ce80bef3341b3d0d864a62ceae01df780/WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_cancel_float_default@3x.png -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_link@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TMWu/WeChatFloatBall/3ee4a97ce80bef3341b3d0d864a62ceae01df780/WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_link@2x.png -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_link@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TMWu/WeChatFloatBall/3ee4a97ce80bef3341b3d0d864a62ceae01df780/WeChatFloatBall/FloatBallManager/FloatBallResource.bundle/demo_link@3x.png -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatTransitionAnimator.h: -------------------------------------------------------------------------------- 1 | // 2 | // FloatTransitionAnimator.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/30. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | /** 转场动画类 */ 13 | @interface FloatTransitionAnimator : NSObject 14 | 15 | /** 转场动画类型 push/pop */ 16 | @property (nonatomic, assign) UINavigationControllerOperation operation; 17 | 18 | @property (nonatomic, strong) id transitionContext; 19 | 20 | /** 悬浮球圆心坐标 */ 21 | @property (nonatomic, assign, readonly) CGPoint center; 22 | /** 悬浮球半径 */ 23 | @property (nonatomic, assign, readonly) CGFloat radius; 24 | /** 遮罩 */ 25 | @property(nonatomic, strong, readonly) UIView *coverView; 26 | 27 | + (instancetype)animatorWithStartCenter:(CGPoint)center 28 | radius:(CGFloat)radius 29 | operation:(UINavigationControllerOperation)operation; 30 | 31 | - (instancetype)initWithStartCenter:(CGPoint)center 32 | radius:(CGFloat)radius 33 | operation:(UINavigationControllerOperation)operation; 34 | 35 | /** 替换新动画,用于手势拖动到某个位置,执行缩小动画 */ 36 | - (void)replaceAnimation; 37 | /** 继续执行动画,用于控制交互转场,手势结束个的动画执行 */ 38 | - (void)continueAnimationWithFastSliding:(BOOL)fastSliding; 39 | /** 更新交互转场的进度 */ 40 | - (void)updateInteractiveTransition:(CGFloat)percentComplete; 41 | /** 动画反转,用于手势拖动未达到pop条件时,将动画取消 */ 42 | - (void)cancelInteractiveTransition; 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/FloatTransitionAnimator.m: -------------------------------------------------------------------------------- 1 | // 2 | // FloatTransitionAnimator.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/30. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "FloatTransitionAnimator.h" 10 | #import "FloatBallDefine.h" 11 | #import "UIView+FloatFrame.h" 12 | #import "NSObject+ViewController.h" 13 | #import 14 | #import "FloatBallManager.h" 15 | 16 | @interface FloatTransitionAnimator () 17 | 18 | /** 悬浮球圆心坐标 */ 19 | @property (nonatomic, assign) CGPoint center; 20 | /** 悬浮球半径 */ 21 | @property (nonatomic, assign) CGFloat radius; 22 | /** 遮罩 */ 23 | @property(nonatomic, strong) UIView *coverView; 24 | 25 | @end 26 | 27 | @implementation FloatTransitionAnimator 28 | 29 | + (instancetype)animatorWithStartCenter:(CGPoint)center 30 | radius:(CGFloat)radius 31 | operation:(UINavigationControllerOperation)operation 32 | { 33 | return [[self alloc] initWithStartCenter:center radius:radius operation:operation]; 34 | } 35 | 36 | - (instancetype)initWithStartCenter:(CGPoint)center 37 | radius:(CGFloat)radius 38 | operation:(UINavigationControllerOperation)operation 39 | { 40 | self = [super init]; 41 | if (self) { 42 | _center = center; 43 | _radius = radius; 44 | _operation = operation; 45 | } 46 | return self; 47 | } 48 | 49 | - (NSTimeInterval)transitionDuration:(nullable id )transitionContext 50 | { 51 | return AnimationDuration; 52 | } 53 | 54 | - (void)animateTransition:(id )transitionContext 55 | { 56 | self.transitionContext = transitionContext; 57 | if (_operation == UINavigationControllerOperationPush) { 58 | //push转场动画执行前,隐藏悬浮球 59 | [[NSNotificationCenter defaultCenter] postNotificationName:AnimationWillBeginKey object:nil]; 60 | [self animatePushAnimation:transitionContext]; 61 | } 62 | else if (_operation == UINavigationControllerOperationPop) { 63 | [self animatePopAnimation:transitionContext]; 64 | } 65 | } 66 | 67 | #pragma mark Push转场动画 68 | - (void)animatePushAnimation:(id)transitionContext 69 | { 70 | UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 71 | UIView *containerView = [transitionContext containerView]; 72 | [containerView addSubview:self.coverView]; 73 | [containerView addSubview:toVC.view]; 74 | CGRect floatRect = CGRectMake(self.center.x - self.radius, self.center.y - self.radius, self.radius * 2, self.radius *2); 75 | UIBezierPath *startPath = [UIBezierPath bezierPathWithRoundedRect:floatRect cornerRadius:self.radius]; 76 | UIBezierPath *endPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(-self.radius, -self.radius, FloatScreenWidth + self.radius * 2, FloatScreenHeight + self.radius * 2) cornerRadius:self.radius]; 77 | CAShapeLayer *maskLayer = [CAShapeLayer layer]; 78 | maskLayer.path = endPath.CGPath; 79 | toVC.view.layer.mask = maskLayer; 80 | CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; 81 | maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath); 82 | maskLayerAnimation.toValue = (__bridge id)((endPath.CGPath)); 83 | maskLayerAnimation.duration = AnimationDuration; 84 | maskLayerAnimation.delegate = self; 85 | [maskLayer addAnimation:maskLayerAnimation forKey:@"xw_path"]; 86 | 87 | self.coverView.alpha = 0; 88 | [UIView animateWithDuration:AnimationDuration animations:^{ 89 | self.coverView.alpha = 0.8; 90 | }]; 91 | } 92 | 93 | #pragma mark Pop转场动画 94 | 95 | - (void)animatePopAnimation:(id)transitionContext 96 | { 97 | UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 98 | UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 99 | UIView *containerView = [transitionContext containerView]; 100 | containerView.backgroundColor = [UIColor whiteColor]; 101 | [containerView insertSubview:toVC.view atIndex:0]; 102 | 103 | NSNumber *popWithPanGesNumber = objc_getAssociatedObject([NSObject currentNavigationController], &kPopWithPanGes); 104 | BOOL popWithPanGes = [popWithPanGesNumber boolValue]; 105 | if (popWithPanGes) { 106 | //模拟系统动画,底部view也有一个向右偏移动画 107 | toVC.view.x = -FloatScreenWidth / 4.0; 108 | } 109 | else { 110 | [toVC.view addSubview:self.coverView]; 111 | 112 | CGRect floatRect = CGRectMake(self.center.x - self.radius, self.center.y - self.radius, self.radius * 2, self.radius *2); 113 | 114 | UIBezierPath *endPath = [UIBezierPath bezierPathWithRoundedRect:floatRect cornerRadius:self.radius]; 115 | CAShapeLayer *maskLayer = (CAShapeLayer *)fromVC.view.layer.mask; 116 | CGPathRef startPath = maskLayer.path; 117 | maskLayer.path = endPath.CGPath; 118 | CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; 119 | maskLayerAnimation.fromValue = (__bridge id)(startPath); 120 | maskLayerAnimation.toValue = (__bridge id)(endPath.CGPath); 121 | maskLayerAnimation.duration = AnimationDuration; 122 | maskLayerAnimation.delegate = self; 123 | [maskLayer addAnimation:maskLayerAnimation forKey:@"xw_path"]; 124 | 125 | self.coverView.alpha = 1.0; 126 | [UIView animateWithDuration:AnimationDuration animations:^{ 127 | self.coverView.alpha = 0; 128 | }]; 129 | } 130 | } 131 | 132 | /** 替换新动画,用于手势拖动到某个位置,执行缩小动画 */ 133 | - (void)replaceAnimation 134 | { 135 | self.coverView.alpha = 0; 136 | UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 137 | UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 138 | [fromVC.view addSubview:self.coverView]; 139 | //当前fromVC.view有偏移,需要重置 140 | CGFloat currentX = fromVC.view.x; 141 | fromVC.view.x = 0; 142 | CGFloat endFloatX = self.center.x - self.radius; 143 | CGRect floatRect = CGRectMake(endFloatX, self.center.y - self.radius, self.radius * 2, self.radius *2); 144 | 145 | UIBezierPath *startPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(currentX, -self.radius, FloatScreenWidth + self.radius * 2, FloatScreenHeight + self.radius * 2) cornerRadius:self.radius]; 146 | UIBezierPath *endPath = [UIBezierPath bezierPathWithRoundedRect:floatRect cornerRadius:self.radius]; 147 | CAShapeLayer *maskLayer = [CAShapeLayer layer]; 148 | maskLayer.path = endPath.CGPath; 149 | fromVC.view.layer.mask = maskLayer; 150 | CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; 151 | maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath); 152 | maskLayerAnimation.toValue = (__bridge id)(endPath.CGPath); 153 | maskLayerAnimation.duration = ContinueAnimationDuration; 154 | maskLayerAnimation.delegate = self; 155 | [maskLayer addAnimation:maskLayerAnimation forKey:@"xw_path"]; 156 | 157 | [UIView animateWithDuration:ContinueAnimationDuration animations:^{ 158 | self.coverView.alpha = 0.3; 159 | toVC.view.x = 0; 160 | }]; 161 | } 162 | 163 | /** 继续执行动画,用于控制交互转场,手势结束个的动画执行 */ 164 | - (void)continueAnimationWithFastSliding:(BOOL)fastSliding 165 | { 166 | UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 167 | UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 168 | CGFloat duration = ContinueAnimationDuration; 169 | if (!fastSliding) { 170 | duration = (1 - fromVC.view.x / FloatScreenWidth) * AnimationDuration; 171 | } 172 | [UIView animateWithDuration:duration animations:^{ 173 | fromVC.view.x = FloatScreenWidth; 174 | toVC.view.x = 0; 175 | } completion:^(BOOL finished) { 176 | [self p_endAnimator]; 177 | }]; 178 | } 179 | 180 | /** 更新交互转场的进度 */ 181 | - (void)updateInteractiveTransition:(CGFloat)percentComplete 182 | { 183 | UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 184 | UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 185 | fromVC.view.x = FloatScreenWidth * percentComplete; 186 | toVC.view.x = -(FloatScreenWidth / 4.0 * (1- percentComplete)); 187 | } 188 | 189 | /** 动画反转,用于手势拖动未达到pop条件时,将动画取消 */ 190 | - (void)cancelInteractiveTransition 191 | { 192 | UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 193 | UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 194 | CGFloat percentComplete = fromVC.view.x / FloatScreenWidth; 195 | [UIView animateWithDuration:AnimationDuration * percentComplete animations:^{ 196 | fromVC.view.x = 0; 197 | toVC.view.x = -FloatScreenWidth / 4.0; 198 | } completion:^(BOOL finished) { 199 | toVC.view.x = 0; 200 | [self.transitionContext completeTransition:![self.transitionContext transitionWasCancelled]]; 201 | }]; 202 | } 203 | 204 | #pragma mark - Private 205 | 206 | - (void)p_endAnimator 207 | { 208 | [_coverView removeFromSuperview]; 209 | if (_operation == UINavigationControllerOperationPop) { 210 | //执行完之后清理绑定,防止循环引用 211 | UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 212 | [[NSNotificationCenter defaultCenter] postNotificationName:AnimationDidEndKey object:fromVC]; 213 | } 214 | [self.transitionContext completeTransition:![self.transitionContext transitionWasCancelled]]; 215 | } 216 | 217 | #pragma mark - CAAnimationDelegate 218 | 219 | - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag 220 | { 221 | //pop转场动画执行完,显示悬浮球 222 | if (_operation == UINavigationControllerOperationPop) { 223 | [[NSNotificationCenter defaultCenter] postNotificationName:AnimationWillEndKey object:nil]; 224 | } 225 | [self p_endAnimator]; 226 | } 227 | 228 | #pragma mark - getter 229 | 230 | - (UIView *)coverView 231 | { 232 | if (!_coverView) { 233 | _coverView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds]; 234 | _coverView.backgroundColor = [UIColor blackColor]; 235 | }; 236 | return _coverView; 237 | } 238 | 239 | @end 240 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/NSObject+ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+ViewController.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/31. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface NSObject (ViewController) 13 | 14 | + (UIViewController *)currentViewController; 15 | 16 | + (UINavigationController *)currentNavigationController; 17 | 18 | + (UITabBarController *)currentTabBarController; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/NSObject+ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+ViewController.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/31. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "NSObject+ViewController.h" 10 | 11 | @implementation NSObject (ViewController) 12 | 13 | + (UIViewController *)currentViewController 14 | { 15 | UIViewController *result = nil; 16 | UIWindow *window = [[UIApplication sharedApplication] keyWindow]; 17 | if (window.windowLevel != UIWindowLevelNormal) { 18 | NSArray *windows = [[UIApplication sharedApplication] windows]; 19 | for(UIWindow *tmpWin in windows) { 20 | if (tmpWin.windowLevel == UIWindowLevelNormal) { 21 | window = tmpWin; 22 | break; 23 | } 24 | } 25 | } 26 | 27 | UIView *frontView = [[window subviews] objectAtIndex:0]; 28 | id nextResponder = [frontView nextResponder]; 29 | 30 | if ([nextResponder isKindOfClass:[UIViewController class]]) { 31 | UIViewController *vc = (UIViewController *)nextResponder; 32 | if ([vc isKindOfClass:[UITabBarController class]]) { 33 | vc = ((UITabBarController *) vc).selectedViewController; 34 | } 35 | if ([vc isKindOfClass:[UINavigationController class]]) { 36 | vc = ((UINavigationController *) vc).visibleViewController; 37 | } 38 | if (vc.presentedViewController) { 39 | vc = vc.presentedViewController; 40 | } 41 | while (vc.childViewControllers.count) { 42 | vc = [vc.childViewControllers lastObject]; 43 | } 44 | result = vc; 45 | } 46 | else { 47 | result = window.rootViewController; 48 | } 49 | return result; 50 | } 51 | 52 | + (UINavigationController *)currentNavigationController 53 | { 54 | return [self currentViewController].navigationController; 55 | } 56 | 57 | + (UITabBarController *)currentTabBarController 58 | { 59 | return [self currentViewController].tabBarController; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/UINavigationController+FloatBall.h: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+FloatBall.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/9/3. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UINavigationController (FloatBall) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/UINavigationController+FloatBall.m: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+FloatBall.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/9/3. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "UINavigationController+FloatBall.h" 10 | #import "FloatBallManager.h" 11 | #import 12 | 13 | @implementation UINavigationController (FloatBall) 14 | 15 | //+ (void)load 16 | //{ 17 | // method_exchangeImplementations(class_getInstanceMethod([self class], @selector(popViewControllerAnimated:)), class_getInstanceMethod([self class], @selector(float_popViewControllerAnimated:))); 18 | // 19 | // method_exchangeImplementations(class_getInstanceMethod([self class], @selector(pushViewController:animated:)), class_getInstanceMethod([self class], @selector(float_pushViewController:animated:))); 20 | //} 21 | // 22 | //- (UIViewController *)float_popViewControllerAnimated:(BOOL)animated 23 | //{ 24 | // UIViewController *vc = [self float_popViewControllerAnimated:animated]; 25 | // //pop操作有可能取消,在内部重新禁用系统手势 26 | // if (vc && [[FloatBallManager shared].classes containsObject:NSStringFromClass([vc class])]) { 27 | // self.interactivePopGestureRecognizer.enabled = YES; 28 | // } 29 | // return vc; 30 | //} 31 | // 32 | //- (void)float_pushViewController:(UIViewController *)viewController animated:(BOOL)animated{ 33 | // //push到要监控的控制器时,将系统右滑手势禁用 34 | // if ([[FloatBallManager shared].classes containsObject:NSStringFromClass([viewController class])]) { 35 | // self.interactivePopGestureRecognizer.enabled = NO; 36 | // // 边缘手势 37 | // UIScreenEdgePanGestureRecognizer *gesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:[FloatBallManager shared] action:@selector(handleNavigationTransition:)]; 38 | // gesture.edges = UIRectEdgeLeft; 39 | // gesture.delegate = [FloatBallManager shared]; 40 | // [viewController.view addGestureRecognizer:gesture]; 41 | // } 42 | // else { 43 | // self.interactivePopGestureRecognizer.enabled = YES; 44 | // } 45 | // [self float_pushViewController:viewController animated:animated]; 46 | //} 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/UIView+FloatFrame.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+FloatFrame.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/31. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIView (FloatFrame) 12 | 13 | @property (nonatomic, assign) CGFloat x; 14 | @property (nonatomic, assign) CGFloat y; 15 | @property (nonatomic, assign) CGFloat width; 16 | @property (nonatomic, assign) CGFloat height; 17 | @property (nonatomic, assign) CGSize size; 18 | @property (nonatomic, assign) CGPoint origin; 19 | @property (nonatomic, assign) CGFloat centerX; 20 | @property (nonatomic, assign) CGFloat centerY; 21 | 22 | @property (nonatomic, assign) CGFloat top; 23 | @property (nonatomic, assign) CGFloat bottom; 24 | @property (nonatomic, assign) CGFloat left; 25 | @property (nonatomic, assign) CGFloat right; 26 | 27 | @property (nonatomic, assign, readonly) CGFloat middleX; 28 | @property (nonatomic, assign, readonly) CGFloat middleY; 29 | @property (nonatomic, assign, readonly) CGPoint middlePoint; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /WeChatFloatBall/FloatBallManager/UIView+FloatFrame.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+FloatFrame.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/31. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "UIView+FloatFrame.h" 10 | 11 | @implementation UIView (FloatFrame) 12 | 13 | - (CGFloat)x 14 | { 15 | return self.frame.origin.x; 16 | } 17 | 18 | - (void)setX:(CGFloat)x 19 | { 20 | CGRect frame = self.frame; 21 | frame.origin.x = x; 22 | self.frame = frame; 23 | } 24 | 25 | - (CGFloat)y 26 | { 27 | return self.frame.origin.y; 28 | } 29 | 30 | - (void)setY:(CGFloat)y 31 | { 32 | CGRect frame = self.frame; 33 | frame.origin.y = y; 34 | self.frame = frame; 35 | } 36 | 37 | - (CGFloat)width 38 | { 39 | return self.frame.size.width; 40 | } 41 | 42 | - (void)setWidth:(CGFloat)width 43 | { 44 | CGRect frame = self.frame; 45 | frame.size.width = width; 46 | self.frame = frame; 47 | } 48 | 49 | - (CGFloat)height 50 | { 51 | return self.frame.size.height; 52 | } 53 | 54 | - (void)setHeight:(CGFloat)height 55 | { 56 | CGRect frame = self.frame; 57 | frame.size.height = height; 58 | self.frame = frame; 59 | } 60 | 61 | - (CGSize)size 62 | { 63 | return self.frame.size; 64 | } 65 | 66 | - (void)setSize:(CGSize)size 67 | { 68 | if (isnan(size.width)) 69 | { 70 | size.width = 0; 71 | } 72 | if (isnan(size.height)) 73 | { 74 | size.height = 0; 75 | } 76 | CGRect frame = self.frame; 77 | frame.size = size; 78 | self.frame = frame; 79 | } 80 | 81 | - (CGPoint)origin 82 | { 83 | return self.frame.origin; 84 | } 85 | 86 | - (void)setOrigin:(CGPoint)origin 87 | { 88 | CGRect frame = self.frame; 89 | frame.origin = origin; 90 | self.frame = frame; 91 | } 92 | 93 | - (CGFloat)centerX 94 | { 95 | return self.center.x; 96 | } 97 | 98 | - (void)setCenterX:(CGFloat)centerX 99 | { 100 | CGPoint center = self.center; 101 | center.x = centerX; 102 | self.center = center; 103 | } 104 | 105 | - (CGFloat)centerY 106 | { 107 | return self.center.y; 108 | } 109 | 110 | - (void)setCenterY:(CGFloat)centerY 111 | { 112 | CGPoint center = self.center; 113 | center.y = centerY; 114 | self.center = center; 115 | } 116 | 117 | - (CGFloat)top 118 | { 119 | 120 | return self.frame.origin.y; 121 | } 122 | 123 | - (void)setTop:(CGFloat)top 124 | { 125 | 126 | CGRect newFrame = self.frame; 127 | newFrame.origin.y = top; 128 | self.frame = newFrame; 129 | } 130 | 131 | - (CGFloat)bottom 132 | { 133 | 134 | return self.frame.origin.y + self.frame.size.height; 135 | } 136 | 137 | - (void)setBottom:(CGFloat)bottom 138 | { 139 | 140 | CGRect newFrame = self.frame; 141 | newFrame.origin.y = bottom - self.frame.size.height; 142 | self.frame = newFrame; 143 | } 144 | 145 | - (CGFloat)left 146 | { 147 | 148 | return self.frame.origin.x; 149 | } 150 | 151 | - (void)setLeft:(CGFloat)left 152 | { 153 | 154 | CGRect newFrame = self.frame; 155 | newFrame.origin.x = left; 156 | self.frame = newFrame; 157 | } 158 | 159 | - (CGFloat)right 160 | { 161 | 162 | return self.frame.origin.x + self.frame.size.width; 163 | } 164 | 165 | - (void)setRight:(CGFloat)right 166 | { 167 | 168 | CGRect newFrame = self.frame; 169 | newFrame.origin.x = right - self.frame.size.width; 170 | self.frame = newFrame; 171 | } 172 | 173 | - (CGFloat)middleX 174 | { 175 | 176 | return CGRectGetWidth(self.bounds) / 2.f; 177 | } 178 | 179 | - (CGFloat)middleY 180 | { 181 | 182 | return CGRectGetHeight(self.bounds) / 2.f; 183 | } 184 | 185 | - (CGPoint)middlePoint 186 | { 187 | 188 | return CGPointMake(CGRectGetWidth(self.bounds) / 2.f, CGRectGetHeight(self.bounds) / 2.f); 189 | } 190 | 191 | @end 192 | -------------------------------------------------------------------------------- /WeChatFloatBall/SecondViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SecondViewController.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/30. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SecondViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /WeChatFloatBall/SecondViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SecondViewController.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/30. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "SecondViewController.h" 10 | #import "ViewController.h" 11 | #import "UIView+FloatFrame.h" 12 | 13 | @interface SecondViewController () 14 | @property (nonatomic, strong) UIButton *pushButton; 15 | @end 16 | 17 | @implementation SecondViewController 18 | 19 | - (void)dealloc 20 | { 21 | NSLog(@"SecondViewController(测试页面) -- dealloc"); 22 | } 23 | 24 | - (void)viewDidLoad 25 | { 26 | [super viewDidLoad]; 27 | self.title = @"测试页面"; 28 | self.view.backgroundColor = [UIColor whiteColor]; 29 | [self.view addSubview:self.pushButton]; 30 | } 31 | 32 | - (void)viewDidLayoutSubviews 33 | { 34 | [super viewDidLayoutSubviews]; 35 | self.pushButton.size = CGSizeMake(100, 40); 36 | self.pushButton.center = self.view.center; 37 | } 38 | 39 | - (void)pushAction:(id)sender 40 | { 41 | ViewController *vc = [ViewController new]; 42 | [self.navigationController pushViewController:vc animated:YES]; 43 | } 44 | 45 | #pragma mark - getter 46 | 47 | - (UIButton *)pushButton 48 | { 49 | if (!_pushButton) { 50 | _pushButton = [UIButton buttonWithType:UIButtonTypeSystem]; 51 | [_pushButton setTitle:@"跳转正常页面" forState:UIControlStateNormal]; 52 | [_pushButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; 53 | [_pushButton addTarget:self action:@selector(pushAction:) forControlEvents:UIControlEventTouchUpInside]; 54 | } 55 | return _pushButton; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /WeChatFloatBall/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /WeChatFloatBall/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /WeChatFloatBall/Supporting Files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /WeChatFloatBall/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | 高仿微信悬浮球 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /WeChatFloatBall/Supporting Files/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/29. 6 | // Copyright © 2018年 TIM. 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 | -------------------------------------------------------------------------------- /WeChatFloatBall/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/29. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /WeChatFloatBall/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // WebViewDemo 4 | // 5 | // Created by TIM on 2018/8/29. 6 | // Copyright © 2018年 TIM. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "UIView+FloatFrame.h" 11 | #import "SecondViewController.h" 12 | #import "FloatBallDefine.h" 13 | 14 | @interface ViewController () 15 | @property (nonatomic, strong) UIButton *pushButton; 16 | @end 17 | 18 | @implementation ViewController 19 | 20 | - (void)dealloc 21 | { 22 | NSLog(@"ViewController(正常页面) -- dealloc"); 23 | } 24 | 25 | - (void)viewDidLoad 26 | { 27 | [super viewDidLoad]; 28 | self.title = @"正常页面"; 29 | self.view.backgroundColor = [UIColor whiteColor]; 30 | [self.view addSubview:self.pushButton]; 31 | } 32 | 33 | - (void)viewDidLayoutSubviews 34 | { 35 | [super viewDidLayoutSubviews]; 36 | self.pushButton.size = CGSizeMake(100, 40); 37 | self.pushButton.center = self.view.center; 38 | } 39 | 40 | - (void)pushAction:(id)sender 41 | { 42 | SecondViewController *vc = [SecondViewController new]; 43 | vc.view.backgroundColor = [UIColor colorWithRed:(arc4random() % 255) / 255.0 green:(arc4random() % 255) / 255.0 blue:(arc4random() % 255) / 255.0 alpha:1.0]; 44 | [self.navigationController pushViewController:vc animated:YES]; 45 | } 46 | 47 | #pragma mark - getter 48 | 49 | - (UIButton *)pushButton 50 | { 51 | if (!_pushButton) { 52 | _pushButton = [UIButton buttonWithType:UIButtonTypeSystem]; 53 | [_pushButton setTitle:@"跳转测试页面" forState:UIControlStateNormal]; 54 | [_pushButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; 55 | [_pushButton addTarget:self action:@selector(pushAction:) forControlEvents:UIControlEventTouchUpInside]; 56 | } 57 | return _pushButton; 58 | } 59 | 60 | @end 61 | --------------------------------------------------------------------------------