遮罩层引导更优方案解析

1,268 阅读5分钟

前情回顾

上周利用周末时间开源了ZSYFrameWork,后来自己在使用过程中,还是发现了不足之处。总结如下:

  • 自定义程度虽然很高,但也带来了负面影响,那就是使用起来过于麻烦了
  • 只支持传Frame的方式,导致我们使用的时候需要自己去计算Frame,其实整个功能这一步就是最麻烦的一步,很多小伙伴要用库就是想省略这一步。我后来反思,如果这一步都不做,这个库也就失去了最大的意义
  • 遮罩不支持调节边距的属性,当然因为之前设计的是传Frame,如果能支持传View的话,那就需要支持调节边距

优化之前,感受一下传Frame的痛苦过程。。。

if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? BigButtonTableViewCell {
    let cellRect = tableView.convert(cell.frame, to: view)
    maskFrame[0] = [CGRect(x: cellRect.origin.x + leftMargin, y: cellRect.origin.y, width: cellRect.width / 3 - leftMargin * 2, height: cellRect.height), CGRect(x: cellRect.origin.x + cellRect.width / 3 * 2 + leftMargin, y: cellRect.origin.y, width: cellRect.width / 3 - leftMargin * 2, height: cellRect.height)]
    maskFrame[1] = [CGRect(x: cellRect.origin.x + cellRect.width / 3 + leftMargin, y: cellRect.origin.y, width: cellRect.width / 3 - leftMargin * 2, height: cellRect.height)]
}
if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 1)) as? TitleTableViewCell {
    let cellRect = tableView.convert(cell.frame, to: view)
    maskFrame[2] = [CGRect(x: cellRect.origin.x, y: cellRect.origin.y, width: cellRect.width, height: noEventTodayHeight)]
    if let events = calendarDetail?.events, events.count > todayItemCount {
        maskFrame[2] = [CGRect(x: cellRect.origin.x, y: cellRect.origin.y, width: cellRect.width, height: overTwoEventTodayHeight)]
    }
    if let events = calendarDetail?.events, events.count > 0, events.count <= todayItemCount {
        maskFrame[2] = [CGRect(x: cellRect.origin.x, y: cellRect.origin.y, width: cellRect.width, height: todayTitleHeight + todayItemHeight + todayItemHeight * CGFloat(integerLiteral: events.count))]
    }
}
if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 2)) as? TitleTableViewCell {
    let cellRect = tableView.convert(cell.frame, to: view)
    if cellRect.origin.y > view.height - customerCenterHeight - tabbarHeight - 42 {
        maskFrame[3] = [CGRect(x: cellRect.origin.x, y: view.height - customerCenterHeight - tabbarHeight - 42, width: cellRect.width, height: customerCenterHeight)]
    } else {
        maskFrame[3] = [CGRect(x: cellRect.origin.x, y: cellRect.origin.y, width: cellRect.width, height: customerCenterHeight)]
    }
}

制定目标

基于自己的糟糕使用体验,我为自己重新定了一个目标。

  • 使用者最好能一行代码调用,傻瓜式集成,只需要传递相应View就能实现完美遮罩效果。

提出问题

想要达到上面的目标,有几个场景需要思考。静态页面初步一想应该没啥问题,问题在于动态View,比如ScrollView或者TableView,当然我这边的实际案例就是基于TableView。

  • ScrollView,需要遮罩的View已经接近底部,一半在屏幕内,一半在屏幕外,遮罩是正确的,但用户体验不完美,如果能实现上滑到完全可见,再进行遮罩会更完美。
  • ScrollView,需要遮罩的View已经完全在屏幕之外看不见,我们能否帮助使用者将View滑动到屏幕内可见,并进行遮罩。
  • TableView,需要遮罩的View在屏幕以外,但TableView跟ScrollView不同,因为存在复用机制,使用者都拿不到那个还未渲染出来的View,我们能否通过其它方式实现滚动后遮罩。

思考问题

思考问题的过程其实就是理清思路的过程,是一个库完整设计的过程,我的理解应该是核心过程。

  • 动态View,我们就需要判断遮罩View是否在屏幕内,如果超出屏幕,我们需要滚动到屏幕内再进行遮罩。这样想来ScrollView可能问题不是很大,毕竟可以通过判断View的origin.y和屏幕高度的大小,来确认是否在屏幕以外,只是我需要验证一下scrollRectToVisible这个方法在View一半可见一半不可见时能否起作用,后来实践证明是可行的。

  • 但是scrollRectToVisible这个方法需要ScrollView去调用,我们库很难去判断使用者需要遮罩的View是否存在于滚动视图,使用者有可能嵌套了很多View,我们去拿它的superView一层层去遍历不现实,对性能也多多少少会有影响,设计上不应该这样。那么最后我们采取一个折中方案,用户在明确是一个ScrollView的时候,把它传进来,这样对于他使用体验而言其实并无多大差别。

  • 滚动完成后的重新绘制可以放在UIScrollView的代理方法

extension ZSYMaskViewController: UIScrollViewDelegate {
    public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        ///重新绘制
    }
}
  • TableView的难点在于复用机制,使用的时候可能连View都拿不到。库能做到的是通过IndexPath实现滚动遮罩。所以能确认拿到的可以继续传View,有可能拿不到的可以传IndexPath。
  • 有了IndexPath,用TableView的scrollToRow方法,滚到相应IndexPath。
open func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool)
  • 滚动完成,用上面的UIScrollView代理监听,继续重新绘制即可。

解决问题

解决问题的过程其实就是编码的过程了,这里贴出使用案例,详细内容可去ZSYMaskView查看Demo代码

静态页面调用:

let vc = MaskForStaticViewController()
vc.maskView = [0: [greenView, orangeView], 1: [purpleView]]
present(vc, animated: true, completion: nil)

ScrollView页面调用:(多了一个scrollView参数)

let vc = ZSYMaskViewController()
vc.maskView = [0: [greenView], 1: [blueView], 2: [yellowView]]
vc.scrollView = scrollView
present(vc, animated: true, completion: nil)

TableView页面调用:(多了一个tableView参数、invisibleIndexPath参数)

let vc = ZSYMaskViewController()
if let cellView = tableView.cellForRow(at: IndexPath(row: 3, section: 0)) as? CustomTableViewCell {
    vc.maskView = [0: [sender], 1: [tableView.cellForRow(at: IndexPath(row: 1, section: 0))!], 2: [cellView.cellView2]]
    vc.invisibleIndexPath = [3: IndexPath(row: 7, section: 0)]
    vc.tableView = tableView
    present(vc, animated: true, completion: nil)
}

通过key-value的形式传入相应View数组,key代表遮罩步骤,因为同一页面可能有多步遮罩,value设计成数组是因为,单步遮罩可能有多个View需要镂空,就比如我们的案例

案例完整展示

总结

还是就像我上一篇说的,优化之路永无止境,一个库要能真正做到能用、易用是一个持续思考的过程,很庆幸,我暂时很享受这个过程。当然要是能开源给更多人使用,给大家带来实质性的便利,才是开源的真正意义。

最后

Github地址:ZSYMaskView

如果这篇文章能给到你们启发,就点个赞👍👍👍吧

如果这个库有帮到你们,就点个star🌟🌟🌟吧

最后的最后,还是我最爱的Tony Stark