Rework menu actions focus item layout.

pull/2/head
Matthew Chen 6 years ago
parent 4f06e6dd6e
commit a8e9b87f03

@ -211,6 +211,8 @@ typedef enum : NSUInteger {
@property (nonatomic, readonly) ConversationSearchController *searchController;
@property (nonatomic, nullable) NSString *lastSearchedText;
@property (nonatomic) BOOL isShowingSearchUI;
@property (nonatomic, nullable) MenuActionsViewController *menuActionsViewController;
@property (nonatomic) CGFloat contentInsetPadding;
@end
@ -746,6 +748,8 @@ typedef enum : NSUInteger {
// We want to set the initial scroll state the first time we enter the view.
if (!self.viewHasEverAppeared) {
[self scrollToDefaultPosition];
} else if (self.menuActionsViewController != nil) {
[self scrollToFocusInteraction:NO];
}
[self updateLastVisibleSortId];
@ -1257,7 +1261,7 @@ typedef enum : NSUInteger {
self.isViewCompletelyAppeared = NO;
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
[self dismissMenuActions];
}
- (void)viewDidDisappear:(BOOL)animated
@ -1965,61 +1969,156 @@ typedef enum : NSUInteger {
#pragma mark - MenuActionsViewControllerDelegate
- (void)menuActionsDidHide:(MenuActionsViewController *)menuActionsViewController
- (void)menuActionsWillPresent:(MenuActionsViewController *)menuActionsViewController
{
OWSLogVerbose(@"");
// While the menu actions are presented, temporarily use extra content
// inset padding so that interactions near the top or bottom of the
// collection view can be scrolled anywhere within the viewport.
//
// e.g. In a new conversation, there might be only a single message
// which we might want to scroll to the bottom of the screen to
// pin above the menu actions popup.
CGSize mainScreenSize = UIScreen.mainScreen.bounds.size;
self.contentInsetPadding = MAX(mainScreenSize.width, mainScreenSize.height);
UIEdgeInsets contentInset = self.collectionView.contentInset;
contentInset.top += self.contentInsetPadding;
contentInset.bottom += self.contentInsetPadding;
self.collectionView.contentInset = contentInset;
self.menuActionsViewController = menuActionsViewController;
}
- (void)menuActionsIsPresenting:(MenuActionsViewController *)menuActionsViewController
{
OWSLogVerbose(@"");
// Changes made in this "is presenting" callback are animated by the caller.
[self scrollToFocusInteraction:NO];
}
- (void)menuActionsDidPresent:(MenuActionsViewController *)menuActionsViewController
{
OWSLogVerbose(@"");
[self scrollToFocusInteraction:NO];
}
- (void)menuActionsIsDismissing:(MenuActionsViewController *)menuActionsViewController
{
OWSLogVerbose(@"");
// Changes made in this "is dismissing" callback are animated by the caller.
[self clearMenuActionsState];
}
- (void)menuActionsDidDismiss:(MenuActionsViewController *)menuActionsViewController
{
OWSLogVerbose(@"");
[self dismissMenuActions];
}
- (void)dismissMenuActions
{
OWSLogVerbose(@"");
[self clearMenuActionsState];
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
}
- (void)menuActions:(MenuActionsViewController *)menuActionsViewController
isPresentingWithVerticalFocusChange:(CGFloat)verticalChange
- (void)clearMenuActionsState
{
UIEdgeInsets oldInset = self.collectionView.contentInset;
CGPoint oldOffset = self.collectionView.contentOffset;
OWSLogVerbose(@"");
UIEdgeInsets newInset = oldInset;
CGPoint newOffset = oldOffset;
if (self.menuActionsViewController == nil) {
return;
}
// In case the message is at the very top or bottom edge of the conversation we have to have these additional
// insets to be sure we can sufficiently scroll the contentOffset.
newInset.top += verticalChange;
newInset.bottom -= verticalChange;
newOffset.y -= verticalChange;
UIEdgeInsets contentInset = self.collectionView.contentInset;
contentInset.top -= self.contentInsetPadding;
contentInset.bottom -= self.contentInsetPadding;
self.collectionView.contentInset = contentInset;
OWSLogDebug(@"verticalChange: %f, insets: %@ -> %@",
verticalChange,
NSStringFromUIEdgeInsets(oldInset),
NSStringFromUIEdgeInsets(newInset));
self.menuActionsViewController = nil;
self.contentInsetPadding = 0;
}
// Because we're in the context of the frame-changing animation, these adjustments should happen
// in lockstep with the messageActions frame change.
self.collectionView.contentOffset = newOffset;
self.collectionView.contentInset = newInset;
- (void)scrollToFocusInteractionIfNecessary
{
if (self.menuActionsViewController != nil) {
[self scrollToFocusInteraction:NO];
}
}
- (void)menuActions:(MenuActionsViewController *)menuActionsViewController
isDismissingWithVerticalFocusChange:(CGFloat)verticalChange
- (void)scrollToFocusInteraction:(BOOL)animated
{
UIEdgeInsets oldInset = self.collectionView.contentInset;
CGPoint oldOffset = self.collectionView.contentOffset;
NSValue *_Nullable contentOffset = [self contentOffsetForFocusInteraction];
if (contentOffset == nil) {
OWSFailDebug(@"Missing contentOffset.");
return;
}
[self.collectionView setContentOffset:contentOffset.CGPointValue animated:animated];
}
UIEdgeInsets newInset = oldInset;
CGPoint newOffset = oldOffset;
- (nullable NSValue *)contentOffsetForFocusInteraction
{
NSString *_Nullable focusedInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
if (focusedInteractionId == nil) {
// This is expected if there is no focus interaction.
return nil;
}
CGPoint modalTopWindow = [self.menuActionsViewController.focusUI convertPoint:CGPointZero toView:nil];
CGPoint modalTopLocal = [self.view convertPoint:modalTopWindow fromView:nil];
CGPoint offset = modalTopLocal;
CGFloat focusTop = offset.y - self.menuActionsViewController.vSpacing;
// In case the message is at the very top or bottom edge of the conversation we have to have these additional
// insets to be sure we can sufficiently scroll the contentOffset.
newInset.top -= verticalChange;
newInset.bottom += verticalChange;
newOffset.y += verticalChange;
NSIndexPath *_Nullable indexPath = nil;
for (NSUInteger i = 0; i < self.viewItems.count; i++) {
id<ConversationViewItem> viewItem = self.viewItems[i];
if ([viewItem.interaction.uniqueId isEqualToString:focusedInteractionId]) {
indexPath = [NSIndexPath indexPathForRow:(NSInteger)i inSection:0];
break;
}
}
if (indexPath == nil) {
// This is expected if the focus interaction is being deleted.
return nil;
}
UICollectionViewLayoutAttributes *_Nullable layoutAttributes =
[self.layout layoutAttributesForItemAtIndexPath:indexPath];
if (layoutAttributes == nil) {
OWSFailDebug(@"Missing layoutAttributes.");
return nil;
}
CGRect cellFrame = layoutAttributes.frame;
return [NSValue valueWithCGPoint:CGPointMake(0, CGRectGetMaxY(cellFrame) - focusTop)];
}
OWSLogDebug(@"verticalChange: %f, insets: %@ -> %@",
verticalChange,
NSStringFromUIEdgeInsets(oldInset),
NSStringFromUIEdgeInsets(newInset));
- (void)dismissMenuActionsIfNecessary
{
if (self.shouldDismissMenuActions) {
[self dismissMenuActions];
}
}
// Because we're in the context of the frame-changing animation, these adjustments should happen
// in lockstep with the messageActions frame change.
self.collectionView.contentOffset = newOffset;
self.collectionView.contentInset = newInset;
- (BOOL)shouldDismissMenuActions
{
if (!OWSWindowManager.sharedManager.isPresentingMenuActions) {
return NO;
}
NSString *_Nullable focusedInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
if (focusedInteractionId == nil) {
return NO;
}
for (id<ConversationViewItem> viewItem in self.viewItems) {
if ([viewItem.interaction.uniqueId isEqualToString:focusedInteractionId]) {
return NO;
}
}
return YES;
}
#pragma mark - ConversationViewCellDelegate
@ -2068,11 +2167,12 @@ typedef enum : NSUInteger {
- (void)presentMessageActions:(NSArray<MenuAction *> *)messageActions withFocusedCell:(ConversationViewCell *)cell
{
MenuActionsViewController *menuActionsViewController =
[[MenuActionsViewController alloc] initWithFocusedView:cell actions:messageActions];
[[MenuActionsViewController alloc] initWithFocusedInteraction:cell.viewItem.interaction
focusedView:cell
actions:messageActions];
menuActionsViewController.delegate = self;
self.conversationViewModel.mostRecentMenuActionsViewItem = cell.viewItem;
[[OWSWindowManager sharedManager] showMenuActionsWindow:menuActionsViewController];
}
@ -3754,8 +3854,12 @@ typedef enum : NSUInteger {
//
// Always reserve room for the input accessory, which we display even
// if the keyboard is not active.
newInsets.top = 0;
newInsets.bottom = MAX(0, self.view.height - self.bottomLayoutGuide.length - keyboardEndFrameConverted.origin.y);
newInsets.top += self.contentInsetPadding;
newInsets.bottom += self.contentInsetPadding;
BOOL wasScrolledToBottom = [self isScrolledToBottom];
void (^adjustInsets)(void) = ^(void) {
@ -4410,6 +4514,13 @@ typedef enum : NSUInteger {
- (CGPoint)collectionView:(UICollectionView *)collectionView
targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
if (self.menuActionsViewController != nil) {
NSValue *_Nullable contentOffset = [self contentOffsetForFocusInteraction];
if (contentOffset != nil) {
return contentOffset.CGPointValue;
}
}
if (self.scrollContinuity == kScrollContinuityBottom && self.lastKnownDistanceFromBottom) {
NSValue *_Nullable contentOffset =
[self contentOffsetForLastKnownDistanceFromBottom:self.lastKnownDistanceFromBottom.floatValue];
@ -4659,6 +4770,7 @@ typedef enum : NSUInteger {
[self updateBackButtonUnreadCount];
[self updateNavigationBarSubtitleLabel];
[self dismissMenuActionsIfNecessary];
if (self.isGroupConversation) {
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
@ -4866,13 +4978,6 @@ typedef enum : NSUInteger {
[self scrollToBottomAnimated:NO];
}
- (void)conversationViewModelDidDeleteMostRecentMenuActionsViewItem
{
OWSAssertIsOnMainThread();
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
}
#pragma mark - Orientation
- (void)viewWillTransitionToSize:(CGSize)size
@ -4886,7 +4991,7 @@ typedef enum : NSUInteger {
// in the content of this view. It's easier to dismiss the
// "message actions" window when the device changes orientation
// than to try to ensure this works in that case.
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
[self dismissMenuActions];
// Snapshot the "last visible row".
NSIndexPath *_Nullable lastVisibleIndexPath = self.lastVisibleIndexPath;
@ -4912,7 +5017,9 @@ typedef enum : NSUInteger {
[strongSelf updateInputToolbarLayout];
if (lastVisibleIndexPath) {
if (self.menuActionsViewController != nil) {
[self scrollToFocusInteraction:NO];
} else if (lastVisibleIndexPath) {
[strongSelf.collectionView scrollToItemAtIndexPath:lastVisibleIndexPath
atScrollPosition:UICollectionViewScrollPositionBottom
animated:NO];

@ -74,8 +74,6 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
// to prod the view to reset its scroll state, etc.
- (void)conversationViewModelDidReset;
- (void)conversationViewModelDidDeleteMostRecentMenuActionsViewItem;
- (ConversationStyle *)conversationStyle;
@end
@ -87,7 +85,6 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
@property (nonatomic, readonly) NSArray<id<ConversationViewItem>> *viewItems;
@property (nonatomic, nullable) NSString *focusMessageIdOnOpen;
@property (nonatomic, readonly, nullable) ThreadDynamicInteractions *dynamicInteractions;
@property (nonatomic, nullable) id<ConversationViewItem> mostRecentMenuActionsViewItem;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithThread:(TSThread *)thread

@ -587,12 +587,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
return;
}
NSString *_Nullable mostRecentMenuActionsInterationId = self.mostRecentMenuActionsViewItem.interaction.uniqueId;
if (mostRecentMenuActionsInterationId != nil &&
[diff.removedItemIds containsObject:mostRecentMenuActionsInterationId]) {
[self.delegate conversationViewModelDidDeleteMostRecentMenuActionsViewItem];
}
NSMutableSet<NSString *> *diffAddedItemIds = [diff.addedItemIds mutableCopy];
NSMutableSet<NSString *> *diffRemovedItemIds = [diff.removedItemIds mutableCopy];
NSMutableSet<NSString *> *diffUpdatedItemIds = [diff.updatedItemIds mutableCopy];

@ -21,9 +21,12 @@ public class MenuAction: NSObject {
@objc
protocol MenuActionsViewControllerDelegate: class {
func menuActionsDidHide(_ menuActionsViewController: MenuActionsViewController)
func menuActions(_ menuActionsViewController: MenuActionsViewController, isPresentingWithVerticalFocusChange: CGFloat)
func menuActions(_ menuActionsViewController: MenuActionsViewController, isDismissingWithVerticalFocusChange: CGFloat)
func menuActionsWillPresent(_ menuActionsViewController: MenuActionsViewController)
func menuActionsIsPresenting(_ menuActionsViewController: MenuActionsViewController)
func menuActionsDidPresent(_ menuActionsViewController: MenuActionsViewController)
func menuActionsIsDismissing(_ menuActionsViewController: MenuActionsViewController)
func menuActionsDidDismiss(_ menuActionsViewController: MenuActionsViewController)
}
@objc
@ -32,18 +35,20 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
@objc
weak var delegate: MenuActionsViewControllerDelegate?
@objc
public let focusedInteraction: TSInteraction?
private let focusedView: UIView
private let actionSheetView: MenuActionSheetView
deinit {
Logger.verbose("")
assert(didInformDelegateOfDismissalAnimation)
assert(didInformDelegateThatDisappearenceCompleted)
}
@objc
required init(focusedView: UIView, actions: [MenuAction]) {
required init(focusedInteraction: TSInteraction?, focusedView: UIView, actions: [MenuAction]) {
self.focusedView = focusedView
self.focusedInteraction = focusedInteraction
self.actionSheetView = MenuActionSheetView(actions: actions)
super.init(nibName: nil, bundle: nil)
@ -86,8 +91,7 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
// When the user has manually dismissed the menu, we do a nice animation
// but if the view otherwise disappears (e.g. due to resigning active),
// we still want to give the delegate the information it needs to restore it's UI.
ensureDelegateIsInformedOfDismissalAnimation()
ensureDelegateIsInformedThatDisappearenceCompleted()
delegate?.menuActionsDidDismiss(self)
}
// MARK: Orientation
@ -98,7 +102,6 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
// MARK: Present / Dismiss animations
var presentationFocusOffset: CGFloat?
var snapshotView: UIView?
private func addSnapshotFocusedView() -> UIView? {
@ -150,6 +153,7 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
let oldFocusFrame = self.view.convert(focusedView.frame, from: focusedViewSuperview)
NSLayoutConstraint.deactivate([actionSheetViewVerticalConstraint])
self.actionSheetViewVerticalConstraint = self.actionSheetView.autoPinEdge(toSuperviewEdge: .bottom)
self.delegate?.menuActionsWillPresent(self)
UIView.animate(withDuration: 0.2,
delay: backgroundDuration,
options: .curveEaseOut,
@ -160,35 +164,36 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
var newFocusFrame = oldFocusFrame
// Position focused item just over the action sheet.
let padding: CGFloat = 10
let overlap: CGFloat = (oldFocusFrame.maxY + padding) - newSheetFrame.minY
let overlap: CGFloat = (oldFocusFrame.maxY + self.vSpacing) - newSheetFrame.minY
newFocusFrame.origin.y = oldFocusFrame.origin.y - overlap
snapshotView.frame = newFocusFrame
let offset = -overlap
self.presentationFocusOffset = offset
self.delegate?.menuActions(self, isPresentingWithVerticalFocusChange: offset)
self.delegate?.menuActionsIsPresenting(self)
},
completion: nil)
completion: { (_) in
self.delegate?.menuActionsDidPresent(self)
})
}
@objc
public let vSpacing: CGFloat = 10
@objc
public func focusUI() -> UIView {
return actionSheetView
}
private func animateDismiss(action: MenuAction?) {
guard let actionSheetViewVerticalConstraint = self.actionSheetViewVerticalConstraint else {
owsFailDebug("actionSheetVerticalConstraint was unexpectedly nil")
self.delegate?.menuActionsDidHide(self)
delegate?.menuActionsDidDismiss(self)
return
}
guard let snapshotView = self.snapshotView else {
owsFailDebug("snapshotView was unexpectedly nil")
self.delegate?.menuActionsDidHide(self)
return
}
guard let presentationFocusOffset = self.presentationFocusOffset else {
owsFailDebug("presentationFocusOffset was unexpectedly nil")
self.delegate?.menuActionsDidHide(self)
delegate?.menuActionsDidDismiss(self)
return
}
@ -203,48 +208,20 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
animations: {
self.view.backgroundColor = UIColor.clear
self.actionSheetView.superview?.layoutIfNeeded()
snapshotView.frame.origin.y -= presentationFocusOffset
// this helps when focused view is above navbars, etc.
snapshotView.alpha = 0
self.ensureDelegateIsInformedOfDismissalAnimation()
self.delegate?.menuActionsIsDismissing(self)
},
completion: { _ in
self.view.isHidden = true
self.ensureDelegateIsInformedThatDisappearenceCompleted()
self.delegate?.menuActionsDidDismiss(self)
if let action = action {
action.block(action)
}
})
}
var didInformDelegateThatDisappearenceCompleted = false
func ensureDelegateIsInformedThatDisappearenceCompleted() {
guard !didInformDelegateThatDisappearenceCompleted else {
Logger.debug("ignoring redundant 'disappeared' notification")
return
}
didInformDelegateThatDisappearenceCompleted = true
self.delegate?.menuActionsDidHide(self)
}
var didInformDelegateOfDismissalAnimation = false
func ensureDelegateIsInformedOfDismissalAnimation() {
guard !didInformDelegateOfDismissalAnimation else {
Logger.debug("ignoring redundant 'dismissal' notification")
return
}
didInformDelegateOfDismissalAnimation = true
guard let presentationFocusOffset = self.presentationFocusOffset else {
owsFailDebug("presentationFocusOffset was unexpectedly nil")
self.delegate?.menuActionsDidHide(self)
return
}
self.delegate?.menuActions(self, isDismissingWithVerticalFocusChange: presentationFocusOffset)
}
// MARK: Actions
@objc

16486
temp.txt

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save