iOS 高仿 Timi 记账
gvxb2966
8年前
<p>本人还属于菜鸟级别,代码写得不规范,望见谅!</p> <h2><strong>项目视频演练 -> <a href="/misc/goto?guid=4959714366929282635" rel="nofollow,noindex">点我</a></strong></h2> <h2><strong>Demo -> <a href="/misc/goto?guid=4959714367026116135" rel="nofollow,noindex">Timi</a> 不要忘记star支持哟</strong></h2> <h2><strong>高仿版本:3.6.1</strong></h2> <h2><strong>使用语言:Objective-C</strong></h2> <h2><strong>开发工具及调试神器:Xcode 7.3.1,Reveal 1.6.3</strong></h2> <h2><strong>用到的三方库及扩展库</strong></h2> <table> <thead> <tr> <th>Name</th> <th>Explain</th> </tr> </thead> <tbody> <tr> <td>Masonry</td> <td><a href="/misc/goto?guid=4958877303436721101" rel="nofollow,noindex">纯代码Autolayout</a></td> </tr> <tr> <td>MBProgressHUD</td> <td><a href="/misc/goto?guid=4958870672840018332" rel="nofollow,noindex">未使用,后更改为使用SVProgressHUD</a></td> </tr> <tr> <td>MMDrawerController</td> <td><a href="/misc/goto?guid=4958870677314802488" rel="nofollow,noindex">抽屉</a></td> </tr> <tr> <td>SVProgressHUD</td> <td><a href="/misc/goto?guid=4959671431027398613" rel="nofollow,noindex">HUD</a></td> </tr> <tr> <td>YYText</td> <td><a href="/misc/goto?guid=4958971667887126889" rel="nofollow,noindex">著名库YYKit下的一个富文本</a></td> </tr> <tr> <td>iCarousel</td> <td><a href="/misc/goto?guid=4958870674500642597" rel="nofollow,noindex">一个类似UIScrollView的控件</a></td> </tr> <tr> <td>ColorCube</td> <td><a href="/misc/goto?guid=4959714367292881833" rel="nofollow,noindex">图片颜色提取</a></td> </tr> <tr> <td>UITextView_PlaceHolder</td> <td><a href="/misc/goto?guid=4959654888015231719" rel="nofollow,noindex">给UITextView添加PlaceHolder</a></td> </tr> <tr> <td>SZCalendarPicker</td> <td><a href="/misc/goto?guid=4959714367415816429" rel="nofollow,noindex">日历</a></td> </tr> <tr> <td>TYPagerController</td> <td><a href="/misc/goto?guid=4959714367497144013" rel="nofollow,noindex">左右滚动ViewController</a> VTMagic</td> </tr> <tr> <td>Realm</td> <td><a href="/misc/goto?guid=4959714367594357972" rel="nofollow,noindex">移动端数据库新王者</a></td> </tr> </tbody> </table> <h2><strong>数据库设计</strong></h2> <p>TMBill(账单)</p> <table> <thead> <tr> <th>Key</th> <th>Identity</th> <th>Column</th> <th>Data Type</th> <th>length</th> <th>Allowed Null</th> <th>Default</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>√</td> <td>√</td> <td>billID</td> <td>NSString</td> <td>64</td> <td> </td> <td> </td> <td>主键</td> </tr> <tr> <td> </td> <td> </td> <td> </td> <td>dateStr</td> <td>NSString</td> <td>10</td> <td>当前年月日</td> <td>时间</td> </tr> <tr> <td> </td> <td> </td> <td> </td> <td>reMarks</td> <td>NSString</td> <td>40</td> <td>nil</td> <td>备注</td> </tr> <tr> <td> </td> <td> </td> <td>remarkPhoto</td> <td>NSData</td> <td> </td> <td>√</td> <td>nil</td> <td>图片备注</td> </tr> <tr> <td> </td> <td> </td> <td>isIncome</td> <td>BOOL</td> <td>1</td> <td> </td> <td>0</td> <td>类型(收支)</td> </tr> <tr> <td> </td> <td> </td> <td>money</td> <td>float</td> <td>13</td> <td> </td> <td>0</td> <td>金额</td> </tr> <tr> <td>FK</td> <td> </td> <td>category</td> <td>TMCategory</td> <td> </td> <td> </td> <td> </td> <td>类别</td> </tr> <tr> <td>FK</td> <td> </td> <td>book</td> <td>TMBooks</td> <td> </td> <td> </td> <td> </td> <td>账本</td> </tr> </tbody> </table> <p><img src="https://simg.open-open.com/show/614ae9da78742f0c096ed3f8c20b2483.jpg"></p> <p style="text-align:center">TMBill(账单).png</p> <p>TMCategory(类别)</p> <table> <thead> <tr> <th>Key</th> <th>Identity</th> <th>Column</th> <th>Data Type</th> <th>length</th> <th>Allowed Null</th> <th>Default</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>√</td> <td>√</td> <td>categoryID</td> <td>NSString</td> <td>64</td> <td> </td> <td> </td> <td>主键</td> </tr> <tr> <td> </td> <td> </td> <td> </td> <td>categoryImageFileNmae</td> <td>NSString</td> <td>64</td> <td> </td> <td>类别icon文件名</td> </tr> <tr> <td> </td> <td> </td> <td> </td> <td>categoryTitle</td> <td>NSString</td> <td>3</td> <td> </td> <td>类别标题</td> </tr> <tr> <td> </td> <td> </td> <td>isIncome</td> <td>BOOL</td> <td>1</td> <td> </td> <td> </td> <td>类型(收支)</td> </tr> </tbody> </table> <p><img src="https://simg.open-open.com/show/3ef1d2e7d655b9a25d92a9607cfdb071.jpg"></p> <p style="text-align:center">TMCategory(类别).png</p> <p>TMBook(账本)</p> <table> <thead> <tr> <th>Key</th> <th>Identity</th> <th>Column</th> <th>Data Type</th> <th>length</th> <th>Allowed Null</th> <th>Default</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>√</td> <td>√</td> <td>bookID</td> <td>NSString</td> <td>64</td> <td> </td> <td> </td> <td>主键</td> </tr> <tr> <td> </td> <td> </td> <td> </td> <td>bookName</td> <td>NSString</td> <td>6</td> <td> </td> <td>账本标题</td> </tr> <tr> <td> </td> <td> </td> <td> </td> <td>imageIndex</td> <td>int</td> <td>2</td> <td> </td> <td>账本对应icon下标</td> </tr> <tr> <td> </td> <td> </td> <td>bookImageFileName</td> <td>NSString</td> <td>64</td> <td> </td> <td> </td> <td>类别icon文件名</td> </tr> </tbody> </table> <p><img src="https://simg.open-open.com/show/fce5ffe07206dc489a95f76c23a2a355.jpg"></p> <p style="text-align:center">TMBook(账本).png</p> <p>TMAddCategory(新增类别)</p> <table> <thead> <tr> <th>Key</th> <th>Identity</th> <th>Column</th> <th>Data Type</th> <th>length</th> <th>Allowed Null</th> <th>Default</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>√</td> <td>√</td> <td>categoryID</td> <td>NSString</td> <td>64</td> <td> </td> <td> </td> <td>主键</td> </tr> <tr> <td> </td> <td> </td> <td>√</td> <td>categoryImageFileNmae</td> <td>NSString</td> <td>64</td> <td> </td> <td>类别icon文件名</td> </tr> <tr> <td> </td> <td> </td> <td>isIncome</td> <td>BOOL</td> <td>1</td> <td> </td> <td> </td> <td>类型(收支)</td> </tr> </tbody> </table> <p><img src="https://simg.open-open.com/show/1b8050d66035c68e63a2081fe984e4a8.jpg"></p> <p style="text-align:center">TMAddCategory(新增类别).png</p> <p>TMCategory(类别),TMAddCategory(新增类别)都是采用plist表的方式先存储。当App每次启动的时候就会先检查数据库对应的表是否为空,为空则从plist表读取数据,存储到本地数据库。</p> <h2><strong>项目整体结构</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/29b9b2db9d796a52d80c77d8eed69496.png"></p> <p style="text-align:center">TimiStructure.png</p> <h2><strong>温馨提醒</strong></h2> <p>项目里面95%都是使用的纯代码方式布局(Masonry),如果不懂的 Masonry 纯代码布局的请先去了解一下。 <a href="/misc/goto?guid=4959714367684959809" rel="nofollow,noindex">传送门=>串哥的深入讲解 AutoLayout 和 Masonry</a></p> <h2><strong>时光轴界面(HomePageViewController)</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/0953cb9e139d14096dbe34b82c608380.gif"></p> <p style="text-align:center">2016-07-01 14.58.02.gif</p> <h2><strong>UI布局之header部分(TMHeaderView)</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/5da1ba414f2061479f1dbd98e7943e2a.png"></p> <p style="text-align:center">Paste_Image.png</p> <p>其实headerView部分没有什么好说的,那个饼图是用 UIBezierPath 和 CAShapeLayer 绘制而成,我把它单独封装出来了,因为在后面的饼图部分也用到了。关于饼图的加载数据时候的动画我是使用的 CABasicAnimation 具体的操作可以看demo的对应文件( TMPieView )</p> <h2><strong>UI布局之数据显示部分(HomePageViewController | TMTimeLineCell)</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/a87c25edb20a43214b8922bbc7b1263c.png"></p> <p style="text-align:center">Paste_Image.png</p> <p>数据的显示全部在一个section里面,并没有分section显示,而且cell也只有一个样式,我是通过收支类型来判断的该那边显示数据。</p> <p>时间轴上面,相同时间(同一天)时间label和金额label以及时间点不显示出来,我是在模型层加了一个BOOL变量来判断,同时在获取数据之后进行数据的重置,具体的操作可以看 HomePageViewController 的 getDataAndResetBill 函数。</p> <p>然后在自定义cell( TMTimeLineCell )重写 timeLineBill 属性,通过判断来显示数据。</p> <p>下图应该清楚的看懂整个cell的布局</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/8b6fd881028b0ca1ff7f1a55484ad421.png"></p> <p style="text-align:center">Paste_Image.png</p> <p>其实这种做法并不好,一个cell是能完成,但是代码看起来就有点乱糟糟的感觉,正确的做法是应该有两种样式的cell。分别是账单类型为收入,账单类型为支出两种样式。</p> <p>很多人都应该碰到过,滑动tableView的时候Cell的数据会出现混乱,我是这样解决的,在自定义cell重写 - (void)prepareForReuse 函数,将cell里面的控件元素的属性和对象统统置为nil。</p> <pre> <code class="language-objectivec">//* 解决tableView滚动导致数据混乱 准备重用,防止滚动出现数据错乱 */ - (void)prepareForReuse { [super prepareForReuse]; self.timeLineBill = nil; self.categoryImageBtn.imageView.image = nil; self.leftCategoryNameLabel.text = nil; self.leftMoneyLabel.text = nil; self.leftRemarkLabel.text = nil; self.rightCategoryNameLabel.text = nil; self.rightMoneyLabel.text = nil; self.rightRemarkLabel.text = nil; self.lastBill = NO; }</code></pre> <p>细心的人可能看到了我在下滑tableview的时候,中间的时光轴线也跟着变长。当我下滑到一定程度,然后松手就会push到新增账单界面,而且这个push动画不是系统自带的push动画。</p> <p>下面我一一为大家解答:</p> <h2><strong>时光轴的线条是怎么变长的?</strong></h2> <p>第一步、我是新增的一个UIView,默认frame为 (SCREEN_SIZE.width-1)/2,0 , 1, 0) ,将它加到tableview上面。</p> <pre> <code class="language-objectivec">self.dropdownLineView = [[UIView alloc] initWithFrame:CGRectMake((SCREEN_SIZE.width-1)/2,0 , 1, 0)]; self.dropdownLineView.backgroundColor = LineColor; [self.tableView addSubview:self.dropdownLineView];</code></pre> <p>第二步、在UIScrollViewDelegate的 - (void)scrollViewDidScroll:(UIScrollView *)scrollView 代理函数里面获取滑动的y值。判断其方向并重新设置 dropdownLineView 的frame即可</p> <pre> <code class="language-objectivec">- (void)scrollViewDidScroll:(UIScrollView *)scrollView { /** 当下拉的时候才有动画 y>0下拉,y<0上划*/ CGFloat y = [scrollView.panGestureRecognizer translationInView:self.tableView].y; // NSLog(@"%s--%d---y = %f",__func__,__LINE__,y); if (y>0) { /** * 疑问:为什么是`y`是`-y`不是`0`,因为`dropdownLineView`是添加到`tableView`的,所以当`tabelView`拉下的时候`dropdownLineView`也会跟着向下移动。 * 当`y`是`-y`的时候`dropdownLineView`会向上移动`y`个单位,才会达到我们理想的效果 */ self.dropdownLineView.frame = CGRectMake((SCREEN_SIZE.width-1)/2, -y, 1, y); [self.tableView bringSubviewToFront:self.dropdownLineView]; /** 饼图+号按钮动画*/ [self.headerView animationWithCreateBtnDuration:1.0f angle:y]; } }</code></pre> <h2><strong>时光轴界面到添加账单(修改账单)界面的转场动画(LYPushTransition,LYPopTransition)</strong></h2> <p>使用的是自定义的转场动画,具体如何使用请看 <a href="/misc/goto?guid=4959714367769217447" rel="nofollow,noindex">喵神</a> 和 <a href="/misc/goto?guid=4959714367851353499" rel="nofollow,noindex">KittenYang</a> 的blog,推荐 <a href="/misc/goto?guid=4959714367942606759" rel="nofollow,noindex">几句代码快速集成自定义转场效果+全手势驱动</a></p> <p>1.首先定一个 class ,继承至 NSObject ,遵守 UIViewControllerAnimatedTransitioning 协议。</p> <p>2.需要实现两个方法</p> <pre> <code class="language-objectivec">/** 动画时间 */ - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext /** 转场动画内容(怎么转) */ - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext</code></pre> <h3><strong>Push代码细节讲解(是一个反向prensent转场动画)</strong></h3> <pre> <code class="language-objectivec">/** 动画时间 */ - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext { return 0.5f; } /** 动画内容(如何转场) */ - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { /** * 1.transitionContext 过渡内容上下文,可以通过它调用`viewControllerForKey:`拿到对应的过渡控制器 key:UITransitionContextToViewControllerKey 目的控制器 UITransitionContextFromViewControllerKey 开始控制器 2.拿到对应的过渡控制器之后需要设置view的frame `finalFrameForViewController:` 可以拿到最后的frame,最后即完成动画后的frame `initialFrameForViewController:` 拿到初始化的frame,开始动画之前的frame 3.然后添加到`transitionContext的containerView` 4.设置动画的其他附带属性动画 5.做动画... `UIView的block动画` 6.在动画结束后我们必须向context报告VC切换完成,是否成功。系统在接收到这个消息后,将对VC状态进行维护。 * */ //1... UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *toView = toVC.view; //2... CGRect finalFrame = [transitionContext finalFrameForViewController:toVC]; //(dx, dy) eg:dx偏移多少 toView.frame = CGRectOffset(finalFrame, 0, -SCREEN_SIZE.height); //3.... UIView *containerView = [transitionContext containerView]; [containerView addSubview:toView]; //4... //5... [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{ toView.frame = finalFrame; } completion:^(BOOL finished) { //6... [transitionContext completeTransition:YES]; }]; }</code></pre> <h3><strong>Pop做Push的相反操作即可</strong></h3> <p>3. ViewController如何使用自定义转场动画</p> <ul> <li> <p>pushViewController</p> <p>在push的控制器设置 navigationController 的 delegate 为 self</p> <pre> <code class="language-objectivec">self.navigationController.delegate = self;</code></pre> <p>实现协议方法</p> <pre> <code class="language-objectivec">- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { if (operation == UINavigationControllerOperationPush) { LYPushTransition *push = [LYPushTransition new]; return push; } else if (operation == UINavigationControllerOperationPop) { LYPopTransition *pop = [LYPopTransition new]; return pop; }else { return nil; } }</code></pre> <p>通过 operation 判断是 push 操作还是 pop 操作,然后然后对于的动画即可</p> <p>pop 控制器不需要做任何操作</p> <p>如果使用 push ,则会发现 NavigationBar 没有变化,会一直处于那个地方,很丑...</p> <p>然而使用 present 就可以避免这种现象</p> </li> <li> <p>presentViewController</p> 设置 presentViewController 的 ViewController 的 transitioningDelegate 为 self <p>注意,如果是present的 UINavigationController ,则需要设置 NavigationController 的 transitioningDelegate 为 self</p> <pre> <code class="language-objectivec">UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; SecondViewController *secondVC = [storyboard instantiateViewControllerWithIdentifier:@"second"]; secondVC.delegate = self; //* present */ UINavigationController *navi = [[UINavigationController alloc] initWithRootViewController:secondVC]; //* 如果present的NavigationController则需要设置NavigationController的transitioningDelegate为self */ navi.transitioningDelegate = self; [self presentViewController:navi animated:YES completion:nil];</code></pre> <p>实现 transitioningDelegate 协议方法</p> </li> </ul> <pre> <code class="language-objectivec">/** prensent */ - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return self.push; } /** dismiss */ - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return self.pop; }</code></pre> <pre> <code class="language-objectivec">dismiss`控制器则需要写一个代理,告诉`present`的那个控制器`dismiss`即可</code></pre> <p>NavigationItemTitleView按钮的边框&点击切换时候的颜色动画</p> <pre> <code class="language-objectivec">/** 设置边框宽度 */ titleBtn.layer.borderWidth = 1.5; //* 设置Btn的边框颜色 */ titleBtn.layer.borderColor = [UIColor whiteColor].CGColor;</code></pre> <p><strong>关于点击按钮切换时候的动画我是使用的两个UIView的动画</strong></p> <pre> <code class="language-objectivec">//* 改变NavigationTitleBtn的颜色 */ [UIView animateWithDuration:0.3f delay:0.2f options:UIViewAnimationOptionCurveEaseOut animations:^{ [weakSelf.navigationTitleBtn setBackgroundColor:[UIColor colorWithRed:1.000 green:0.812 blue:0.124 alpha:1.000]]; } completion:^(BOOL finished) { [UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseOut animations:^{ [weakSelf.navigationTitleBtn setBackgroundColor:[UIColor colorWithWhite:0.278 alpha:0.500]]; } completion:^(BOOL finished) { }]; }];</code></pre> <p>点击类别按钮弹出菜单(TMTimeLineMenuView)</p> <p>我不是在每个cell下面都添加了deleteBtn,updateBtn,因为这样会使性能大大降低。</p> <p>我是自定义的一个UIView( TMTimeLineMenuView ),这里面有三个控件,分别是 deleteBtn , updateBtn , categoryBtn 。</p> <p>这个categoryBtn是放在deleteBtn,updateBtn上面的。因为在deleteBtn和updateBtn弹出的时候我把 TMTimeLineMenuView 放到了最顶层</p> <pre> <code class="language-objectivec">//* 置顶 */ [weakSelf.superview bringSubviewToFront:weakSelf];</code></pre> <p>也就意味着tableView是在TMTimeLineMenuView的下面。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/90b4ed800969b616e5cf2d9806b92e63.png"></p> <p style="text-align:center">Paste_Image.png</p> <p>如果没有categoryBtn,弹出deleteBtn和updateBtn就感觉是直接在tableViewCell上面做的动画,会很丑。所以添加一个categoryBtn放在updateBtn和deleteBtn上面,就感觉deleteBtn和updateBtn是放在tableViewCell下面的。给用户很好的用户体验。</p> <p><strong>如何将TMTimeLineMenuView中的控件显示到对应的位置?(HomePageViewController->didClickCategoryBtnWithIndexPath:)</strong></p> <p>第一步:获取到点击的cell对应的indexPath</p> <p>第二步:获取对应cell在tableview中的rect</p> <p>第三步:将获取到的rect转换成在self.view中的rect</p> <pre> <code class="language-objectivec">/** 获取cell在tableView中的位置 */ CGRect rect = [self.tableView rectForRowAtIndexPath:indexPath]; //* 转换成在self.view中的位置 */ CGRect rectInSuperview = [self.tableView convertRect:rect toView:[self.tableView superview]]; self.timeLineMenuView.currentImage = self.timeLineCell.categoryImageBtn.currentImage; [self.timeLineMenuView showTimeLineMenuViewWithRect:rectInSuperview ];</code></pre> <h2>创建账单界面(TMCreateBillViewController)</h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/1adbbca57c40bc0917ec057f38154d26.gif"></p> <p style="text-align:center">TimiAddBillController.gif</p> <h2><strong>选择类别动画之类别图片动画(应该使用UI Dynamics)</strong></h2> <p><strong>第一步:</strong></p> <p>在创建账单界面添加一个 UIImageView 控件,大小跟collectionViewCell里面的 categoryImageView 一样,放在屏幕外。并设置圆角。</p> <pre> <code class="language-objectivec">- (UIImageView *)selectCategoryImageView { if (!_selectCategoryImageView) { UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, -30, kCollectionCellWidth-20, kCollectionCellWidth-20)]; imageView.layer.cornerRadius = (kCollectionCellWidth - 20)/2; imageView.layer.masksToBounds = YES; imageView.contentMode = UIViewContentModeScaleAspectFill; _selectCategoryImageView = imageView; } return _selectCategoryImageView; }</code></pre> <p><strong>第二步: 获取点击的位置</strong></p> <p>1.拿到对应cell</p> <pre> <code class="language-objectivec">cell = (TMCategotyCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];</code></pre> <p>2.将cell对应的类别图片赋值给 _selectCategoryImageView<br> 然后获取到cell的 center ,这个 centter的y 仅仅是它在collectionView的位置,所以还需要修改y值,然后使用UIView的block动画移动到headerView上面对应的点。在动画完成之后将它放到最底层</p> <pre> <code class="language-objectivec">/** 选择类别之后的类别图片动画 */ - (void)animationWithCell:(TMCategotyCollectionViewCell *)cell { self.selectCategoryImageView.image = cell.categoryImageView.image; CGPoint center = cell.center; /** 在collectionView中的y */ CGFloat y = CGRectGetMaxY(cell.frame); center.y = kMaxNBY + y + 10; self.selectCategoryImageView.center = center; WEAKSELF [UIView animateWithDuration:0.05 animations:^{ weakSelf.selectCategoryImageView.center = kHeaderCategoryImageCenter; } completion:^(BOOL finished) { [weakSelf.view sendSubviewToBack:weakSelf.selectCategoryImageView]; }]; [self.view bringSubviewToFront:self.selectCategoryImageView]; }</code></pre> <h2><strong>选择类别动画之HeaderView颜色动画</strong></h2> <p><strong>第一步:提取颜色</strong></p> <p>我使用的是一个三方库, <a href="https://github.com/search?utf8=%E2%9C%93&q=ColorExtraction" rel="nofollow,noindex">ColorExtraction</a></p> <pre> <code class="language-objectivec">//* 颜色提取 */ CCColorCube *imageColor = [[CCColorCube alloc] init]; NSArray *colors = [imageColor extractColorsFromImage:category.categoryImage flags:CCAvoidBlack count:1];</code></pre> <p><strong>第二步:动画</strong></p> <p>我是使用UIBezierPath和CAShapeLayer结合CABasicAnimation做的动画。</p> <p>UIBezierPath的path如何而来?</p> <p>path就是一条线,path的 moveToPoint 点就是 self.bounds.origin 点即左上点</p> <p>addLineToPoint 点就是 self.bounds.origin.x 点和 self.bounds.size.height 点即左下点</p> <p>然后通过CABasicAnimation改变 lineWidth</p> <pre> <code class="language-objectivec">- (void)animationWithBgColor:(UIColor *)color { //* 如果选择的类别图片的颜色和上次选择的一样 直接return */ if ([color isEqual: self.previousSelectColor]) return; //* 修改背景颜色为上一次选择的颜色,不然就会是最开始默认的颜色,动画会很丑,给用户的体验很不好 */ self.backgroundColor = self.previousSelectColor; CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"lineWidth"]; animation.fromValue = @0.0; animation.toValue = @(self.bounds.size.width * 2); animation.duration = 0.3f; //* 设置填充色 */ self.bgColorlayer.fillColor = color.CGColor; //* 设置边框色 */ self.bgColorlayer.strokeColor = color.CGColor; self.previousSelectColor = color; //* 保持动画 */ animation.removedOnCompletion = NO; animation.fillMode = kCAFillModeForwards; [self.bgColorlayer addAnimation:animation forKey:@"bgColorAnimation"]; //* 将子控件放在最上面,不然layer会覆盖 */ [self bringSubviewToFront:self.categoryImageView]; [self bringSubviewToFront:self.moneyLabel]; [self bringSubviewToFront:self.categoryNameBtn]; }</code></pre> <h2>饼图(TMPiewViewController)</h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/3ddd2c1edbb6d7e7992c1f65bf982d12.gif"></p> <p style="text-align:center">TMPie.gif</p> <h2><strong>饼图HeaderView部分</strong></h2> <p>控件是使用三方库 iCarousel <a href="/misc/goto?guid=4958870674500642597" rel="nofollow,noindex">链接</a></p> <p><strong>数据源如何而来?</strong></p> <p>1.先把每个月中文对应的英文缩写保存到一个数组中</p> <pre> <code class="language-objectivec">- (NSArray *)items { if (!_items) { _items = @[@"JAN\n1月",@"FEB\n2月",@"MAR\n3月",@"APR\n4月",@"MAY\n5月",@"JUN\n6月",@"JUL\n7月",@"AUG\n8月",@"SEP\n9月",@"OCT\n10月",@"NOV\n11月",@"DEC\n12月",@"ALL\n全部"]; } return _items; }</code></pre> <p>疑问:为什么数据每个元素,中间有个 \n</p> <p>答:我是使用的是一个UILabel \n 用于换行</p> <p>2.拿到筛选过后的数据,是一个NSDictionary。额...说一下,这个筛选过后的数据的一个结构,因为同一天我们可能会记多笔账,所以把同一天的 dateStr 作为 key ,然后把所有属于这一天的账单数据当作一个 value ,目前为止只是过滤掉同一天的时间字符串。</p> <p>然后下一步我们要做的就是过滤掉同一年的相同月份</p> <pre> <code class="language-objectivec">/** 过滤掉同年相同月份 */ - (void)filterMonthWithDateArray:(NSArray *)array { for (NSString *dateStr in array) { NSString *yearAndMonth = [dateStr substringToIndex:7]; BOOL contains = [self containsMonth:yearAndMonth]; if (!contains) { NSString *month = [self conversionDateStringIntoMonth:dateStr]; [self.dic setValue:month forKey:dateStr]; } } [self.dic setValue:self.items.lastObject forKey:@"ALL"]; self.sortDicKeys = [self sortArray:self.dic.allKeys ascending:YES]; [self.iCar reloadData]; } /** 把时间字符串转换成月份 */ - (NSString *)conversionDateStringIntoMonth:(NSString *)dateString { NSRange range = NSMakeRange(5, 2); NSString *month = [dateString substringWithRange:range]; return self.items[month.integerValue - 1]; } /** 判断字典里面是否已经包含这个对象 */ - (BOOL)containsMonth:(NSString *)yearAndMonth { if (self.dic.allKeys.count==0) { return NO; } else { for (NSInteger i=0; i<self.dic.allKeys.count ; i++) { if ([[self.dic.allKeys[i] substringToIndex:7] isEqualToString:yearAndMonth]) { return YES; } } } return NO; }</code></pre> <h2><strong>获取layer的位置</strong></h2> <pre> <code class="language-objectivec">- (NSInteger)getLayerIndexWithPoint:(CGPoint)point { for (NSInteger i=0; i<[self.containerLayer sublayers].count; i++) { CAShapeLayer *layer = (CAShapeLayer *)[self.containerLayer sublayers][i]; CGPathRef path = [layer path]; if (CGPathContainsPoint(path, NULL, point, 0)) { return i; } } return -1; }</code></pre> <p>拿到所有的sublayer,取出layer的path,通过 CGPathContainsPoint 判断触摸的点是否在这个path里面</p> <p>类别详细界面(TMPiewCategoryDetailViewController)</p> <p>解决cell重用导致数据 年月日label 显示混乱,在模型定义两个 BOOL 变量 same,partSame<br> 拿到数据之后将数据进行“重置”</p> <pre> <code class="language-objectivec">(void)resetBill { self.bills = [NSMutableArray array]; NSString *previous; for (NSInteger i=0; i<self.results.count; i++) { TMBill *bill = self.results[i]; if (i==0) {//第一个数据永远是不相同的 [self.bills addObject:bill]; previous = bill.dateStr; continue; } else { TMBill *theBill = [TMBill new]; if ([previous isEqualToString:bill.dateStr]) {//完全相同,时间日期 theBill = bill; theBill.same = YES; [self.bills addObject:theBill]; } else if ([[previous substringToIndex:7] isEqualToString:[bill.dateStr substringToIndex:7]]) {//部分相同,年月份相同,具体时间不同 theBill = bill; theBill.partSame = YES; [self.bills addObject:theBill]; } else {//不同 [self.bills addObject:bill]; } previous = bill.dateStr; } } }</code></pre> <h2><strong>侧滑控制器,使用的是MMDrawerController库</strong></h2> <p>本来 MMDrawerController 是支持在屏幕向右滑就能出现左边的菜单栏,由于使用了 TYPagerController 出现了手势之间的冲突</p> <p>解决和 TYPagerController 手势冲突的问题</p> <pre> <code class="language-objectivec">UIScreenEdgePanGestureRecognizer *screenEdgeGR = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(clickMenuBtn:)]; screenEdgeGR.edges = UIRectEdgeLeft; [self.view addGestureRecognizer:screenEdgeGR];</code></pre> <pre> <code class="language-objectivec">- (void)clickMenuBtn:(UIButton *)sender { [self.mm_drawerController toggleDrawerSide:MMDrawerSideLeft animated:YES completion:nil]; }</code></pre> <p>如果直接是这样的话则会出现下面的情况</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/c0ed7d1508f86d9a5140a2f2aee2c895.gif"></p> <p style="text-align:center">2016-06-25 22.50.25.gif</p> <p>因为 UIScreenEdgePanGestureRecognizer 是一个持续响应事件,也就是说你的手指没离开屏幕则会一直响应这个函数,因为 toggleDrawerSide 在内部会判断菜单栏是打开还是关闭,打开则关闭,关闭则会打开,所以也就会出现上面这种情况了。</p> <p>解决办法</p> <pre> <code class="language-objectivec">if (self.mm_drawerController.openSide == MMDrawerSideNone) { [self.mm_drawerController toggleDrawerSide:MMDrawerSideLeft animated:YES completion:nil]; }</code></pre> <h2>账本控制器(TMSideViewController)</h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/9a0a2efc7fb633c5750e10a8a6bc7903.gif"></p> <p style="text-align:center">books.gif</p> <p>如何抖动?在cell上添加一个 UILongPressGestureRecognizer 长按手势</p> <pre> <code class="language-objectivec">//* 长按手势 */ UILongPressGestureRecognizer *longGR = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longGR:)]; longGR.minimumPressDuration = 1.0; longGR.numberOfTouchesRequired = 1; longGR.allowableMovement = 10; [self addGestureRecognizer:longGR];</code></pre> <p>给cell添加一个代理</p> <pre> <code class="language-objectivec">@protocol TMSideCellDelegate <NSObject> @required @optional - (void)TMSideCellWithIndexPath:(NSIndexPath *)indexPath withLongPress:(UILongPressGestureRecognizer *)longPress; @end</code></pre> <p>当控制器接收到响应事件的时候只需要做三件事</p> <pre> <code class="language-objectivec">self.editSelectedIndexPath = indexPath; //1 self.edit = YES; //2 [self.collectionView reloadData]; //3</code></pre> <p>在 - (UICollectionViewCell *)collectionView: cellForItemAtIndexPath: 添加判断代码</p> <pre> <code class="language-objectivec">//* edit mode on shake ->ture*/ if (self.isEdit) { if ([indexPath isEqual:self.editSelectedIndexPath]) { cell.editSelectedItemImageView.hidden = NO; [self shakeCell:cell]; } else { cell.editSelectedItemImageView.hidden = YES; } } else { cell.editSelectedItemImageView.hidden = YES; cell.transform = CGAffineTransformIdentity; }</code></pre> <pre> <code class="language-objectivec">/** 抖动动画 */ - (void)shakeCell:(TMSideCell *)cell { [UIView animateWithDuration:0.1 delay:0 options:0 animations:^{ cell.transform=CGAffineTransformMakeRotation(-0.02); } completion:^(BOOL finished) { [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionRepeat|UIViewAnimationOptionAutoreverse | UIViewAnimationOptionAllowUserInteraction animations:^{ cell.transform=CGAffineTransformMakeRotation(0.02); } completion:nil]; }]; }</code></pre> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/d3dbf8dba11a</p> <p> </p>