基本结构
- 最外层是一个UITableView,称为mainScrollView
- mainScrollView的最后一个section的header是一个menuBar,cell是一个UIScrollView,负责处理横向移动
- UIScrollView里有四个UIScrollView,称为subScrollView
主要难点及解决方法
mainScrollView和subScrollView在滚动的时候会产生冲突
- 当滚动subScrollView至临界点时,无法指定哪个UIPanGestureRecognizer被相应。如果将其中一个enable置为false,此时手势被中断,手指需要离开屏幕,重新滚动才能生效。
- 解决方式:实现mainScrollView的shouldRecognizeSimultaneouslyWith方法,能同时识别两个gesture。
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
}
滚动过程中对contentOffset的处理
- 在滚动过程中需要将其中一个scrollView的contentOffset设为固定值,让另一个scrollView滚动。由于滚动时两个scrollView的contentOffset都会改变,所以判断条件经常会不满足。
- 解决方式:在两个scrollView的scrollViewDidScroll方法里都进行处理。
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard mainScrollView == scrollView else {
return
}
if (subScrollView != nil && subScrollView!.contentOffset.y > 0) || scrollView.contentOffset.y > topCellHeight - fixHeight {
mainScrollView.setContentOffset(CGPoint(x: 0, y: topCellHeight - fixHeight), animated: false)
} else if scrollView.contentOffset.y < topCellHeight - fixHeight {
for subController in self.childViewControllers {
guard let vc = subController as? BaseViewController else {
return
}
vc.tableView.setContentOffset(.zero, animated: false)
}
}
}
// 在subScrollView的scrollViewDidScroll方法里调用该方法
func subViewDidScroll(_ scrollView: UIScrollView) {
subScrollView = scrollView
if mainScrollView.contentOffset.y < topCellHeight - fixHeight {
subScrollView.setContentOffset(.zero, animated: false)
}
}
惯性效果无法在内外层传递
- 如果是OC代码,是有惯性传递效果的。
- 当滑动subScrollView时,当到达临界值时,最初滚动的scrollView停止时,剩余的惯性不会传递出去。
- 解决方式:先禁止subScrollView原有的线性减速逻辑,再用UIDynamicItem手动实现一个线性减速的效果。
1、UIDynamicItem:用来描述一个力学物体的状态,其实就是实现了UIDynamicItem委托的对象,或者抽象为有面积有旋转的质点;
2、UIDynamicBehavior:动力行为的描述,用来指定UIDynamicItem应该如何运动,即定义适用的物理规则。一般我们使用这个类的子类对象来对一组UIDynamicItem应该遵守的行为规则进行描述;
3、UIDynamicAnimator;动画的播放者,动力行为(UIDynamicBehavior)的容器,添加到容器内的行为将发挥作用;
4、ReferenceView:等同于力学参考系,如果你的初中物理不是语文老师教的话,我想你知道这是啥..只有当想要添加力学的UIView是ReferenceView的子view时,动力UI才发生作用。
DynamicItem的实例可以看作是一个质点, 在垂直方向上, 它的位置(center)可以用来计算两帧动画之间scrollView移动的距离, , 它的 transform 属性可以不用考虑.
// 遵循UIDynamicItem协议的质点(有位置速度,无尺寸)
class MyDynamicItem: NSObject, UIDynamicItem {
var center: CGPoint = .zero
var bounds: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
var transform: CGAffineTransform
override init() {
transform = CGAffineTransform()
super.init()
}
}
// 禁止SubScrollView原有的线性减速逻辑
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
DispatchQueue.main.async {
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
}
guard let delegate = delegate else { return }
delegate.subViewWillEndDragging(scrollView, velocity: velocity.y * 500)
}
var dynamicItem = MyDynamicItem()
var animator: UIDynamicAnimator?
var lastCenter: CGPoint = .zero
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.animator?.removeAllBehaviors()
}
// 在subScrollView的scrollViewWillEndDragging方法里调用该方法
func subViewWillEndDragging(_ subScrollView: UIScrollView, velocity: CGFloat) {
if (velocity < 0 && subScrollView.contentOffset.y > 0) || (velocity > 0 && mainTableView.contentOffset.y < self.topCellHeight - self.fixHeight) {
DispatchQueue.main.async {
subScrollView.setContentOffset(subScrollView.contentOffset, animated: false)
}
dynamicItem.center = CGPoint(x: 0, y: mainTableView.contentOffset.y)
lastCenter = dynamicItem.center
let behavior = UIDynamicItemBehavior(items: [dynamicItem])
behavior.addLinearVelocity(CGPoint(x: 0, y: velocity), for: dynamicItem)
behavior.resistance = 2
behavior.action = { [weak self] in
guard let `self` = self else { return }
if velocity < 0 { // 向下滑
let mainOffset = self.mainTableView.contentOffset.y
let subOffset = subScrollView.contentOffset.y
let scrollDistance = (self.lastCenter.y - self.dynamicItem.center.y)
if subOffset - scrollDistance <= 0 { // subScrollView滑动到顶部,需要把惯性传递给mainScrollView
subScrollView.contentOffset.y = 0
self.mainTableView.contentOffset.y = mainOffset - (scrollDistance - subOffset)
} else if self.bounceBehavior != nil { // 在回弹过程中,scrollDistance为负数,保证mainScrollView的offset不超过0
subScrollView.contentOffset.y = 0
self.mainTableView.contentOffset.y = min(mainOffset - (scrollDistance - subOffset), 0)
} else { // subScrollView未滑动到顶部,正常减速
subScrollView.contentOffset.y = subOffset - scrollDistance
self.mainTableView.contentOffset.y = self.topCellHeight - self.fixHeight
}
} else if velocity > 0 { // 向上滑
let mainOffset = self.mainTableView.contentOffset.y
let subOffset = subScrollView.contentOffset.y
let scrollDistance = (self.dynamicItem.center.y - self.lastCenter.y)
if mainOffset + scrollDistance >= self.topCellHeight - self.fixHeight { // mainScrollView滑动到极限值,需要把惯性传递给subScrollView
self.mainTableView.contentOffset.y = self.topCellHeight - self.fixHeight
subScrollView.contentOffset.y = min(subOffset + mainOffset + scrollDistance - (self.topCellHeight - self.fixHeight), subScrollView.contentSize.height - subScrollView.frame.height)
} else { // mainScrollView滑动未到极限值,正常减速
self.mainTableView.contentOffset.y = mainOffset + scrollDistance
subScrollView.contentOffset.y = 0
}
}
self.lastCenter = self.dynamicItem.center
self.bounceAnimate()
}
decelerateBehavior = behavior
animator?.addBehavior(behavior)
}
}
弹簧效果
private func bounceAnimate() {
let outsideFrame = mainTableView.contentOffset.y < 0
if outsideFrame, let animator = self.animator, let _ = decelerateBehavior, bounceBehavior == nil {
var target: CGPoint = .zero
if mainTableView.contentOffset.y < 0 {
dynamicItem.center = mainTableView.contentOffset
target = .zero
mainTableView.bounces = false
let behavior = UIAttachmentBehavior(item: dynamicItem, attachedToAnchor: target)
behavior.length = 0
behavior.damping = 1
behavior.frequency = 2
self.bounceBehavior = behavior
animator.addBehavior(behavior)
}
}
}