一款滚动菜单的实现

lxktest123 8年前
   <p>滚动菜单是app展示数据常用的控件,使用频率极高,实现的过程就是对UIScrollView的应用,大体都是顶部一个标题滚动视图,下边一个用来铺显示数据的View的滚动视图,然后实现联动效果,基本差不多了。</p>    <h2><strong>需求分析</strong></h2>    <ul>     <li>从笔者安装的应用上观察,大体要实现功能 <strong>标题下划线自适应</strong> , <strong>菜单的半透明</strong> , <strong>左右的不同按钮</strong> (参考优酷,网易云音乐)。</li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/04602fc8873c17cd3cdb0818dd88aec7.png"></p>    <p style="text-align:center">屏幕快照 2016-10-11 10.31.31.png</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/59ac48b70ab85fd37a333188c7e94587.png"></p>    <p style="text-align:center">屏幕快照 2016-10-11 10.26.41.png</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/01fef3eb0130faf1ce8558655fe8fcbb.png"></p>    <p style="text-align:center">屏幕快照 2016-10-11 10.26.53.png</p>    <h2><strong>实现</strong></h2>    <ul>     <li> <p><strong>总思想</strong></p> <p>将标题视图,展示数据的视图放到一个View 上,View 来管理不同的子视图的滑动,子视图由各自的控制管理,要做的就是封装好这个视图,名字就叫HYPageView。</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/18f29293241bcb2a88afc67b7005c965.png"></p>    <p style="text-align:center">屏幕快照 2016-10-11 10.53.26.png</p>    <p>简单的说就是把控制器的View放到scrollView,滑到某一页,就加载某一页的控制器,并且将控制器的View放到这一页。需要注意的是, 创建的控制器必需要强引用,因为局部变量会导致"控制器死掉" 造成控制器里边所有事件无法响应,比如tabView没有Cell,Button点上去没反应等等,都是因为控制器从内存释放了。</p>    <ul>     <li><strong>下划线自适应</strong></li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fba6b25f1b1e499ebeccc07f7ab77512.gif"></p>    <p style="text-align:center">HYPageView00.gif</p>    <p>就是一边滑,一边动,根据当前滑动的位置改变线条的位置,线条的长度也总是趋于下一个位置的长度,这种很细节的东西吸引了笔者,如何实现呢?</p>    <p>需要计算两点 <strong>位置</strong>,<strong>长度</strong> ;</p>    <ul>     <li> <p>方法1(坑)</p> <p>首先想到是 <strong>计算相对偏移量</strong> ,我们总是能知道当前位置和下一个位置,只要计算线条位置相对与手指拖拽的位置的偏移量,通过加减就可以算出准确的位置,线条的长度一个道理。感觉有点麻烦,但我尝试的这么做了。</p> <p>在UIScrollViewDelegate 里的scrollViewDidScroll 方法中算出并记录每一次偏移量,然后累计到下划线上,结果是效果差不多。但总是有误差,误差的原因是误差累计,与越界,前者是在计算的过程精度的丢失,多次累加产生的误差,后者是在滑动的过程,结束的位置不会保证是下一个位置的结束点,我也尝试的修复这种误差,但滑的越快误差就越大。然后我就尝试寻找更好的方法。</p> </li>    </ul>    <p>找到了一个例子,和方法1的坑一毛一样</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f1313797f4ad387081839c0639b5de07.gif"></p>    <p style="text-align:center">反例1.gif</p>    <p>越来长的原因:可能是越界的问题,从图中可以看出,在返回自身位置时是一个累减的过程,而返回的最后一段距离终点不一定恰好是零界点,这段长度可能没有减去,拖着不放,误差越来越大。从图中也可以看出作者在结束滚动时对线条进行了校正。</p>    <ul>     <li> <p>方法2</p> <p>想要精确无误,就想到了利用 <strong>数学的知识</strong> 来解决,通过观察想到了<strong>contentOffset的值与线条点的位置唯一对应</strong> ,也就是说 <strong>线条点的位置是关于contentOffset的一个函数</strong>,不难发现就是一个<strong>分段函数</strong> ,而且每一段都是一个 一次函数 形如 <strong>y=kx+b</strong> 这里<strong>x是 contentOffset</strong>,<strong>y 是 线条的点或长度</strong> ,写一个方法,传入contentOffset返回点的位置或线条的宽度,显然要一组k与b,而k 与 b 在 构造标题位置的就可以计算出来 k = △y/△x ,得到k后b也可以得到。</p> 然后在 - (void)scrollViewDidScroll:(UIScrollView *)scrollView; <p>方法里只有两行代码</p> <pre>  <code class="language-objectivec">_lineBottom.center = CGPointMake([self getTitleWidth:scrollView.contentOffset.x], _lineBottom.center.y);  _lineBottom.bounds = CGRectMake(0, 0, [self getTitlePoint:scrollView.contentOffset.x], LINEBOTTOM_HEIGHT);</code></pre> </li>    </ul>    <p>计算的代码</p>    <pre>  <code class="language-objectivec">#pragma mark - Calculation Method    - (CGFloat)getTitleWidth:(CGFloat)offset{      NSInteger index = (NSInteger)(offset / _selfFrame.size.width);      CGFloat k = [_width_k_array[index] floatValue];      CGFloat b = [_width_b_array[index] floatValue];      CGFloat x = offset;      return  k * x + b;  }  - (CGFloat)getTitlePoint:(CGFloat)offset{      NSInteger index = (NSInteger)(offset / _selfFrame.size.width);      CGFloat k = [_point_k_array[index] floatValue];      CGFloat b = [_point_b_array[index] floatValue];      CGFloat x = offset;      return  k * x + b;  }</code></pre>    <p>这样就达到完美的线条自适应效果如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/429a449a17b5135a641b85c691273934.gif"></p>    <p style="text-align:center">HYPageView03.gif</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4aa93d9f6d1839bc3018042ec9eeef53.gif"></p>    <p style="text-align:center">HYPageView08.gif</p>    <ul>     <li> <p><strong>菜单的半透明</strong><br> 菜单的半透明很简单,设置好ScrollView的contentInset和frame就好了,需要注意的是有navigationController时控制器的automaticallyAdjustsScrollViewInsets 属性 和 edgesForExtendedLayout属性在开启navigationBar 半透明效果时 navigationController会对 控制器的 contentInset 和 frame进行调整 以达到半透明效果。</p> </li>     <li> <p><strong>左右的不同按钮</strong><br> 前边的步骤做好了这里也很简单,设计可以根据navigationBar 的左右item来设计。笔者直接添加Button然后重新计算顶部菜单滚动视图的宽,效果如下:</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c74ec84399e81b898529ccfb8296530e.gif"></p>    <p style="text-align:center">HYPageView05.gif</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0b8751d1cb756b80ae1018c5db646681.gif"></p>    <p style="text-align:center">HYPageView06.gif</p>    <ul>     <li> <p><strong>接口的设计</strong></p> </li>    </ul>    <pre>  <code class="language-objectivec">/**   Initializes and returns a newly allocated view object with the specified frame rectangle.     @param frame       ...   @param titles      Some title   @param controllers Name of some controllers   @param parameters  You need to set a property called "parameter" for your controller to receive.     @return self   */  - (instancetype)initWithFrame:(CGRect)frame withTitles:(NSArray *)titles withViewControllers:(NSArray *)controllers withParameters:(NSArray *)parameters;</code></pre>    <p>frame,titles 不用说,controllers是传控制器的名称,这样可以在需要的时候再创建控制器,parameters是要给控制器传的参数,传递的方式是KVC,当然传递的时候需要检查参数是否存在,控制器是否有parameters参数,具体实现可以看demo。</p>    <p> </p>    <p> </p>    <p> </p>