读源码之RESideMenu

RESideMenu是github上比较出名的一个开源库,主要是实现侧滑菜单,现在有三千多个star了。效果如下。

据说创意来源于dribbble的一个设计,还是比较好看的。感兴趣的可以去github上搜residemenu,地址就不贴了,选择这个开源库主要原因是带大家学习一下创建一个自定义的viewcontroller容器是怎样的步骤。
其实视图容器大家每天都在用,什么navigationcontroller,tabbarcontroller,pageviewcontroller,可能第三个大家比较少用,系统相册的点击浏览大图,然后左右滑动可以看下一个图的那个页面就是pageviewcontroller,比较像tableviewcontroller,也是数据驱动的。
为什么要用视图容器相信也不用我赘述了,好处多多,想象一下如果navigationcontroller里存入栈的不是viewcontroller而是一个个view,估计你的主view类代码要超过一万行了。
大家都知道,navigationcontroller里push和pop是这么回事。

虽然图片丑了点,但是大概意思相信大家清楚。但是大家有没有想过,你每次执行

[self.navigationController pushViewController:vc1 animated:YES];

这句话的时候,系统到底干了什么?其实系统是这么做的。

 [self addChildViewController:vc1];                 // 1
 vc1.view.frame = [self frameForContentController]; // 2
 [self.view addSubview:vc1.view];
 [vc1 didMoveToParentViewController:self];          // 3

这里的self你们可以当成navigationcontroller。
第一步,navigationcontroller用addChildViewController这个函数把vc1这个controller添加作为自己的子视图控制器。
第二步,设置vc1这个视图控制器里根view的frame。
第三步,把vc1.view添加到navigationcontroller.view上。
第四步,用didMoveToParentViewController这个方法通知已经push完毕。
说完push了,那我们说说pop这个操作。这个操作其实是这么回事。假设你在之前已经alloc和init完毕了这个叫vc1的Viewcontroller。

[vc1 willMoveToParentViewController:nil];  // 1
[vc1.view removeFromSuperview];            // 2
[vc1 removeFromParentViewController];

第一步,用willMoveToParentViewController这个方法,并把参数设置为nil,通知vc1即将被移除父视图控制器。
第二步,把vc1.view移除出父视图控制器的view。
第三步,然后用removeFromParentViewcontroller这个方法把vc1彻底移除。
(这就是我反复强调的套路,套路的意思就是你只须知其然,不知其所以然就行了。因为人设计API的时候就是这么设计的。你不用问为什么这么设计。)
官方文档地址在这里。https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/CreatingCustomContainerViewControllers/CreatingCustomContainerViewControllers.html#//apple_ref/doc/uid/TP40007457-CH18-SW6

当时我看到这的时候我以为我懂了,其实我不懂。
有下面几个问题。
1.父容器是不是在添加子视图控制器的时候只能加一个,如果加第二个子视图控制器的时候是不是得先remove掉前一个。
答案:废话,当然不是只能加一个,你push->push->push的时候明明addchildviewcontroller了三回,然后你pop->pop->pop的时候才会执行removefromparentViewcontroller。也是执行了3回。

2.假如父视图容器添加了3个子视图容器,到底能同时显示几个子视图容器的view,是不是只能显示一个?
答案:当然不是,如果你愿意,你可以同时显示一百个。因为frame就是你自己设置的。你想设置多少设置多少。

OK,相信你如果你理解了上面两个问题,你应该理解所谓的父视图控制器到底是个怎么回事。

那我们看下RESideMenu的源码。

RESideMenu *sideMenuViewController = [[RESideMenu alloc] initWithContentViewController:navigationController
                                                                leftMenuViewController:leftMenuViewController
                                                               rightMenuViewController:rightMenuViewController];

这是REsideMenu的一个初始化方法,其实方法名字挺容易理解的,设置了一个内容viewcontroller,这个内容viewcontroller是个navigationcontroller,然后添加了一个左菜单viewcontroller,和一个右菜单viewcontroller。
大家猜到没有,这个函数到底执行了什么操作?
很简单,执行了三次这个东西呗。

 [self addChildViewController:content];                 // 1
 content.view.frame = [self frameForContentController]; // 2
 [self.view addSubview:self.currentClientView];
 [content didMoveToParentViewController:self];

