|
@@ -1,691 +0,0 @@
|
|
|
-//
|
|
|
-// TOScrollBar.m
|
|
|
-//
|
|
|
-// Copyright 2016-2017 Timothy Oliver. All rights reserved.
|
|
|
-//
|
|
|
-// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
-// of this software and associated documentation files (the "Software"), to
|
|
|
-// deal in the Software without restriction, including without limitation the
|
|
|
-// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
|
-// sell copies of the Software, and to permit persons to whom the Software is
|
|
|
-// furnished to do so, subject to the following conditions:
|
|
|
-//
|
|
|
-// The above copyright notice and this permission notice shall be included in
|
|
|
-// all copies or substantial portions of the Software.
|
|
|
-//
|
|
|
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
|
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
|
-// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
|
-// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
-
|
|
|
-#import "TOScrollBar.h"
|
|
|
-#import "UIScrollView+TOScrollBar.h"
|
|
|
-#import "TOScrollBarGestureRecognizer.h"
|
|
|
-
|
|
|
-/** Default values for the scroll bar */
|
|
|
-static const CGFloat kTOScrollBarTrackWidth = 2.0f; // The default width of the scrollable space indicator
|
|
|
-static const CGFloat kTOScrollBarHandleWidth = 4.0f; // The default width of the handle control
|
|
|
-static const CGFloat kTOScrollBarEdgeInset = 7.5f; // The distance from the edge of the view to the center of the track
|
|
|
-static const CGFloat kTOScrollBarHandleMinHeight = 64.0f; // The minimum usable size to which the handle can shrink
|
|
|
-static const CGFloat kTOScrollBarWidth = 20.0f; // The width of this control (44 is minimum recommended tapping space) TWS how handleWidth
|
|
|
-static const CGFloat kTOScrollBarVerticalPadding = 10.0f; // The default padding at the top and bottom of the view
|
|
|
-static const CGFloat kTOScrollBarMinimumContentScale = 5.0f; // The minimum scale of the content view before showing the scroll view is necessary
|
|
|
-
|
|
|
-/************************************************************************/
|
|
|
-
|
|
|
-// A struct to hold the scroll view's previous state before this bar was applied
|
|
|
-struct TOScrollBarScrollViewState {
|
|
|
- BOOL showsVerticalScrollIndicator;
|
|
|
-};
|
|
|
-typedef struct TOScrollBarScrollViewState TOScrollBarScrollViewState;
|
|
|
-
|
|
|
-/************************************************************************/
|
|
|
-// Private interface exposure for scroll view category
|
|
|
-
|
|
|
-@interface UIScrollView () //TOScrollBar
|
|
|
-- (void)setTo_scrollBar:(TOScrollBar *)scrollBar;
|
|
|
-@end
|
|
|
-
|
|
|
-/************************************************************************/
|
|
|
-
|
|
|
-@interface TOScrollBar () <UIGestureRecognizerDelegate> {
|
|
|
- TOScrollBarScrollViewState _scrollViewState;
|
|
|
-}
|
|
|
-
|
|
|
-@property (nonatomic, weak, readwrite) UIScrollView *scrollView; // The parent scroll view in which we belong
|
|
|
-
|
|
|
-@property (nonatomic, assign) BOOL userHidden; // View was explicitly hidden by the user as opposed to us
|
|
|
-
|
|
|
-@property (nonatomic, strong) UIImageView *trackView; // The track indicating the scrollable distance
|
|
|
-@property (nonatomic, strong) UIImageView *handleView; // The handle that may be dragged in the scroll bar
|
|
|
-
|
|
|
-@property (nonatomic, assign, readwrite) BOOL dragging; // The user is presently dragging the handle
|
|
|
-@property (nonatomic, assign) CGFloat yOffset; // The offset from the center of the thumb
|
|
|
-
|
|
|
-@property (nonatomic, assign) CGFloat originalYOffset; // The original placement of the scroll bar when the user started dragging
|
|
|
-@property (nonatomic, assign) CGFloat originalHeight; // The original height of the scroll bar when the user started dragging
|
|
|
-@property (nonatomic, assign) CGFloat originalTopInset; // The original safe area inset of the scroll bar when the user started dragging
|
|
|
-
|
|
|
-@property (nonatomic, assign) CGFloat horizontalOffset; // The horizontal offset when the edge inset is too small for the touch region
|
|
|
-
|
|
|
-@property (nonatomic, assign) BOOL disabled; // Disabled when there's not enough scroll content to merit showing this
|
|
|
-
|
|
|
-@property (nonatomic, strong) UIImpactFeedbackGenerator *feedbackGenerator; // Taptic feedback for iPhone 7 and above
|
|
|
-
|
|
|
-@property (nonatomic, strong) TOScrollBarGestureRecognizer *gestureRecognizer; // Our custom recognizer for handling user interactions with the scroll bar
|
|
|
-
|
|
|
-@end
|
|
|
-
|
|
|
-/************************************************************************/
|
|
|
-
|
|
|
-@implementation TOScrollBar
|
|
|
-
|
|
|
-#pragma mark - Class Creation -
|
|
|
-
|
|
|
-- (instancetype)initWithStyle:(TOScrollBarStyle)style
|
|
|
-{
|
|
|
- if (self = [super initWithFrame:CGRectZero]) {
|
|
|
- _style = style;
|
|
|
- [self setUpInitialProperties];
|
|
|
- }
|
|
|
-
|
|
|
- return self;
|
|
|
-}
|
|
|
-
|
|
|
-- (instancetype)initWithFrame:(CGRect)frame
|
|
|
-{
|
|
|
- if (self = [super initWithFrame:frame]) {
|
|
|
- [self setUpInitialProperties];
|
|
|
- }
|
|
|
-
|
|
|
- return self;
|
|
|
-}
|
|
|
-
|
|
|
-- (instancetype)initWithCoder:(NSCoder *)aDecoder
|
|
|
-{
|
|
|
- if (self = [super initWithCoder:aDecoder]) {
|
|
|
- [self setUpInitialProperties];
|
|
|
- }
|
|
|
-
|
|
|
- return self;
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - Set-up -
|
|
|
-
|
|
|
-- (void)setUpInitialProperties
|
|
|
-{
|
|
|
- _trackWidth = kTOScrollBarTrackWidth;
|
|
|
- _handleWidth = kTOScrollBarHandleWidth;
|
|
|
- _edgeInset = kTOScrollBarEdgeInset;
|
|
|
- _handleMinimiumHeight = kTOScrollBarHandleMinHeight;
|
|
|
- _minimumContentHeightScale = kTOScrollBarMinimumContentScale;
|
|
|
- _verticalInset = UIEdgeInsetsMake(kTOScrollBarVerticalPadding, 0.0f, kTOScrollBarVerticalPadding, 0.0f);
|
|
|
- _feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
|
|
|
- _gestureRecognizer = [[TOScrollBarGestureRecognizer alloc] initWithTarget:self action:@selector(scrollBarGestureRecognized:)];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setUpViews
|
|
|
-{
|
|
|
- if (self.trackView || self.handleView) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- self.backgroundColor = [UIColor clearColor];
|
|
|
-
|
|
|
- // Create and add the track view
|
|
|
- self.trackView = [[UIImageView alloc] initWithImage:[TOScrollBar verticalCapsuleImageWithWidth:self.trackWidth]];
|
|
|
- [self addSubview:self.trackView];
|
|
|
-
|
|
|
- // Add the handle view
|
|
|
- self.handleView = [[UIImageView alloc] initWithImage:[TOScrollBar verticalCapsuleImageWithWidth:self.handleWidth]];
|
|
|
- [self addSubview:self.handleView];
|
|
|
-
|
|
|
- // Add the initial styling
|
|
|
- [self configureViewsForStyle:self.style];
|
|
|
-
|
|
|
- // Add gesture recognizer
|
|
|
- [self addGestureRecognizer:self.gestureRecognizer];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)configureViewsForStyle:(TOScrollBarStyle)style
|
|
|
-{
|
|
|
- BOOL dark = (style == TOScrollBarStyleDark);
|
|
|
-
|
|
|
- CGFloat whiteColor = 0.0f;
|
|
|
- if (dark) {
|
|
|
- whiteColor = 1.0f;
|
|
|
- }
|
|
|
- self.trackView.tintColor = [UIColor colorWithWhite:whiteColor alpha:0.1f];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)dealloc
|
|
|
-{
|
|
|
- [self restoreScrollView:self.scrollView];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)configureScrollView:(UIScrollView *)scrollView
|
|
|
-{
|
|
|
- if (scrollView == nil) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Make a copy of the scroll view's state and then configure
|
|
|
- _scrollViewState.showsVerticalScrollIndicator = self.scrollView.showsVerticalScrollIndicator;
|
|
|
- scrollView.showsVerticalScrollIndicator = NO;
|
|
|
-
|
|
|
- //Key-value Observers
|
|
|
- [scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
|
|
|
- [scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)restoreScrollView:(UIScrollView *)scrollView
|
|
|
-{
|
|
|
- if (scrollView == nil) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Restore the scroll view's state
|
|
|
- scrollView.showsVerticalScrollIndicator = _scrollView.showsVerticalScrollIndicator;
|
|
|
-
|
|
|
- // Remove the observers
|
|
|
- [scrollView removeObserver:self forKeyPath:@"contentOffset"];
|
|
|
- [scrollView removeObserver:self forKeyPath:@"contentSize"];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)willMoveToSuperview:(UIView *)newSuperview
|
|
|
-{
|
|
|
- [super willMoveToSuperview:newSuperview];
|
|
|
- [self setUpViews];
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - Content Layout -
|
|
|
-
|
|
|
-- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
|
|
|
- change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
|
|
|
-{
|
|
|
- [self updateStateForScrollView];
|
|
|
- if (self.hidden) { return; }
|
|
|
- [self layoutInScrollView];
|
|
|
- [self setNeedsLayout];
|
|
|
-}
|
|
|
-
|
|
|
-- (CGFloat)heightOfHandleForContentSize
|
|
|
-{
|
|
|
- if (_scrollView == nil) {
|
|
|
- return _handleMinimiumHeight;
|
|
|
- }
|
|
|
-
|
|
|
- CGFloat heightRatio = self.scrollView.frame.size.height / self.scrollView.contentSize.height;
|
|
|
- CGFloat height = self.frame.size.height * heightRatio;
|
|
|
-
|
|
|
- return MAX(floorf(height), _handleMinimiumHeight);
|
|
|
-}
|
|
|
-
|
|
|
-- (void)updateStateForScrollView
|
|
|
-{
|
|
|
- CGRect frame = _scrollView.frame;
|
|
|
- CGSize contentSize = _scrollView.contentSize;
|
|
|
- self.disabled = (contentSize.height / frame.size.height) < _minimumContentHeightScale;
|
|
|
- [self setHidden:(self.disabled || self.userHidden) animated:NO];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)layoutInScrollView
|
|
|
-{
|
|
|
- CGRect scrollViewFrame = _scrollView.frame;
|
|
|
- UIEdgeInsets insets = _scrollView.contentInset;
|
|
|
- CGPoint contentOffset = _scrollView.contentOffset;
|
|
|
- CGFloat halfWidth = (kTOScrollBarWidth * 0.5f);
|
|
|
-
|
|
|
- if (@available(iOS 11.0, *)) {
|
|
|
- insets = _scrollView.adjustedContentInset;
|
|
|
- }
|
|
|
-
|
|
|
- // Contract the usable space by the scroll view's content inset (eg navigation/tool bars)
|
|
|
- scrollViewFrame.size.height -= (insets.top + insets.bottom);
|
|
|
-
|
|
|
- CGFloat largeTitleDelta = 0.0f;
|
|
|
- if (_insetForLargeTitles) {
|
|
|
- largeTitleDelta = fabs(MIN(insets.top + contentOffset.y, 0.0f));
|
|
|
- }
|
|
|
-
|
|
|
- // Work out the final height be further contracting by the padding
|
|
|
- CGFloat height = (scrollViewFrame.size.height - (_verticalInset.top + _verticalInset.bottom)) - largeTitleDelta;
|
|
|
-
|
|
|
- // Work out how much we have to offset the track by to make sure all of the parent view
|
|
|
- // is visible at the edge of the screen (Or else we'll be unable to tap properly)
|
|
|
- CGFloat horizontalOffset = halfWidth - _edgeInset;
|
|
|
- self.horizontalOffset = (horizontalOffset > 0.0f) ? horizontalOffset : 0.0f;
|
|
|
-
|
|
|
- // Work out the frame for the scroll view
|
|
|
- CGRect frame = CGRectZero;
|
|
|
-
|
|
|
- // Size
|
|
|
- frame.size.width = kTOScrollBarWidth;
|
|
|
- frame.size.height = (_dragging ? _originalHeight : height);
|
|
|
-
|
|
|
- // Horizontal placement
|
|
|
- frame.origin.x = scrollViewFrame.size.width - (_edgeInset + halfWidth);
|
|
|
- if (@available(iOS 11.0, *)) { frame.origin.x -= _scrollView.safeAreaInsets.right; }
|
|
|
- frame.origin.x = MIN(frame.origin.x, scrollViewFrame.size.width - kTOScrollBarWidth);
|
|
|
-
|
|
|
- // Vertical placement in scroll view
|
|
|
- if (_dragging) {
|
|
|
- frame.origin.y = _originalYOffset;
|
|
|
- }
|
|
|
- else {
|
|
|
- frame.origin.y = _verticalInset.top;
|
|
|
- frame.origin.y += insets.top;
|
|
|
- frame.origin.y += largeTitleDelta;
|
|
|
- }
|
|
|
- frame.origin.y += contentOffset.y;
|
|
|
-
|
|
|
- // Set the frame
|
|
|
- self.frame = frame;
|
|
|
-
|
|
|
- // Bring the scroll bar to the front in case other subviews were subsequently added over it
|
|
|
- [self.superview bringSubviewToFront:self];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)layoutSubviews
|
|
|
-{
|
|
|
- CGRect frame = self.frame;
|
|
|
-
|
|
|
- // The frame of the track
|
|
|
- CGRect trackFrame = CGRectZero;
|
|
|
- trackFrame.size.width = _trackWidth;
|
|
|
- trackFrame.size.height = frame.size.height;
|
|
|
- trackFrame.origin.x = ceilf(((frame.size.width - _trackWidth) * 0.5f) + _horizontalOffset);
|
|
|
- self.trackView.frame = CGRectIntegral(trackFrame);
|
|
|
-
|
|
|
- // Don't handle automatic layout when dragging; we'll do that manually elsewhere
|
|
|
- if (self.dragging || self.disabled) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // The frame of the handle
|
|
|
- CGRect handleFrame = CGRectZero;
|
|
|
- handleFrame.size.width = _handleWidth;
|
|
|
- handleFrame.size.height = [self heightOfHandleForContentSize];
|
|
|
- handleFrame.origin.x = ceilf(((frame.size.width - _handleWidth) * 0.5f) + _horizontalOffset);
|
|
|
-
|
|
|
- // Work out the y offset of the handle
|
|
|
- UIEdgeInsets contentInset = _scrollView.contentInset;
|
|
|
- if (@available(iOS 11.0, *)) {
|
|
|
- contentInset = _scrollView.safeAreaInsets;
|
|
|
- }
|
|
|
-
|
|
|
- CGPoint contentOffset = _scrollView.contentOffset;
|
|
|
- CGSize contentSize = _scrollView.contentSize;
|
|
|
- CGRect scrollViewFrame = _scrollView.frame;
|
|
|
-
|
|
|
- CGFloat scrollableHeight = (contentSize.height + contentInset.top + contentInset.bottom) - scrollViewFrame.size.height;
|
|
|
- CGFloat scrollProgress = (contentOffset.y + contentInset.top) / scrollableHeight;
|
|
|
- handleFrame.origin.y = (frame.size.height - handleFrame.size.height) * scrollProgress;
|
|
|
-
|
|
|
- // If the scroll view expanded beyond its scrollable range, shrink the handle to match the rubber band effect
|
|
|
- if (contentOffset.y < -contentInset.top) { // The top
|
|
|
- handleFrame.size.height -= (-contentOffset.y - contentInset.top);
|
|
|
- handleFrame.size.height = MAX(handleFrame.size.height, (_trackWidth * 2 + 2));
|
|
|
- }
|
|
|
- else if (contentOffset.y + scrollViewFrame.size.height > contentSize.height + contentInset.bottom) { // The bottom
|
|
|
- CGFloat adjustedContentOffset = contentOffset.y + scrollViewFrame.size.height;
|
|
|
- CGFloat delta = adjustedContentOffset - (contentSize.height + contentInset.bottom);
|
|
|
- handleFrame.size.height -= delta;
|
|
|
- handleFrame.size.height = MAX(handleFrame.size.height, (_trackWidth * 2 + 2));
|
|
|
- handleFrame.origin.y = frame.size.height - handleFrame.size.height;
|
|
|
- }
|
|
|
-
|
|
|
- // Clamp to the bounds of the frame
|
|
|
- handleFrame.origin.y = MAX(handleFrame.origin.y, 0.0f);
|
|
|
- handleFrame.origin.y = MIN(handleFrame.origin.y, (frame.size.height - handleFrame.size.height));
|
|
|
-
|
|
|
- self.handleView.frame = handleFrame;
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setScrollYOffsetForHandleYOffset:(CGFloat)yOffset animated:(BOOL)animated
|
|
|
-{
|
|
|
- CGFloat heightRange = _trackView.frame.size.height - _handleView.frame.size.height;
|
|
|
- yOffset = MAX(0.0f, yOffset);
|
|
|
- yOffset = MIN(heightRange, yOffset);
|
|
|
-
|
|
|
- CGFloat positionRatio = yOffset / heightRange;
|
|
|
-
|
|
|
- CGRect frame = _scrollView.frame;
|
|
|
- UIEdgeInsets inset = _scrollView.contentInset;
|
|
|
- CGSize contentSize = _scrollView.contentSize;
|
|
|
-
|
|
|
- if (@available(iOS 11.0, *)) {
|
|
|
- inset = _scrollView.adjustedContentInset;
|
|
|
- }
|
|
|
- inset.top = _originalTopInset;
|
|
|
-
|
|
|
- CGFloat totalScrollSize = (contentSize.height + inset.top + inset.bottom) - frame.size.height;
|
|
|
- CGFloat scrollOffset = totalScrollSize * positionRatio;
|
|
|
- scrollOffset -= inset.top;
|
|
|
-
|
|
|
- CGPoint contentOffset = _scrollView.contentOffset;
|
|
|
- contentOffset.y = scrollOffset;
|
|
|
-
|
|
|
- // Animate to help coax the large title navigation bar to behave
|
|
|
- if (@available(iOS 11.0, *)) {
|
|
|
- [UIView animateWithDuration:animated ? 0.1f : 0.00001f animations:^{
|
|
|
- [self.scrollView setContentOffset:contentOffset animated:NO];
|
|
|
- }];
|
|
|
- }
|
|
|
- else {
|
|
|
- [self.scrollView setContentOffset:contentOffset animated:NO];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - Scroll View Integration -
|
|
|
-
|
|
|
-- (void)addToScrollView:(UIScrollView *)scrollView
|
|
|
-{
|
|
|
- if (scrollView == self.scrollView) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Restore the previous scroll view
|
|
|
- [self restoreScrollView:self.scrollView];
|
|
|
-
|
|
|
- // Assign the new scroll view
|
|
|
- self.scrollView = scrollView;
|
|
|
-
|
|
|
- // Apply the observers/settings to the new scroll view
|
|
|
- [self configureScrollView:scrollView];
|
|
|
-
|
|
|
- // Add the scroll bar to the scroll view's content view
|
|
|
- [self.scrollView addSubview:self];
|
|
|
-
|
|
|
- // Add ourselves as a property of the scroll view
|
|
|
- [self.scrollView setTo_scrollBar:self];
|
|
|
-
|
|
|
- // Begin layout
|
|
|
- [self layoutInScrollView];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)removeFromScrollView
|
|
|
-{
|
|
|
- [self restoreScrollView:self.scrollView];
|
|
|
- [self removeFromSuperview];
|
|
|
- [self.scrollView setTo_scrollBar:nil];
|
|
|
- self.scrollView = nil;
|
|
|
-}
|
|
|
-
|
|
|
-- (UIEdgeInsets)adjustedTableViewSeparatorInsetForInset:(UIEdgeInsets)inset
|
|
|
-{
|
|
|
- inset.right = _edgeInset * 2.0f;
|
|
|
- return inset;
|
|
|
-}
|
|
|
-
|
|
|
-- (UIEdgeInsets)adjustedTableViewCellLayoutMarginsForMargins:(UIEdgeInsets)layoutMargins manualOffset:(CGFloat)offset
|
|
|
-{
|
|
|
- layoutMargins.right = (_edgeInset * 2.0f) + 15.0f; // Magic system number is 20, but we can't infer that from here on time
|
|
|
- layoutMargins.right += offset;
|
|
|
- return layoutMargins;
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - User Interaction -
|
|
|
-- (void)scrollBarGestureRecognized:(TOScrollBarGestureRecognizer *)recognizer
|
|
|
-{
|
|
|
- CGPoint touchPoint = [recognizer locationInView:self];
|
|
|
-
|
|
|
- switch (recognizer.state) {
|
|
|
- case UIGestureRecognizerStateBegan:
|
|
|
- [self gestureBeganAtPoint:touchPoint];
|
|
|
- break;
|
|
|
- case UIGestureRecognizerStateChanged:
|
|
|
- [self gestureMovedToPoint:touchPoint];
|
|
|
- break;
|
|
|
- case UIGestureRecognizerStateEnded:
|
|
|
- case UIGestureRecognizerStateCancelled:
|
|
|
- [self gestureEnded];
|
|
|
- break;
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (void)gestureBeganAtPoint:(CGPoint)touchPoint
|
|
|
-{
|
|
|
- if (self.disabled) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Warm-up the feedback generator
|
|
|
- [_feedbackGenerator prepare];
|
|
|
-
|
|
|
- self.scrollView.scrollEnabled = NO;
|
|
|
- self.dragging = YES;
|
|
|
-
|
|
|
- // Capture the original position
|
|
|
- self.originalHeight = self.frame.size.height;
|
|
|
- self.originalYOffset = self.frame.origin.y - self.scrollView.contentOffset.y;
|
|
|
-
|
|
|
- if (@available(iOS 11.0, *)) {
|
|
|
- self.originalTopInset = _scrollView.adjustedContentInset.top;
|
|
|
- } else {
|
|
|
- self.originalTopInset = _scrollView.contentInset.top;
|
|
|
- }
|
|
|
-
|
|
|
- // Check if the user tapped inside the handle
|
|
|
- CGRect handleFrame = self.handleView.frame;
|
|
|
- if (touchPoint.y > (handleFrame.origin.y - 20) &&
|
|
|
- touchPoint.y < handleFrame.origin.y + (handleFrame.size.height + 20))
|
|
|
- {
|
|
|
- self.yOffset = (touchPoint.y - handleFrame.origin.y);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!self.handleExclusiveInteractionEnabled) {
|
|
|
- // User tapped somewhere else, animate the handle to that point
|
|
|
- CGFloat halfHeight = (handleFrame.size.height * 0.5f);
|
|
|
-
|
|
|
- CGFloat destinationYOffset = touchPoint.y - halfHeight;
|
|
|
- destinationYOffset = MAX(0.0f, destinationYOffset);
|
|
|
- destinationYOffset = MIN(self.frame.size.height - halfHeight, destinationYOffset);
|
|
|
-
|
|
|
- self.yOffset = (touchPoint.y - destinationYOffset);
|
|
|
- handleFrame.origin.y = destinationYOffset;
|
|
|
-
|
|
|
- [UIView animateWithDuration:0.2f
|
|
|
- delay:0.0f
|
|
|
- usingSpringWithDamping:1.0f
|
|
|
- initialSpringVelocity:0.1f options:UIViewAnimationOptionBeginFromCurrentState
|
|
|
- animations:^{
|
|
|
- self.handleView.frame = handleFrame;
|
|
|
- } completion:nil];
|
|
|
-
|
|
|
- [self setScrollYOffsetForHandleYOffset:floorf(destinationYOffset) animated:NO];
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (void)gestureMovedToPoint:(CGPoint)touchPoint
|
|
|
-{
|
|
|
- if (self.disabled) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- CGFloat delta = 0.0f;
|
|
|
- CGRect handleFrame = _handleView.frame;
|
|
|
- CGRect trackFrame = _trackView.frame;
|
|
|
- CGFloat minimumY = 0.0f;
|
|
|
- CGFloat maximumY = trackFrame.size.height - handleFrame.size.height;
|
|
|
-
|
|
|
- if (self.handleExclusiveInteractionEnabled) {
|
|
|
- if (touchPoint.y < (handleFrame.origin.y - 20) ||
|
|
|
- touchPoint.y > handleFrame.origin.y + (handleFrame.size.height + 20))
|
|
|
- {
|
|
|
- // This touch is not on the handle; eject.
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Apply the updated Y value plus the previous offset
|
|
|
- delta = handleFrame.origin.y;
|
|
|
- handleFrame.origin.y = touchPoint.y - _yOffset;
|
|
|
-
|
|
|
- //Clamp the handle, and adjust the y offset to counter going outside the bounds
|
|
|
- if (handleFrame.origin.y < minimumY) {
|
|
|
- _yOffset += handleFrame.origin.y;
|
|
|
- _yOffset = MAX(minimumY, _yOffset);
|
|
|
- handleFrame.origin.y = minimumY;
|
|
|
- }
|
|
|
- else if (handleFrame.origin.y > maximumY) {
|
|
|
- CGFloat handleOverflow = CGRectGetMaxY(handleFrame) - trackFrame.size.height;
|
|
|
- _yOffset += handleOverflow;
|
|
|
- _yOffset = MIN(self.yOffset, handleFrame.size.height);
|
|
|
- handleFrame.origin.y = MIN(handleFrame.origin.y, maximumY);
|
|
|
- }
|
|
|
-
|
|
|
- _handleView.frame = handleFrame;
|
|
|
-
|
|
|
- delta -= handleFrame.origin.y;
|
|
|
- delta = fabs(delta);
|
|
|
-
|
|
|
- // If the delta is not 0.0, but we're at either extreme,
|
|
|
- // this is first frame we've since reaching that point.
|
|
|
- // Play a taptic feedback impact
|
|
|
- if (delta > FLT_EPSILON && (CGRectGetMinY(handleFrame) < FLT_EPSILON || CGRectGetMinY(handleFrame) >= maximumY - FLT_EPSILON)) {
|
|
|
- [_feedbackGenerator impactOccurred];
|
|
|
- }
|
|
|
-
|
|
|
- // If the user is doing really granualar swipes, add a subtle amount
|
|
|
- // of vertical animation so the scroll view isn't jumping on each frame
|
|
|
- [self setScrollYOffsetForHandleYOffset:floorf(handleFrame.origin.y) animated:NO]; //(delta < 0.51f)
|
|
|
-}
|
|
|
-
|
|
|
-- (void)gestureEnded
|
|
|
-{
|
|
|
- self.scrollView.scrollEnabled = YES;
|
|
|
- self.dragging = NO;
|
|
|
-
|
|
|
- [UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.5f options:0 animations:^{
|
|
|
- [self layoutInScrollView];
|
|
|
- [self layoutIfNeeded];
|
|
|
- } completion:nil];
|
|
|
-}
|
|
|
-
|
|
|
-- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
|
|
|
-{
|
|
|
- if (!self.handleExclusiveInteractionEnabled) {
|
|
|
- return [super pointInside:point withEvent:event];
|
|
|
- }
|
|
|
- else {
|
|
|
- CGFloat handleMinY = CGRectGetMinY(self.handleView.frame);
|
|
|
- CGFloat handleMaxY = CGRectGetMaxY(self.handleView.frame);
|
|
|
- return (0 <= point.x) && (handleMinY <= point.y) && (point.y <= handleMaxY);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
|
|
|
-{
|
|
|
- UIView *result = [super hitTest:point withEvent:event];
|
|
|
-
|
|
|
- if (self.disabled || self.dragging) {
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
- // If the user contacts the screen in a swiping motion,
|
|
|
- // the scroll view will automatically highjack the touch
|
|
|
- // event unless we explicitly override it here.
|
|
|
-
|
|
|
- self.scrollView.scrollEnabled = (result != self);
|
|
|
- return result;
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - Accessors -
|
|
|
-- (void)setStyle:(TOScrollBarStyle)style
|
|
|
-{
|
|
|
- _style = style;
|
|
|
- [self configureViewsForStyle:style];
|
|
|
-}
|
|
|
-
|
|
|
-- (UIColor *)trackTintColor { return self.trackView.tintColor; }
|
|
|
-
|
|
|
-- (void)setTrackTintColor:(UIColor *)trackTintColor
|
|
|
-{
|
|
|
- self.trackView.tintColor = trackTintColor;
|
|
|
-}
|
|
|
-
|
|
|
-- (UIColor *)handleTintColor { return self.handleView.tintColor; }
|
|
|
-
|
|
|
-- (void)setHandleTintColor:(UIColor *)handleTintColor
|
|
|
-{
|
|
|
- self.handleView.tintColor = handleTintColor;
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setHidden:(BOOL)hidden
|
|
|
-{
|
|
|
- self.userHidden = hidden;
|
|
|
- [self setHidden:hidden animated:NO];
|
|
|
-}
|
|
|
-
|
|
|
-- (void)setHidden:(BOOL)hidden animated:(BOOL)animated
|
|
|
-{
|
|
|
- // Override. It cannot be shown if it's disabled
|
|
|
- if (_disabled) {
|
|
|
- super.hidden = YES;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Simply show or hide it if we're not animating
|
|
|
- if (animated == NO) {
|
|
|
- super.hidden = hidden;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // Show it if we're going to animate it
|
|
|
- if (self.hidden && hidden == NO) {
|
|
|
- super.hidden = NO;
|
|
|
- [self layoutInScrollView];
|
|
|
- [self setNeedsLayout];
|
|
|
- }
|
|
|
-
|
|
|
- CGRect fromFrame = self.frame;
|
|
|
- CGRect toFrame = self.frame;
|
|
|
-
|
|
|
- CGFloat widestElement = MAX(_trackWidth, _handleWidth);
|
|
|
- CGFloat hiddenOffset = fromFrame.origin.x + _edgeInset + (widestElement * 2.0f);
|
|
|
- if (hidden == NO) {
|
|
|
- fromFrame.origin.x = hiddenOffset;
|
|
|
- }
|
|
|
- else {
|
|
|
- toFrame.origin.x = hiddenOffset;
|
|
|
- }
|
|
|
-
|
|
|
- self.frame = fromFrame;
|
|
|
- [UIView animateWithDuration:0.3f
|
|
|
- delay:0.0f
|
|
|
- usingSpringWithDamping:1.0f
|
|
|
- initialSpringVelocity:0.1f
|
|
|
- options:UIViewAnimationOptionBeginFromCurrentState
|
|
|
- animations:^{
|
|
|
- self.frame = toFrame;
|
|
|
- } completion:^(BOOL finished) {
|
|
|
- super.hidden = hidden;
|
|
|
- }];
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
-#pragma mark - Image Generation -
|
|
|
-+ (UIImage *)verticalCapsuleImageWithWidth:(CGFloat)width
|
|
|
-{
|
|
|
- UIImage *image = nil;
|
|
|
- CGFloat radius = width * 0.5f;
|
|
|
- CGRect frame = (CGRect){0, 0, width+1, width+1};
|
|
|
-
|
|
|
- UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f);
|
|
|
- [[UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius] fill];
|
|
|
- image = UIGraphicsGetImageFromCurrentImageContext();
|
|
|
- UIGraphicsEndImageContext();
|
|
|
-
|
|
|
- image = [image resizableImageWithCapInsets:UIEdgeInsetsMake(radius, radius, radius, radius) resizingMode:UIImageResizingModeStretch];
|
|
|
- image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
|
|
-
|
|
|
- return image;
|
|
|
-}
|
|
|
-
|
|
|
-@end
|