chromium/ios/chrome/browser/contextual_panel/ui/panel_content_view_controller.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/contextual_panel/ui/panel_content_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/check_op.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/contextual_panel/ui/contextual_sheet_display_controller.h"
#import "ios/chrome/browser/contextual_panel/ui/panel_block_data.h"
#import "ios/chrome/browser/contextual_panel/ui/panel_block_metrics_data.h"
#import "ios/chrome/browser/contextual_panel/ui/panel_item_collection_view_cell.h"
#import "ios/chrome/browser/contextual_panel/ui/trait_collection_change_delegate.h"
#import "ios/chrome/browser/contextual_panel/utils/contextual_panel_metrics.h"
#import "ios/chrome/browser/shared/public/commands/contextual_sheet_commands.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/public/provider/chrome/browser/font/font_api.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {
// Top margin between the header logo and the top of the panel.
const CGFloat kLogoTopMargin = 24;

// Bottom margin between the header logo and the top of the collection view
const CGFloat kLogoBottomMargin = 18;

// Size of the close button.
const CGFloat kCloseButtonIconSize = 30;

// Margin between the close button and the trailing edge of the screen.
const CGFloat kCloseButtonTrailingMargin = 16;

// Height of the drag handle view.
const CGFloat kDragHandleHeight = 5;

// Width of the drag handle view.
const CGFloat kDragHandleWidth = 36;

// Top margin between the drag handle view and the panel.
const CGFloat kDragHandleTopMargin = 5;

// The size of the logo image.
constexpr CGFloat kLogoSize = 22;

// The top logo has a specific font size for branding reasons.
const CGFloat kLogoLabelFontSize = 18;

// The margin between the bottom of the content and the collection view.
const CGFloat kContentBottomMargin = 16;

// Threshold for how long a view is onscreen to count as visible.
const base::TimeDelta kVisibleTimeThreshold = base::Milliseconds(10);

// Identifier for the one section in this collection view.
NSString* const kSectionIdentifier = @"section1";

NSString* const kViewAccessibilityIdentifier = @"PanelContentViewAXID";

NSString* const kCloseButtonAccessibilityIdentifier = @"PanelCloseButtonAXID";

UIImage* CloseButtonImage(BOOL highlighted) {
  NSArray<UIColor*>* palette = @[
    [UIColor colorNamed:kGrey600Color],
    [UIColor colorNamed:kBackgroundColor],
  ];

  if (highlighted) {
    NSMutableArray<UIColor*>* transparentPalette =
        [[NSMutableArray alloc] init];
    [palette enumerateObjectsUsingBlock:^(UIColor* color, NSUInteger idx,
                                          BOOL* stop) {
      [transparentPalette addObject:[color colorWithAlphaComponent:0.6]];
    }];
    palette = [transparentPalette copy];
  }

  return SymbolWithPalette(
      DefaultSymbolWithPointSize(kXMarkCircleFillSymbol, kCloseButtonIconSize),
      palette);
}

}  // namespace

@interface PanelContentViewController () <UICollectionViewDelegate,
                                          UIPointerInteractionDelegate>

@end

@implementation PanelContentViewController {
  // The background visual effect view behind all the content.
  UIVisualEffectView* _backgroundVisualEffectView;

  // The header view at the top of the panel.
  UIVisualEffectView* _headerView;

  // Background for the header when the Reduce Transparency accessibility
  // setting is on.
  UIView* _headerViewAccessibilityBackground;

  // The button to close the view.
  UIButton* _closeButton;

  // The view for the small drag handle at the top of the panel.
  UIView* _dragHandleView;

  // The collection view managed by this view controller
  UICollectionView* _collectionView;

  // The data source for this collection view.
  UICollectionViewDiffableDataSource<NSString*, NSString*>* _diffableDataSource;

  // The blocks currently being displayed.
  NSArray<PanelBlockData*>* _panelBlocks;

  NSMutableDictionary<NSString*, PanelBlockMetricsData*>*
      _panelBlocksMetricsDataDict;

  // The stored height of the expanded bottom toolbar.
  CGFloat _bottomToolbarHeight;

  // Stored time the panel appeared.
  base::Time _appearanceTime;
}

