#import "TGMenuSheetView.h" #import "TGMenuSheetItemView.h" #import "TGMenuSheetController.h" #import "LegacyComponentsInternal.h" #import "TGImageUtils.h" #import "TGColor.h" NSString *const TGMenuDividerTop = @"top"; NSString *const TGMenuDividerBottom = @"bottom"; const bool TGMenuSheetUseEffectView = false; const CGFloat TGMenuSheetCornerRadius = 14.5f; const UIEdgeInsets TGMenuSheetPhoneEdgeInsets = { 10.0f, 10.0f, 10.0f, 10.0f }; const CGFloat TGMenuSheetInterSectionSpacing = 8.0f; @implementation TGMenuSheetScrollView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self != nil) { self.scrollsToTop = false; self.showsHorizontalScrollIndicator = false; self.showsVerticalScrollIndicator = false; } return self; } - (BOOL)touchesShouldCancelInContentView:(UIView *)__unused view { return true; } @end @interface TGMenuSheetBackgroundView : UIView { UIVisualEffectView *_effectView; UIImageView *_imageView; } @end @implementation TGMenuSheetBackgroundView - (instancetype)initWithFrame:(CGRect)frame sizeClass:(UIUserInterfaceSizeClass)sizeClass dark:(bool)dark { self = [super initWithFrame:frame]; if (self != nil) { self.clipsToBounds = true; if (dark) { if (iosMajorVersion() >= 8) { self.layer.cornerRadius = TGMenuSheetCornerRadius; _effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]]; _effectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _effectView.frame = self.bounds; [self addSubview:_effectView]; if (@available(iOS 11.0, *)) { _effectView.accessibilityIgnoresInvertColors = true; } } else { self.backgroundColor = UIColorRGBA(0x181818, 0.9f); } } else { if (TGMenuSheetUseEffectView) { self.layer.cornerRadius = TGMenuSheetCornerRadius; _effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]]; _effectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _effectView.frame = self.bounds; [self addSubview:_effectView]; } else { self.backgroundColor = [UIColor whiteColor]; } } [self updateTraitsWithSizeClass:sizeClass]; } return self; } - (void)setMaskEnabled:(bool)enabled { if (_effectView != nil) return; self.layer.cornerRadius = enabled ? TGMenuSheetCornerRadius : 0.0f; } - (void)updateTraitsWithSizeClass:(UIUserInterfaceSizeClass)sizeClass { bool hidden = (sizeClass == UIUserInterfaceSizeClassRegular); _effectView.hidden = hidden; _imageView.hidden = hidden; [self setMaskEnabled:!hidden]; } @end @interface TGMenuSheetView () { TGMenuSheetBackgroundView *_headerBackgroundView; TGMenuSheetBackgroundView *_mainBackgroundView; TGMenuSheetBackgroundView *_footerBackgroundView; TGMenuSheetScrollView *_scrollView; NSMutableArray *_itemViews; NSMutableDictionary *_dividerViews; UIUserInterfaceSizeClass _sizeClass; bool _dark; bool _borderless; id _panHandlingItemView; bool _expectsPreciseContentTouch; id _context; TGMenuSheetPallete *_pallete; } @end @implementation TGMenuSheetView - (instancetype)initWithContext:(id)context pallete:(TGMenuSheetPallete *)pallete itemViews:(NSArray *)itemViews sizeClass:(UIUserInterfaceSizeClass)sizeClass dark:(bool)dark borderless:(bool)borderless { self = [super initWithFrame:CGRectZero]; if (self != nil) { _context = context; _borderless = borderless; _dark = dark; _pallete = pallete; _itemViews = [[NSMutableArray alloc] init]; _dividerViews = [[NSMutableDictionary alloc] init]; _sizeClass = sizeClass; self.backgroundColor = [UIColor clearColor]; [self addItemViews:itemViews]; } return self; } - (void)didChangeAbsoluteFrame { for (TGMenuSheetItemView *itemView in _itemViews) { [itemView didChangeAbsoluteFrame]; } } #pragma mark - - (void)setHandleInternalPan:(void (^)(UIPanGestureRecognizer *))handleInternalPan { _handleInternalPan = [handleInternalPan copy]; for (TGMenuSheetItemView *itemView in self.itemViews) { itemView.handleInternalPan = handleInternalPan; } } - (void)addItemsView:(TGMenuSheetItemView *)itemView { [self addItemView:itemView hasHeader:self.hasHeader hasFooter:self.hasFooter]; } - (void)addItemView:(TGMenuSheetItemView *)itemView hasHeader:(bool)hasHeader hasFooter:(bool)hasFooter { TGMenuSheetItemView *previousItemView = nil; itemView.sizeClass = _sizeClass; itemView.tag = _itemViews.count; itemView.handleInternalPan = [self.handleInternalPan copy]; if (_dark) [itemView setDark]; switch (itemView.type) { case TGMenuSheetItemTypeDefault: { if (hasFooter) [_itemViews insertObject:itemView atIndex:_itemViews.count - 1]; else [_itemViews addObject:itemView]; if (_mainBackgroundView == nil) { _mainBackgroundView = [[TGMenuSheetBackgroundView alloc] initWithFrame:CGRectZero sizeClass:_sizeClass dark:_dark]; [self insertSubview:_mainBackgroundView atIndex:0]; if (_pallete != nil) _mainBackgroundView.backgroundColor = _pallete.backgroundColor; _scrollView = [[TGMenuSheetScrollView alloc] initWithFrame:CGRectZero]; if (@available(iOS 11.0, *)) { _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } _scrollView.delegate = self; [_mainBackgroundView addSubview:_scrollView]; } [_scrollView addSubview:itemView]; UIView *divider = [self createDividerForItemView:itemView previousItemView:previousItemView]; if (divider != nil) [_scrollView addSubview:divider]; if (itemView.requiresClearBackground) { _mainBackgroundView.backgroundColor = [UIColor clearColor]; _expectsPreciseContentTouch = true; } } break; case TGMenuSheetItemTypeHeader: { if (hasHeader) return; [_itemViews insertObject:itemView atIndex:0]; if (_headerBackgroundView == nil) { _headerBackgroundView = [[TGMenuSheetBackgroundView alloc] initWithFrame:CGRectZero sizeClass:_sizeClass dark:_dark]; [self insertSubview:_headerBackgroundView atIndex:0]; if (_pallete != nil) _headerBackgroundView.backgroundColor = _pallete.backgroundColor; } [_headerBackgroundView addSubview:itemView]; } break; case TGMenuSheetItemTypeFooter: { if (hasFooter) return; [_itemViews addObject:itemView]; if (_footerBackgroundView == nil) { _footerBackgroundView = [[TGMenuSheetBackgroundView alloc] initWithFrame:CGRectZero sizeClass:_sizeClass dark:_dark]; [self insertSubview:_footerBackgroundView atIndex:0]; if (_pallete != nil) _footerBackgroundView.backgroundColor = _pallete.backgroundColor; } [_footerBackgroundView addSubview:itemView]; } break; default: break; } __weak TGMenuSheetView *weakSelf = self; itemView.layoutUpdateBlock = ^ { __strong TGMenuSheetView *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf layoutSubviews]; if (strongSelf.menuRelayout != nil) strongSelf.menuRelayout(); }; itemView.highlightUpdateBlock = ^(__unused bool highlighted) { }; } - (void)addItemViews:(NSArray *)itemViews { bool hasHeader = self.hasHeader; bool hasFooter = self.hasFooter; for (TGMenuSheetItemView *itemView in itemViews) { if (_pallete != nil) [itemView setPallete:_pallete]; [self addItemView:itemView hasHeader:hasHeader hasFooter:hasFooter]; if (itemView.type == TGMenuSheetItemTypeHeader) hasHeader = true; else if (itemView.type == TGMenuSheetItemTypeFooter) hasFooter = true; } } - (void)setItemViews:(NSArray *)itemViews animated:(bool)animated { NSMutableArray *itemViewsToDelete = [[NSMutableArray alloc] init]; for (TGMenuSheetItemView *itemView in _itemViews) { if (![itemViews containsObject:itemView]) { [itemViewsToDelete addObject:itemView]; } } if (animated) { } else { } } - (UIView *)createDividerForItemView:(TGMenuSheetItemView *)itemView previousItemView:(TGMenuSheetItemView *)previousItemView { if (!itemView.requiresDivider) return nil; UIView *topDivider = nil; if (previousItemView != nil) topDivider = _dividerViews[@(previousItemView.tag)][TGMenuDividerBottom]; UIView *bottomDivider = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, TGScreenPixel)]; bottomDivider.backgroundColor = _dark ? UIColorRGBA(0xffffff, 0.18f) : TGSeparatorColor(); if (_pallete != nil) bottomDivider.backgroundColor = _pallete.separatorColor; NSMutableDictionary *dividers = [[NSMutableDictionary alloc] init]; if (topDivider != nil) dividers[TGMenuDividerTop] = topDivider; dividers[TGMenuDividerBottom] = bottomDivider; _dividerViews[@(itemView.tag)] = dividers; return bottomDivider; } #pragma mark - - (void)updateTraitsWithSizeClass:(UIUserInterfaceSizeClass)sizeClass { _sizeClass = sizeClass; bool hideNonRegularItems = (_sizeClass == UIUserInterfaceSizeClassRegular); for (TGMenuSheetItemView *itemView in _itemViews) { itemView.sizeClass = sizeClass; if (itemView.type == TGMenuSheetItemTypeHeader || itemView.type == TGMenuSheetItemTypeFooter) [itemView setHidden:hideNonRegularItems animated:false]; } [_headerBackgroundView updateTraitsWithSizeClass:sizeClass]; [_mainBackgroundView updateTraitsWithSizeClass:sizeClass]; [_footerBackgroundView updateTraitsWithSizeClass:sizeClass]; } #pragma mark - - (UIEdgeInsets)edgeInsets { if (_sizeClass == UIUserInterfaceSizeClassRegular || _borderless) return UIEdgeInsetsZero; return TGMenuSheetPhoneEdgeInsets; } - (CGFloat)interSectionSpacing { return TGMenuSheetInterSectionSpacing; } - (CGSize)menuSize { return CGSizeMake(self.menuWidth, self.menuHeight); } - (CGFloat)menuHeight { CGFloat maxHeight = [_context fullscreenBounds].size.height; if (self.maxHeight > FLT_EPSILON) maxHeight = MIN(self.maxHeight, maxHeight); CGFloat edgeInsetLeft = _narrowInLandscape ? self.edgeInsets.left : MAX(self.edgeInsets.left, self.safeAreaInset.left); CGFloat edgeInsetRight = _narrowInLandscape ? self.edgeInsets.right : MAX(self.edgeInsets.right, self.safeAreaInset.right); CGFloat width = self.menuWidth - edgeInsetLeft - edgeInsetRight; return MIN(maxHeight, [self menuHeightForWidth:width]); } - (CGFloat)menuHeightForWidth:(CGFloat)width { CGFloat height = 0.0f; CGFloat screenHeight = [_context fullscreenBounds].size.height; UIEdgeInsets edgeInsets = self.edgeInsets; bool hasRegularItems = false; bool hasHeader = false; bool hasFooter = false; for (TGMenuSheetItemView *itemView in self.itemViews) { bool skip = false; switch (itemView.type) { case TGMenuSheetItemTypeDefault: hasRegularItems = true; break; case TGMenuSheetItemTypeHeader: if (_sizeClass == UIUserInterfaceSizeClassRegular) skip = true; else hasHeader = true; break; case TGMenuSheetItemTypeFooter: if (_sizeClass == UIUserInterfaceSizeClassRegular) skip = true; else hasFooter = true; break; default: break; } if (!skip) { height += [itemView preferredHeightForWidth:width screenHeight:screenHeight]; height += itemView.contentHeightCorrection; } } if (hasRegularItems || hasHeader || hasFooter) height += self.edgeInsets.top + self.edgeInsets.bottom; if ((hasRegularItems && hasHeader) || (hasRegularItems && hasFooter) || (hasHeader && hasFooter)) height += self.interSectionSpacing; if (hasHeader && hasFooter && hasRegularItems) height += self.interSectionSpacing; if (self.keyboardOffset > 0) { height += self.keyboardOffset; height -= [self.footerItemView preferredHeightForWidth:width screenHeight:screenHeight] + self.interSectionSpacing; } if (fabs(height - screenHeight) <= edgeInsets.top) height = screenHeight; return height; } - (CGFloat)contentHeightCorrection { CGFloat height = 0.0f; for (TGMenuSheetItemView *itemView in self.itemViews) height += itemView.contentHeightCorrection; return height; } #pragma mark - - (TGMenuSheetItemView *)headerItemView { if (_sizeClass == UIUserInterfaceSizeClassRegular) return nil; if ([(TGMenuSheetItemView *)self.itemViews.firstObject type] == TGMenuSheetItemTypeHeader) return self.itemViews.firstObject; return nil; } - (TGMenuSheetItemView *)footerItemView { if (_sizeClass == UIUserInterfaceSizeClassRegular) return nil; if ([(TGMenuSheetItemView *)self.itemViews.lastObject type] == TGMenuSheetItemTypeFooter) return self.itemViews.lastObject; return nil; } - (bool)hasHeader { if (_sizeClass == UIUserInterfaceSizeClassRegular) return nil; return (self.headerItemView != nil); } - (bool)hasFooter { if (_sizeClass == UIUserInterfaceSizeClassRegular) return nil; return (self.footerItemView != nil); } - (NSValue *)mainFrame { if (_mainBackgroundView != nil) return [NSValue valueWithCGRect:_mainBackgroundView.frame]; return nil; } - (NSValue *)headerFrame { if (_headerBackgroundView != nil) return [NSValue valueWithCGRect:_headerBackgroundView.frame]; return nil; } - (NSValue *)footerFrame { if (_footerBackgroundView != nil) return [NSValue valueWithCGRect:_footerBackgroundView.frame]; return nil; } #pragma mark - - (CGRect)activePanRect { if (_panHandlingItemView == nil) { for (TGMenuSheetItemView *itemView in _itemViews) { if (itemView.handlesPan) { _panHandlingItemView = itemView; break; } } if (_panHandlingItemView == nil) _panHandlingItemView = [NSNull null]; } if ([_panHandlingItemView isKindOfClass:[NSNull class]]) { if (_scrollView.frame.size.height < _scrollView.contentSize.height) return [self convertRect:_scrollView.frame toView:self.superview.superview]; else return CGRectNull; } TGMenuSheetItemView *itemView = (TGMenuSheetItemView *)_panHandlingItemView; return [itemView convertRect:itemView.bounds toView:self.superview.superview]; } - (bool)passPanOffset:(CGFloat)offset { if (_scrollView.frame.size.height < _scrollView.contentSize.height) { CGFloat bottomContentOffset = (_scrollView.contentSize.height - _scrollView.frame.size.height); if (bottomContentOffset > 0 && _scrollView.contentOffset.y > bottomContentOffset) return false; bool atTop = (_scrollView.contentOffset.y < FLT_EPSILON); bool atBottom = (_scrollView.contentOffset.y - bottomContentOffset > -FLT_EPSILON); if (atTop && offset > FLT_EPSILON) return true; if (atBottom && offset < 0) return true; return false; } else if ([_panHandlingItemView isKindOfClass:[NSNull class]]) { return true; } TGMenuSheetItemView *itemView = (TGMenuSheetItemView *)_panHandlingItemView; return [itemView passPanOffset:offset]; } - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { if (!_expectsPreciseContentTouch) return [super pointInside:point withEvent:event]; for (TGMenuSheetItemView *itemView in _itemViews) { if ([itemView pointInside:[self convertPoint:point toView:itemView] withEvent:event]) return true; } return false; } #pragma mark - - (void)scrollViewDidScroll:(UIScrollView *)scrollView { CGFloat bottomContentOffset = (scrollView.contentSize.height - scrollView.frame.size.height); bool atTop = (scrollView.contentOffset.y < FLT_EPSILON); bool atBottom = (scrollView.contentOffset.y - bottomContentOffset > -FLT_EPSILON); if ((atTop || atBottom) && _sizeClass == UIUserInterfaceSizeClassCompact) { if (scrollView.isTracking && scrollView.bounces && (scrollView.contentOffset.y - bottomContentOffset) < 20.0f) { scrollView.bounces = false; if (atTop) scrollView.contentOffset = CGPointMake(0, 0); else if (atBottom) scrollView.contentOffset = CGPointMake(0, bottomContentOffset); } } else { scrollView.bounces = true; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { CGFloat bottomContentOffset = (scrollView.contentSize.height - scrollView.frame.size.height); bool atTop = (scrollView.contentOffset.y < FLT_EPSILON); bool atBottom = (scrollView.contentOffset.y - bottomContentOffset > -FLT_EPSILON); if ((atTop || atBottom) && scrollView.bounces && !scrollView.isTracking && _sizeClass == UIUserInterfaceSizeClassCompact) scrollView.bounces = false; } #pragma mark - - (void)menuWillAppearAnimated:(bool)animated { for (TGMenuSheetItemView *itemView in self.itemViews) [itemView menuView:self willAppearAnimated:animated]; } - (void)menuDidAppearAnimated:(bool)animated { for (TGMenuSheetItemView *itemView in self.itemViews) [itemView menuView:self didAppearAnimated:animated]; } - (void)menuWillDisappearAnimated:(bool)animated { for (TGMenuSheetItemView *itemView in self.itemViews) [itemView menuView:self willDisappearAnimated:animated]; } - (void)menuDidDisappearAnimated:(bool)animated { for (TGMenuSheetItemView *itemView in self.itemViews) [itemView menuView:self didDisappearAnimated:animated]; } - (void)setSafeAreaInset:(UIEdgeInsets)safeAreaInset { _safeAreaInset = safeAreaInset; [self setNeedsLayout]; } - (void)layoutSubviews { CGFloat edgeInsetLeft = _narrowInLandscape ? self.edgeInsets.left : MAX(self.edgeInsets.left, self.safeAreaInset.left); CGFloat edgeInsetRight = _narrowInLandscape ? self.edgeInsets.right : MAX(self.edgeInsets.right, self.safeAreaInset.right); CGFloat width = self.menuWidth - edgeInsetLeft - edgeInsetRight; CGFloat maxHeight = _sizeClass == UIUserInterfaceSizeClassCompact ? [_context fullscreenBounds].size.height : self.frame.size.height; if (_sizeClass == UIUserInterfaceSizeClassCompact && self.maxHeight > FLT_EPSILON) maxHeight = MIN(self.maxHeight , maxHeight); CGFloat screenHeight = maxHeight; bool fullscreen = fabs(maxHeight - [_context fullscreenBounds].size.height) < FLT_EPSILON; if (_sizeClass == UIUserInterfaceSizeClassCompact) { if (self.headerItemView != nil) maxHeight -= [self.headerItemView preferredHeightForWidth:width screenHeight:screenHeight] + self.interSectionSpacing; if (self.keyboardOffset > FLT_EPSILON) maxHeight -= self.keyboardOffset; else if (self.footerItemView != nil) maxHeight -= [self.footerItemView preferredHeightForWidth:width screenHeight:screenHeight] + self.interSectionSpacing; } CGFloat contentHeight = 0; bool hasRegularItems = false; NSUInteger i = 0; TGMenuSheetItemView *condensableItemView = nil; for (TGMenuSheetItemView *itemView in self.itemViews) { if (itemView.type == TGMenuSheetItemTypeDefault) { hasRegularItems = true; CGFloat height = [itemView preferredHeightForWidth:width screenHeight:screenHeight]; itemView.screenHeight = screenHeight; itemView.frame = CGRectMake(0, contentHeight, width, height); contentHeight += height; NSUInteger lastItem = (self.footerItemView != nil) ? self.itemViews.count - 2 : self.itemViews.count - 1; if (itemView.requiresDivider && i != lastItem) { UIView *divider = _dividerViews[@(itemView.tag)][TGMenuDividerBottom]; if (divider != nil) divider.frame = CGRectMake(0, CGRectGetMaxY(itemView.frame) - divider.frame.size.height, width, divider.frame.size.height); } if (itemView.condensable) condensableItemView = itemView; } i++; } contentHeight += self.contentHeightCorrection; UIEdgeInsets edgeInsets = self.edgeInsets; CGSize statusBarSize = [[LegacyComponentsGlobals provider] statusBarFrame].size; CGFloat statusBarHeight = MIN(statusBarSize.width, statusBarSize.height); statusBarHeight = MAX(statusBarHeight, 20.0f); if (_safeAreaInset.top > FLT_EPSILON) statusBarHeight = _safeAreaInset.top; if (fullscreen) { if (contentHeight > maxHeight - edgeInsets.top - edgeInsets.bottom) edgeInsets.top = statusBarHeight; if (fabs(contentHeight - maxHeight + edgeInsets.bottom) <= statusBarHeight) edgeInsets.top = statusBarHeight; } if (_sizeClass == UIUserInterfaceSizeClassRegular) edgeInsets = UIEdgeInsetsZero; maxHeight -= edgeInsets.top + edgeInsets.bottom; if (self.keyboardOffset > FLT_EPSILON && contentHeight > maxHeight && condensableItemView != nil) { CGFloat difference = contentHeight - maxHeight; contentHeight -= difference; CGRect frame = condensableItemView.frame; condensableItemView.frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height - difference); if (condensableItemView.requiresDivider) { UIView *divider = _dividerViews[@(condensableItemView.tag)][TGMenuDividerBottom]; if (divider != nil) { CGRect dividerFrame = divider.frame; divider.frame = CGRectMake(dividerFrame.origin.x, dividerFrame.origin.y - difference, dividerFrame.size.width, dividerFrame.size.height); } } bool moveNextItems = false; for (TGMenuSheetItemView *itemView in self.itemViews) { if (moveNextItems) { CGRect frame = itemView.frame; itemView.frame = CGRectMake(frame.origin.x, frame.origin.y - difference, frame.size.width, frame.size.height); if (itemView.requiresDivider) { UIView *divider = _dividerViews[@(itemView.tag)][TGMenuDividerBottom]; if (divider != nil) { CGRect dividerFrame = divider.frame; divider.frame = CGRectMake(dividerFrame.origin.x, dividerFrame.origin.y - difference, dividerFrame.size.width, dividerFrame.size.height); } } } else if (itemView == condensableItemView) { moveNextItems = true; } } } for (TGMenuSheetItemView *itemView in self.itemViews) [itemView _didLayoutSubviews]; CGFloat topInset = edgeInsets.top; if (self.headerItemView != nil) { _headerBackgroundView.frame = CGRectMake(edgeInsetLeft, topInset, width, [self.headerItemView preferredHeightForWidth:width screenHeight:screenHeight]); self.headerItemView.frame = _headerBackgroundView.bounds; topInset = CGRectGetMaxY(_headerBackgroundView.frame) + TGMenuSheetInterSectionSpacing; } if (hasRegularItems) { CGFloat additionalHeight = _borderless ? 256.0f : 0.0f; _mainBackgroundView.frame = CGRectMake(edgeInsetLeft, topInset, width, MIN(contentHeight, maxHeight) + additionalHeight); _scrollView.frame = CGRectMake(0.0f, 0.0f, _mainBackgroundView.frame.size.width, _mainBackgroundView.frame.size.height - additionalHeight); _scrollView.contentSize = CGSizeMake(width, contentHeight); } if (self.footerItemView != nil) { CGFloat height = [self.footerItemView preferredHeightForWidth:width screenHeight:screenHeight]; CGFloat top = self.menuHeight - edgeInsets.bottom - height; if (hasRegularItems && self.keyboardOffset < FLT_EPSILON) top = CGRectGetMaxY(_mainBackgroundView.frame) + TGMenuSheetInterSectionSpacing; _footerBackgroundView.frame = CGRectMake(edgeInsetLeft, top, width, height); self.footerItemView.frame = _footerBackgroundView.bounds; } } @end