增加换肤功能

This commit is contained in:
启星
2025-08-14 10:07:49 +08:00
parent f6964c1e89
commit 4f9318d98e
8789 changed files with 978530 additions and 2 deletions

View File

@@ -0,0 +1,29 @@
//
// TUIChatFlexViewController.h
// TUIChat
//
// Created by wyl on 2022/10/27.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TIMDefine.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIChatFlexViewController : UIViewController
@property(nonatomic, strong) UIView *topGestureView;
@property(nonatomic, strong) UIImageView *topImgView;
@property(nonatomic, strong) UIView *containerView;
- (void)updateSubContainerView;
- (void)setnormalTop;
- (void)setNormalBottom;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,163 @@
//
// TUIChatFlexViewController.m
// TUIChat
//
// Created by wyl on 2022/10/27.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatFlexViewController.h"
typedef enum : NSUInteger {
FLEX_TOP,
FLEX_Bottom,
} FLEX_Location;
CGFloat topMargin = NavBar_Height + 30;
@interface TUIChatFlexViewController ()
@property(nonatomic, assign) FLEX_Location currentLoaction;
@property(nonatomic, strong) UIPanGestureRecognizer *panCover;
@property(nonatomic, strong) UITapGestureRecognizer *singleTap;
@end
@implementation TUIChatFlexViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:.5];
self.containerView.backgroundColor = [UIColor whiteColor];
self.topImgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:TUIChatImagePath_Minimalist(@"icon_flex_arrow")]];
[self.topGestureView addSubview:self.topImgView];
[self addSingleTapGesture];
if (!_currentLoaction) {
self.currentLoaction = FLEX_TOP;
}
[self updateSubContainerView];
}
- (void)addSingleTapGesture {
// When clicking on the shadow, the page disappears
self.view.userInteractionEnabled = YES;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)];
singleTap.cancelsTouchesInView = NO;
[self.view addGestureRecognizer:singleTap];
}
- (void)singleTap:(UITapGestureRecognizer *)tap {
CGPoint translation = [tap locationInView:self.containerView];
if (translation.x < 0 || translation.y < 0) {
[self dismissViewControllerAnimated:YES completion:nil];
} else if (translation.x > self.containerView.frame.size.width || translation.y > self.containerView.frame.size.height) {
[self dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)setnormalTop {
self.currentLoaction = FLEX_TOP;
}
- (void)setNormalBottom {
self.currentLoaction = FLEX_Bottom;
}
- (void)setCurrentLoaction:(FLEX_Location)currentLoaction {
_currentLoaction = currentLoaction;
if (currentLoaction == FLEX_TOP) {
self.containerView.frame = CGRectMake(0, topMargin, self.view.frame.size.width, self.view.frame.size.height - topMargin);
} else if (currentLoaction == FLEX_Bottom) {
self.containerView.frame = CGRectMake(0, self.view.frame.size.height - kScale390(393), self.view.frame.size.width, kScale390(393));
}
}
#pragma mark - lazy
- (UIView *)containerView {
if (_containerView == nil) {
_containerView = [[UIView alloc] init];
_containerView.layer.cornerRadius = kScale390(12);
[self.view addSubview:_containerView];
}
return _containerView;
}
- (UIView *)topGestureView {
if (_topGestureView == nil) {
_topGestureView = [[UIView alloc] init];
[_topGestureView addGestureRecognizer:self.panCover];
[self.containerView addSubview:_topGestureView];
}
return _topGestureView;
}
- (UIPanGestureRecognizer *)panCover {
if (_panCover == nil) {
_panCover = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onPanCover:)];
}
return _panCover;
}
- (void)onPanCover:(UIPanGestureRecognizer *)pan {
CGPoint translation = [pan translationInView:self.topGestureView];
CGFloat absX = fabs(translation.x);
CGFloat absY = fabs(translation.y);
if (MAX(absX, absY) < 2) return;
if (absX > absY) {
if (translation.x < 0) {
// scroll left
} else {
// scroll right
}
} else if (absY > absX) {
if (translation.y < 0) {
// scroll up
[self.topGestureView removeGestureRecognizer:self.panCover];
[UIView animateWithDuration:0.3
animations:^{
self.currentLoaction = FLEX_TOP;
[self.topGestureView addGestureRecognizer:self.panCover];
}
completion:^(BOOL finished) {
if (finished) {
[self updateSubContainerView];
}
}];
} else {
// scroll down
if (self.currentLoaction == FLEX_Bottom) {
[self dismissViewControllerAnimated:YES completion:nil];
}
[self.topGestureView removeGestureRecognizer:self.panCover];
[UIView animateWithDuration:0.3
animations:^{
self.currentLoaction = FLEX_Bottom;
[self.topGestureView addGestureRecognizer:self.panCover];
}
completion:^(BOOL finished) {
if (finished) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self updateSubContainerView];
});
}
}];
}
}
}
- (void)updateSubContainerView {
self.topGestureView.frame = CGRectMake(0, 0, self.containerView.frame.size.width, kScale390(40));
self.topImgView.frame = CGRectMake((self.topGestureView.frame.size.width - kScale390(24)) * 0.5, kScale390(22), kScale390(24), kScale390(6));
}
@end