#pragma mark - UIViewController

- (instancetype)init {
  self = [super init];
  if (self) {
    _panelBlocksMetricsDataDict = [[NSMutableDictionary alloc] init];
  }
  return self;
}

- (void)viewDidLoad {
  [super viewDidLoad];

  self.view.accessibilityIdentifier = kViewAccessibilityIdentifier;

  [self createBackground];
  [self.view addSubview:_backgroundVisualEffectView];
  AddSameConstraints(self.view, _backgroundVisualEffectView);

  [self createCollectionView];
  [self.view addSubview:_collectionView];
  AddSameConstraints(self.view, _collectionView);

  // Create and set up the header view. This should be added after the
  // collection view because the header should go above the collection view.
  UIBlurEffect* headerBlurEffect =
      [UIBlurEffect effectWithStyle:UIBlurEffectStyleRegular];
  _headerView = [[UIVisualEffectView alloc] initWithEffect:headerBlurEffect];
  _headerView.translatesAutoresizingMaskIntoConstraints = NO;
  [self.view addSubview:_headerView];

  [NSLayoutConstraint activateConstraints:@[
    [self.view.leadingAnchor constraintEqualToAnchor:_headerView.leadingAnchor],
    [self.view.trailingAnchor
        constraintEqualToAnchor:_headerView.trailingAnchor],
    [self.view.topAnchor constraintEqualToAnchor:_headerView.topAnchor],
  ]];

  _headerViewAccessibilityBackground = [[UIView alloc] init];
  _headerViewAccessibilityBackground.translatesAutoresizingMaskIntoConstraints =
      NO;
  _headerViewAccessibilityBackground.backgroundColor =
      [UIColor colorNamed:kGrey100Color];
  [_headerView.contentView addSubview:_headerViewAccessibilityBackground];
  AddSameConstraints(_headerView, _headerViewAccessibilityBackground);
  _headerViewAccessibilityBackground.hidden =
      !UIAccessibilityIsReduceTransparencyEnabled();

  [self createDragHandleView];
  [_headerView.contentView addSubview:_dragHandleView];
  [NSLayoutConstraint activateConstraints:@[
    [_headerView.centerXAnchor
        constraintEqualToAnchor:_dragHandleView.centerXAnchor],
    [_dragHandleView.topAnchor
        constraintEqualToAnchor:_headerView.contentView.topAnchor
                       constant:kDragHandleTopMargin],
  ]];

#if BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)
  UIImage* logoImage = MakeSymbolMulticolor(
      CustomSymbolWithPointSize(kMulticolorChromeballSymbol, kLogoSize));
#else
  UIImage* logoImage =
      CustomSymbolWithPointSize(kChromeProductSymbol, kLogoSize);
#endif  // BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)

  UIImageView* logoImageView = [[UIImageView alloc] initWithImage:logoImage];
  logoImageView.translatesAutoresizingMaskIntoConstraints = NO;

  UILabel* logoLabel = [[UILabel alloc] init];
  logoLabel.translatesAutoresizingMaskIntoConstraints = NO;
  logoLabel.text =
      l10n_util::GetNSString(IDS_IOS_CONTEXTUAL_PANEL_BRANDING_TITLE);
  UIFont* productFont =
      ios::provider::GetBrandedProductRegularFont(kLogoLabelFontSize);
  logoLabel.font = [[[UIFontMetrics alloc]
      initForTextStyle:UIFontTextStyleCaption1] scaledFontForFont:productFont];
  logoLabel.adjustsFontForContentSizeCategory = YES;
  logoLabel.textColor = [UIColor colorNamed:kGrey700Color];

  UIStackView* logo = [[UIStackView alloc]
      initWithArrangedSubviews:@[ logoImageView, logoLabel ]];
  logo.translatesAutoresizingMaskIntoConstraints = NO;
  logo.spacing = 5;
  logo.alignment = UIStackViewAlignmentCenter;

  logo.isAccessibilityElement = true;
  logo.accessibilityLabel = l10n_util::GetNSString(
      IDS_IOS_CONTEXTUAL_PANEL_BRANDING_ACCESSIBILITY_LABEL);

  [_headerView.contentView addSubview:logo];
  [NSLayoutConstraint activateConstraints:@[
    [logo.centerXAnchor
        constraintEqualToAnchor:_headerView.contentView.centerXAnchor],
    [logo.topAnchor constraintEqualToAnchor:_headerView.contentView.topAnchor
                                   constant:kLogoTopMargin],
    [_headerView.bottomAnchor constraintEqualToAnchor:logo.bottomAnchor
                                             constant:kLogoBottomMargin],
  ]];

  [self createCloseButton];
  [_headerView.contentView addSubview:_closeButton];
  [NSLayoutConstraint activateConstraints:@[
    [_headerView.contentView.trailingAnchor
        constraintEqualToAnchor:_closeButton.trailingAnchor
                       constant:kCloseButtonTrailingMargin],
    [_closeButton.centerYAnchor constraintEqualToAnchor:logo.centerYAnchor],
  ]];

  [self.view layoutIfNeeded];
  [self.sheetDisplayController
      setContentHeight:[self preferredHeightForContent]];

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(accessibilityReduceTransparencySettingDidChange)
             name:UIAccessibilityReduceTransparencyStatusDidChangeNotification
           object:nil];
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  _appearanceTime = base::Time::Now();
  UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
                                  _headerView);
}

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  base::UmaHistogramTimes("IOS.ContextualPanel.VisibleTime",
                          base::Time::Now() - _appearanceTime);

  // First alert all visible cells that they will disappear.
  for (NSIndexPath* indexPath in _collectionView.indexPathsForVisibleItems) {
    UICollectionViewCell* cell =
        [_collectionView cellForItemAtIndexPath:indexPath];
    PanelItemCollectionViewCell* panelCell =
        base::apple::ObjCCast<PanelItemCollectionViewCell>(cell);
    [self updateTimeDisplayedForCell:panelCell atIndexPath:indexPath];

    [panelCell cellDidDisappear];
  }

  std::string entrypointBlockName =
      base::SysNSStringToUTF8([self.metricsDelegate entrypointInfoBlockName]);
  BOOL wasLoudEntrypoint = [self.metricsDelegate wasLoudEntrypoint];

  for (NSString* key in _panelBlocksMetricsDataDict) {
    PanelBlockMetricsData* data = _panelBlocksMetricsDataDict[key];
    BOOL wasVisible = data.timeVisible >= kVisibleTimeThreshold;
    std::string blockName = base::SysNSStringToUTF8(key);

    std::string uptimeHistogramName =
        std::string("IOS.ContextualPanel.InfoBlockUptime.").append(blockName);
    base::UmaHistogramTimes(uptimeHistogramName, data.timeVisible);

    std::string impressionTypeHistogramName =
        std::string("IOS.ContextualPanel.InfoBlockImpression.")
            .append(blockName);
    PanelBlockImpressionType blockImpressionType;
    if (!wasVisible) {
      blockImpressionType = PanelBlockImpressionType::NeverVisible;
    } else {
      if (blockName == entrypointBlockName) {
        if (wasLoudEntrypoint) {
          blockImpressionType =
              PanelBlockImpressionType::VisibleAndLoudEntrypoint;
        } else {
          blockImpressionType =
              PanelBlockImpressionType::VisibleAndSmallEntrypoint;
        }
      } else {
        if (wasLoudEntrypoint) {
          blockImpressionType =
              PanelBlockImpressionType::VisibleAndOtherWasLoudEntrypoint;
        } else {
          blockImpressionType =
              PanelBlockImpressionType::VisibleAndOtherWasSmallEntrypoint;
        }
      }
    }
    base::UmaHistogramEnumeration(impressionTypeHistogramName,
                                  blockImpressionType);
  }
}