那个content分别是我们的左菜单视图,右菜单视图和内容视图。
不信我们来看下他的源码。

哎呦,我去,好尴尬,没执行上面的代码,只是进行了赋值操作,是这个RESideMenu类有三个成员变量,分别是

哎,我还不信邪了。继续往下看。看到viewDidLoad()这个方法。

我就晓得我的判断没有错,还是有这个方法吧。那就是说在REsideMenu加载到viewdidload的时候,判断left和right存不存在,如果不为nil就让residemenu把这两个controller外加content controller添加为childviewcontroller。

需要注意的是这列添加子视图控制器view的顺序,先加left的view,再加right的view,再加content的view。那么你脑海里应该有个图,是content的view在最上头,因为人们进入app之后肯定第一眼看到的是内容视图,我不能一进去就让用户看见菜单视图对吧,得让用户用手势拉开或者点一下左上角的button(一般是三条杠的那种button,俗称汉堡button)这时候才会显示菜单视图。(一般都是这样啊,不排除有些奇葩app一进去就给人看菜单。)

那么重头戏来了,到底是怎么把左边菜单呼叫出来的,然后内容视图没完全消失,而是只漏出一部分。如图

然后我们在源码里找到这么一段

其实也蛮好懂的,为了显示左边菜单视图,把视图的hidden设成no,右侧视图的hidden设成yes,估计是之前都设成yes了,这里让左侧视图显示出来。然后就是设置左侧菜单的内容什么的。
然后添加动画,给contentviewcontroller的view做了个scale,估计是把contentviewcontroller的view压缩了一下,然后压缩之后改变contentViewContainer的center,哎等一下,怎么有个contentviewContainer?
我又往前面看了一下,哦,原来residemenu有两个成员变量是uiview类型的,一个用来放菜单子视图的view,一个用来放内容视图的view。
然后我们来梳理下流程。
1.当用户点击汉堡按钮来显示左菜单的时候,先把左视图的hidden设成no。这样左视图显示。
2.为了有第一张图的效果,把内容视图的view 做个transform里的scale,XY压缩的小一点,然后改变内容视图的center,让他靠右。
然后就形成了这种效果。

那么显示右边菜单应该也是差不多的流程。

那么基础流程懂了,其实剩余的就是怎么美化这些过程。

给大家体格问题,一般侧滑菜单这种东西都是为了点击菜单切换内容视图的,那么切换内容视图的时候用了哪个方法呢?

最后,可能有人好奇RESidemenu怎么做到随着手指向右滑动,然后动画慢慢执行的过程的。
那么仔细看看

- (void)panGestureRecognized:(UIPanGestureRecognizer *)recognizer

 

时间: 2024-08-06 15:05:42

读源码之RESideMenu的相关文章

这样读源码,不牛X也难

程序员在工作过程中,会遇到很多需要阅读源码的场景,比如技术预研.选择技术框架.接手以前的项目.review他人的代码.维护老产品等等.可以说,阅读源代码是程序员的基本功,这项基本功是否扎实,会在很大程度上影响一个程序员在技术上的成长速度. 2014年写<Qt on Android核心编程>和<Qt Quick核心编程>时,很多内容都是通过分析Qt源码搞明白的.这阵子研究CEF和PPAPI,也主要靠研究源代码来搞明白用法.最近工作上要修改已有项目的一个子系统,也是得硬着头皮先读懂代码

Java读源码学设计模式:适配器Adapter

适配器模式相关源码:slf4j-1.6.1.hibernate-3.6.7 大家都知道,log4j是一个广泛使用的日志工具,除此之外,sun公司在JDK中也有自己的日志工具,也就是java.util.logging.Logger.当然还有其他一些日志工具. 多种日志工具功能和使用方式类似,一般都包含debug.info.warn.error等日志级别的方法,但却没有实现共同的接口.这一点不像JDBC,虽然关系型数据库种类很多,例如MySQL.Oracle等,但是有一套统一的接口,也就是JDBC.

