mirror of
https://github.com/danog/Telegram.git
synced 2024-12-03 09:57:46 +01:00
1259 lines
49 KiB
Objective-C
1259 lines
49 KiB
Objective-C
#import "TGMediaPickerGalleryVideoScrubber.h"
|
|
|
|
#import "pop/POP.h"
|
|
|
|
#import "TGFont.h"
|
|
#import "TGImageUtils.h"
|
|
#import "UIControl+HitTestEdgeInsets.h"
|
|
|
|
#import "TGPhotoEditorInterfaceAssets.h"
|
|
|
|
#import "TGMediaPickerGalleryVideoScrubberThumbnailView.h"
|
|
#import "TGMediaPickerGalleryVideoTrimView.h"
|
|
|
|
const CGFloat TGVideoScrubberMinimumTrimDuration = 1.0f;
|
|
const CGFloat TGVideoScrubberZoomActivationInterval = 0.25f;
|
|
const CGFloat TGVideoScrubberTrimRectEpsilon = 3.0f;
|
|
|
|
typedef enum
|
|
{
|
|
TGMediaPickerGalleryVideoScrubberPivotSourceHandle,
|
|
TGMediaPickerGalleryVideoScrubberPivotSourceTrimStart,
|
|
TGMediaPickerGalleryVideoScrubberPivotSourceTrimEnd
|
|
} TGMediaPickerGalleryVideoScrubberPivotSource;
|
|
|
|
@interface TGMediaPickerGalleryVideoScrubber () <UIGestureRecognizerDelegate>
|
|
{
|
|
UILabel *_currentTimeLabel;
|
|
UILabel *_inverseTimeLabel;
|
|
|
|
UIControl *_wrapperView;
|
|
UIView *_summaryThumbnailSnapshotView;
|
|
UIView *_zoomedThumbnailWrapperView;
|
|
UIView *_summaryThumbnailWrapperView;
|
|
TGMediaPickerGalleryVideoTrimView *_trimView;
|
|
UIView *_leftCurtainView;
|
|
UIView *_rightCurtainView;
|
|
UIControl *_scrubberHandle;
|
|
|
|
UIPanGestureRecognizer *_panGestureRecognizer;
|
|
UILongPressGestureRecognizer *_pressGestureRecognizer;
|
|
|
|
bool _beganInteraction;
|
|
bool _endedInteraction;
|
|
|
|
bool _scrubbing;
|
|
CGFloat _scrubbingPosition;
|
|
|
|
NSTimeInterval _duration;
|
|
|
|
bool _ignoreThumbnailLoad;
|
|
bool _fadingThumbnailViews;
|
|
CGFloat _thumbnailAspectRatio;
|
|
NSArray *_summaryTimestamps;
|
|
NSMutableArray *_summaryThumbnailViews;
|
|
|
|
CGSize _originalSize;
|
|
CGRect _cropRect;
|
|
UIImageOrientation _cropOrientation;
|
|
|
|
bool _zoomedIn;
|
|
bool _preparingToZoomIn;
|
|
bool _cancelledZoomIn;
|
|
bool _animatingZoomIn;
|
|
bool _animatingZoomOut;
|
|
|
|
TGMediaPickerGalleryVideoScrubberPivotSource _pivotSource;
|
|
NSTimeInterval _zoomedDuration;
|
|
NSTimeInterval _zoomPivotPosition;
|
|
CGFloat _zoomPivotCenter;
|
|
CGFloat _zoomPivotOffset;
|
|
NSInteger _zoomedPivotTimestampIndex;
|
|
NSArray *_zoomedTimestamps;
|
|
NSMutableArray *_zoomedThumbnailViews;
|
|
}
|
|
@end
|
|
|
|
@implementation TGMediaPickerGalleryVideoScrubber
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
{
|
|
self = [super initWithFrame:frame];
|
|
if (self != nil)
|
|
{
|
|
_allowsTrimming = true;
|
|
|
|
_currentTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(8, 4, 100, 15)];
|
|
_currentTimeLabel.font = TGSystemFontOfSize(12.0f);
|
|
_currentTimeLabel.backgroundColor = [UIColor clearColor];
|
|
_currentTimeLabel.text = @"0:00";
|
|
_currentTimeLabel.textColor = [UIColor whiteColor];
|
|
[self addSubview:_currentTimeLabel];
|
|
|
|
_inverseTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(frame.size.width - 108, 4, 100, 15)];
|
|
_inverseTimeLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
|
|
_inverseTimeLabel.font = TGSystemFontOfSize(12.0f);
|
|
_inverseTimeLabel.backgroundColor = [UIColor clearColor];
|
|
_inverseTimeLabel.text = @"-0:00";
|
|
_inverseTimeLabel.textAlignment = NSTextAlignmentRight;
|
|
_inverseTimeLabel.textColor = [UIColor whiteColor];
|
|
[self addSubview:_inverseTimeLabel];
|
|
|
|
_wrapperView = [[UIControl alloc] initWithFrame:CGRectMake(8, 24, 0, 36)];
|
|
_wrapperView.hitTestEdgeInsets = UIEdgeInsetsMake(-5, -10, -5, -10);
|
|
[self addSubview:_wrapperView];
|
|
|
|
_zoomedThumbnailWrapperView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 32)];
|
|
[_wrapperView addSubview:_zoomedThumbnailWrapperView];
|
|
|
|
_summaryThumbnailWrapperView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 32)];
|
|
_summaryThumbnailWrapperView.clipsToBounds = true;
|
|
[_wrapperView addSubview:_summaryThumbnailWrapperView];
|
|
|
|
_leftCurtainView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
|
|
_leftCurtainView.backgroundColor = [[TGPhotoEditorInterfaceAssets toolbarBackgroundColor] colorWithAlphaComponent:0.8f];
|
|
[_wrapperView addSubview:_leftCurtainView];
|
|
|
|
_rightCurtainView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
|
|
_rightCurtainView.backgroundColor = [[TGPhotoEditorInterfaceAssets toolbarBackgroundColor] colorWithAlphaComponent:0.8f];
|
|
[_wrapperView addSubview:_rightCurtainView];
|
|
|
|
__weak TGMediaPickerGalleryVideoScrubber *weakSelf = self;
|
|
_trimView = [[TGMediaPickerGalleryVideoTrimView alloc] initWithFrame:CGRectZero];
|
|
_trimView.exclusiveTouch = true;
|
|
_trimView.trimmingEnabled = _allowsTrimming;
|
|
_trimView.didBeginEditing = ^(bool start)
|
|
{
|
|
__strong TGMediaPickerGalleryVideoScrubber *strongSelf = weakSelf;
|
|
if (strongSelf == nil)
|
|
return;
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = strongSelf.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubberDidBeginEditing:)])
|
|
[delegate videoScrubberDidBeginEditing:strongSelf];
|
|
|
|
[strongSelf cancelZoomIn];
|
|
if ([strongSelf zoomAvailable])
|
|
{
|
|
if (start)
|
|
strongSelf->_pivotSource = TGMediaPickerGalleryVideoScrubberPivotSourceTrimStart;
|
|
else
|
|
strongSelf->_pivotSource = TGMediaPickerGalleryVideoScrubberPivotSourceTrimEnd;
|
|
|
|
[strongSelf performSelector:@selector(zoomIn) withObject:nil afterDelay:TGVideoScrubberZoomActivationInterval];
|
|
}
|
|
|
|
[strongSelf->_trimView setTrimming:true animated:true];
|
|
|
|
[strongSelf setScrubberHandleHidden:true animated:false];
|
|
};
|
|
_trimView.didEndEditing = ^
|
|
{
|
|
__strong TGMediaPickerGalleryVideoScrubber *strongSelf = weakSelf;
|
|
if (strongSelf == nil)
|
|
return;
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = strongSelf.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubberDidEndEditing:)])
|
|
[delegate videoScrubberDidEndEditing:strongSelf];
|
|
|
|
CGRect newTrimRect = strongSelf->_trimView.frame;
|
|
CGRect trimRect = [strongSelf _scrubbingRect];
|
|
CGRect normalScrubbingRect = [strongSelf _scrubbingRectZoomedIn:false];
|
|
CGFloat maxWidth = trimRect.size.width + normalScrubbingRect.origin.x * 2;
|
|
|
|
CGFloat leftmostPosition = trimRect.origin.x - normalScrubbingRect.origin.x;
|
|
if (newTrimRect.origin.x < leftmostPosition + TGVideoScrubberTrimRectEpsilon)
|
|
{
|
|
CGFloat delta = leftmostPosition - newTrimRect.origin.x;
|
|
|
|
newTrimRect.origin.x += delta;
|
|
newTrimRect.size.width = MIN(maxWidth, newTrimRect.size.width - delta);
|
|
}
|
|
|
|
CGFloat rightmostPosition = maxWidth;
|
|
if (CGRectGetMaxX(newTrimRect) > maxWidth - TGVideoScrubberTrimRectEpsilon)
|
|
{
|
|
CGFloat delta = rightmostPosition - CGRectGetMaxX(newTrimRect);
|
|
|
|
newTrimRect.size.width = MIN(maxWidth, newTrimRect.size.width + delta);
|
|
}
|
|
|
|
strongSelf->_trimView.frame = newTrimRect;
|
|
|
|
NSTimeInterval trimStartPosition = 0.0;
|
|
NSTimeInterval trimEndPosition = 0.0;
|
|
|
|
[strongSelf _trimStartPosition:&trimStartPosition trimEndPosition:&trimEndPosition forTrimFrame:newTrimRect duration:strongSelf.duration];
|
|
|
|
strongSelf->_trimStartValue = trimStartPosition;
|
|
strongSelf->_trimEndValue = trimEndPosition;
|
|
|
|
bool isTrimmed = (strongSelf->_trimStartValue > FLT_EPSILON || fabs(strongSelf->_trimEndValue - strongSelf->_duration) > FLT_EPSILON);
|
|
|
|
[strongSelf->_trimView setTrimming:isTrimmed animated:true];
|
|
|
|
[strongSelf setScrubberHandleHidden:false animated:true];
|
|
|
|
[strongSelf cancelZoomIn];
|
|
if (strongSelf->_zoomedIn)
|
|
[strongSelf zoomOut];
|
|
};
|
|
_trimView.startHandleMoved = ^(CGPoint translation)
|
|
{
|
|
__strong TGMediaPickerGalleryVideoScrubber *strongSelf = weakSelf;
|
|
if (strongSelf == nil)
|
|
return;
|
|
|
|
if (strongSelf->_animatingZoomIn)
|
|
return;
|
|
|
|
UIView *trimView = strongSelf->_trimView;
|
|
|
|
CGRect availableTrimRect = [strongSelf _scrubbingRect];
|
|
CGRect normalScrubbingRect = [strongSelf _scrubbingRectZoomedIn:false];
|
|
CGFloat originX = MAX(0, trimView.frame.origin.x + translation.x);
|
|
CGFloat delta = originX - trimView.frame.origin.x;
|
|
CGFloat maxWidth = availableTrimRect.size.width + normalScrubbingRect.origin.x * 2 - originX;
|
|
|
|
CGRect trimViewRect = CGRectMake(originX,
|
|
trimView.frame.origin.y,
|
|
MIN(maxWidth, trimView.frame.size.width - delta),
|
|
trimView.frame.size.height);
|
|
|
|
NSTimeInterval trimStartPosition = 0.0;
|
|
NSTimeInterval trimEndPosition = 0.0;
|
|
[strongSelf _trimStartPosition:&trimStartPosition trimEndPosition:&trimEndPosition forTrimFrame:trimViewRect duration:strongSelf.duration];
|
|
|
|
if (trimEndPosition - trimStartPosition < TGVideoScrubberMinimumTrimDuration)
|
|
return;
|
|
|
|
trimView.frame = trimViewRect;
|
|
|
|
[strongSelf _layoutTrimCurtainViews];
|
|
|
|
strongSelf->_trimStartValue = trimStartPosition;
|
|
strongSelf->_trimEndValue = trimEndPosition;
|
|
|
|
[strongSelf setValue:strongSelf->_trimStartValue];
|
|
|
|
UIView *handle = strongSelf->_scrubberHandle;
|
|
handle.center = CGPointMake(trimView.frame.origin.x + 12 + handle.frame.size.width / 2, handle.center.y);
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = strongSelf.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubber:editingStartValueDidChange:)])
|
|
[delegate videoScrubber:strongSelf editingStartValueDidChange:trimStartPosition];
|
|
|
|
[strongSelf cancelZoomIn];
|
|
if ([strongSelf zoomAvailable])
|
|
{
|
|
strongSelf->_pivotSource = TGMediaPickerGalleryVideoScrubberPivotSourceTrimStart;
|
|
[strongSelf performSelector:@selector(zoomIn) withObject:nil afterDelay:TGVideoScrubberZoomActivationInterval];
|
|
}
|
|
};
|
|
_trimView.endHandleMoved = ^(CGPoint translation)
|
|
{
|
|
__strong TGMediaPickerGalleryVideoScrubber *strongSelf = weakSelf;
|
|
if (strongSelf == nil)
|
|
return;
|
|
|
|
if (strongSelf->_animatingZoomIn)
|
|
return;
|
|
|
|
UIView *trimView = strongSelf->_trimView;
|
|
|
|
CGRect availableTrimRect = [strongSelf _scrubbingRect];
|
|
CGRect normalScrubbingRect = [strongSelf _scrubbingRectZoomedIn:false];
|
|
CGFloat localOriginX = trimView.frame.origin.x - availableTrimRect.origin.x + normalScrubbingRect.origin.x;
|
|
CGFloat maxWidth = availableTrimRect.size.width + normalScrubbingRect.origin.x * 2 - localOriginX;
|
|
|
|
CGRect trimViewRect = CGRectMake(trimView.frame.origin.x,
|
|
trimView.frame.origin.y,
|
|
MIN(maxWidth, trimView.frame.size.width + translation.x),
|
|
trimView.frame.size.height);
|
|
|
|
NSTimeInterval trimStartPosition = 0.0;
|
|
NSTimeInterval trimEndPosition = 0.0;
|
|
[strongSelf _trimStartPosition:&trimStartPosition trimEndPosition:&trimEndPosition forTrimFrame:trimViewRect duration:strongSelf.duration];
|
|
|
|
if (trimEndPosition - trimStartPosition < TGVideoScrubberMinimumTrimDuration)
|
|
return;
|
|
|
|
trimView.frame = trimViewRect;
|
|
|
|
[strongSelf _layoutTrimCurtainViews];
|
|
|
|
strongSelf->_trimStartValue = trimStartPosition;
|
|
strongSelf->_trimEndValue = trimEndPosition;
|
|
|
|
[strongSelf setValue:strongSelf->_trimEndValue];
|
|
|
|
UIView *handle = strongSelf->_scrubberHandle;
|
|
handle.center = CGPointMake(CGRectGetMaxX(trimView.frame) - 12 - handle.frame.size.width / 2, handle.center.y);
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = strongSelf.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubber:editingEndValueDidChange:)])
|
|
[delegate videoScrubber:strongSelf editingEndValueDidChange:trimEndPosition];
|
|
|
|
[strongSelf cancelZoomIn];
|
|
if ([strongSelf zoomAvailable])
|
|
{
|
|
strongSelf->_pivotSource = TGMediaPickerGalleryVideoScrubberPivotSourceTrimEnd;
|
|
[strongSelf performSelector:@selector(zoomIn) withObject:nil afterDelay:TGVideoScrubberZoomActivationInterval];
|
|
}
|
|
};
|
|
[_wrapperView addSubview:_trimView];
|
|
|
|
_scrubberHandle = [[UIControl alloc] initWithFrame:CGRectMake(0, -1, 8, 38.5f)];
|
|
_scrubberHandle.hitTestEdgeInsets = UIEdgeInsetsMake(-5, -10, -5, -10);
|
|
[_wrapperView addSubview:_scrubberHandle];
|
|
|
|
static UIImage *handleViewImage = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^
|
|
{
|
|
UIGraphicsBeginImageContextWithOptions(CGSizeMake(_scrubberHandle.frame.size.width, _scrubberHandle.frame.size.height), false, 0.0f);
|
|
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
CGContextSetShadowWithColor(context, CGSizeMake(0, 1.5f), 0.5f, [UIColor colorWithWhite:0.0f alpha:0.35f].CGColor);
|
|
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
|
|
|
|
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0.5f, 0.5f, _scrubberHandle.frame.size.width - 1, _scrubberHandle.frame.size.height - 2.5f)
|
|
cornerRadius:3];
|
|
[path fill];
|
|
|
|
handleViewImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
UIGraphicsEndImageContext();
|
|
});
|
|
|
|
UIImageView *scrubberImageView = [[UIImageView alloc] initWithFrame:_scrubberHandle.bounds];
|
|
scrubberImageView.image = handleViewImage;
|
|
[_scrubberHandle addSubview:scrubberImageView];
|
|
|
|
_pressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePress:)];
|
|
_pressGestureRecognizer.delegate = self;
|
|
_pressGestureRecognizer.minimumPressDuration = 0.1f;
|
|
[_scrubberHandle addGestureRecognizer:_pressGestureRecognizer];
|
|
|
|
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
|
|
_panGestureRecognizer.delegate = self;
|
|
[_scrubberHandle addGestureRecognizer:_panGestureRecognizer];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (bool)zoomAvailable
|
|
{
|
|
if (_zoomedIn || _preparingToZoomIn || _summaryTimestamps.count == 0)
|
|
return false;
|
|
|
|
return _duration > 1.0f;
|
|
}
|
|
|
|
- (void)zoomIn
|
|
{
|
|
if (![self zoomAvailable])
|
|
return;
|
|
|
|
_preparingToZoomIn = true;
|
|
|
|
NSTimeInterval trimStartValue = 0.0;
|
|
NSTimeInterval trimEndValue = 0.0;
|
|
|
|
[self _trimStartPosition:&trimStartValue trimEndPosition:&trimEndValue forTrimFrame:_trimView.frame duration:_duration];
|
|
|
|
switch (_pivotSource)
|
|
{
|
|
case TGMediaPickerGalleryVideoScrubberPivotSourceTrimStart:
|
|
_zoomPivotCenter = [self _zoomPivotCenterForTrimStart];
|
|
_zoomPivotPosition = trimStartValue;
|
|
break;
|
|
|
|
case TGMediaPickerGalleryVideoScrubberPivotSourceTrimEnd:
|
|
_zoomPivotCenter = [self _zoomPivotCenterForTrimEnd];
|
|
_zoomPivotPosition = trimEndValue;
|
|
break;
|
|
|
|
default:
|
|
_zoomPivotCenter = [self _zoomPivotCenterForHandle];
|
|
_zoomPivotPosition = [self _positionForScrubberPosition:_scrubberHandle.center duration:_duration];
|
|
break;
|
|
}
|
|
|
|
if (_summaryTimestamps.count > 1)
|
|
_zoomedDuration = [_summaryTimestamps[1] doubleValue] - [_summaryTimestamps[0] doubleValue];
|
|
|
|
__block NSTimeInterval minimalInterval = DBL_MAX;
|
|
__block NSUInteger timestampIndex = 0;
|
|
[_summaryTimestamps enumerateObjectsUsingBlock:^(NSNumber *timestamp, NSUInteger index, __unused BOOL *stop)
|
|
{
|
|
NSTimeInterval timestampValue = timestamp.doubleValue;
|
|
NSTimeInterval interval = fabs(timestampValue - _zoomPivotPosition);
|
|
if (interval < minimalInterval)
|
|
{
|
|
minimalInterval = interval;
|
|
timestampIndex = index;
|
|
}
|
|
}];
|
|
|
|
_zoomedPivotTimestampIndex = timestampIndex;
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDataSource> dataSource = self.dataSource;
|
|
|
|
NSInteger leftSummaryTimestampIndex = MAX(0, (NSInteger)_zoomedPivotTimestampIndex - 1);
|
|
NSInteger rightSummaryTimestampIndex = MIN((NSInteger)_zoomedPivotTimestampIndex + 1, (NSInteger)_summaryTimestamps.count - 1);
|
|
|
|
NSTimeInterval leftTimestamp = [[_summaryTimestamps objectAtIndex:leftSummaryTimestampIndex] doubleValue];
|
|
NSTimeInterval rightTimestamp = [[_summaryTimestamps objectAtIndex:rightSummaryTimestampIndex] doubleValue];
|
|
|
|
if ((NSUInteger)rightSummaryTimestampIndex == _summaryTimestamps.count - 1)
|
|
rightTimestamp = _duration;
|
|
|
|
CGSize thumbnailImageSize = [self _thumbnailSize];
|
|
CGFloat countMultiplier = 1.0f;
|
|
if (_zoomedPivotTimestampIndex > 01)
|
|
countMultiplier = 2.1f;
|
|
NSInteger thumbnailCount = (NSInteger)CGCeil(_summaryThumbnailWrapperView.frame.size.width / thumbnailImageSize.width * countMultiplier);
|
|
|
|
if ([dataSource respondsToSelector:@selector(videoScrubber:evenlySpacedTimestamps:startingAt:endingAt:)])
|
|
_zoomedTimestamps = [dataSource videoScrubber:self evenlySpacedTimestamps:thumbnailCount startingAt:leftTimestamp endingAt:rightTimestamp];
|
|
|
|
CGFloat scale = MIN(2.0f, TGScreenScaling());
|
|
thumbnailImageSize = CGSizeMake(thumbnailImageSize.width * scale, thumbnailImageSize.height * scale);
|
|
|
|
_zoomedThumbnailViews = [[NSMutableArray alloc] init];
|
|
|
|
if ([dataSource respondsToSelector:@selector(videoScrubber:requestThumbnailImagesForTimestamps:size:isSummaryThumbnails:)])
|
|
[dataSource videoScrubber:self requestThumbnailImagesForTimestamps:_zoomedTimestamps size:thumbnailImageSize isSummaryThumbnails:false];
|
|
}
|
|
|
|
- (CGFloat)_zoomPivotCenterForHandle
|
|
{
|
|
CGFloat fractValue = (CGFloat)_value / (CGFloat)_duration * 2 - 1;
|
|
return _scrubberHandle.center.x - [self _scrubbingRectZoomedIn:false].origin.x + fractValue * _scrubberHandle.frame.size.width / 2;
|
|
}
|
|
|
|
- (CGFloat)_zoomPivotCenterForTrimStart
|
|
{
|
|
return CGRectGetMinX(_trimView.frame);
|
|
}
|
|
|
|
- (CGFloat)_zoomPivotCenterForTrimEnd
|
|
{
|
|
CGRect scrubbingRect = [self _scrubbingRectZoomedIn:false];
|
|
return CGRectGetMaxX(_trimView.frame) - scrubbingRect.origin.x * 2;
|
|
}
|
|
|
|
- (void)commitZoomIn
|
|
{
|
|
if (!_preparingToZoomIn)
|
|
return;
|
|
|
|
_zoomedIn = true;
|
|
_preparingToZoomIn = false;
|
|
_animatingZoomIn = true;
|
|
|
|
[self _layoutZoomedThumbnailViewsStacked:true];
|
|
|
|
NSTimeInterval pivotTimestamp = [_summaryTimestamps[_zoomedPivotTimestampIndex] doubleValue];
|
|
CGRect normalPivotFrame = [_summaryThumbnailViews[_zoomedPivotTimestampIndex] frame];
|
|
CGRect normalScrubbingRect = [self _scrubbingRectZoomedIn:false];
|
|
CGRect zoomedScrubbingRect = [self _scrubbingRectZoomedIn:true];
|
|
|
|
CGFloat zoomedPivotPosition = (zoomedScrubbingRect.size.width - [self _thumbnailSize].width) * (CGFloat)pivotTimestamp / (CGFloat)_duration;
|
|
_zoomPivotOffset = zoomedPivotPosition - normalPivotFrame.origin.x + zoomedScrubbingRect.origin.x - normalScrubbingRect.origin.x;
|
|
|
|
_summaryThumbnailWrapperView.clipsToBounds = false;
|
|
[UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionLayoutSubviews | UIViewAnimationOptionAllowUserInteraction animations:^
|
|
{
|
|
[self _layoutZoomedThumbnailViewsStacked:false];
|
|
[self _layoutSummaryThumbnailViewsForZoom:true];
|
|
[self _layoutTrimViewZoomedIn:_zoomedIn];
|
|
} completion:^(__unused BOOL finished)
|
|
{
|
|
_animatingZoomIn = false;
|
|
}];
|
|
}
|
|
|
|
- (void)cancelZoomIn
|
|
{
|
|
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(zoomIn) object:nil];
|
|
|
|
if (!_preparingToZoomIn)
|
|
return;
|
|
|
|
_preparingToZoomIn = false;
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = self.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubberDidCancelRequestingThumbnails:)])
|
|
[delegate videoScrubberDidCancelRequestingThumbnails:self];
|
|
|
|
[self _resetZooming];
|
|
}
|
|
|
|
- (void)zoomOut
|
|
{
|
|
_animatingZoomOut = true;
|
|
_trimView.userInteractionEnabled = false;
|
|
|
|
[UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionLayoutSubviews animations:^
|
|
{
|
|
[self _layoutSummaryThumbnailViewsForZoom:false];
|
|
[self _layoutZoomedThumbnailViewsStacked:true];
|
|
[self _layoutTrimViewZoomedIn:false];
|
|
[self _updateScrubberAnimationsAndResetCurrentPosition:true zoomedIn:false];
|
|
} completion:^(__unused BOOL finished)
|
|
{
|
|
_zoomedIn = false;
|
|
_animatingZoomOut = false;
|
|
|
|
[self _resetZooming];
|
|
|
|
_trimView.userInteractionEnabled = true;
|
|
_summaryThumbnailWrapperView.clipsToBounds = true;
|
|
}];
|
|
}
|
|
|
|
- (void)_resetZooming
|
|
{
|
|
_zoomedIn = false;
|
|
_zoomedDuration = 0.0;
|
|
_zoomPivotPosition = 0.0f;
|
|
_zoomPivotOffset = 0.0f;
|
|
_zoomedPivotTimestampIndex = -1;
|
|
for (UIView *view in _zoomedThumbnailWrapperView.subviews)
|
|
[view removeFromSuperview];
|
|
_zoomedThumbnailViews = nil;
|
|
}
|
|
|
|
- (void)reloadThumbnails
|
|
{
|
|
[self resetThumbnails];
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDataSource> dataSource = self.dataSource;
|
|
|
|
_summaryThumbnailViews = [[NSMutableArray alloc] init];
|
|
|
|
if ([dataSource respondsToSelector:@selector(videoScrubberOriginalSize:cropRect:cropOrientation:)])
|
|
_originalSize = [dataSource videoScrubberOriginalSize:self cropRect:&_cropRect cropOrientation:&_cropOrientation];
|
|
|
|
CGFloat originalAspectRatio = 1.0f;
|
|
CGFloat frameAspectRatio = 1.0f;
|
|
if ([dataSource respondsToSelector:@selector(videoScrubberThumbnailAspectRatio:)])
|
|
originalAspectRatio = [dataSource videoScrubberThumbnailAspectRatio:self];
|
|
|
|
if (!CGRectEqualToRect(_cropRect, CGRectZero))
|
|
frameAspectRatio = _cropRect.size.width / _cropRect.size.height;
|
|
else
|
|
frameAspectRatio = originalAspectRatio;
|
|
|
|
_thumbnailAspectRatio = frameAspectRatio;
|
|
|
|
NSInteger thumbnailCount = (NSInteger)CGCeil(_summaryThumbnailWrapperView.frame.size.width / [self _thumbnailSizeWithAspectRatio:frameAspectRatio orientation:_cropOrientation].width);
|
|
|
|
if ([dataSource respondsToSelector:@selector(videoScrubber:evenlySpacedTimestamps:startingAt:endingAt:)])
|
|
_summaryTimestamps = [dataSource videoScrubber:self evenlySpacedTimestamps:thumbnailCount startingAt:0 endingAt:_duration];
|
|
|
|
CGSize thumbnailImageSize = [self _thumbnailSizeWithAspectRatio:originalAspectRatio orientation:UIImageOrientationUp];
|
|
CGFloat scale = MIN(2.0f, TGScreenScaling());
|
|
thumbnailImageSize = CGSizeMake(thumbnailImageSize.width * scale, thumbnailImageSize.height * scale);
|
|
|
|
if ([dataSource respondsToSelector:@selector(videoScrubber:requestThumbnailImagesForTimestamps:size:isSummaryThumbnails:)])
|
|
[dataSource videoScrubber:self requestThumbnailImagesForTimestamps:_summaryTimestamps size:thumbnailImageSize isSummaryThumbnails:true];
|
|
}
|
|
|
|
- (void)ignoreThumbnails
|
|
{
|
|
_ignoreThumbnailLoad = true;
|
|
}
|
|
|
|
- (void)resetThumbnails
|
|
{
|
|
_ignoreThumbnailLoad = false;
|
|
|
|
if (_summaryThumbnailViews.count < _summaryTimestamps.count || _zoomedThumbnailViews.count < _zoomedTimestamps.count)
|
|
{
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = self.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubberDidCancelRequestingThumbnails:)])
|
|
[delegate videoScrubberDidCancelRequestingThumbnails:self];
|
|
}
|
|
|
|
for (UIView *view in _summaryThumbnailWrapperView.subviews)
|
|
[view removeFromSuperview];
|
|
|
|
for (UIView *view in _zoomedThumbnailWrapperView.subviews)
|
|
[view removeFromSuperview];
|
|
|
|
_summaryThumbnailViews = nil;
|
|
_zoomedThumbnailViews = nil;
|
|
|
|
_summaryTimestamps = nil;
|
|
_zoomedTimestamps = nil;
|
|
|
|
[self _resetZooming];
|
|
}
|
|
|
|
- (void)reloadData
|
|
{
|
|
[self reloadDataAndReset:true];
|
|
}
|
|
|
|
- (void)reloadDataAndReset:(bool)reset
|
|
{
|
|
id<TGMediaPickerGalleryVideoScrubberDataSource> dataSource = self.dataSource;
|
|
if ([dataSource respondsToSelector:@selector(videoScrubberDuration:)])
|
|
_duration = [dataSource videoScrubberDuration:self];
|
|
else
|
|
return;
|
|
|
|
if (!reset && _summaryThumbnailViews.count > 0 && _summaryThumbnailSnapshotView == nil)
|
|
{
|
|
_summaryThumbnailSnapshotView = [_summaryThumbnailWrapperView snapshotViewAfterScreenUpdates:false];
|
|
_summaryThumbnailSnapshotView.frame = _summaryThumbnailWrapperView.frame;
|
|
[_summaryThumbnailWrapperView.superview insertSubview:_summaryThumbnailSnapshotView aboveSubview:_summaryThumbnailWrapperView];
|
|
}
|
|
else if (reset)
|
|
{
|
|
[_summaryThumbnailSnapshotView removeFromSuperview];
|
|
_summaryThumbnailSnapshotView = nil;
|
|
}
|
|
|
|
[self _layoutTrimViewZoomedIn:false];
|
|
|
|
[self reloadThumbnails];
|
|
}
|
|
|
|
- (void)setThumbnailImage:(UIImage *)image forTimestamp:(NSTimeInterval)__unused timestamp isSummaryThubmnail:(bool)isSummaryThumbnail
|
|
{
|
|
TGMediaPickerGalleryVideoScrubberThumbnailView *thumbnailView = [[TGMediaPickerGalleryVideoScrubberThumbnailView alloc] initWithImage:image originalSize:_originalSize cropRect:_cropRect cropOrientation:_cropOrientation];
|
|
|
|
if (isSummaryThumbnail)
|
|
{
|
|
[_summaryThumbnailWrapperView addSubview:thumbnailView];
|
|
[_summaryThumbnailViews addObject:thumbnailView];
|
|
}
|
|
else
|
|
{
|
|
[_zoomedThumbnailWrapperView addSubview:thumbnailView];
|
|
[_zoomedThumbnailViews addObject:thumbnailView];
|
|
}
|
|
|
|
if ((isSummaryThumbnail && _summaryThumbnailViews.count == _summaryTimestamps.count)
|
|
|| (!isSummaryThumbnail && _zoomedThumbnailViews.count == _zoomedTimestamps.count))
|
|
{
|
|
if (!_ignoreThumbnailLoad)
|
|
{
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = self.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubberDidFinishRequestingThumbnails:)])
|
|
[delegate videoScrubberDidFinishRequestingThumbnails:self];
|
|
}
|
|
_ignoreThumbnailLoad = false;
|
|
|
|
if (isSummaryThumbnail)
|
|
{
|
|
[self _layoutSummaryThumbnailViewsForZoom:false];
|
|
|
|
UIView *snapshotView = _summaryThumbnailSnapshotView;
|
|
_summaryThumbnailSnapshotView = nil;
|
|
|
|
if (snapshotView != nil)
|
|
{
|
|
_fadingThumbnailViews = true;
|
|
[UIView animateWithDuration:0.3f animations:^
|
|
{
|
|
snapshotView.alpha = 0.0f;
|
|
} completion:^(__unused BOOL finished)
|
|
{
|
|
_fadingThumbnailViews = false;
|
|
[snapshotView removeFromSuperview];
|
|
}];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self commitZoomIn];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (CGSize)_thumbnailSize
|
|
{
|
|
return [self _thumbnailSizeWithAspectRatio:_thumbnailAspectRatio orientation:_cropOrientation];
|
|
}
|
|
|
|
- (CGSize)_thumbnailSizeWithAspectRatio:(CGFloat)aspectRatio orientation:(UIImageOrientation)orientation
|
|
{
|
|
if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight)
|
|
aspectRatio = 1.0f / aspectRatio;
|
|
return CGSizeMake(CGCeil(32 * aspectRatio), 32);
|
|
}
|
|
|
|
- (void)_layoutSummaryThumbnailViewsForZoom:(bool)forZoom
|
|
{
|
|
if (_summaryThumbnailViews.count == 0)
|
|
return;
|
|
|
|
CGSize thumbnailViewSize = [self _thumbnailSize];
|
|
CGFloat totalWidth = thumbnailViewSize.width * _summaryThumbnailViews.count;
|
|
CGFloat originX = (_summaryThumbnailWrapperView.frame.size.width - totalWidth) / 2;
|
|
|
|
if (!forZoom)
|
|
{
|
|
[_summaryThumbnailViews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop)
|
|
{
|
|
view.frame = CGRectMake(originX + thumbnailViewSize.width * index, 0, thumbnailViewSize.width, thumbnailViewSize.height);
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
CGRect leftThumbnailFrame = [_zoomedThumbnailViews.firstObject frame];
|
|
CGRect rightThumbnailFrame = [_zoomedThumbnailViews.lastObject frame];
|
|
|
|
CGRect pivotThumbnailFrame = CGRectMake(originX + thumbnailViewSize.width * _zoomedPivotTimestampIndex + _zoomPivotOffset, 0, thumbnailViewSize.width, thumbnailViewSize.height);
|
|
|
|
[_summaryThumbnailViews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop)
|
|
{
|
|
if ((NSInteger)index == _zoomedPivotTimestampIndex)
|
|
{
|
|
view.frame = pivotThumbnailFrame;
|
|
}
|
|
else
|
|
{
|
|
if ((NSInteger)index < _zoomedPivotTimestampIndex)
|
|
{
|
|
CGFloat delta = pivotThumbnailFrame.origin.x - leftThumbnailFrame.origin.x + leftThumbnailFrame.size.width;
|
|
view.frame = CGRectMake(pivotThumbnailFrame.origin.x - delta * (_zoomedPivotTimestampIndex - index), 0, thumbnailViewSize.width, thumbnailViewSize.height);
|
|
}
|
|
else
|
|
{
|
|
CGFloat delta = rightThumbnailFrame.origin.x + rightThumbnailFrame.size.width - pivotThumbnailFrame.origin.x;
|
|
view.frame = CGRectMake(pivotThumbnailFrame.origin.x + delta * (index - _zoomedPivotTimestampIndex), 0, thumbnailViewSize.width, thumbnailViewSize.height);
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)_layoutZoomedThumbnailViewsStacked:(bool)stacked
|
|
{
|
|
if (_zoomedThumbnailViews.count == 0 || _summaryThumbnailViews.count == 0)
|
|
return;
|
|
|
|
CGSize thumbnailViewSize = [self _thumbnailSize];
|
|
CGRect stackFrame = [_summaryThumbnailViews[_zoomedPivotTimestampIndex] frame];
|
|
|
|
if (stacked)
|
|
{
|
|
[_zoomedThumbnailViews enumerateObjectsUsingBlock:^(UIView *view, __unused NSUInteger index, __unused BOOL *stop)
|
|
{
|
|
view.frame = stackFrame;
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
NSTimeInterval zoomedPivotThumbnailTimestamp = [_summaryTimestamps[_zoomedPivotTimestampIndex] doubleValue];
|
|
|
|
__block NSInteger i = 1;
|
|
[_zoomedThumbnailViews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop)
|
|
{
|
|
NSTimeInterval timestamp = [_zoomedTimestamps[index] doubleValue];
|
|
if (timestamp >= zoomedPivotThumbnailTimestamp)
|
|
{
|
|
view.frame = CGRectMake(stackFrame.origin.x + thumbnailViewSize.width * i + _zoomPivotOffset, 0, thumbnailViewSize.width, thumbnailViewSize.height);
|
|
i++;
|
|
}
|
|
}];
|
|
|
|
i = 1;
|
|
[_zoomedThumbnailViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop)
|
|
{
|
|
NSTimeInterval timestamp = [_zoomedTimestamps[index] doubleValue];
|
|
if (timestamp < zoomedPivotThumbnailTimestamp)
|
|
{
|
|
view.frame = CGRectMake(stackFrame.origin.x - thumbnailViewSize.width * i + _zoomPivotOffset, 0, thumbnailViewSize.width, thumbnailViewSize.height);
|
|
i++;
|
|
}
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)setIsPlaying:(bool)isPlaying
|
|
{
|
|
_isPlaying = isPlaying;
|
|
|
|
if (_isPlaying)
|
|
[self _updateScrubberAnimationsAndResetCurrentPosition:false];
|
|
else
|
|
[self removeHandleAnimation];
|
|
}
|
|
|
|
- (void)setValue:(NSTimeInterval)value
|
|
{
|
|
[self setValue:value resetPosition:false];
|
|
}
|
|
|
|
- (void)setValue:(NSTimeInterval)value resetPosition:(bool)resetPosition
|
|
{
|
|
if (_duration < FLT_EPSILON)
|
|
return;
|
|
|
|
if (value > _duration)
|
|
value = _duration;
|
|
|
|
_value = value;
|
|
|
|
[self _updateTimeLabels];
|
|
if (resetPosition)
|
|
[self _updateScrubberAnimationsAndResetCurrentPosition:true];
|
|
}
|
|
|
|
- (void)_updateScrubberAnimationsAndResetCurrentPosition:(bool)resetCurrentPosition
|
|
{
|
|
[self _updateScrubberAnimationsAndResetCurrentPosition:resetCurrentPosition zoomedIn:_zoomedIn];
|
|
}
|
|
|
|
- (void)_updateScrubberAnimationsAndResetCurrentPosition:(bool)resetCurrentPosition zoomedIn:(bool)zoomedIn
|
|
{
|
|
if (_duration < FLT_EPSILON)
|
|
return;
|
|
|
|
CGPoint point = [self _scrubberPositionForPosition:_value duration:_duration zoomedIn:zoomedIn];
|
|
CGRect frame = CGRectMake(CGFloor(point.x) - _scrubberHandle.frame.size.width / 2, _scrubberHandle.frame.origin.y, _scrubberHandle.frame.size.width, _scrubberHandle.frame.size.height);
|
|
|
|
if (_trimStartValue > DBL_EPSILON && fabs(_value - _trimStartValue) < 0.01)
|
|
{
|
|
frame = CGRectMake(_trimView.frame.origin.x + [self _scrubbingRect].origin.x, _scrubberHandle.frame.origin.y, _scrubberHandle.frame.size.width, _scrubberHandle.frame.size.height);
|
|
}
|
|
else if (fabs(_value - _trimEndValue) < 0.01)
|
|
{
|
|
frame = CGRectMake(_trimView.frame.origin.x + _trimView.frame.size.width - [self _scrubbingRect].origin.x - _scrubberHandle.frame.size.width, _scrubberHandle.frame.origin.y, _scrubberHandle.frame.size.width, _scrubberHandle.frame.size.height);
|
|
}
|
|
|
|
if (_isPlaying)
|
|
{
|
|
if (resetCurrentPosition)
|
|
_scrubberHandle.frame = frame;
|
|
|
|
CGRect scrubbingRect = [self _scrubbingRectZoomedIn:zoomedIn];
|
|
CGFloat maxPosition = scrubbingRect.origin.x + scrubbingRect.size.width - _scrubberHandle.frame.size.width / 2;
|
|
NSTimeInterval duration = _duration;
|
|
NSTimeInterval value = _value;
|
|
|
|
if (self.allowsTrimming)
|
|
{
|
|
maxPosition = MIN(maxPosition, CGRectGetMaxX(_trimView.frame) - scrubbingRect.origin.x - _scrubberHandle.frame.size.width / 2);
|
|
duration = _trimEndValue - _trimStartValue;
|
|
value = _value - _trimStartValue;
|
|
}
|
|
|
|
CGRect endFrame = CGRectMake(maxPosition - _scrubberHandle.frame.size.width / 2, frame.origin.y, _scrubberHandle.frame.size.width, _scrubberHandle.frame.size.height);
|
|
|
|
[self addHandleAnimationFromFrame:_scrubberHandle.frame toFrame:endFrame duration:MAX(0.0, duration - value)];
|
|
}
|
|
else
|
|
{
|
|
[self removeHandleAnimation];
|
|
_scrubberHandle.frame = frame;
|
|
}
|
|
}
|
|
|
|
- (void)addHandleAnimationFromFrame:(CGRect)fromFrame toFrame:(CGRect)toFrame duration:(NSTimeInterval)duration
|
|
{
|
|
[self removeHandleAnimation];
|
|
|
|
POPBasicAnimation *animation = [POPBasicAnimation animationWithPropertyNamed:kPOPViewFrame];
|
|
animation.fromValue = [NSValue valueWithCGRect:fromFrame];
|
|
animation.toValue = [NSValue valueWithCGRect:toFrame];
|
|
animation.duration = duration;
|
|
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
|
|
animation.clampMode = kPOPAnimationClampBoth;
|
|
animation.roundingFactor = 0.5f;
|
|
|
|
[_scrubberHandle pop_addAnimation:animation forKey:@"progress"];
|
|
}
|
|
|
|
- (void)removeHandleAnimation
|
|
{
|
|
[_scrubberHandle pop_removeAnimationForKey:@"progress"];
|
|
}
|
|
|
|
- (void)resetToStart
|
|
{
|
|
_value = _trimStartValue;
|
|
[self _updateTimeLabels];
|
|
|
|
[self removeHandleAnimation];
|
|
_scrubberHandle.center = CGPointMake(_trimView.frame.origin.x + [self _scrubbingRect].origin.x + _scrubberHandle.frame.size.width / 2, _scrubberHandle.center.y);
|
|
}
|
|
|
|
- (void)_updateTimeLabels
|
|
{
|
|
_currentTimeLabel.text = [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.value];
|
|
_inverseTimeLabel.text = [NSString stringWithFormat:@"-%@", [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.duration - (NSInteger)self.value]];
|
|
}
|
|
|
|
#pragma mark - Scrubber Handle
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
|
{
|
|
if (gestureRecognizer.view != otherGestureRecognizer.view)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
- (void)handlePress:(UILongPressGestureRecognizer *)gestureRecognizer
|
|
{
|
|
switch (gestureRecognizer.state)
|
|
{
|
|
case UIGestureRecognizerStateBegan:
|
|
{
|
|
if (_beganInteraction)
|
|
return;
|
|
|
|
_scrubbing = true;
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = self.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubberDidBeginScrubbing:)])
|
|
[delegate videoScrubberDidBeginScrubbing:self];
|
|
|
|
[self cancelZoomIn];
|
|
if ([self zoomAvailable])
|
|
{
|
|
_pivotSource = TGMediaPickerGalleryVideoScrubberPivotSourceHandle;
|
|
[self performSelector:@selector(zoomIn) withObject:nil afterDelay:TGVideoScrubberZoomActivationInterval];
|
|
}
|
|
|
|
_endedInteraction = false;
|
|
_beganInteraction = true;
|
|
}
|
|
break;
|
|
|
|
case UIGestureRecognizerStateEnded:
|
|
case UIGestureRecognizerStateCancelled:
|
|
{
|
|
_beganInteraction = false;
|
|
|
|
if (_endedInteraction)
|
|
return;
|
|
|
|
_scrubbing = false;
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = self.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubberDidEndScrubbing:)])
|
|
[delegate videoScrubberDidEndScrubbing:self];
|
|
|
|
[self cancelZoomIn];
|
|
if (_zoomedIn)
|
|
[self zoomOut];
|
|
|
|
_endedInteraction = true;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer
|
|
{
|
|
CGPoint translation = [gestureRecognizer translationInView:self];
|
|
[gestureRecognizer setTranslation:CGPointZero inView:self];
|
|
|
|
switch (gestureRecognizer.state)
|
|
{
|
|
case UIGestureRecognizerStateBegan:
|
|
{
|
|
if (_beganInteraction)
|
|
return;
|
|
|
|
_scrubbing = true;
|
|
|
|
[self removeHandleAnimation];
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = self.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubberDidBeginScrubbing:)])
|
|
[delegate videoScrubberDidBeginScrubbing:self];
|
|
|
|
[self cancelZoomIn];
|
|
|
|
_endedInteraction = false;
|
|
_beganInteraction = true;
|
|
}
|
|
break;
|
|
|
|
case UIGestureRecognizerStateChanged:
|
|
{
|
|
if (_animatingZoomIn || _animatingZoomOut)
|
|
return;
|
|
|
|
CGRect scrubbingRect = [self _scrubbingRect];
|
|
CGRect normalScrubbingRect = [self _scrubbingRectZoomedIn:false];
|
|
CGFloat minPosition = scrubbingRect.origin.x + _scrubberHandle.frame.size.width / 2;
|
|
CGFloat maxPosition = scrubbingRect.origin.x + scrubbingRect.size.width - _scrubberHandle.frame.size.width / 2;
|
|
if (self.allowsTrimming)
|
|
{
|
|
minPosition = MAX(minPosition, _trimView.frame.origin.x + normalScrubbingRect.origin.x + _scrubberHandle.frame.size.width / 2);
|
|
maxPosition = MIN(maxPosition, CGRectGetMaxX(_trimView.frame) - normalScrubbingRect.origin.x - _scrubberHandle.frame.size.width / 2);
|
|
}
|
|
|
|
_scrubberHandle.center = CGPointMake(MIN(MAX(_scrubberHandle.center.x + translation.x, minPosition), maxPosition), _scrubberHandle.center.y);
|
|
|
|
NSTimeInterval position = [self _positionForScrubberPosition:_scrubberHandle.center duration:_duration];
|
|
|
|
if (self.allowsTrimming)
|
|
{
|
|
if (ABS(_scrubberHandle.center.x - minPosition) < FLT_EPSILON)
|
|
position = _trimStartValue;
|
|
else if (ABS(_scrubberHandle.center.x - maxPosition) < FLT_EPSILON)
|
|
position = _trimEndValue;
|
|
}
|
|
|
|
_value = position;
|
|
[self _updateTimeLabels];
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = self.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubber:valueDidChange:)])
|
|
[delegate videoScrubber:self valueDidChange:position];
|
|
|
|
[self cancelZoomIn];
|
|
if ([self zoomAvailable])
|
|
{
|
|
_pivotSource = TGMediaPickerGalleryVideoScrubberPivotSourceHandle;
|
|
[self performSelector:@selector(zoomIn) withObject:nil afterDelay:TGVideoScrubberZoomActivationInterval];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case UIGestureRecognizerStateEnded:
|
|
case UIGestureRecognizerStateCancelled:
|
|
{
|
|
_beganInteraction = false;
|
|
|
|
if (_endedInteraction)
|
|
return;
|
|
|
|
_scrubbing = false;
|
|
|
|
id<TGMediaPickerGalleryVideoScrubberDelegate> delegate = self.delegate;
|
|
if ([delegate respondsToSelector:@selector(videoScrubberDidEndScrubbing:)])
|
|
[delegate videoScrubberDidEndScrubbing:self];
|
|
|
|
[self cancelZoomIn];
|
|
if (_zoomedIn)
|
|
[self zoomOut];
|
|
|
|
_endedInteraction = true;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)setScrubberHandleHidden:(bool)hidden animated:(bool)animated
|
|
{
|
|
if (animated)
|
|
{
|
|
_scrubberHandle.hidden = false;
|
|
[UIView animateWithDuration:0.25f animations:^
|
|
{
|
|
_scrubberHandle.alpha = hidden ? 0.0f : 1.0f;
|
|
} completion:^(BOOL finished)
|
|
{
|
|
if (finished)
|
|
_scrubberHandle.hidden = hidden;
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
_scrubberHandle.hidden = hidden;
|
|
_scrubberHandle.alpha = hidden ? 0.0f : 1.0f;
|
|
}
|
|
}
|
|
|
|
- (CGPoint)_scrubberPositionForPosition:(NSTimeInterval)position duration:(NSTimeInterval)duration
|
|
{
|
|
return [self _scrubberPositionForPosition:position duration:duration zoomedIn:_zoomedIn];
|
|
}
|
|
|
|
- (CGPoint)_scrubberPositionForPosition:(NSTimeInterval)position duration:(NSTimeInterval)duration zoomedIn:(bool)zoomedIn
|
|
{
|
|
CGRect scrubbingRect = [self _scrubbingRectZoomedIn:zoomedIn];
|
|
|
|
if (duration < FLT_EPSILON)
|
|
{
|
|
position = 0.0;
|
|
duration = 1.0;
|
|
}
|
|
|
|
return CGPointMake(_scrubberHandle.frame.size.width / 2 + scrubbingRect.origin.x + (CGFloat)(position / duration) * (scrubbingRect.size.width - _scrubberHandle.frame.size.width), CGRectGetMidY([self _scrubbingRect]));
|
|
}
|
|
|
|
- (NSTimeInterval)_positionForScrubberPosition:(CGPoint)scrubberPosition duration:(NSTimeInterval)duration
|
|
{
|
|
CGRect scrubbingRect = [self _scrubbingRect];
|
|
return (scrubberPosition.x - _scrubberHandle.frame.size.width / 2 - scrubbingRect.origin.x) / (scrubbingRect.size.width - _scrubberHandle.frame.size.width) * duration;
|
|
}
|
|
|
|
- (CGRect)_scrubbingRect
|
|
{
|
|
return [self _scrubbingRectZoomedIn:_zoomedIn];
|
|
}
|
|
|
|
- (CGRect)_scrubbingRectZoomedIn:(bool)zoomedIn
|
|
{
|
|
CGFloat width = self.frame.size.width;
|
|
CGFloat origin = 0;
|
|
if (self.allowsTrimming)
|
|
{
|
|
width = width - 12 * 2 - 16;
|
|
origin = 12;
|
|
}
|
|
else
|
|
{
|
|
width = width - 2 * 2 - 16;
|
|
origin = 2;
|
|
}
|
|
|
|
if (zoomedIn)
|
|
{
|
|
CGFloat newWidth = (CGFloat)(width * _duration / _zoomedDuration);
|
|
CGFloat newPosition = _zoomPivotCenter * newWidth / width;
|
|
|
|
origin += _zoomPivotCenter - newPosition;
|
|
|
|
width = newWidth;
|
|
}
|
|
|
|
return CGRectMake(origin, 24, width, 40);
|
|
}
|
|
|
|
#pragma mark - Trimming
|
|
|
|
- (bool)hasTrimming
|
|
{
|
|
return (_allowsTrimming && (_trimStartValue > FLT_EPSILON || _trimEndValue < _duration));
|
|
}
|
|
|
|
- (void)setAllowsTrimming:(bool)allowsTrimming
|
|
{
|
|
_allowsTrimming = allowsTrimming;
|
|
_trimView.trimmingEnabled = allowsTrimming;
|
|
}
|
|
|
|
- (void)setTrimStartValue:(NSTimeInterval)trimStartValue
|
|
{
|
|
_trimStartValue = trimStartValue;
|
|
|
|
[self _layoutTrimViewZoomedIn:_zoomedIn];
|
|
|
|
if (_value < _trimStartValue)
|
|
{
|
|
[self setValue:_trimStartValue];
|
|
_scrubberHandle.center = CGPointMake(_trimView.frame.origin.x + 12 + _scrubberHandle.frame.size.width / 2, _scrubberHandle.center.y);
|
|
}
|
|
}
|
|
|
|
- (void)setTrimEndValue:(NSTimeInterval)trimEndValue
|
|
{
|
|
_trimEndValue = trimEndValue;
|
|
|
|
[self _layoutTrimViewZoomedIn:_zoomedIn];
|
|
|
|
if (_value > _trimEndValue)
|
|
{
|
|
[self setValue:_trimEndValue];
|
|
_scrubberHandle.center = CGPointMake(CGRectGetMaxX(_trimView.frame) - 12 - _scrubberHandle.frame.size.width / 2, _scrubberHandle.center.y);
|
|
}
|
|
}
|
|
|
|
- (void)setTrimApplied:(bool)trimApplied
|
|
{
|
|
[_trimView setTrimming:trimApplied animated:false];
|
|
}
|
|
|
|
- (void)_trimStartPosition:(NSTimeInterval *)trimStartPosition trimEndPosition:(NSTimeInterval *)trimEndPosition forTrimFrame:(CGRect)trimFrame duration:(NSTimeInterval)duration
|
|
{
|
|
if (trimStartPosition == NULL || trimEndPosition == NULL)
|
|
return;
|
|
|
|
CGRect trimRect = [self _scrubbingRect];
|
|
|
|
*trimStartPosition = (CGRectGetMinX(trimFrame) + 12 - trimRect.origin.x) / trimRect.size.width * duration;
|
|
*trimEndPosition = (CGRectGetMaxX(trimFrame) - 12 - trimRect.origin.x) / trimRect.size.width * duration;
|
|
}
|
|
|
|
- (CGRect)_trimFrameForStartPosition:(NSTimeInterval)startPosition endPosition:(NSTimeInterval)endPosition duration:(NSTimeInterval)duration zoomedIn:(bool)zoomedIn
|
|
{
|
|
CGRect trimRect = [self _scrubbingRectZoomedIn:zoomedIn];
|
|
CGRect normalScrubbingRect = [self _scrubbingRectZoomedIn:false];
|
|
|
|
CGFloat minX = (CGFloat)startPosition * trimRect.size.width / (CGFloat)duration + trimRect.origin.x - normalScrubbingRect.origin.x;
|
|
CGFloat maxX = (CGFloat)endPosition * trimRect.size.width / (CGFloat)duration + trimRect.origin.x + normalScrubbingRect.origin.x;
|
|
|
|
return CGRectMake(minX, 0, maxX - minX, 36);
|
|
}
|
|
|
|
- (void)_layoutTrimViewZoomedIn:(bool)zoomedIn
|
|
{
|
|
if (_duration > DBL_EPSILON)
|
|
{
|
|
NSTimeInterval endPosition = _trimEndValue;
|
|
if (endPosition < DBL_EPSILON)
|
|
endPosition = _duration;
|
|
|
|
_trimView.frame = [self _trimFrameForStartPosition:_trimStartValue endPosition:_trimEndValue duration:_duration zoomedIn:zoomedIn];
|
|
}
|
|
else
|
|
{
|
|
_trimView.frame = _wrapperView.bounds;
|
|
}
|
|
|
|
[self _layoutTrimCurtainViews];
|
|
}
|
|
|
|
- (void)_layoutTrimCurtainViews
|
|
{
|
|
_leftCurtainView.hidden = !self.allowsTrimming;
|
|
_rightCurtainView.hidden = !self.allowsTrimming;
|
|
|
|
if (self.allowsTrimming)
|
|
{
|
|
CGRect scrubbingRect = [self _scrubbingRect];
|
|
CGRect normalScrubbingRect = [self _scrubbingRectZoomedIn:false];
|
|
|
|
_leftCurtainView.frame = CGRectMake(scrubbingRect.origin.x, 2, _trimView.frame.origin.x - scrubbingRect.origin.x + normalScrubbingRect.origin.x, 32);
|
|
_rightCurtainView.frame = CGRectMake(CGRectGetMaxX(_trimView.frame), 2, scrubbingRect.origin.x + scrubbingRect.size.width - CGRectGetMaxX(_trimView.frame) - scrubbingRect.origin.x + normalScrubbingRect.origin.x, 32);
|
|
}
|
|
}
|
|
|
|
#pragma mark - Layout
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
_wrapperView.frame = CGRectMake(8, 24, self.frame.size.width - 16, 36);
|
|
[self _layoutTrimViewZoomedIn:_zoomedIn];
|
|
|
|
CGRect scrubbingRect = [self _scrubbingRect];
|
|
_summaryThumbnailWrapperView.frame = CGRectMake(scrubbingRect.origin.x, 2, scrubbingRect.size.width, 32);
|
|
_zoomedThumbnailWrapperView.frame = _summaryThumbnailWrapperView.frame;
|
|
|
|
[self _updateScrubberAnimationsAndResetCurrentPosition:true];
|
|
}
|
|
|
|
+ (NSString *)_stringFromTotalSeconds:(NSInteger)totalSeconds
|
|
{
|
|
NSInteger hours = (NSInteger)totalSeconds / 3600;
|
|
NSInteger minutes = (NSInteger)(totalSeconds / 60) % 60;
|
|
NSInteger seconds = (NSInteger)(totalSeconds % 60);
|
|
|
|
if (hours > 0)
|
|
return [NSString stringWithFormat:@"%02d:%02d:%02d", (int)hours, (int)minutes, (int)seconds];
|
|
else
|
|
return [NSString stringWithFormat:@"%d:%02d", (int)minutes, (int)seconds];
|
|
}
|
|
|
|
@end
|