- (void)viewDidLayoutSubviews {
  [super viewDidLayoutSubviews];

  [self addAccessibilityTransparencyWorkaround];

  [self setCollectionViewContentInset];
  [self setCollectionViewScrollIndicatorInsets];
}

- (void)accessibilityReduceTransparencySettingDidChange {
  [self addAccessibilityTransparencyWorkaround];

  _headerViewAccessibilityBackground.hidden =
      !UIAccessibilityIsReduceTransparencyEnabled();
}

- (void)viewSafeAreaInsetsDidChange {
  [super viewSafeAreaInsetsDidChange];

  [self setCollectionViewScrollIndicatorInsets];
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];

  [self.traitCollectionDelegate traitCollectionDidChangeForViewController:self];
}

// Removes the white-ish background color of one of UIVisualEffectView's
// subviews that is not desired for this feature.
- (void)addAccessibilityTransparencyWorkaround {
  for (UIView* subview in _headerView.subviews) {
    // Replace any non-nil backgrounds with clear.
    if (subview.backgroundColor) {
      subview.backgroundColor = UIColor.clearColor;
    }
  }
}

#pragma mark - Public methods

- (void)setPanelBlocks:(NSArray<PanelBlockData*>*)panelBlocks {
  _panelBlocks = [panelBlocks copy];

  if (_diffableDataSource) {
    [_diffableDataSource applySnapshot:[self dataSnapshot]
                  animatingDifferences:NO];
  }
}

