Interactive/Cancelable slide left for details

// FREEBIE
This commit is contained in:
Michael Kirk 2017-10-24 10:25:17 -07:00
parent ac8d59bb7d
commit d87f000051
6 changed files with 227 additions and 8 deletions

View File

@ -147,6 +147,8 @@
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */; };
4521C3C11F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */; };
4523149C1F7D7F81003A428C /* OWSMessagesBubbleImageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149B1F7D7F81003A428C /* OWSMessagesBubbleImageFactory.swift */; };
4523149E1F7E916B003A428C /* SlideOffAnimatedTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */; };
452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */; };
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; };
452C46901E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; };
452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; };
@ -615,6 +617,8 @@
4520D8D41D417D8E00123472 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; };
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldHelper.swift; sourceTree = "<group>"; };
4523149B1F7D7F81003A428C /* OWSMessagesBubbleImageFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSMessagesBubbleImageFactory.swift; sourceTree = "<group>"; };
4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlideOffAnimatedTransition.swift; path = UserInterface/SlideOffAnimatedTransition.swift; sourceTree = "<group>"; };
4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionalPanGestureRecognizer.swift; sourceTree = "<group>"; };
452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = "<group>"; };
452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = "<group>"; };
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerView.swift; sourceTree = "<group>"; };
@ -1211,6 +1215,7 @@
34B3F8331E8DF1700035BE1A /* ViewControllers */,
76EB052B18170B33006006FC /* Views */,
4523149B1F7D7F81003A428C /* OWSMessagesBubbleImageFactory.swift */,
4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */,
);
name = UserInterface;
sourceTree = "<group>";
@ -1519,6 +1524,7 @@
45A6DAD51EBBF85500893231 /* ReminderView.swift */,
450D19111F85236600970622 /* RemoteVideoView.h */,
450D19121F85236600970622 /* RemoteVideoView.m */,
4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */,
);
name = Views;
path = views;
@ -2283,6 +2289,7 @@
76EB068618170B34006006FC /* ContactTableViewCell.m in Sources */,
3497DBEF1ECE2E4700DB2605 /* DomainFrontingCountryViewController.m in Sources */,
34B3F8881E8DF1700035BE1A /* OversizeTextMessageViewController.swift in Sources */,
452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */,
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
34B3F8A21E8EA6040035BE1A /* ViewControllerUtils.m in Sources */,
34CA1C271F7156F300E51C51 /* MessageMetadataViewController.swift in Sources */,
@ -2317,6 +2324,7 @@
34B3F8911E8DF1710035BE1A /* ShowGroupMembersViewController.m in Sources */,
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
4523149E1F7E916B003A428C /* SlideOffAnimatedTransition.swift in Sources */,
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */,
34B3F8711E8DF1700035BE1A /* AboutTableViewController.m in Sources */,

View File

@ -0,0 +1,51 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import UIKit
@objc
class SlideOffAnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)?.view else {
owsFail("No fromView")
return
}
guard let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)?.view else {
owsFail("No toView")
return
}
let width = containerView.frame.width
let offsetLeft = fromView.frame.offsetBy(dx: -width, dy: 0)
toView.frame = fromView.frame
fromView.layer.shadowRadius = 15.0
fromView.layer.shadowOpacity = 1.0
toView.layer.opacity = 0.9
containerView.insertSubview(toView, belowSubview: fromView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay:0, options: .curveLinear, animations: {
fromView.frame = offsetLeft
toView.layer.opacity = 1.0
fromView.layer.shadowOpacity = 0.1
}, completion: { _ in
toView.layer.opacity = 1.0
toView.layer.shadowOpacity = 0
fromView.layer.opacity = 1.0
fromView.layer.shadowOpacity = 0
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
}

View File

@ -24,6 +24,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem
attachmentPointer:(TSAttachmentPointer *)attachmentPointer;
- (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message;
- (void)didPanWithGestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer
viewItem:(ConversationViewItem *)conversationItem;
- (void)showMetadataViewForMessage:(TSMessage *)message;

View File

@ -162,6 +162,12 @@ NS_ASSUME_NONNULL_BEGIN
UILongPressGestureRecognizer *longPress =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
[self addGestureRecognizer:longPress];
PanDirectionGestureRecognizer *panGesture =
[[PanDirectionGestureRecognizer alloc] initWithDirection:PanDirectionHorizontal
target:self
action:@selector(handlePanGesture:)];
[self addGestureRecognizer:panGesture];
}
+ (NSString *)cellReuseIdentifier
@ -1029,6 +1035,13 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)panRecognizer
{
OWSAssert(self.delegate);
[self.delegate didPanWithGestureRecognizer:panRecognizer viewItem:self.viewItem];
}
#pragma mark - UIMenuController
- (void)showMenuController:(CGPoint)fromLocation

View File

