From 44a6e284e4934adf0c1331a40304dbdd54ffdba8 Mon Sep 17 00:00:00 2001 From: Peter <> Date: Sat, 1 Dec 2018 02:42:21 +0400 Subject: [PATCH] Improved ListView scroll indicator Fixed PresentationContext ordering --- Display/ListView.swift | 141 +++++++++++++++++------------- Display/PresentationContext.swift | 53 ++++++++--- Display/WindowContent.swift | 2 +- 3 files changed, 120 insertions(+), 76 deletions(-) diff --git a/Display/ListView.swift b/Display/ListView.swift index 8878e6a18b..42971a3df8 100644 --- a/Display/ListView.swift +++ b/Display/ListView.swift @@ -2523,7 +2523,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.updateItemHeaders(leftInset: listInsets.left, rightInset: listInsets.right, transition: headerNodesTransition, animateInsertion: animated || !requestItemInsertionAnimationsIndices.isEmpty) - if let offset = offset , abs(offset) > CGFloat.ulpOfOne { + if let offset = offset, !offset.isZero { let lowestNodeToInsertBelow = self.lowestNodeToInsertBelow() for itemNode in temporaryPreviousNodes { itemNode.frame = itemNode.frame.offsetBy(dx: 0.0, dy: offset) @@ -2550,6 +2550,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } let animation: CABasicAnimation + let reverseAnimation: CABasicAnimation switch scrollToItem.curve { case let .Spring(duration): let springAnimation = makeSpringAnimation("sublayerTransform") @@ -2566,7 +2567,17 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } springAnimation.speed = speed * Float(springAnimation.duration / duration) + let reverseSpringAnimation = makeSpringAnimation("sublayerTransform") + reverseSpringAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, offset, 0.0)) + reverseSpringAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + reverseSpringAnimation.isRemovedOnCompletion = true + reverseSpringAnimation.isAdditive = true + reverseSpringAnimation.fillMode = kCAFillModeForwards + + reverseSpringAnimation.speed = speed * Float(reverseSpringAnimation.duration / duration) + animation = springAnimation + reverseAnimation = reverseSpringAnimation case let .Default(duration): if let duration = duration { let basicAnimation = CABasicAnimation(keyPath: "sublayerTransform") @@ -2576,17 +2587,36 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) basicAnimation.isRemovedOnCompletion = true basicAnimation.isAdditive = true + + let reverseBasicAnimation = CABasicAnimation(keyPath: "sublayerTransform") + reverseBasicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + reverseBasicAnimation.duration = duration * UIView.animationDurationFactor() + reverseBasicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, offset, 0.0)) + reverseBasicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + reverseBasicAnimation.isRemovedOnCompletion = true + reverseBasicAnimation.isAdditive = true + animation = basicAnimation + reverseAnimation = reverseBasicAnimation } else { let basicAnimation = CABasicAnimation(keyPath: "sublayerTransform") basicAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.33, 0.52, 0.25, 0.99) - //basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) basicAnimation.duration = (duration ?? 0.3) * UIView.animationDurationFactor() basicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -offset, 0.0)) basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) basicAnimation.isRemovedOnCompletion = true basicAnimation.isAdditive = true + + let reverseBasicAnimation = CABasicAnimation(keyPath: "sublayerTransform") + reverseBasicAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.33, 0.52, 0.25, 0.99) + reverseBasicAnimation.duration = (duration ?? 0.3) * UIView.animationDurationFactor() + reverseBasicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, offset, 0.0)) + reverseBasicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity) + reverseBasicAnimation.isRemovedOnCompletion = true + reverseBasicAnimation.isAdditive = true + animation = basicAnimation + reverseAnimation = reverseBasicAnimation } } animation.completion = { _ in @@ -2610,6 +2640,9 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } } self.layer.add(animation, forKey: nil) + if let verticalScrollIndicator = self.verticalScrollIndicator { + verticalScrollIndicator.layer.add(reverseAnimation, forKey: nil) + } } else { if useBackgroundDeallocation { assertionFailure() @@ -2969,100 +3002,86 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel var topIndexAndBoundary: (Int, CGFloat, CGFloat)? var bottomIndexAndBoundary: (Int, CGFloat, CGFloat)? for itemNode in self.itemNodes { - if itemNode.apparentFrame.maxY > 0.0, let index = itemNode.index { + if itemNode.apparentFrame.maxY > self.insets.top, let index = itemNode.index { topIndexAndBoundary = (index, itemNode.apparentFrame.minY, itemNode.apparentFrame.height) break } } for itemNode in self.itemNodes.reversed() { - if itemNode.apparentFrame.minY <= self.visibleSize.height, let index = itemNode.index { + if itemNode.apparentFrame.minY <= self.visibleSize.height - self.insets.bottom, let index = itemNode.index { bottomIndexAndBoundary = (index, itemNode.apparentFrame.maxY, itemNode.apparentFrame.height) break } } if let topIndexAndBoundary = topIndexAndBoundary, let bottomIndexAndBoundary = bottomIndexAndBoundary { - let rangeItemCount = max(1, bottomIndexAndBoundary.0 - topIndexAndBoundary.0 + 1) - //let visibleRangeHeight = max(0.0, bottomIndexAndBoundary.1 - topIndexAndBoundary.1) - //let averageRangeItemHeight = visibleRangeHeight / CGFloat(rangeItemCount) - let averageRangeItemHeight: CGFloat = 44.0 + let averageRangeItemHeight: CGFloat = 44.0 //(bottomIndexAndBoundary.1 - topIndexAndBoundary.1) / CGFloat(bottomIndexAndBoundary.0 - topIndexAndBoundary.0 + 1) - let visibleRangeHeight = CGFloat(rangeItemCount) * averageRangeItemHeight let upperItemsHeight = floor(averageRangeItemHeight * CGFloat(topIndexAndBoundary.0)) - let lowerItemsHeight = floor(averageRangeItemHeight * CGFloat(self.items.count - bottomIndexAndBoundary.0)) - let approximateContentHeight = upperItemsHeight + visibleRangeHeight + lowerItemsHeight + let approximateContentHeight = CGFloat(self.items.count) * averageRangeItemHeight - let convertedTopBoundary: CGFloat - if topIndexAndBoundary.1 < 0.0 { - convertedTopBoundary = topIndexAndBoundary.1 * averageRangeItemHeight / topIndexAndBoundary.2 + var convertedTopBoundary: CGFloat + if topIndexAndBoundary.1 < self.insets.top { + convertedTopBoundary = (topIndexAndBoundary.1 - self.insets.top) * averageRangeItemHeight / topIndexAndBoundary.2 } else { - convertedTopBoundary = topIndexAndBoundary.1 + convertedTopBoundary = topIndexAndBoundary.1 - self.insets.top } + convertedTopBoundary -= upperItemsHeight + + let approximateOffset = -convertedTopBoundary var convertedBottomBoundary: CGFloat = 0.0 - if bottomIndexAndBoundary.1 > self.visibleSize.height { - convertedBottomBoundary = (bottomIndexAndBoundary.1 - self.visibleSize.height) * averageRangeItemHeight / bottomIndexAndBoundary.2 + if bottomIndexAndBoundary.1 > self.visibleSize.height - self.insets.bottom { + convertedBottomBoundary = ((self.visibleSize.height - self.insets.bottom) - bottomIndexAndBoundary.1) * averageRangeItemHeight / bottomIndexAndBoundary.2 + } else { + convertedBottomBoundary = (self.visibleSize.height - self.insets.bottom) - bottomIndexAndBoundary.1 } - convertedBottomBoundary += convertedTopBoundary + CGFloat(rangeItemCount) * averageRangeItemHeight + convertedBottomBoundary += CGFloat(bottomIndexAndBoundary.0 + 1) * averageRangeItemHeight - let approximateFirstItemOffset = convertedTopBoundary - upperItemsHeight - let approximateLastItemOffset = convertedBottomBoundary + let approximateVisibleHeight = max(0.0, convertedBottomBoundary - approximateOffset) + + let approximateScrollingProgress = approximateOffset / (approximateContentHeight - approximateVisibleHeight) + /*#if targetEnvironment(simulator) + print("approximateOffset = \(approximateOffset), convertedBottomBoundary = \(convertedBottomBoundary) / \(vanillaBoundary), approximateScrollingProgress = \(approximateScrollingProgress)") + #endif*/ - let approximateOffset = -approximateFirstItemOffset + self.insets.top - let approximateBottomOffset = -approximateLastItemOffset + self.insets.top let indicatorInsets: CGFloat = 3.0 + let minIndicatorContentHeight: CGFloat = 12.0 + let minIndicatorHeight: CGFloat = 6.0 - //print("convertedTopBoundary = \(convertedTopBoundary), topIndexAndBoundary.1 = \(topIndexAndBoundary.1), upperItemsHeight = \(upperItemsHeight), approximateOffset = \(approximateOffset), approximateBottomOffset = \(approximateBottomOffset)") + let visibleHeightWithoutIndicatorInsets = self.visibleSize.height - self.scrollIndicatorInsets.top - self.scrollIndicatorInsets.bottom - indicatorInsets * 2.0 + let indicatorHeight: CGFloat = max(minIndicatorContentHeight, floor(visibleHeightWithoutIndicatorInsets * (self.visibleSize.height - self.insets.top - self.insets.bottom) / approximateContentHeight)) - let visibleHeightWithoutInsets = self.visibleSize.height - self.insets.top - self.insets.bottom - let visibleHeightWithoutIndicatorInsets = visibleHeightWithoutInsets - indicatorInsets * 2.0 + let upperBound = self.scrollIndicatorInsets.top + indicatorInsets + let lowerBound = self.visibleSize.height - self.scrollIndicatorInsets.bottom - indicatorInsets - indicatorHeight - // visibleHeightWithoutIndicatorInsets -> approximateContentHeight - // x -> approximateOffset - // x = visibleHeightWithoutIndicatorInsets * approximateOffset / approximateContentHeight + let indicatorOffset = ceilToScreenPixels(upperBound * (1.0 - approximateScrollingProgress) + lowerBound * approximateScrollingProgress) - // visibleHeightWithoutIndicatorInsets -> approximateContentHeight - // x -> visibleHeightWithoutInsets - - let indicatorOffset = ceilToScreenPixels(visibleHeightWithoutIndicatorInsets * approximateOffset / approximateContentHeight) - let approximateIndicatorHeight = ceilToScreenPixels(visibleHeightWithoutIndicatorInsets * visibleHeightWithoutInsets / approximateContentHeight) - - let minHeight: CGFloat = 6.0 - let indicatorHeight = max(minHeight, approximateIndicatorHeight) - - var indicatorFrame = CGRect(origin: CGPoint(x: self.rotated ? indicatorInsets : (self.visibleSize.width - 3.0 - indicatorInsets), y: self.scrollIndicatorInsets.top + indicatorInsets + indicatorOffset), size: CGSize(width: 3.0, height: indicatorHeight)) + var indicatorFrame = CGRect(origin: CGPoint(x: self.rotated ? indicatorInsets : (self.visibleSize.width - 3.0 - indicatorInsets), y: indicatorOffset), size: CGSize(width: 3.0, height: indicatorHeight)) if indicatorFrame.minY < self.scrollIndicatorInsets.top + indicatorInsets { indicatorFrame.size.height -= self.scrollIndicatorInsets.top + indicatorInsets - indicatorFrame.minY indicatorFrame.origin.y = self.scrollIndicatorInsets.top + indicatorInsets - indicatorFrame.size.height = max(minHeight, indicatorFrame.height) + indicatorFrame.size.height = max(minIndicatorHeight, indicatorFrame.height) } - if verticalScrollIndicator.isHidden { - verticalScrollIndicator.isHidden = false + if indicatorFrame.maxY > self.visibleSize.height - (self.scrollIndicatorInsets.bottom + indicatorInsets) { + indicatorFrame.size.height -= indicatorFrame.maxY - (self.visibleSize.height - (self.scrollIndicatorInsets.bottom + indicatorInsets)) + indicatorFrame.size.height = max(minIndicatorHeight, indicatorFrame.height) + indicatorFrame.origin.y = self.visibleSize.height - (self.scrollIndicatorInsets.bottom + indicatorInsets) - indicatorFrame.height + } + + if indicatorHeight >= visibleHeightWithoutIndicatorInsets { + verticalScrollIndicator.isHidden = true verticalScrollIndicator.frame = indicatorFrame } else { - verticalScrollIndicator.frame = indicatorFrame + if verticalScrollIndicator.isHidden { + verticalScrollIndicator.isHidden = false + verticalScrollIndicator.frame = indicatorFrame + } else { + verticalScrollIndicator.frame = indicatorFrame + } } } else { verticalScrollIndicator.isHidden = true } - /*let size = self.visibleSize.height - let range = computeVerticalScrollRange() - let extent = computeVerticalScrollExtent() - let mOffset = computeVerticalScrollOffset() - - let thickness: CGFloat = 3.0 - var length = round(size * extent / range) - var offset = round((size - length) * mOffset / (range - extent)) - - let minLength = thickness * 2.0 - if length < minLength { - length = minLength - } - if offset + length > size { - offset = size - length - } - - let indicatorHeight: CGFloat = max(3.0, 30.0) - verticalScrollIndicator.frame = CGRect(origin: CGPoint(x: self.visibleSize.width - 3.0 - indicatorInsets, y: self.insets.top + offset), size: CGSize(width: 3.0, height: length))*/ } } diff --git a/Display/PresentationContext.swift b/Display/PresentationContext.swift index 75e878cebf..9b7da9bb95 100644 --- a/Display/PresentationContext.swift +++ b/Display/PresentationContext.swift @@ -39,12 +39,31 @@ final class PresentationContext { return self.view != nil && self.layout != nil } - private(set) var controllers: [ViewController] = [] + private(set) var controllers: [(ViewController, PresentationSurfaceLevel)] = [] private var presentationDisposables = DisposableSet() var topLevelSubview: UIView? + private func topLevelSubview(for level: PresentationSurfaceLevel) -> UIView? { + var topController: ViewController? + for (controller, controllerLevel) in self.controllers.reversed() { + if !controller.isViewLoaded || controller.view.superview == nil { + continue + } + if controllerLevel.rawValue > level.rawValue { + topController = controller + } else { + break + } + } + if let topController = topController { + return topController.view + } else { + return topLevelSubview + } + } + private var nextBlockInteractionToken = 0 private var blockInteractionTokens = Set() @@ -67,7 +86,7 @@ final class PresentationContext { } } - public func present(_ controller: ViewController, on: PresentationSurfaceLevel, blockInteraction: Bool = false, completion: @escaping () -> Void) { + public func present(_ controller: ViewController, on level: PresentationSurfaceLevel, blockInteraction: Bool = false, completion: @escaping () -> Void) { let controllerReady = controller.ready.get() |> filter({ $0 }) |> take(1) @@ -103,11 +122,17 @@ final class PresentationContext { if let blockInteractionToken = blockInteractionToken { strongSelf.removeBlockInteraction(blockInteractionToken) } - if strongSelf.controllers.contains(where: { $0 === controller }) { + if strongSelf.controllers.contains(where: { $0.0 === controller }) { return } - strongSelf.controllers.append(controller) + var insertIndex: Int? + for i in (0 ..< strongSelf.controllers.count).reversed() { + if strongSelf.controllers[i].1.rawValue > level.rawValue { + insertIndex = i + } + } + strongSelf.controllers.insert((controller, level), at: insertIndex ?? strongSelf.controllers.count) if let view = strongSelf.view, let layout = strongSelf.layout { controller.navigation_setDismiss({ [weak controller] in if let strongSelf = self, let controller = controller { @@ -117,14 +142,14 @@ final class PresentationContext { controller.setIgnoreAppearanceMethodInvocations(true) if layout != initialLayout { controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) - if let topLevelSubview = strongSelf.topLevelSubview { + if let topLevelSubview = strongSelf.topLevelSubview(for: level) { view.insertSubview(controller.view, belowSubview: topLevelSubview) } else { view.addSubview(controller.view) } controller.containerLayoutUpdated(layout, transition: .immediate) } else { - if let topLevelSubview = strongSelf.topLevelSubview { + if let topLevelSubview = strongSelf.topLevelSubview(for: level) { view.insertSubview(controller.view, belowSubview: topLevelSubview) } else { view.addSubview(controller.view) @@ -138,7 +163,7 @@ final class PresentationContext { } })) } else { - self.controllers.append(controller) + self.controllers.append((controller, level)) } } @@ -147,7 +172,7 @@ final class PresentationContext { } private func dismiss(_ controller: ViewController) { - if let index = self.controllers.index(where: { $0 === controller }) { + if let index = self.controllers.index(where: { $0.0 === controller }) { self.controllers.remove(at: index) controller.viewWillDisappear(false) controller.view.removeFromSuperview() @@ -162,7 +187,7 @@ final class PresentationContext { if wasReady != self.ready { self.readyChanged(wasReady: wasReady) } else if self.ready { - for controller in self.controllers { + for (controller, _) in self.controllers { controller.containerLayoutUpdated(layout, transition: transition) } } @@ -178,7 +203,7 @@ final class PresentationContext { private func addViews() { if let view = self.view, let layout = self.layout { - for controller in self.controllers { + for (controller, _) in self.controllers { controller.viewWillAppear(false) if let topLevelSubview = self.topLevelSubview { view.insertSubview(controller.view, belowSubview: topLevelSubview) @@ -193,7 +218,7 @@ final class PresentationContext { } private func removeViews() { - for controller in self.controllers { + for (controller, _) in self.controllers { controller.viewWillDisappear(false) controller.view.removeFromSuperview() controller.viewDidDisappear(false) @@ -201,7 +226,7 @@ final class PresentationContext { } func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - for controller in self.controllers.reversed() { + for (controller, _) in self.controllers.reversed() { if controller.isViewLoaded { if let result = controller.view.hitTest(point, with: event) { return result @@ -214,7 +239,7 @@ final class PresentationContext { func combinedSupportedOrientations() -> ViewControllerSupportedOrientations { var mask = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all) - for controller in self.controllers { + for (controller, _) in self.controllers { mask = mask.intersection(controller.supportedOrientations) } @@ -224,7 +249,7 @@ final class PresentationContext { func combinedDeferScreenEdgeGestures() -> UIRectEdge { var edges: UIRectEdge = [] - for controller in self.controllers { + for (controller, _) in self.controllers { edges = edges.union(controller.deferScreenEdgeGestures) } diff --git a/Display/WindowContent.swift b/Display/WindowContent.swift index edb9dd5dae..0a63d4e86f 100644 --- a/Display/WindowContent.swift +++ b/Display/WindowContent.swift @@ -992,7 +992,7 @@ public class Window1 { } public func forEachViewController(_ f: (ViewController) -> Bool) { - for controller in self.presentationContext.controllers { + for (controller, _) in self.presentationContext.controllers { if !f(controller) { break }