基本結(jié)構(gòu)
- 最外層是一個(gè)UITableView敏储,稱(chēng)為mainScrollView
- mainScrollView的最后一個(gè)section的header是一個(gè)menuBar,cell是一個(gè)UIScrollView已添,負(fù)責(zé)處理橫向移動(dòng)
- UIScrollView里有四個(gè)UIScrollView,稱(chēng)為subScrollView
主要難點(diǎn)及解決方法
mainScrollView和subScrollView在滾動(dòng)的時(shí)候會(huì)產(chǎn)生沖突
- 當(dāng)滾動(dòng)subScrollView至臨界點(diǎn)時(shí),無(wú)法指定哪個(gè)UIPanGestureRecognizer被相應(yīng)污尉。如果將其中一個(gè)enable置為false,此時(shí)手勢(shì)被中斷酱酬,手指需要離開(kāi)屏幕,重新滾動(dòng)才能生效滔灶。
- 解決方式:實(shí)現(xiàn)mainScrollView的shouldRecognizeSimultaneouslyWith方法普碎,能同時(shí)識(shí)別兩個(gè)gesture。
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
}
滾動(dòng)過(guò)程中對(duì)contentOffset的處理
- 在滾動(dòng)過(guò)程中需要將其中一個(gè)scrollView的contentOffset設(shè)為固定值录平,讓另一個(gè)scrollView滾動(dòng)麻车。由于滾動(dòng)時(shí)兩個(gè)scrollView的contentOffset都會(huì)改變,所以判斷條件經(jīng)常會(huì)不滿足斗这。
- 解決方式:在兩個(gè)scrollView的scrollViewDidScroll方法里都進(jìn)行處理动猬。
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方法里調(diào)用該方法
func subViewDidScroll(_ scrollView: UIScrollView) {
subScrollView = scrollView
if mainScrollView.contentOffset.y < topCellHeight - fixHeight {
subScrollView.setContentOffset(.zero, animated: false)
}
}
慣性效果無(wú)法在內(nèi)外層傳遞
- 如果是OC代碼,是有慣性傳遞效果的表箭。
- 當(dāng)滑動(dòng)subScrollView時(shí)赁咙,當(dāng)?shù)竭_(dá)臨界值時(shí),最初滾動(dòng)的scrollView停止時(shí)免钻,剩余的慣性不會(huì)傳遞出去彼水。
- 解決方式:先禁止subScrollView原有的線性減速邏輯,再用UIDynamicItem手動(dòng)實(shí)現(xiàn)一個(gè)線性減速的效果极舔。
1凤覆、UIDynamicItem:用來(lái)描述一個(gè)力學(xué)物體的狀態(tài),其實(shí)就是實(shí)現(xiàn)了UIDynamicItem委托的對(duì)象拆魏,或者抽象為有面積有旋轉(zhuǎn)的質(zhì)點(diǎn)盯桦;
2慈俯、UIDynamicBehavior:動(dòng)力行為的描述,用來(lái)指定UIDynamicItem應(yīng)該如何運(yùn)動(dòng)拥峦,即定義適用的物理規(guī)則贴膘。一般我們使用這個(gè)類(lèi)的子類(lèi)對(duì)象來(lái)對(duì)一組UIDynamicItem應(yīng)該遵守的行為規(guī)則進(jìn)行描述;
3事镣、UIDynamicAnimator步鉴;動(dòng)畫(huà)的播放者,動(dòng)力行為(UIDynamicBehavior)的容器璃哟,添加到容器內(nèi)的行為將發(fā)揮作用;
4喊递、ReferenceView:等同于力學(xué)參考系随闪,如果你的初中物理不是語(yǔ)文老師教的話,我想你知道這是啥..只有當(dāng)想要添加力學(xué)的UIView是ReferenceView的子view時(shí)骚勘,動(dòng)力UI才發(fā)生作用铐伴。
DynamicItem的實(shí)例可以看作是一個(gè)質(zhì)點(diǎn), 在垂直方向上, 它的位置(center)可以用來(lái)計(jì)算兩幀動(dòng)畫(huà)之間scrollView移動(dòng)的距離, , 它的 transform 屬性可以不用考慮.
// 遵循UIDynamicItem協(xié)議的質(zhì)點(diǎn)(有位置速度,無(wú)尺寸)
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<CGPoint>) {
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方法里調(diào)用該方法
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滑動(dòng)到頂部俏讹,需要把慣性傳遞給mainScrollView
subScrollView.contentOffset.y = 0
self.mainTableView.contentOffset.y = mainOffset - (scrollDistance - subOffset)
} else if self.bounceBehavior != nil { // 在回彈過(guò)程中当宴,scrollDistance為負(fù)數(shù),保證mainScrollView的offset不超過(guò)0
subScrollView.contentOffset.y = 0
self.mainTableView.contentOffset.y = min(mainOffset - (scrollDistance - subOffset), 0)
} else { // subScrollView未滑動(dòng)到頂部泽疆,正常減速
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滑動(dòng)到極限值户矢,需要把慣性傳遞給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滑動(dòng)未到極限值,正常減速
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)
}
}
}