// // KBEmojiPanelView.m // CustomKeyboard // #import "KBEmojiPanelView.h" #import "KBEmojiDataProvider.h" #import "KBSkinManager.h" #import "KBLocalizationManager.h" #import "Masonry.h" #import "KBEmojiCollectionCell.h" #import "KBEmojiBottomBarView.h" @interface KBEmojiPanelView () @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UIButton *backButton; @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) KBEmojiBottomBarView *bottomBar; //@property (nonatomic, strong) UIButton *searchButton; @property (nonatomic, strong) NSArray *tabButtons; @property (nonatomic, strong) KBEmojiDataProvider *dataProvider; @property (nonatomic, copy) NSArray *categories; @property (nonatomic, assign) NSInteger currentIndex; @property (nonatomic, strong) UIView *magnifierView; @property (nonatomic, strong) UILabel *magnifierLabel; @property (nonatomic, strong) UIColor *tabNormalColor; @property (nonatomic, strong) UIColor *tabSelectedColor; @end @implementation KBEmojiPanelView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { _dataProvider = [KBEmojiDataProvider shared]; _currentIndex = 1; [self setupUI]; [self registerNotifications]; [self reloadData]; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Setup - (void)setupUI { self.backgroundColor = [UIColor colorWithWhite:0.08 alpha:1.0]; UIView *topBar = [[UIView alloc] init]; topBar.backgroundColor = [UIColor clearColor]; [self addSubview:topBar]; self.backButton = [UIButton buttonWithType:UIButtonTypeCustom]; // self.backButton.titleLabel.font = [UIFont systemFontOfSize:30 weight:UIFontWeightSemibold]; // [self.backButton setTitle:@"⌨︎" forState:UIControlStateNormal]; [self.backButton setImage:[UIImage imageNamed:@"back_keybord_icon"] forState:UIControlStateNormal]; [self.backButton addTarget:self action:@selector(onBack) forControlEvents:UIControlEventTouchUpInside]; [topBar addSubview:self.backButton]; self.titleLabel = [[UILabel alloc] init]; self.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; self.titleLabel.textColor = [UIColor whiteColor]; [topBar addSubview:self.titleLabel]; [topBar mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self); make.top.equalTo(self.mas_top).offset(4); make.height.mas_equalTo(40); }]; [self.backButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(topBar.mas_left).offset(12); make.centerY.equalTo(topBar); make.width.height.mas_equalTo(32); }]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self); make.centerY.equalTo(topBar); // make.right.lessThanOrEqualTo(topBar.mas_right).offset(-12); }]; UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; layout.scrollDirection = UICollectionViewScrollDirectionVertical; layout.minimumInteritemSpacing = 8; layout.minimumLineSpacing = 12; self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; self.collectionView.backgroundColor = [UIColor clearColor]; self.collectionView.dataSource = self; self.collectionView.delegate = self; self.collectionView.alwaysBounceVertical = YES; [self.collectionView registerClass:KBEmojiCollectionCell.class forCellWithReuseIdentifier:@"KBEmojiCollectionCell"]; [self addSubview:self.collectionView]; self.bottomBar = [[KBEmojiBottomBarView alloc] init]; [self addSubview:self.bottomBar]; [self.bottomBar.deleteButton addTarget:self action:@selector(onDelete) forControlEvents:UIControlEventTouchUpInside]; // self.searchButton = [UIButton buttonWithType:UIButtonTypeSystem]; // self.searchButton.layer.cornerRadius = 20; // self.searchButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightBold]; // [self.searchButton setTitle:KBLocalized(@"Search") forState:UIControlStateNormal]; // [self.searchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // [self.searchButton addTarget:self action:@selector(onSearch) forControlEvents:UIControlEventTouchUpInside]; // [self.bottomBar addSubview:self.searchButton]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.mas_left).offset(12); make.right.equalTo(self.mas_right).offset(-12); make.top.equalTo(topBar.mas_bottom).offset(0); make.bottom.equalTo(self.bottomBar.mas_top).offset(0); }]; [self.bottomBar mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.bottom.equalTo(self); make.height.mas_equalTo(40); }]; UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(onSwipe:)]; leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft; [self addGestureRecognizer:leftSwipe]; UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(onSwipe:)]; rightSwipe.direction = UISwipeGestureRecognizerDirectionRight; [self addGestureRecognizer:rightSwipe]; [self applyTheme:[KBSkinManager shared].current]; } - (void)layoutSubviews { [super layoutSubviews]; [self updateTabButtonCornerRadii]; } - (void)updateTabButtonCornerRadii { for (UIButton *btn in self.tabButtons) { CGFloat radius = MIN(CGRectGetHeight(btn.bounds), CGRectGetWidth(btn.bounds)) / 2.0; if (radius <= 0) { continue; } btn.layer.cornerRadius = radius; if (@available(iOS 13.0, *)) { btn.layer.cornerCurve = kCACornerCurveContinuous; } } UIButton *deleteButton = self.bottomBar.deleteButton; if (deleteButton) { CGFloat radius = MIN(CGRectGetHeight(deleteButton.bounds), CGRectGetWidth(deleteButton.bounds)) / 2.0; if (radius > 0) { deleteButton.layer.cornerRadius = radius; if (@available(iOS 13.0, *)) { deleteButton.layer.cornerCurve = kCACornerCurveContinuous; } } } } - (void)registerNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onEmojiDataChanged) name:KBEmojiRecentsDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onLocalizationChanged) name:KBLocalizationDidChangeNotification object:nil]; } #pragma mark - Data - (void)reloadData { self.categories = self.dataProvider.categories; if (self.categories.count == 0) { self.currentIndex = NSNotFound; [self.collectionView reloadData]; self.titleLabel.text = @""; return; } NSInteger preserved = self.currentIndex; if (preserved < 0 || preserved >= self.categories.count) { preserved = 0; } [self rebuildTabButtons]; [self updateSelectionToIndex:preserved]; } - (void)rebuildTabButtons { UIStackView *stackView = self.bottomBar.tabStackView; if (!stackView) { return; } for (UIView *v in stackView.arrangedSubviews) { [stackView removeArrangedSubview:v]; [v removeFromSuperview]; } NSMutableArray *buttons = [NSMutableArray arrayWithCapacity:self.categories.count]; [self.categories enumerateObjectsUsingBlock:^(KBEmojiCategory * _Nonnull cat, NSUInteger idx, BOOL * _Nonnull stop) { UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; btn.tag = idx; btn.layer.cornerRadius = 16; btn.layer.masksToBounds = YES; btn.titleLabel.font = [UIFont systemFontOfSize:18]; [btn setTitle:cat.iconSymbol ?: @"●" forState:UIControlStateNormal]; [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [btn addTarget:self action:@selector(onTabTapped:) forControlEvents:UIControlEventTouchUpInside]; btn.contentEdgeInsets = UIEdgeInsetsMake(0, 12, 0, 12); btn.translatesAutoresizingMaskIntoConstraints = NO; // [btn.heightAnchor constraintEqualTo:self.bottomBar.tabScrollView.heightAnchor].active = YES; [stackView addArrangedSubview:btn]; [buttons addObject:btn]; }]; self.tabButtons = buttons.copy; [self setNeedsLayout]; } - (void)updateSelectionToIndex:(NSInteger)index { if (self.categories.count == 0) { self.currentIndex = NSNotFound; [self.collectionView reloadData]; self.titleLabel.text = @""; return; } if (index < 0) { index = 0; } if (index >= self.categories.count) { index = self.categories.count - 1; } self.currentIndex = index; KBEmojiCategory *cat = self.categories[index]; self.titleLabel.text = cat.displayTitle; [self.collectionView reloadData]; [self updateTabHighlightStates]; [self scrollTabToVisible:index]; } - (void)selectCategoryAtIndex:(NSInteger)index { [self updateSelectionToIndex:index]; } - (void)updateTabHighlightStates { [self.tabButtons enumerateObjectsUsingBlock:^(UIButton * _Nonnull btn, NSUInteger idx, BOOL * _Nonnull stop) { BOOL selected = (idx == self.currentIndex); btn.backgroundColor = selected ? self.tabSelectedColor : self.tabNormalColor; btn.alpha = selected ? 1.0 : 0.6; }]; } - (void)scrollTabToVisible:(NSInteger)index { if (index < 0 || index >= self.tabButtons.count) return; UIScrollView *scrollView = self.bottomBar.tabScrollView; UIStackView *stackView = self.bottomBar.tabStackView; if (!scrollView || !stackView) { return; } UIButton *btn = self.tabButtons[index]; CGRect rect = [scrollView convertRect:btn.frame fromView:stackView]; rect = CGRectInset(rect, -12, 0); [scrollView scrollRectToVisible:rect animated:YES]; } #pragma mark - Actions - (void)onBack { if ([self.delegate respondsToSelector:@selector(emojiPanelViewDidRequestClose:)]) { [self.delegate emojiPanelViewDidRequestClose:self]; } } - (void)onSearch { if ([self.delegate respondsToSelector:@selector(emojiPanelViewDidTapSearch:)]) { [self.delegate emojiPanelViewDidTapSearch:self]; } } - (void)onDelete { if ([self.delegate respondsToSelector:@selector(emojiPanelViewDidTapDelete:)]) { [self.delegate emojiPanelViewDidTapDelete:self]; } } - (void)onTabTapped:(UIButton *)sender { [self updateSelectionToIndex:sender.tag]; } - (void)onSwipe:(UISwipeGestureRecognizer *)gesture { if (self.categories.count == 0) return; if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) { if (self.currentIndex + 1 < self.categories.count) { [self updateSelectionToIndex:self.currentIndex + 1]; } } else if (gesture.direction == UISwipeGestureRecognizerDirectionRight) { if (self.currentIndex - 1 >= 0) { [self updateSelectionToIndex:self.currentIndex - 1]; } } } - (void)onEmojiDataChanged { [self reloadData]; } - (void)onLocalizationChanged { // [self.searchButton setTitle:KBLocalized(@"Search") forState:UIControlStateNormal]; [self reloadData]; } #pragma mark - Theme - (void)applyTheme:(KBSkinTheme *)theme { UIColor *bg = theme.keyboardBackground ?: [UIColor colorWithWhite:0.08 alpha:1.0]; self.backgroundColor = bg; self.collectionView.backgroundColor = [UIColor clearColor]; self.titleLabel.textColor = theme.keyTextColor ?: [UIColor whiteColor]; UIColor *searchColor = theme.accentColor ?: [UIColor colorWithRed:0.35 green:0.35 blue:0.95 alpha:1]; // self.searchButton.backgroundColor = searchColor; self.tabNormalColor = [UIColor colorWithWhite:1 alpha:0.08]; self.tabSelectedColor = theme.accentColor ?: [UIColor colorWithWhite:1 alpha:0.25]; [self updateTabHighlightStates]; if (self.bottomBar.deleteButton) { self.bottomBar.deleteButton.backgroundColor = self.tabNormalColor; UIColor *deleteTitleColor = theme.keyTextColor ?: [UIColor whiteColor]; [self.bottomBar.deleteButton setTitleColor:deleteTitleColor forState:UIControlStateNormal]; } if (self.magnifierView) { self.magnifierView.backgroundColor = theme.keyBackground ?: [UIColor colorWithWhite:1 alpha:0.9]; } if (self.magnifierLabel) { self.magnifierLabel.textColor = theme.keyTextColor ?: [UIColor blackColor]; } } #pragma mark - Magnifier - (void)showMagnifierForEmoji:(NSString *)emoji fromRect:(CGRect)rect { if (!self.magnifierView) { self.magnifierView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 68, 68)]; self.magnifierView.layer.cornerRadius = 12; self.magnifierView.layer.masksToBounds = YES; self.magnifierView.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.3].CGColor; self.magnifierView.layer.shadowOpacity = 0.6; self.magnifierView.layer.shadowOffset = CGSizeMake(0, 2); self.magnifierView.layer.shadowRadius = 3; self.magnifierLabel = [[UILabel alloc] initWithFrame:self.magnifierView.bounds]; self.magnifierLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.magnifierLabel.textAlignment = NSTextAlignmentCenter; self.magnifierLabel.font = [UIFont systemFontOfSize:40]; [self.magnifierView addSubview:self.magnifierLabel]; self.magnifierView.alpha = 0; [self addSubview:self.magnifierView]; } self.magnifierLabel.text = emoji; CGRect converted = [self convertRect:rect fromView:self.collectionView]; CGFloat targetX = CGRectGetMidX(converted); CGFloat targetY = CGRectGetMinY(converted) - CGRectGetHeight(self.magnifierView.bounds)/2 - 8; targetX = MAX(CGRectGetWidth(self.magnifierView.bounds)/2 + 8, targetX); targetX = MIN(CGRectGetWidth(self.bounds) - CGRectGetWidth(self.magnifierView.bounds)/2 - 8, targetX); if (targetY < CGRectGetHeight(self.magnifierView.bounds)/2 + 10) { targetY = CGRectGetHeight(self.magnifierView.bounds)/2 + 10; } self.magnifierView.center = CGPointMake(targetX, targetY); self.magnifierView.hidden = NO; [UIView animateWithDuration:0.08 animations:^{ self.magnifierView.alpha = 1.0; }]; } - (void)hideMagnifier { if (!self.magnifierView) return; [UIView animateWithDuration:0.08 animations:^{ self.magnifierView.alpha = 0.0; } completion:^(BOOL finished) { self.magnifierView.hidden = YES; }]; } #pragma mark - UICollectionViewDataSource - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if (self.currentIndex == NSNotFound || self.currentIndex >= self.categories.count) { return 0; } KBEmojiCategory *cat = self.categories[self.currentIndex]; return cat.items.count; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { KBEmojiCollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBEmojiCollectionCell" forIndexPath:indexPath]; KBEmojiCategory *cat = self.categories[self.currentIndex]; if (indexPath.item < cat.items.count) { KBEmojiItem *item = cat.items[indexPath.item]; [cell configureWithEmoji:item.value]; } else { [cell configureWithEmoji:@""]; } return cell; } #pragma mark - UICollectionViewDelegate - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (self.currentIndex == NSNotFound || self.currentIndex >= self.categories.count) return; KBEmojiCategory *cat = self.categories[self.currentIndex]; if (indexPath.item >= cat.items.count) return; KBEmojiItem *item = cat.items[indexPath.item]; if (item.value.length == 0) return; [self.dataProvider recordEmojiSelection:item.value]; if ([self.delegate respondsToSelector:@selector(emojiPanelView:didSelectEmoji:)]) { [self.delegate emojiPanelView:self didSelectEmoji:item.value]; } } - (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath { KBEmojiCategory *cat = (self.currentIndex < self.categories.count) ? self.categories[self.currentIndex] : nil; if (indexPath.item >= cat.items.count) return; KBEmojiItem *item = cat.items[indexPath.item]; UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; if (!cell) return; [self showMagnifierForEmoji:item.value fromRect:cell.frame]; } - (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath { [self hideMagnifier]; } #pragma mark - UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { CGFloat availableWidth = collectionView.bounds.size.width; NSInteger columns = 8; CGFloat spacing = 8; CGFloat totalSpacing = spacing * (columns - 1); CGFloat width = floor((availableWidth - totalSpacing) / columns); if (width < 32) { width = 32; } return CGSizeMake(width, width); } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return 12; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return 8; } @end