View File

@@ -0,0 +1,48 @@
//
// TUIChatPopContextController.h
// TUIChat
//
// Created by wyl on 2022/10/24.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <TIMCommon/TUIMessageCell.h>
#import <TIMCommon/TUIMessageCellData.h>
#import <UIKit/UIKit.h>
#import "TUIChatPopContextExtionView.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, BlurEffectStyle) {
BlurEffectStyleLight,
BlurEffectStyleExtraLight,
BlurEffectStyleDarkEffect,
};
@interface TUIChatPopContextController : UIViewController
@property(nonatomic, strong) Class alertCellClass;
@property(nonatomic, strong) TUIMessageCellData *alertViewCellData;
@property(nonatomic, assign) CGRect originFrame;
@property(copy, nonatomic) void (^viewWillShowHandler)(TUIMessageCell *alertView);
@property(copy, nonatomic) void (^viewDidShowHandler)(TUIMessageCell *alertView);
// dismiss controller completed block
@property(nonatomic, copy) void (^dismissComplete)(void);
@property(nonatomic, copy) void (^reactClickCallback)(NSString *faceName);
@property(nonatomic, strong) NSMutableArray<TUIChatPopContextExtionItem *> *items;
- (void)setBlurEffectWithView:(UIView *)view;
- (void)blurDismissViewControllerAnimated:(BOOL)animated completion:(void (^__nullable)(BOOL finished))completion;
- (void)updateExtionView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,462 @@
//
// TUIChatPopContextController.m
// TUIChat
//
// Created by wyl on 2022/10/24.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatPopContextController.h"
#import <TIMCommon/TIMDefine.h>
#import "UIImage+ImageEffects.h"
#import <TUICore/TUICore.h>
@interface TUIChatPopContextController ()<V2TIMAdvancedMsgListener>
@property(nonatomic, strong) UIView *recentView;
@property(nonatomic, strong) UIView *alertContainerView;
@property(nonatomic, strong) TUIMessageCell *alertView;
@property(nonatomic, strong) TUIChatPopContextExtionView *extionView;
@property(nonatomic, strong) UIColor *backgroundColor; // set backgroundColor
@property(nonatomic, strong) UIView *backgroundView; // you set coustom view to it
@property(nonatomic, strong) UITapGestureRecognizer *singleTap;
@property(nonatomic, assign) BOOL backgoundTapDismissEnable; // default NO
@end
@implementation TUIChatPopContextController
- (instancetype)init {
if (self = [super init]) {
[self configureController];
}
return self;
}
- (void)configureController {
self.providesPresentationContextTransitionStyle = YES;
self.definesPresentationContext = YES;
self.modalPresentationStyle = UIModalPresentationCustom;
_backgroundColor = [UIColor clearColor];
_backgoundTapDismissEnable = YES;
[[V2TIMManager sharedInstance] addAdvancedMsgListener:self];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor clearColor];
[self addBackgroundView];
[self addSingleTapGesture];
[self configureAlertView];
[self configRecentView];
[self configExtionView];
[self.view layoutIfNeeded];
[self showHapticFeedback];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (_viewWillShowHandler) {
_viewWillShowHandler(_alertView);
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (_viewDidShowHandler) {
_viewDidShowHandler(_alertView);
}
//
// Too far to the top
CGFloat moveY = 0;
if (self.recentView.frame.origin.y < NavBar_Height) {
CGFloat deal = NavBar_Height - self.recentView.frame.origin.y;
moveY = deal + NavBar_Height + 50;
}
//
// Too far to the right
CGFloat moveX = 0;
if (self.recentView.frame.origin.x + self.recentView.frame.size.width > self.view.frame.size.width) {
CGFloat deal = self.recentView.frame.origin.x + self.recentView.frame.size.width - self.view.frame.size.width;
moveX = deal + 5;
}
//
// too far down
if (self.extionView.frame.origin.y + self.extionView.frame.size.height > self.view.frame.size.height) {
CGFloat deal = self.extionView.frame.origin.y + self.extionView.frame.size.height - self.view.frame.size.height;
moveY = -deal - 50;
}
BOOL oneScreenCanFillCheck = NO;
// Can only one screen fit
if (self.recentView.frame.size.height + self.originFrame.size.height + self.extionView.frame.size.height + kScale390(100) > self.view.bounds.size.height) {
oneScreenCanFillCheck = YES;
}
if (oneScreenCanFillCheck) {
// recentView
CGFloat recentViewMoveY = NavBar_Height + 50;
self.recentView.frame =
CGRectMake(self.recentView.frame.origin.x - moveX, recentViewMoveY, self.recentView.frame.size.width, self.recentView.frame.size.height);
// alertView
[UIView animateWithDuration:0.3
animations:^{
self.alertContainerView.frame =
CGRectMake(0, self.recentView.frame.origin.y + kScale390(8) + self.recentView.frame.size.height,
self.view.frame.size.width, self.originFrame.size.height);
}
completion:^(BOOL finished){
}];
// extionView
CGFloat deal = self.extionView.frame.origin.y + self.extionView.frame.size.height - self.view.frame.size.height;
CGFloat extionViewMoveY = -deal - 50;
self.extionView.frame = CGRectMake(self.extionView.frame.origin.x - moveX, self.extionView.frame.origin.y + extionViewMoveY,
self.extionView.frame.size.width, self.extionView.frame.size.height);
self.extionView.transform = CGAffineTransformMakeScale(0.1, 0.1);
[UIView animateWithDuration:0.5
animations:^{
// Bounces
self.extionView.transform = CGAffineTransformMakeScale(1, 1);
}
completion:^(BOOL finished){
}];
return;
} else {
// When the container need a displacement change
// Or do nothing
if (moveY != 0) {
[UIView animateWithDuration:0.3
animations:^{
self.alertContainerView.frame = CGRectMake(0, self.originFrame.origin.y + moveY, self.view.frame.size.width, self.originFrame.size.height);
}
completion:^(BOOL finished){
}];
}
self.recentView.frame = CGRectMake(self.recentView.frame.origin.x - moveX, self.recentView.frame.origin.y, self.recentView.frame.size.width,
self.recentView.frame.size.height);
[UIView animateWithDuration:0.2
animations:^{
// When recentView needs to have displacement animation
self.recentView.frame = CGRectMake(self.recentView.frame.origin.x, self.recentView.frame.origin.y + moveY,
self.recentView.frame.size.width, self.recentView.frame.size.height);
}
completion:^(BOOL finished){
}];
self.extionView.frame = CGRectMake(self.extionView.frame.origin.x - moveX, self.extionView.frame.origin.y + moveY, self.extionView.frame.size.width,
self.extionView.frame.size.height);
self.extionView.transform = CGAffineTransformMakeScale(0.1, 0.1);
[UIView animateWithDuration:0.5
animations:^{
// Bounces
self.extionView.transform = CGAffineTransformMakeScale(1, 1);
}
completion:^(BOOL finished){
}];
}
}
- (void)addBackgroundView {
if (_backgroundView == nil) {
UIView *backgroundView = [[UIView alloc] init];
backgroundView.backgroundColor = _backgroundColor;
_backgroundView = backgroundView;
}
_backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view insertSubview:_backgroundView atIndex:0];
[self addConstraintToView:_backgroundView edgeInset:UIEdgeInsetsZero];
}
- (void)setBackgroundView:(UIView *)backgroundView {
if (_backgroundView == nil) {
_backgroundView = backgroundView;
} else if (_backgroundView != backgroundView) {
backgroundView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view insertSubview:backgroundView aboveSubview:_backgroundView];
[self addConstraintToView:backgroundView edgeInset:UIEdgeInsetsZero];
backgroundView.alpha = 0;
[UIView animateWithDuration:0.3
animations:^{
backgroundView.alpha = 1;
}
completion:^(BOOL finished) {
[_backgroundView removeFromSuperview];
_backgroundView = backgroundView;
[self addSingleTapGesture];
}];
}
}
- (void)addSingleTapGesture {
self.view.userInteractionEnabled = YES;
_backgroundView.userInteractionEnabled = YES;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)];
singleTap.enabled = _backgoundTapDismissEnable;
[_backgroundView addGestureRecognizer:singleTap];
_singleTap = singleTap;
}
- (void)configureAlertView {
self.alertContainerView = [[UIView alloc] init];
[self.view addSubview:self.alertContainerView];
_alertView = [[self.alertCellClass alloc] init];
[self.alertContainerView addSubview:_alertView];
_alertView.userInteractionEnabled = YES;
if ([self.alertView isKindOfClass:NSClassFromString(@"TUIMergeMessageCell_Minimalist")]) {
_alertView.userInteractionEnabled = NO;
}
[_alertView fillWithData:self.alertViewCellData];
[_alertView layoutIfNeeded];
self.alertContainerView.frame = CGRectMake(0, _originFrame.origin.y, self.view.frame.size.width, _originFrame.size.height);
_alertView.frame = CGRectMake(0, 0, self.alertContainerView.frame.size.width, self.alertContainerView.frame.size.height);
for (UIView *view in _alertView.contentView.subviews) {
if(view != _alertView.container) {
view.hidden = YES;
}
}
[_alertView.container mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(_originFrame.origin.x);
make.top.mas_equalTo(0);
make.size.mas_equalTo(_originFrame.size);
}];
}
- (void)configRecentView {
_recentView = [[UIView alloc] init];
_recentView.backgroundColor = [UIColor clearColor];
[self.view addSubview:_recentView];
_recentView.frame = CGRectMake(_originFrame.origin.x,
_originFrame.origin.y - kScale390(8 + 40),
MAX(kTIMDefaultEmojiSize.width *8,kScale390(208)),
kScale390(40));
NSDictionary *param = @{TUICore_TUIChatExtension_ChatPopMenuReactRecentView_Delegate : self};
[TUICore raiseExtension:TUICore_TUIChatExtension_ChatPopMenuReactRecentView_MinimalistExtensionID parentView:self.recentView param:param];
}
- (void)configExtionView {
_extionView = [[TUIChatPopContextExtionView alloc] init];
_extionView.backgroundColor = [UIColor tui_colorWithHex:@"f9f9f9"];
_extionView.layer.cornerRadius = kScale390(16);
[self.view addSubview:_extionView];
CGFloat height = [self configAndCaculateExtionHeight];
_extionView.frame = CGRectMake(_originFrame.origin.x, _originFrame.origin.y + _originFrame.size.height + kScale390(8), kScale390(180), height);
}
- (CGFloat)configAndCaculateExtionHeight {
NSMutableArray *items = self.items;
CGFloat height = 0;
for (int i = 0; i < items.count; i++) {
height += kScale390(40);
}
CGFloat topMargin = kScale390(6);
CGFloat bottomMargin = kScale390(6);
height += topMargin;
height += bottomMargin;
[_extionView configUIWithItems:items topBottomMargin:topMargin];
return height;
}
- (void)updateExtionView {
CGFloat height = [self configAndCaculateExtionHeight];
_extionView.frame = CGRectMake(_extionView.frame.origin.x, _extionView.frame.origin.y, _extionView.frame.size.width, height);
}
- (void)setBackgoundTapDismissEnable:(BOOL)backgoundTapDismissEnable {
_backgoundTapDismissEnable = backgoundTapDismissEnable;
_singleTap.enabled = backgoundTapDismissEnable;
}
- (void)addConstraintToView:(UIView *)view edgeInset:(UIEdgeInsets)edgeInset {
[self addConstraintWithView:view topView:self.view leftView:self.view bottomView:self.view rightView:self.view edgeInset:edgeInset];
}
- (void)addConstraintWithView:(UIView *)view
topView:(UIView *)topView
leftView:(UIView *)leftView
bottomView:(UIView *)bottomView
rightView:(UIView *)rightView
edgeInset:(UIEdgeInsets)edgeInset {
if (topView) {
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:topView
attribute:NSLayoutAttributeTop
multiplier:1
constant:edgeInset.top]];
}
if (leftView) {
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:leftView
attribute:NSLayoutAttributeLeft
multiplier:1
constant:edgeInset.left]];
}
if (rightView) {
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:rightView
attribute:NSLayoutAttributeRight
multiplier:1
constant:edgeInset.right]];
}
if (bottomView) {
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:bottomView
attribute:NSLayoutAttributeBottom
multiplier:1
constant:edgeInset.bottom]];
}
}
#pragma mark - public
- (void)setBlurEffectWithView:(UIView *)view {
[self setBlurEffectWithView:view style:BlurEffectStyleDarkEffect];
}
- (void)setBlurEffectWithView:(UIView *)view style:(BlurEffectStyle)blurStyle {
UIImage *snapshotImage = [UIImage snapshotImageWithView:view];
UIImage *blurImage = [self blurImageWithSnapshotImage:snapshotImage style:blurStyle];
dispatch_async(dispatch_get_main_queue(), ^{
UIImageView *blurImageView = [[UIImageView alloc] initWithImage:blurImage];
self.backgroundView = blurImageView;
});
}
- (void)setBlurEffectWithView:(UIView *)view effectTintColor:(UIColor *)effectTintColor {
UIImage *snapshotImage = [UIImage snapshotImageWithView:view];
UIImage *blurImage = [snapshotImage applyTintEffectWithColor:effectTintColor];
UIImageView *blurImageView = [[UIImageView alloc] initWithImage:blurImage];
self.backgroundView = blurImageView;
}
#pragma mark - private
- (void)showHapticFeedback {
if (@available(iOS 10.0, *)) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
[generator prepare];
[generator impactOccurred];
});
} else {
// Fallback on earlier versions
}
}
- (UIImage *)blurImageWithSnapshotImage:(UIImage *)snapshotImage style:(BlurEffectStyle)blurStyle {
switch (blurStyle) {
case BlurEffectStyleLight:
return [snapshotImage applyLightEffect];
case BlurEffectStyleDarkEffect:
return [snapshotImage applyDarkEffect];
case BlurEffectStyleExtraLight:
return [snapshotImage applyExtraLightEffect];
default:
return nil;
}
}
- (void)blurDismissViewControllerAnimated:(BOOL)animated completion:(void (^__nullable)(BOOL finished))completion {
[self dismissViewControllerAnimated:animated];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (completion) {
completion(YES);
}
});
}
- (void)dismissViewControllerAnimated:(BOOL)animated {
[UIView animateWithDuration:0.3
animations:^{
self.recentView.frame = CGRectMake(self.recentView.frame.origin.x, self.originFrame.origin.y - self.recentView.frame.size.height,
self.recentView.frame.size.width, self.recentView.frame.size.height);
}
completion:^(BOOL finished){
}];
[UIView animateWithDuration:0.3
animations:^{
self.alertContainerView.frame = CGRectMake(0, self.originFrame.origin.y , self.view.frame.size.width, self.originFrame.size.height);
}
completion:^(BOOL finished) {
if (finished) {
[self dismissViewControllerAnimated:animated completion:self.dismissComplete];
}
}];
self.extionView.transform = CGAffineTransformMakeScale(1, 1);
[UIView animateWithDuration:0.2
animations:^{
self.extionView.transform = CGAffineTransformMakeScale(0.1, 0.1);
}
completion:^(BOOL finished) {
if (finished) {
self.extionView.transform = CGAffineTransformMakeScale(0, 0);
}
}];
}
#pragma mark - action
- (void)singleTap:(UITapGestureRecognizer *)sender {
[self dismissViewControllerAnimated:NO];
}
// MARK: V2TIMAdvancedMsgListener
- (void)onRecvMessageRevoked:(NSString *)msgID operateUser:(V2TIMUserFullInfo *)operateUser reason:(NSString *)reason {
if ([msgID isEqualToString:self.alertViewCellData.msgID]) {
UIViewController *controller = self;
while(controller.presentingViewController != nil){
controller = controller.presentingViewController;
}
[controller dismissViewControllerAnimated:YES completion:^{
[self blurDismissViewControllerAnimated:NO completion:nil];
}];
}
}
@end