#pragma mark - Private

// Generates and returns a data source snapshot for the current data.
- (NSDiffableDataSourceSnapshot<NSString*, NSString*>*)dataSnapshot {
  NSDiffableDataSourceSnapshot<NSString*, NSString*>* snapshot =
      [[NSDiffableDataSourceSnapshot alloc] init];
  [snapshot appendSectionsWithIdentifiers:@[ kSectionIdentifier ]];
  NSMutableArray<NSString*>* itemIdentifiers = [[NSMutableArray alloc] init];
  for (PanelBlockData* data in _panelBlocks) {
    [itemIdentifiers addObject:data.blockType];
  }
  [snapshot appendItemsWithIdentifiers:itemIdentifiers];
  return snapshot;
}

// Target for the close button.
- (void)closeButtonTapped {
  base::UmaHistogramEnumeration("IOS.ContextualPanel.DismissedReason",
                                ContextualPanelDismissedReason::UserDismissed);
  [self.contextualSheetCommandHandler closeContextualSheet];
}

// Looks up the correct registration for the provided item. Wrapper for
// UICollectionViewDiffableDataSource's cellProvider parameter.
- (UICollectionViewCell*)
    diffableDataSourceCellProviderForCollectionView:
        (UICollectionView*)collectionView
                                          indexPath:(NSIndexPath*)indexPath
                                     itemIdentifier:(id)itemIdentifier {
  UICollectionViewCellRegistration* registration =
      [_panelBlocks[indexPath.row] cellRegistration];
  UICollectionViewCell* cell = [collectionView
      dequeueConfiguredReusableCellWithRegistration:registration
                                       forIndexPath:indexPath
                                               item:itemIdentifier];
  return cell;
}

- (CGFloat)preferredHeightForContent {
  // The collection view takes up the entire view, so find the preferred size
  // of the collection view.
  CGFloat height = _collectionView.contentSize.height +
                   _collectionView.contentInset.top +
                   _collectionView.contentInset.bottom;

  return height;
}

- (void)updateTimeDisplayedForCell:(PanelItemCollectionViewCell*)cell
                       atIndexPath:(NSIndexPath*)indexPath {
  PanelBlockData* panelData = _panelBlocks[indexPath.row];

  if (!_panelBlocksMetricsDataDict[panelData.blockType]) {
    _panelBlocksMetricsDataDict[panelData.blockType] =
        [[PanelBlockMetricsData alloc] init];
  }
  PanelBlockMetricsData* metricsData =
      _panelBlocksMetricsDataDict[panelData.blockType];
  metricsData.timeVisible += cell.timeSinceAppearance;
}

- (void)setCollectionViewScrollIndicatorInsets {
  // The bottom inset should not include the safe area height.
  _collectionView.verticalScrollIndicatorInsets = UIEdgeInsetsMake(
      _headerView.bounds.size.height, 0,
      _bottomToolbarHeight - self.view.safeAreaInsets.bottom, 0);
}

