揭开UIScrollView的神秘面纱 | 掘金技术征文-双节特别篇

965 阅读3分钟

​一、概念理解

​Frame: 

  • 该view在父view坐标系统中的位置和大小(参考系是父view的坐标系)

​Bounds:

  • 该view在自己坐标系统中的位置和大小(参考系是自己view的坐标系)
  • 影响子view的布局
  • scrollView的滚动是改变bounds的origin来进行的

​contentOffset:

  • contentView的顶部和scrollview的顶部的offset是Y值,左边界的差是X值
  • 另一种理解方式:bounds的orgin
var contentOffset: CGPoint { 
    get { return bounds.origin }
    set {
        var bounds = self.bounds
        bounds.origin = newValue  
        self.bounds = bounds
    }   
}

​contentSize:

  • The size of the content view.
contentView包括:
1.cell
2.sectionHeader / sectionFooter
3.tableHeaderView / tableFooterView

​contentInset:

  • The custom distance that the content view is inset from the safe area or scroll view edges.
  • contentInset的会影响contentOffset的最大值和最小值
1、没有inset.top时,contentOffset.y >= 0,当设置inset.top = 88时,contentOffset.y >= -88
2、没有inset.bottom时,contentOffset.y <= (contentView.heigth - scrollView.height),当设置inset.bottom = 40时,contentOffset.y <= (contentView.heigth - scrollView.height + 40)

​bounce:

  • 根据contentOffset和contentInset我们可以模拟一下bounce的实现代码
var contentOffset: CGPoint {
    get { return bounds.origin }
    set {
        var bounds = self.bounds
        if (bounce && !isDragging) || !bounce {
            if newValue.y <= -contentInset.top {
                newValue.y = -contentOffset.top
            }
            if newValue.y >= 
                (contentView.height-frame.height+contentInset.bottom) {
                newValue.y = contentView.height-frame.height+contentInset.bottom
            }
        }
        bounds.origin = newValue
        self.bounds = bounds
    }
}
  • 在view上增加一个panGestureRecognizer,当拖拽时isDragging设置为true,将偏移量设置给contentOffset,当停止拖拽时将isDragging设置为false,同时也设置偏移量,如果这个时候的偏移量已经超出了contentSize,设置动画就会产生弹簧效果

​上面三个属性的图解:

​adjustedContentIndet:

  • Use this property to obtain the adjusted area in which to draw content. The contentInsetAdjustmentBehavior property determines whether the safe area insets are included in the adjustment.** The safe area insets are then added to the values in the contentInset property to obtain the final value of this property**.(文档描述)

  • contentInsetAdjustmentBehavior(ios 11之后)不是never时,系统会在原有的contentInset上加上safeAreaInset,设置inset的时候起作用的也是该属性

​safeAreaInset:

  • iPhone X系列,竖屏(top: 44,left: 0, bottom: 34, right: 0),横屏(top: 0,left: 44, bottom: 21, right: 34)

​二、实际案例

2.1、计算cell出现在期望区域的时机

  • 需求背景:用户主动触发refresh或者loadmore时显示引导tips,refresh后在刷新的第二个cell上显示,loadmore之后在新出现的第二个cell上显示
  • loadmore的情况比较复杂需要进行计算,假定是在第12个cell上显示tips,计算contentOffset.y的临界值来判断cell的位置
  • 参考位置:第12个cell底部与collectionView的底部平齐,此时contentOffset.y的临界值为
12个cell完全出现在collectionView上
临界值 = 12 x cell.height - collectionView.frame.size.height
contentOffset.y >= 临界值
  • 期望位置:第12个cell的底部与tabBar的顶部平齐,即该cell出现在用户视野中tips显示
12个cell出现在tabBar以上
临界值 = 12 x cell.height - frame的height + tabBar.heigth
contentOffset.y >= 临界值
  • 继续拓展,期望位置:第12个cell向上滚动直至消失在用户视野中(不是需求)
12个cell正好被navBar遮盖
临界值 = 12 x cell.height - frame的height + (frame的height-navbar.height)
contentOffset.y >= 临界值

2.2、MJRefresh中的应用

  • MJHeader:这里写的比源码简单,只表达了header的停留和消失的处理逻辑
// 刷新的时刻
let criticalValue = self.mj_h + originalInset.top // 临界值
let insetT = abs(self.scrollView.mj_offsetY) > criticalValue ? 
                                          criticalValue : originalInset.top
if (self.scrollView.mj_insetT != insetT) {
  // 增大了inset.top,意味着offsetY的最小值比原来更小
  self.scrollView.mj_insetT = insetT
}
// 完成刷新
self.scrollView.mj_insetT = originalInset.top
  • MJFooter:和本文的2.1的案例相似,当contentOffset.y >= 临界值时,让footer出现然后变化刷新状态

​三、总结

  • contentInset影响contentOffset的最大值和最小值,也可以做到refresh tips停留效果

  • 找到contentOffset的临界值可以判断某个cell的显示位置,建议把cell刚好完全出现在collectionView/scrollView的时机作为参考点

  • 在使用scrollView的时候建议将系统调整inset设置为never,有我们自己来设置inset

​References:

🏆 掘金技术征文|双节特别篇