@ -136,6 +136,10 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
ConversationInputToolbarDelegate,
GifPickerViewControllerDelegate>
// Show message info animation
@property (nullable, nonatomic) UIPercentDrivenInteractiveTransition *showMessageDetailsTransition;
@property (nullable, nonatomic) UIPanGestureRecognizer *currentShowMessageDetailsPanGesture;
@property (nonatomic) TSThread *thread;
@property (nonatomic) YapDatabaseConnection *editingDatabaseConnection;
@ -988,9 +992,14 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[super viewWillDisappear:animated];
self.isViewVisible = NO;
[self.inputToolbar viewWillDisappear:animated];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
self.userHasScrolled = NO;
self.isViewVisible = NO;
[self.audioAttachmentPlayer stop];
self.audioAttachmentPlayer = nil;
@ -1005,12 +1014,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
self.isUserScrolling = NO;
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
self.userHasScrolled = NO;
}
#pragma mark - Initiliazers
- (void)setNavigationTitle
@ -4036,6 +4039,110 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
return cell;
}
#pragma mark - swipe to show message details
- (void)didPanWithGestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer
viewItem:(ConversationViewItem *)conversationItem
{
self.currentShowMessageDetailsPanGesture = gestureRecognizer;
const CGFloat leftTranslation = -1 * [gestureRecognizer translationInView:self.view].x;
const CGFloat percent = MAX(leftTranslation, 0) / self.view.frame.size.width;
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan: {
TSInteraction *interaction = conversationItem.interaction;
if ([interaction isKindOfClass:[TSIncomingMessage class]] ||
[interaction isKindOfClass:[TSOutgoingMessage class]]) {
// Canary check in case we later have another reason to set navigationController.delegate - we don't
// want to inadvertently clobber it here.
OWSAssert(self.navigationController.delegate == nil) self.navigationController.delegate = self;
TSMessage *message = (TSMessage *)interaction;
MessageMetadataViewController *view = [[MessageMetadataViewController alloc] initWithMessage:message];
[self.navigationController pushViewController:view animated:YES];
} else {
OWSFail(@"%@ Can't show message metadata for message of type: %@", self.tag, [interaction class]);
}
break;
}
case UIGestureRecognizerStateChanged: {
UIPercentDrivenInteractiveTransition *transition = self.showMessageDetailsTransition;
if (!transition) {
DDLogVerbose(@"%@ transition not set up yet", self.tag);
return;
}
[transition updateInteractiveTransition:percent];
break;
}
case UIGestureRecognizerStateEnded: {
const CGFloat velocity = [gestureRecognizer velocityInView:self.view].x;
UIPercentDrivenInteractiveTransition *transition = self.showMessageDetailsTransition;
if (!transition) {
DDLogVerbose(@"%@ transition not set up yet", self.tag);
return;
}
// Complete the transition if moved sufficiently far or fast
// Note this is trickier for incoming, since you are already on the left, and have less space.
if (percent > 0.3 || velocity < -800) {
[transition finishInteractiveTransition];
} else {
[transition cancelInteractiveTransition];
}
break;
}
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed: {
UIPercentDrivenInteractiveTransition *transition = self.showMessageDetailsTransition;
if (!transition) {
DDLogVerbose(@"%@ transition not set up yet", self.tag);
return;
}
[transition cancelInteractiveTransition];
break;
}
default:
break;
}
}
- (nullable id<UIViewControllerAnimatedTransitioning>)navigationController:
(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
return [SlideOffAnimatedTransition new];
}
- (nullable id<UIViewControllerInteractiveTransitioning>)
navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
// We needed to be the navigation controller delegate to specify the interactive "slide left for message details"
// animation But we may not want to be the navigation controller delegate permanently.
self.navigationController.delegate = nil;
DDLogInfo(@"%@ >>>> in %s", self.tag, __PRETTY_FUNCTION__);
UIPanGestureRecognizer *recognizer = self.currentShowMessageDetailsPanGesture;
if (recognizer == nil) {
OWSFail(@"currentShowMessageDetailsPanGesture was unexpectedly nil");
return nil;
}
if (recognizer.state == UIGestureRecognizerStateBegan) {
self.showMessageDetailsTransition = [UIPercentDrivenInteractiveTransition new];
self.showMessageDetailsTransition.completionCurve = UIViewAnimationCurveEaseOut;
} else {
self.showMessageDetailsTransition = nil;
}
return self.showMessageDetailsTransition;
}
#pragma mark - UICollectionViewDelegate
- (void)collectionView:(UICollectionView *)collectionView

View File

@ -0,0 +1,38 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import UIKit.UIGestureRecognizerSubclass
@objc
enum PanDirection: Int {
case vertical
case horizontal
}
@objc
class PanDirectionGestureRecognizer: UIPanGestureRecognizer {
let direction: PanDirection
init(direction: PanDirection, target: AnyObject, action: Selector) {
self.direction = direction
super.init(target: target, action: action)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if state == .began {
let vel = velocity(in: view)
switch direction {
case .horizontal where fabs(vel.y) > fabs(vel.x):
state = .cancelled
case .vertical where fabs(vel.x) > fabs(vel.y):
state = .cancelled
default:
break
}
}
}
}