MMDrawerController 侧边抽屉导航完全指南:集成、手势配置与自定义动画
移动开发UI组件【免费下载链接】MMDrawerControllerA lightweight, easy to use, Side Drawer Navigation Controller项目地址https://gitcode.com/gh_mirrors/mm/MMDrawerController点击查看免费下载MMDrawerController是一个轻量、易用的侧边抽屉Side Drawer导航容器控制器专为采用抽屉导航范式的 iOS 应用设计。它本质上与UINavigationController、UITabBarController类似是一种容器控制器最多容纳 Center、LeftDrawer、RightDrawer 三个子控制器并专门提供「打开/关闭抽屉」的自定义动画能力。阅读完本文你将掌握如何通过 CocoaPods 集成该库、用几个初始化方法搭建抽屉、按位掩码精细控制打开/关闭手势、用回调 Block 定制任意视觉动画以及状态恢复、iOS 7 状态栏等进阶能力。一、MMDrawerController 是什么官方描述见 README.md将它定位为「专门以轻量、聚焦的方式支持侧边抽屉导航的库」它不做顶部/底部抽屉、不做同时展示两个抽屉、不做最小抽屉宽度也不支持UITabBarController、UISplitViewController等容器作为中心控制器更不支持像 Google 那样把抽屉覆盖在中心控制器之上。这种「明确不做」的取舍正是它保持可维护性的代价。从源码结构看核心代码集中在 MMDrawerController/ 目录下由五个文件构成完整的体系文件职责MMDrawerController.h公开 API、三个枚举MMDrawerSide、MMOpenDrawerGestureMode、MMCloseDrawerGestureMode与MMDrawerOpenCenterInteractionModeMMDrawerController.m容器视图管理、手势回调、开合动画、状态恢复、阴影与状态栏实现约 1479 行MMDrawerVisualState.h / .m预置的四种抽屉视觉状态 BlockUIViewControllerMMDrawerController.h让任意子控制器反向访问父级抽屉控制器MMDrawerControllerSubclass.h暴露给子类使用的受保护方法MMDrawerBarButtonItem.h汉堡菜单按钮UIBarButtonItem子类配套的示例工程位于 KitchenSink/ExampleFiles/其中 MMAppDelegate.m 展示了完整的集成写法可作为本文所有示例的真实落地参照。二、安装 MMDrawerController官方推荐通过 CocoaPods 安装在Podfile中添加pod MMDrawerController, ~ 0.5.7当前仓库中的 MMDrawerController.podspec 声明的版本为0.6.0对应 CHANGELOG.md 中 2015 年 4 月 2 日发布的 0.6.0该版本移除了 iOS 6 支持平台要求为ios, 7.0并启用requires_arc。podspec 将库拆分为四个 subspec可按需引入Core核心容器与UIViewControllerMMDrawerController分类依赖QuartzCore框架MMDrawerBarButtonItem汉堡菜单按钮依赖 CoreMMDrawerVisualStates预置动画 Block依赖 CoreSubclass子类化头文件依赖 Core。因此若只需要核心功能可以直接pod MMDrawerController/Core要用预置动画再叠加MMDrawerVisualStatessubspec。三、快速创建 Drawer Controller官方在 README 中给出的创建方式非常简单先创建中心控制器与抽屉控制器再一次性init即可UIViewController * leftDrawer [[UIViewController alloc] init]; UIViewController * center [[UIViewController alloc] init]; UIViewController * rightDrawer [[UIViewController alloc] init]; MMDrawerController * drawerController [[MMDrawerController alloc] initWithCenterViewController:center leftDrawerViewController:leftDrawer rightDrawerViewController:rightDrawer];对照 MMDrawerController.hMMDrawerController共提供三个初始化方法initWithCenterViewController:leftDrawerViewController:rightDrawerViewController:—— 完整形态中心控制器不能为 nil源码中NSParameterAssert(centerViewController)见 MMDrawerController.minitWithCenterViewController:leftDrawerViewController:—— 只有左侧抽屉initWithCenterViewController:rightDrawerViewController:—— 只有右侧抽屉。后两者在实现上分别把另一侧抽屉传入 nil。抽屉控制器的 frame 完全由容器管理左右抽屉的尺寸依据maximumLeftDrawerWidth/maximumRightDrawerWidth自动计算中心控制器的尺寸自动等于容器尺寸见 MMDrawerController.h因此不要从外部手动修改这些子控制器的 frame。完成创建后把drawerController设置为 window 的 rootViewController 即可示例工程正是这样做的见 MMAppDelegate.m。四、核心属性与默认值MMDrawerController的可调参数集中在commonSetup中初始化见 MMDrawerController.m下表汇总了各属性的默认值均为源码确认的事实属性默认值说明maximumLeftDrawerWidth/maximumRightDrawerWidth280.0f抽屉最大宽度对应抽屉为 nil 时读取返回 0animationVelocity840.0f点/秒约为默认抽屉宽度的 3 倍即约 1/3 秒开合完毕内置动画有 0.15 秒的最短时长下限MMDrawerMinimumAnimationDurationshouldStretchDrawerYES是否允许拖拽超出最大宽度时产生「拉伸」效果openDrawerGestureModeMaskMMOpenDrawerGestureModeNone打开抽屉的手势掩码closeDrawerGestureModeMaskMMCloseDrawerGestureModeNone关闭抽屉的手势掩码centerHiddenInteractionModeMMDrawerOpenCenterInteractionModeNavigationBarOnly抽屉打开时中心控制器的可交互范围showsShadowYES抽屉打开时中心控制器是否绘制阴影shadowRadius/shadowOpacity/shadowOffset/shadowColor10.0f / 0.8f / (0, -3) / 黑色中心控制器阴影参数showsStatusBarBackgroundViewNO是否绘制状态栏背景视图statusBarViewBackgroundColor[UIColor blackColor]状态栏背景视图颜色bezelPanningCenterViewRange20.0f中心视图边缘「边框」手势的有效范围点panVelocityXAnimationThreshold200.0f手势松手时判定「快速滑动开/关」的横向速度阈值点/秒此外还有只读属性openSide当前打开的抽屉侧MMDrawerSideNone/MMDrawerSideLeft/MMDrawerSideRight、visibleLeftDrawerWidth与visibleRightDrawerWidth当前可见宽度换中心控制器做全屏关闭动画时可能大于最大宽度。常用操作 API 归纳如下声明见 MMDrawerController.htoggleDrawerSide:animated:completion:—— 切换开合若你请求打开的一侧与当前打开侧不同则不会发生任何切换completion 会以finished NO回调openDrawerSide:animated:completion:/closeDrawerAnimated:completion:—— 显式打开/关闭setCenterViewController:withCloseAnimation:completion:—— 更换中心控制器并从当前位置带动画关闭抽屉setCenterViewController:withFullCloseAnimation:completion:—— 更换中心控制器旧中心控制器先滑出屏幕、设置新中心控制器后跨全屏宽度关闭抽屉setMaximumLeftDrawerWidth:animated:completion:/setMaximumRightDrawerWidth:animated:completion:—— 抽屉打开时可动画调整宽度宽度必须大于 0bouncePreviewForDrawerSide:completion:—— 让抽屉「弹一下」以提示用户它的存在。五、手势支持打开与关闭掩码MMDrawerController通过两套位掩码控制手势一套用于打开openDrawerGestureModeMask一套用于关闭closeDrawerGestureModeMask枚举定义见 MMDrawerController.h。MMOpenDrawerGestureMode打开手势选项行为MMOpenDrawerGestureModePanningNavigationBar在导航栏任意位置拖动打开MMOpenDrawerGestureModePanningCenterView在中心视图任意位置拖动打开MMOpenDrawerGestureModeBezelPanningCenterView在距屏幕边缘 20 点内开始拖动打开MMOpenDrawerGestureModeCustom由开发者提供的回调 Block 决定是否识别手势MMOpenDrawerGestureModeAll上述所有方式的总和MMCloseDrawerGestureMode关闭手势选项行为MMCloseDrawerGestureModePanningNavigationBar在导航栏任意位置拖动关闭MMCloseDrawerGestureModePanningCenterView在中心视图任意位置拖动关闭MMCloseDrawerGestureModeBezelPanningCenterView在中心视图边缘范围内开始拖动关闭MMCloseDrawerGestureModeTapNavigationBar点击导航栏关闭MMCloseDrawerGestureModeTapCenterView点击中心视图关闭MMCloseDrawerGestureModePanningDrawerView在抽屉视图上任意位置拖动关闭MMCloseDrawerGestureModeCustom开发者回调 Block 决定是否识别MMCloseDrawerGestureModeAll上述所有方式的总和打开与关闭可以自由组合任意搭配。需要特别注意的是这些手势会拦截发给子控制器的触摸事件必须谨慎使用。README 给出的反面教材是——如果中心控制器是MKMapView绝不要开启MMOpenDrawerGestureModePanningCenterView否则拖动画地图的手势会被抽屉手势抢走。掩码的工作机制从源码可确认每个抽屉控制器在viewDidLoad时注册一个UIPanGestureRecognizer和一个UITapGestureRecognizer见 MMDrawerController.mMMDrawerController自身作为手势代理。gestureRecognizer:shouldReceiveTouch:会依据当前openSide决定走「打开判定」还是「关闭判定」MMDrawerController.m当抽屉关闭时把触摸点与导航栏区域、中心内容区域、左右边缘区域做命中测试isPointContainedWithinNavigationRect:等MMDrawerController.m再与掩码做位与运算只有命中掩码中的选项才把触摸交给手势。示例工程在 MMAppDelegate.m 中直接启用了MMOpenDrawerGestureModeAll与MMCloseDrawerGestureModeAll即几乎所有方式都能开关抽屉而侧边抽屉示例 MMExampleSideDrawerViewController.m 则用^逐位切换各手势项演示了掩码的按位操作。5.1 自定义手势识别Custom Gesture Recognizer Support自 0.3.0 起可以通过setGestureShouldRecognizeTouchBlock:提供一个回调 Block自行决定某个手势是否应被识别。Block 接收三个参数抽屉控制器、手势对象、触摸对象只有当掩码中包含了MMOpenDrawerGestureModeCustom或MMCloseDrawerGestureModeCustom时该 Block 才会被咨询。README 给出的典型场景中心视图里有若干子视图只想让「触摸起点落在某个子视图内」时才允许拖动打开抽屉[myDrawerController setGestureShouldRecognizeTouchBlock:^BOOL(MMDrawerController *drawerController, UIGestureRecognizer *gesture, UITouch *touch) { BOOL shouldRecognizeTouch NO; if(drawerController.openSide MMDrawerSideNone [gesture isKindOfClass:[UIPanGestureRecognizer class]]){ UIView * customView [drawerController.centerViewController myCustomSubview]; CGPoint location [touch locationInView:customView]; shouldRecognizeTouch (CGRectContainsPoint(customView.bounds, location)); } return shouldRecognizeTouch; }];注意这种情况下不要再让openDrawerGestureModeMask包含MMOpenDrawerGestureModePanningCenterView否则系统会自动在中心视图任意位置应用该手势自定义判断就失效了。源码层面自定义 Block 的结果会被合并进possibleOpenGestureModes/possibleCloseGestureModes见 MMDrawerController.m与 README 的描述完全一致。另外setGestureCompletionBlock:可以在一次手势开合动画完成后收到通知此时查询openSide即可得知抽屉的新状态见 MMDrawerController.h。六、中心控制器交互模式当抽屉处于打开状态时可以通过centerHiddenInteractionMode控制用户对中心视图的交互能力枚举见 MMDrawerController.hMMDrawerOpenCenterInteractionModeNone无法与中心视图的任何内容交互MMDrawerOpenCenterInteractionModeFull可以与中心视图的全部内容交互MMDrawerOpenCenterInteractionModeNavigationBarOnly只能与导航栏内容交互保证菜单按钮仍可点击以关闭抽屉这是默认设置。该模式的拦截是在MMDrawerCenterContainerView的hitTest:withEvent:中实现的MMDrawerController.m当抽屉打开时若命中点不在导航栏区域内且模式为NavigationBarOnly或模式为None则直接返回 nil 让触摸穿透。注意该设置对MMCloseDrawerGestureMode没有任何影响——也就是说即使中心视图完全不可交互通过导航栏/抽屉视图关闭抽屉的手势依然有效。七、从子控制器访问抽屉控制器在任何子控制器内都可以借助UIViewControllerMMDrawerController分类直接查询父级抽屉控制器mm_drawerController向上遍历整个父控制器链即使子控制器被嵌在UINavigationController里、导航控制器才是直接父级也能正确返回最外层的MMDrawerController实现见 UIViewControllerMMDrawerController.mmm_visibleDrawerFrame抽屉控制器坐标系下当前侧抽屉的可见矩形若调用者不是某个抽屉则返回CGRectNull见 UIViewControllerMMDrawerController.m。示例工程大量使用了这一能力例如中心控制器里的菜单按钮回调MMExampleCenterTableViewController.m-(void)leftDrawerButtonPress:(id)sender{ [self.mm_drawerController toggleDrawerSide:MMDrawerSideLeft animated:YES completion:nil]; }以及双击中心视图触发抽屉「弹跳预览」MMExampleCenterTableViewController.m-(void)doubleTap:(UITapGestureRecognizer*)gesture{ [self.mm_drawerController bouncePreviewForDrawerSide:MMDrawerSideLeft completion:nil]; }八、自定义开合动画与预置动画MMDrawerController通过setDrawerVisualStateBlock:提供一个回调 Block允许你在开合或拖拽事件中自行设置抽屉的视觉状态。Block 接收抽屉控制器、抽屉侧别和percentVisible当前可见比例抽屉空间定义为屏幕边缘到最大抽屉宽度之间。你只负责更新状态alpha、anchorPoint、transform 等动画过程由抽屉控制器完成。README 给出的「透明度从 0 渐变到 1」的示例[drawerController setDrawerVisualStateBlock:^(MMDrawerController *drawerController, MMDrawerSide drawerSide, CGFloat percentVisible) { UIViewController * sideDrawerViewController; if(drawerSide MMDrawerSideLeft){ sideDrawerViewController drawerController.leftDrawerViewController; } else if(drawerSide MMDrawerSideRight){ sideDrawerViewController drawerController.rightDrawerViewController; } [sideDrawerViewController.view setAlpha:percentVisible]; }];两个值得注意的源码细节动画状态回调时机updateDrawerVisualStateForDrawerSide:percentVisible:在开合动画的 animations 闭包中持续调用如 MMDrawerController.m因此 Block 会随拖拽实时更新状态复位动画结束后resetDrawerVisualStateForDrawerSide:会把抽屉视图重置为alpha 1.0、transform CATransform3DIdentity、anchorPoint (0.5, 0.5)MMDrawerController.m避免自定义动画污染下次展示超伸百分比当shouldStretchDrawer YES时percentVisible可能大于 1.0你的 Block 必须妥善处理该情形。8.1 预置动画MMDrawerVisualState库随MMDrawerVisualStatessubspec 提供了四种开箱即用的动画声明见 MMDrawerVisualState.hSlide滑动抽屉与中心控制器以相同速率滑动等价于 parallaxFactor 为 1.0 的视差动画MMDrawerVisualState.mSlide and Scale滑动缩放抽屉滑动同时从 90% 缩放到 100%、沿 x 轴平移 50 点alpha 从 0.0 渐变到 1.0MMDrawerVisualState.m类似 Mailbox.app 的体验Swinging Door旋转门抽屉沿中心控制器边缘的「铰链」旋转进入左抽屉锚点为 (1.0, 0.5)、右抽屉为 (0.0, 0.5)并开启离屏渲染shouldRasterizeMMDrawerVisualState.mParallax视差抽屉以比中心控制器更慢的速度滑入产生视差效果。parallaxVisualStateBlockWithParallaxFactor:的参数必须 ≥ 1.0越接近 1.0 视差越快等于纯滑动传MAX_FLOAT则近似无动画MMDrawerVisualState.m。使用方式一行即可[drawerController setDrawerVisualStateBlock:[MMDrawerVisualState slideAndScaleVisualStateBlock]];若想左右抽屉使用不同动画可以参考示例工程中的 MMExampleDrawerVisualStateManager.m它维护了左右抽屉各自的动画类型MMDrawerAnimationType枚举None/Slide/SlideAndScale/SwingingDoor/Parallax再统一转成对应的视觉状态 Block默认左右均为 Parallax、factor 2.0由MMAppDelegate在setDrawerVisualStateBlock:中代理调用见 MMAppDelegate.m。中心表格示例还提供了在运行时切换动画类型并即时刷新列表的交互MMExampleCenterTableViewController.m。九、iOS 7 状态栏支持9.1 子控制器决定状态栏从 iOS 7 开始默认由子控制器决定状态栏的样式与显隐并且每次openSide状态变化时都会重新更新——这意味着侧边抽屉可以与中心控制器提供不同的状态栏表现。MMDrawerController通过重写childViewControllerForStatusBarStyle与childViewControllerForStatusBarHidden把当前打开侧的控制器返回给系统见 MMDrawerController.m。如果不想让抽屉控制器咨询子控制器README 给出的做法是子类化MMDrawerController重写这两个方法并返回 nil。9.2 自定义状态栏背景视图如果中心控制器与抽屉控制器的颜色对比强烈iOS 7 的自动状态栏处理可能让应用看起来不理想。此时可以开启「自定义状态栏区域」在屏幕顶部绘制一个固定颜色的状态栏背景视图内容绘制在其下方效果近似于 iOS 6 及之前的行为。启用方式非常简单[drawerController setShowsStatusBarBackgroundView:YES]; // 默认黑色背景 [drawerController setStatusBarViewBackgroundColor:yourColor]; // 自定义颜色源码实现中dummyStatusBarView是一个高 20 点的视图showsStatusBarBackgroundView YES时子容器整体下移 20 点、高度相应缩减见 MMDrawerController.m抽屉可见 frame 也会相应扣减高度见 UIViewControllerMMDrawerController.m。十、状态恢复State Restoration自 0.4.0 起支持 iOS 状态恢复。接入步骤README 原文要求为抽屉控制器设置restorationIdentifier若希望centerViewController、leftDrawerViewController、rightDrawerViewController也被恢复需分别为它们设置自己的restorationIdentifier可选restorationClass若应用进入后台时抽屉处于打开状态该打开状态也会一并恢复。源码层面encodeRestorableStateWithCoder:/decodeRestorableStateWithCoder:分别用四个键MMDrawerLeftDrawer、MMDrawerRightDrawer、MMDrawerCenter、MMDrawerOpenSide编码左右抽屉、中心控制器与当前打开侧解码时若记录到打开的抽屉会自动重新打开见 MMDrawerController.m。示例工程是完整的状态恢复范本MMAppDelegate.m每个导航控制器与抽屉控制器都设置了restorationIdentifierAppDelegate 返回 YES 启用shouldSaveApplicationState:/shouldRestoreApplicationState:并在viewControllerWithRestorationIdentifierPath:coder:中按标识符分发恢复各控制器MMAppDelegate.m。十一、子类化Subclassing如果计划子类化MMDrawerController需要导入MMDrawerControllerSubclass.h以访问受保护方法MMDrawerControllerSubclass.h。该头文件中所有方法均标注了__attribute((objc_requires_super))要求子类必须调用 super例如tapGestureCallback:/panGestureCallback:—— 单击与拖拽手势回调拖拽回调只做最小工作量以保证响应流畅gestureRecognizer:shouldReceiveTouch:—— 手势代理判定prepareToPresentDrawer:animated:—— 抽屉展示前的初始条件设置closeDrawerAnimated:velocity:animationOptions:completion:/openDrawerSide:animated:velocity:animationOptions:completion:—— 打开/关闭的核心方法子类可借此在抽屉即将开合时获得回调willRotateToInterfaceOrientation:duration:/willAnimateRotationToInterfaceOrientation:duration:—— 旋转相关确保调用 super。十二、锦上添花的小工具12.1 MMDrawerBarButtonItem 菜单按钮库使用 PaintCode 生成了一个标准的汉堡菜单按钮可直接放入任意UINavigationBar。从 iOS 7 起按钮改用更细的线条颜色方法被废弃颜色由tintColor决定阴影也被移除以符合系统设计语言。源码确认iOS 7 走initWithImage:style:target:action:路线使用一张 26×26 的代码绘制图片见 MMDrawerBarButtonItem.m。示例用法MMExampleCenterTableViewController.m-(void)setupLeftMenuButton{ MMDrawerBarButtonItem * leftDrawerButton [[MMDrawerBarButtonItem alloc] initWithTarget:self action:selector(leftDrawerButtonPress:)]; [self.navigationItem setLeftBarButtonItem:leftDrawerButton animated:YES]; }12.2 Stretchy Drawer弹性抽屉默认情况下用户把抽屉拖拽超过最大宽度时抽屉会产生类似UIScrollView回弹的渐进式拉伸效果。关闭方式[drawerController setShouldStretchDrawer:NO];或者自定义 overshoot 动画编写自定义 visual state Block自行处理percentVisible 1.0时的变换。源码中roundedOriginXForDrawerConstriants:在启用拉伸时会把超出的位移换算为「线性 非线性渐近」的过冲量MMDrawerOvershootPercentage 0.1f、线性区 75%见 MMDrawerController.m未设置 visual state Block 时默认的拉伸由applyOvershootScaleTransformForDrawerSide:percentVisible:以横向缩放实现MMDrawerController.m。12.3 Bounce Preview弹跳预览为了让侧抽屉更容易被发现可以在首次启动时让抽屉「弹一下」[drawerController bouncePreviewForDrawerSide:MMDrawerSideLeft completion:nil];也可以指定弹跳距离bouncePreviewForDrawerSide:distance:completion:。实现上使用包含 32 个关键帧的CAKeyframeAnimation作用于中心容器的position.x形成回弹衰减动画bouncePreviewForDrawerSide:completion:默认距离为 50 点MMDrawerDefaultBounceDistance见 MMDrawerController.m。注意当抽屉已打开或该侧没有抽屉控制器时动画不会执行且 completion 以NO回调。十三、设计边界这个库「不做什么」README 明确列出了为了保持轻量而做的取舍这也是集成前必须评估的约束不支持顶部/底部抽屉视图不支持同时显示左右两个抽屉不支持最小抽屉宽度中心控制器不支持除UINavigationController之外的容器控制器如UITabBarController、UISplitViewController不支持抽屉覆盖在中心控制器之上Google 风格。十四、常见问题与解决思路如何在中心控制器中支持表格的编辑/拖拽单元格最佳实践是编辑期间把openDrawerGestureModeMask/closeDrawerGestureModeMask都设为MMOpenDrawerGestureModeNone/MMCloseDrawerGestureModeNone编辑结束时恢复原掩码。这样手势/触摸才能完整地传递给表格视图README 引用的 issue #184 也指向同一结论。十五、示例工程一览仓库中的 KitchenSink 示例工程MMDrawerControllerKitchenSink.xcodeproj展示了库的全部特性值得对照学习MMAppDelegate.mwindow 搭建、三控制器初始化、MMOpenDrawerGestureModeAll/MMCloseDrawerGestureModeAll、视觉状态代理、状态恢复标识符MMExampleCenterTableViewController.m汉堡按钮、双击弹跳预览、运行时切换左右抽屉动画类型、运行时启用/移除左右抽屉MMExampleSideDrawerViewController.m抽屉宽度选择160/200/240/280/320、阴影开关、各手势掩码的逐位切换、中心交互模式切换、拉伸开关、setCenterViewController:withCloseAnimation:与withFullCloseAnimation:两种换中心控制器动画MMExampleDrawerVisualStateManager.m四种预置动画的统一调度。十六、许可证MMDrawerController基于 MIT 许可证开源详见仓库根目录的 LICENSE 文件。版本演进与各版本修复要点可查阅 CHANGELOG.md。赞分享移动开发UI组件【免费下载链接】MMDrawerControllerA lightweight, easy to use, Side Drawer Navigation Controller项目地址https://gitcode.com/gh_mirrors/mm/MMDrawerController点击查看免费下载相关推荐低光增强新标杆HVI-CIDNet-LOLv1-fp32的14.5dB性能突破低光增强新标杆HVI CIDNet LOLv1 fp32的14.5dB性能突破 HVI CIDNet LOLv1 fp32是一款基于Apple MLX框架的低如何快速掌握AutoHotkeyWindows自动化脚本终极指南如何快速掌握AutoHotkeyWindows自动化脚本终极指南 AutoHotkey是一款完全免费开源的Windows自动化工具专为键盘宏创建和脚本自动化RPAGUI 自动化桌面应用Starlark-go 错误处理与调试解决常见问题的终极指南Starlark go 错误处理与调试解决常见问题的终极指南 Starlark go 是一个用 Go 语言实现的 Starlark 配置语言解释器为开发人员人工智能大模型RAGAI Agent本地部署后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考