|
|
@@ -60,3 +60,111 @@ extension UIScrollView: LNKeyboardNotify {
|
|
|
contentInset = originContentInset
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+extension UIScrollView: UIGestureRecognizerDelegate {
|
|
|
+ func toBeNested() {
|
|
|
+ panGestureRecognizer.delegate = self
|
|
|
+ bounces = false
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - UIGestureRecognizerDelegate
|
|
|
+ public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
|
+ guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer,
|
|
|
+ let currentScrollView = panGesture.view as? UIScrollView else {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ guard currentScrollView.isGestureWithinSelfBounds(panGesture) else {
|
|
|
+ return false // 不在范围内,直接忽略手势
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取滑动方向(向上:y 负,向下:y 正)
|
|
|
+ let translation = panGesture.translation(in: currentScrollView)
|
|
|
+ let isScrollingUp = translation.y < 0 // 向上滑
|
|
|
+ let isScrollingDown = translation.y > 0 // 向下滑
|
|
|
+
|
|
|
+ // 根据滑动方向判断当前 ScrollView 是否可以响应手势
|
|
|
+ if isScrollingUp {
|
|
|
+ return currentScrollView.checkOuterScrollViewsAtBottom(panGesture: panGesture)
|
|
|
+ } else if isScrollingDown {
|
|
|
+ return currentScrollView.checkInnerScrollViewsAtTop(panGesture: panGesture)
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - UI 树遍历核心逻辑
|
|
|
+ /// 上滑:检查所有外层 ScrollView 是否都滚到底部
|
|
|
+ private func checkOuterScrollViewsAtBottom(panGesture: UIPanGestureRecognizer) -> Bool {
|
|
|
+ var currentSuperview = self.superview
|
|
|
+ // 向上遍历父视图,找所有外层 ScrollView
|
|
|
+ while let superview = currentSuperview {
|
|
|
+ if let outerScrollView = superview as? UIScrollView {
|
|
|
+ // 只要有一个外层没滚到底,当前 ScrollView 就不能响应
|
|
|
+ if !outerScrollView.isAtBottom() {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ currentSuperview = superview.superview
|
|
|
+ }
|
|
|
+ // 所有外层都滚到底,当前可以响应
|
|
|
+ return !isAtBottom()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 下滑:检查所有内层 ScrollView 是否都滚到顶部
|
|
|
+ private func checkInnerScrollViewsAtTop(panGesture: UIPanGestureRecognizer) -> Bool {
|
|
|
+ // 向下遍历子视图,找所有内层 ScrollView
|
|
|
+ // 递归遍历子视图的子视图(深层嵌套)
|
|
|
+ if findFirstNotAtTopScrollView(in: self, panGesture: panGesture) != nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ // 所有内层都滚到顶,当前可以响应
|
|
|
+ return !isAtTop()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 递归查找子视图中的第一个 ScrollView(处理深层嵌套)
|
|
|
+ private func findFirstNotAtTopScrollView(
|
|
|
+ in view: UIView,
|
|
|
+ panGesture: UIPanGestureRecognizer) -> UIScrollView? {
|
|
|
+ for subview in view.subviews {
|
|
|
+ if let scrollView = subview as? UIScrollView,
|
|
|
+ scrollView.isGestureWithinSelfBounds(panGesture),
|
|
|
+ !scrollView.isAtTop() {
|
|
|
+ return scrollView
|
|
|
+ }
|
|
|
+ if let found = findFirstNotAtTopScrollView(in: subview, panGesture: panGesture) {
|
|
|
+ return found
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - 滚动边界判断
|
|
|
+ private func isAtTop() -> Bool {
|
|
|
+ let topOffset = contentOffset.y + adjustedContentInset.top
|
|
|
+ return topOffset <= 1 // 允许1pt误差
|
|
|
+ }
|
|
|
+
|
|
|
+ private func isAtBottom() -> Bool {
|
|
|
+ let contentHeight = contentSize.height + adjustedContentInset.bottom
|
|
|
+ let bottomOffset = contentOffset.y + bounds.height
|
|
|
+ return bottomOffset >= contentHeight - 1
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 判断手势触摸点是否在当前 ScrollView 的可视范围内
|
|
|
+ private func isGestureWithinSelfBounds(_ panGesture: UIPanGestureRecognizer) -> Bool {
|
|
|
+ // 1. 获取手势的触摸点(相对于当前 ScrollView)
|
|
|
+ let touchLocation = panGesture.location(in: self)
|
|
|
+ // 2. 转换为 ScrollView 的 bounds 坐标系(处理 contentOffset 和 contentInset)
|
|
|
+ let convertedPoint = CGPoint(
|
|
|
+ x: touchLocation.x - adjustedContentInset.left,
|
|
|
+ y: touchLocation.y - adjustedContentInset.top
|
|
|
+ )
|
|
|
+ // 3. 校验点是否在 ScrollView 的可视区域内(排除超出 bounds 的部分)
|
|
|
+ return bounds.contains(convertedPoint)
|
|
|
+ }
|
|
|
+}
|