View File

@@ -0,0 +1,40 @@
//
// TUIChatPopContextExtionView.h
// TUIEmojiPlugin
//
// Created by wyl on 2023/12/1.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIChatPopContextExtionItem : NSObject
@property(nonatomic, copy) NSString *title;
@property(nonatomic, strong) UIColor *titleColor;
@property(nonatomic, strong) UIFont *titleFont;
@property(nonatomic, assign) CGFloat weight;
@property(nonatomic, strong) UIImage *markIcon;
@property(nonatomic, assign) CGFloat itemHeight;
@property(nonatomic, assign) BOOL needBottomLine;
@property(nonatomic, copy) void (^actionHandler)(TUIChatPopContextExtionItem *item);
- (instancetype)initWithTitle:(NSString *)title markIcon:(UIImage *)markIcon weight:(NSInteger)weight withActionHandler:(void (^)(id action))actionHandler;
@end
@interface TUIChatPopContextExtionView : UIView
- (void)configUIWithItems:(NSMutableArray<TUIChatPopContextExtionItem *> *)items topBottomMargin:(CGFloat)topBottomMargin;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,114 @@
//
// TUIChatPopContextExtionView.m
// TUIEmojiPlugin
//
// Created by wyl on 2023/12/1.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatPopContextExtionView.h"
#import <TIMCommon/NSString+TUIEmoji.h>
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import <TIMCommon/TUIFitButton.h>
#import <TIMCommon/TIMCommonMediator.h>
#import <TIMCommon/TUIEmojiMeditorProtocol.h>
@implementation TUIChatPopContextExtionItem
- (instancetype)initWithTitle:(NSString *)title markIcon:(UIImage *)markIcon weight:(NSInteger)weight withActionHandler:(void (^)(id action))actionHandler {
self = [super init];
if (self) {
_title = title;
_markIcon = markIcon;
_weight = weight;
_actionHandler = actionHandler;
}
return self;
}
@end
@interface TUIChatPopContextExtionItemView : UIView
@property(nonatomic, strong) TUIChatPopContextExtionItem *item;
@property(nonatomic, strong) UIImageView *icon;
@property(nonatomic, strong) UILabel *l;
- (void)configBaseUIWithItem:(TUIChatPopContextExtionItem *)item;
@end
@implementation TUIChatPopContextExtionItemView
- (void)configBaseUIWithItem:(TUIChatPopContextExtionItem *)item {
self.item = item;
CGFloat itemWidth = self.frame.size.width;
CGFloat padding = kScale390(16);
CGFloat itemHeight = self.frame.size.height;
UIImageView *icon = [[UIImageView alloc] init];
[self addSubview:icon];
icon.frame = CGRectMake(itemWidth - padding - kScale390(18), itemHeight * 0.5 - kScale390(18) * 0.5, kScale390(18), kScale390(18));
icon.image = self.item.markIcon;
UILabel *l = [[UILabel alloc] init];
l.frame = CGRectMake(padding, 0, itemWidth * 0.5, itemHeight);
l.text = self.item.title;
l.font = item.titleFont ?: [UIFont systemFontOfSize:kScale390(16)];
l.textAlignment = isRTL()? NSTextAlignmentRight:NSTextAlignmentLeft;
l.textColor = item.titleColor ?: [UIColor blackColor];
l.userInteractionEnabled = false;
[self addSubview:l];
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeSystem];
[backButton addTarget:self action:@selector(buttonclick) forControlEvents:UIControlEventTouchUpInside];
backButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
backButton.frame = CGRectMake(0, 0, itemWidth, itemHeight);
[self addSubview:backButton];
if (item.needBottomLine) {
UIView *line = [UIView new];
line.backgroundColor = [UIColor tui_colorWithHex:@"DDDDDD"];
line.frame = CGRectMake(0, itemHeight - kScale390(0.5), itemWidth, kScale390(0.5));
[self addSubview:line];
}
self.layer.masksToBounds = YES;
if (isRTL()) {
for (UIView *subview in self.subviews) {
[subview resetFrameToFitRTL];
}
}
}
- (void)buttonclick {
if (self.item.actionHandler) {
self.item.actionHandler(self.item);
}
}
@end
@interface TUIChatPopContextExtionView ()
@property(nonatomic, strong) NSMutableArray<TUIChatPopContextExtionItem *> *items;
@end
@implementation TUIChatPopContextExtionView
- (void)configUIWithItems:(NSMutableArray<TUIChatPopContextExtionItem *> *)items topBottomMargin:(CGFloat)topBottomMargin {
if (self.subviews.count > 0) {
for (UIView *subview in self.subviews) {
if (subview) {
[subview removeFromSuperview];
}
}
}
int i = 0;
for (TUIChatPopContextExtionItem *item in items) {
TUIChatPopContextExtionItemView *itemView = [[TUIChatPopContextExtionItemView alloc] init];
itemView.frame = CGRectMake(0, (kScale390(40)) * i + topBottomMargin, kScale390(180), kScale390(40));
[itemView configBaseUIWithItem:item];
[self addSubview:itemView];
i++;
}
}
@end