- (void)setCollectionViewContentInset {
  _collectionView.contentInset =
      UIEdgeInsetsMake(_headerView.bounds.size.height, 0,
                       _bottomToolbarHeight + kContentBottomMargin, 0);
}

#pragma mark - View Initialization

// Creates the layout for the collection view.
- (UICollectionViewLayout*)createLayout {
  NSCollectionLayoutSize* itemSize = [NSCollectionLayoutSize
      sizeWithWidthDimension:[NSCollectionLayoutDimension
                                 fractionalWidthDimension:1.]
             heightDimension:[NSCollectionLayoutDimension
                                 estimatedDimension:200]];
  NSCollectionLayoutItem* item =
      [NSCollectionLayoutItem itemWithLayoutSize:itemSize];

  NSCollectionLayoutSize* groupSize = [NSCollectionLayoutSize
      sizeWithWidthDimension:[NSCollectionLayoutDimension
                                 fractionalWidthDimension:1.]
             heightDimension:[NSCollectionLayoutDimension
                                 estimatedDimension:200]];
  NSCollectionLayoutGroup* group =
      [NSCollectionLayoutGroup verticalGroupWithLayoutSize:groupSize
                                                  subitems:@[ item ]];

  NSCollectionLayoutSection* section =
      [NSCollectionLayoutSection sectionWithGroup:group];
  return [[UICollectionViewCompositionalLayout alloc] initWithSection:section];
}

// Creates and initializes `_collectionView`.
- (void)createCollectionView {
  _collectionView =
      [[UICollectionView alloc] initWithFrame:CGRectZero
                         collectionViewLayout:[self createLayout]];
  _collectionView.translatesAutoresizingMaskIntoConstraints = NO;
  _collectionView.backgroundColor = UIColor.clearColor;
  [self setCollectionViewContentInset];
  [self setCollectionViewScrollIndicatorInsets];
  _collectionView.contentInsetAdjustmentBehavior =
      UIScrollViewContentInsetAdjustmentNever;
  _collectionView.delegate = self;

  __weak __typeof(self) weakSelf = self;
  auto cellProvider =
      ^UICollectionViewCell*(UICollectionView* collectionView,
                             NSIndexPath* indexPath, id itemIdentifier) {
        return [weakSelf
            diffableDataSourceCellProviderForCollectionView:collectionView
                                                  indexPath:indexPath
                                             itemIdentifier:itemIdentifier];
      };

  _diffableDataSource = [[UICollectionViewDiffableDataSource alloc]
      initWithCollectionView:_collectionView
                cellProvider:cellProvider];

  _collectionView.dataSource = _diffableDataSource;

  [_diffableDataSource applySnapshot:[self dataSnapshot]
                animatingDifferences:NO];
}

// Creates and initializes `_closeButton`.
- (void)createCloseButton {
  UIButtonConfiguration* closeButtonConfiguration =
      [UIButtonConfiguration plainButtonConfiguration];
  // The image itself is set below in the configurationUpdateHandler, which
  // is called before the button appears for the first time as well.
  closeButtonConfiguration.contentInsets = NSDirectionalEdgeInsetsZero;
  closeButtonConfiguration.buttonSize = UIButtonConfigurationSizeSmall;
  closeButtonConfiguration.accessibilityLabel =
      l10n_util::GetNSString(IDS_CLOSE);
  __weak __typeof(self) weakSelf = self;
  _closeButton = [UIButton
      buttonWithConfiguration:closeButtonConfiguration
                primaryAction:[UIAction actionWithHandler:^(UIAction* action) {
                  [weakSelf closeButtonTapped];
                }]];
  _closeButton.translatesAutoresizingMaskIntoConstraints = NO;
  _closeButton.accessibilityIdentifier = kCloseButtonAccessibilityIdentifier;
  _closeButton.pointerInteractionEnabled = YES;
  _closeButton.configurationUpdateHandler = ^(UIButton* button) {
    UIButtonConfiguration* updatedConfig = button.configuration;
    switch (button.state) {
      case UIControlStateHighlighted:
        updatedConfig.image = CloseButtonImage(YES);
        break;
      case UIControlStateNormal:
        updatedConfig.image = CloseButtonImage(NO);
        break;
    }
    button.configuration = updatedConfig;
  };
}