CloudGeek读源码系列-cache2go源码解析

一.cache2go是什么 作者说:Concurrency-safe golang caching library with expiration capabilities. 什么意思呢? 有心跳机制的并发安全的go语言缓存库 学习一门语言有一个很好的方法就是阅读优秀的开源项目源码,学习优秀前辈的编码方式,同时发现自己的知识盲区,不断获取新知识!cache2go很精简,代码量很少,非常适合刚接触go语言的同学作为入门级项目来读源码. 下面我会先介绍项目组成,然后讲解核心数据结构,再梳理整个实现逻

杂谈篇之我是怎么读源码的,授之以渔

前言 开心一刻 今天上课不小心睡着了,结果被老师叫起来回答问题,这是背景.无奈之下看向同桌寻求帮助,同桌小声说到选C,结果周围的人都说选C,向同桌投去一个感激的眼神后大声说道选C.刚说完教室就笑开了,老师一脸恨铁不成钢的表情说选你个头,我叫你翻译文言文你选C!你出去,你给我出去.看着同桌挤眉弄眼的表情,劳资真想说,这帮畜生 路漫漫其修远兮,吾将上下而求索! github:https://github.com/youzhibing 码云(gitee):https://gitee.com/youzh

杂谈篇之我是怎么读源码的

读源码的经历 刚参加工作那会,没想过去读源码,更没想过去改框架的源码:总想着别人的框架应该是完美的.万能的,应该不需要改:另外即使我改了源码,怎么样让我的改动生效了? 项目中引用的不还是没改的jar包吗.回想起来觉得那时候的想法确实挺…… 工作了一年多之后准备跳槽了,开始了一轮的面试,其中有几个面试官就问到了相关的源码问题:ArrayList.HashMap的底层实现,spring.mybatis的相关源码.问源码的面试一般就是回去等消息,然后就没然后了. 那时候开始意识到,源码这东西在之前的工

[一起读源码]走进C#并发队列ConcurrentQueue的内部世界

决定从这篇文章开始,开一个读源码系列,不限制平台语言或工具,任何自己感兴趣的都会写.前几天碰到一个小问题又读了一遍ConcurrentQueue的源码,那就拿C#中比较常用的并发队列ConcurrentQueue作为开篇来聊一聊它的实现原理. 话不多说,直奔主题. 要提前说明下的是,本文解析的源码是基于.NET Framework 4.8版本,地址是:https://referencesource.microsoft.com/#mscorlib/system/Collections/Concur

跟大佬一起读源码:CurrentHashMap的扩容机制

并发编程——ConcurrentHashMap#transfer() 扩容逐行分析 前言 ConcurrentHashMap 是并发中的重中之重,也是最常用的数据结果,之前的文章中,我们介绍了 putVal 方法.并发编程之 ConcurrentHashMap(JDK 1.8) putVal 源码分析.其中分析了 initTable 方法和 putVal 方法,但也留下了一句话: 这篇文章仅仅是 ConcurrentHashMap 的开头,关于 ConcurrentHashMap 里面的精华太多

iScroll学习笔记2--浅读源码

iscroll的架子是这样的 (function (window, document, Math){ var utils = (function (){ var me = {}; // 扩展一些常用的工具方法为me的方法 return me; }()); function IScroll(el, options){ // 初始化一些属性和状态 } IScroll.prototype = { constructor: IScroll, // 主体方法都在这里 } }(window, documen

knockoutjs 读源码

1.整个项目文件分为build,spec,src三个文件夹,package.json,Gruntfile.js 2.由于源文件是以Grunt 打包,要编译源文件输出为knockoutjs.js,根据README.md文件 .安装 npm install -g grunt-cli npm install grunt 生成的文件保存在 build/output/ 文件夹下. 3.源代码带有测试文件,是在spec文件夹下.你可以使用PhantomJS运行测试,PhantomJS(它全面支持web而不需