123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691 |
- //
- // 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
|