- (void)createDragHandleView {
  _dragHandleView = [[UIView alloc] init];
  _dragHandleView.translatesAutoresizingMaskIntoConstraints = NO;
  _dragHandleView.backgroundColor = UIColor.systemGray2Color;
  [_dragHandleView
      addInteraction:[[UIPointerInteraction alloc] initWithDelegate:self]];
  _dragHandleView.layer.cornerRadius = kDragHandleHeight / 2;

  [NSLayoutConstraint activateConstraints:@[
    [_dragHandleView.heightAnchor constraintEqualToConstant:kDragHandleHeight],
    [_dragHandleView.widthAnchor constraintEqualToConstant:kDragHandleWidth],
  ]];
}

- (void)createBackground {
  UIBlurEffect* backgroundBlurEffect =
      [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemThickMaterial];
  _backgroundVisualEffectView =
      [[UIVisualEffectView alloc] initWithEffect:backgroundBlurEffect];
  _backgroundVisualEffectView.translatesAutoresizingMaskIntoConstraints = NO;

  UIView* scrim = [[UIView alloc] init];
  scrim.translatesAutoresizingMaskIntoConstraints = NO;
  // The scrim should be black 3% opacity in both light and dark mode.
  scrim.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.03];
  [_backgroundVisualEffectView.contentView addSubview:scrim];
  AddSameConstraints(_backgroundVisualEffectView.contentView, scrim);
}

#pragma mark - PanelContentConsumer

- (void)updateBottomToolbarHeight:(CGFloat)height {
  _bottomToolbarHeight = height;
  if (_collectionView) {
    UIEdgeInsets insets = _collectionView.contentInset;
    insets.bottom = height + kContentBottomMargin;
    _collectionView.contentInset = insets;
    [self setCollectionViewScrollIndicatorInsets];
    [self.sheetDisplayController
        setContentHeight:[self preferredHeightForContent]];
  }
}

#pragma mark - UICollectionViewDelegate

- (void)collectionView:(UICollectionView*)collectionView
       willDisplayCell:(UICollectionViewCell*)cell
    forItemAtIndexPath:(NSIndexPath*)indexPath {
  PanelItemCollectionViewCell* panelCell =
      base::apple::ObjCCast<PanelItemCollectionViewCell>(cell);
  [panelCell cellWillAppear];
}

- (void)collectionView:(UICollectionView*)collectionView
    didEndDisplayingCell:(UICollectionViewCell*)cell
      forItemAtIndexPath:(NSIndexPath*)indexPath {
  PanelItemCollectionViewCell* panelCell =
      base::apple::ObjCCast<PanelItemCollectionViewCell>(cell);
  [self updateTimeDisplayedForCell:panelCell atIndexPath:indexPath];

  [panelCell cellDidDisappear];
}

#pragma mark - UIPointerInteractionDelegate

- (UIPointerStyle*)pointerInteraction:(UIPointerInteraction*)interaction
                       styleForRegion:(UIPointerRegion*)region {
  // If the view is no longer in a window due to a race condition, no
  // pointer style is needed.
  if (!interaction.view.window) {
    return nil;
  }

  DCHECK_EQ(_dragHandleView, interaction.view);

  UITargetedPreview* preview =
      [[UITargetedPreview alloc] initWithView:_dragHandleView];
  UIPointerEffect* effect =
      [UIPointerHighlightEffect effectWithPreview:preview];

  // Make the pointer frame slightly larger than the view, like the system drag
  // handle.
  CGRect pointerFrame = CGRectInset(_dragHandleView.frame, -5, -5);
  UIPointerShape* shape = [UIPointerShape shapeWithRoundedRect:pointerFrame];

  UIPointerStyle* style = [UIPointerStyle styleWithEffect:effect shape:shape];
  return style;
}

@end