View File

@@ -0,0 +1,30 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
/*
File: UIImage+ImageEffects.h
Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code youll want to look out to find out how
to use vImage to efficiently calculate a blur. Version: 1.0
*/
@import UIKit;
@interface UIImage (ImageEffects)
- (UIImage *)applyLightEffect;
- (UIImage *)applyExtraLightEffect;
- (UIImage *)applyDarkEffect;
- (UIImage *)applyTintEffectWithColor:(UIColor *)tintColor;
- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius
tintColor:(UIColor *)tintColor
saturationDeltaFactor:(CGFloat)saturationDeltaFactor
maskImage:(UIImage *)maskImage;
@end
@interface UIImage (SnapshotImage)
+ (UIImage *)snapshotImageWithView:(UIView *)view;
@end

View File

@@ -0,0 +1,202 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "UIImage+ImageEffects.h"
@import Accelerate;
#import <float.h>
@implementation UIImage (ImageEffects)
- (UIImage *)applyLightEffect {
UIColor *tintColor = [UIColor colorWithWhite:1.0 alpha:0.3];
return [self applyBlurWithRadius:10 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
- (UIImage *)applyExtraLightEffect {
UIColor *tintColor = [UIColor colorWithWhite:0.97 alpha:0.82];
return [self applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
- (UIImage *)applyDarkEffect {
UIColor *tintColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4];
return [self applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
- (UIImage *)applyTintEffectWithColor:(UIColor *)tintColor {
const CGFloat effectColorAlpha = 0.6;
UIColor *effectColor = tintColor;
int componentCount = CGColorGetNumberOfComponents(tintColor.CGColor);
if (componentCount == 2) {
CGFloat b;
if ([tintColor getWhite:&b alpha:NULL]) {
effectColor = [UIColor colorWithWhite:b alpha:effectColorAlpha];
}
} else {
CGFloat r, g, b;
if ([tintColor getRed:&r green:&g blue:&b alpha:NULL]) {
effectColor = [UIColor colorWithRed:r green:g blue:b alpha:effectColorAlpha];
}
}
return [self applyBlurWithRadius:10 tintColor:effectColor saturationDeltaFactor:-1.0 maskImage:nil];
}
- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius
tintColor:(UIColor *)tintColor
saturationDeltaFactor:(CGFloat)saturationDeltaFactor
maskImage:(UIImage *)maskImage {
// Check pre-conditions.
if (self.size.width < 1 || self.size.height < 1) {
NSLog(@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", self.size.width, self.size.height, self);
return nil;
}
if (!self.CGImage) {
NSLog(@"*** error: image must be backed by a CGImage: %@", self);
return nil;
}
if (maskImage && !maskImage.CGImage) {
NSLog(@"*** error: maskImage must be backed by a CGImage: %@", maskImage);
return nil;
}
CGRect imageRect = {CGPointZero, self.size};
UIImage *effectImage = self;
BOOL hasBlur = blurRadius > __FLT_EPSILON__;
BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__;
if (hasBlur || hasSaturationChange) {
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef effectInContext = UIGraphicsGetCurrentContext();
CGContextScaleCTM(effectInContext, 1.0, -1.0);
CGContextTranslateCTM(effectInContext, 0, -self.size.height);
CGContextDrawImage(effectInContext, imageRect, self.CGImage);
vImage_Buffer effectInBuffer;
effectInBuffer.data = CGBitmapContextGetData(effectInContext);
effectInBuffer.width = CGBitmapContextGetWidth(effectInContext);
effectInBuffer.height = CGBitmapContextGetHeight(effectInContext);
effectInBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectInContext);
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef effectOutContext = UIGraphicsGetCurrentContext();
vImage_Buffer effectOutBuffer;
effectOutBuffer.data = CGBitmapContextGetData(effectOutContext);
effectOutBuffer.width = CGBitmapContextGetWidth(effectOutContext);
effectOutBuffer.height = CGBitmapContextGetHeight(effectOutContext);
effectOutBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectOutContext);
if (hasBlur) {
// A description of how to compute the box kernel width from the Gaussian
// radius (aka standard deviation) appears in the SVG spec:
// http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
//
// For larger values of 's' (s >= 2.0), an approximation can be used: Three
// successive box-blurs build a piece-wise quadratic convolution kernel, which
// approximates the Gaussian kernel to within roughly 3%.
//
// let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
//
// ... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
//
CGFloat inputRadius = blurRadius * [[UIScreen mainScreen] scale];
NSUInteger radius = floor(inputRadius * 3. * sqrt(2 * M_PI) / 4 + 0.5);
if (radius % 2 != 1) {
radius += 1; // force radius to be odd so that the three box-blur methodology works.
}
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
}
BOOL effectImageBuffersAreSwapped = NO;
if (hasSaturationChange) {
CGFloat s = saturationDeltaFactor;
CGFloat floatingPointSaturationMatrix[] = {
0.0722 + 0.9278 * s,
0.0722 - 0.0722 * s,
0.0722 - 0.0722 * s,
0,
0.7152 - 0.7152 * s,
0.7152 + 0.2848 * s,
0.7152 - 0.7152 * s,
0,
0.2126 - 0.2126 * s,
0.2126 - 0.2126 * s,
0.2126 + 0.7873 * s,
0,
0,
0,
0,
1,
};
const int32_t divisor = 256;
NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix) / sizeof(floatingPointSaturationMatrix[0]);
int16_t saturationMatrix[matrixSize];
for (NSUInteger i = 0; i < matrixSize; ++i) {
saturationMatrix[i] = (int16_t)roundf(floatingPointSaturationMatrix[i] * divisor);
}
if (hasBlur) {
vImageMatrixMultiply_ARGB8888(&effectOutBuffer, &effectInBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
effectImageBuffersAreSwapped = YES;
} else {
vImageMatrixMultiply_ARGB8888(&effectInBuffer, &effectOutBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
}
}
if (!effectImageBuffersAreSwapped) effectImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (effectImageBuffersAreSwapped) effectImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
// Set up output context.
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef outputContext = UIGraphicsGetCurrentContext();
CGContextScaleCTM(outputContext, 1.0, -1.0);
CGContextTranslateCTM(outputContext, 0, -self.size.height);
// Draw base image.
CGContextDrawImage(outputContext, imageRect, self.CGImage);
// Draw effect image.
if (hasBlur) {
CGContextSaveGState(outputContext);
if (maskImage) {
CGContextClipToMask(outputContext, imageRect, maskImage.CGImage);
}
CGContextDrawImage(outputContext, imageRect, effectImage.CGImage);
CGContextRestoreGState(outputContext);
}
// Add in color tint.
if (tintColor) {
CGContextSaveGState(outputContext);
CGContextSetFillColorWithColor(outputContext, tintColor.CGColor);
CGContextFillRect(outputContext, imageRect);
CGContextRestoreGState(outputContext);
}
// Output image is ready.
UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return outputImage;
}
@end
@implementation UIImage (SnapshotImage)
+ (UIImage *)snapshotImageWithView:(UIView *)view {
// currentView The current view creates a bitmap-based graphics context and specifies the size of
UIGraphicsBeginImageContextWithOptions(view.bounds.size, YES, [UIScreen mainScreen].scale);
// renderInContext Renders the receiver and its subscopes to the specified context
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO];
// Returns an image based on the current graphics context
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// generated image
UIGraphicsEndImageContext();
return image;
}
@end