增加换肤功能

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,13 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import <UIKit/UIKit.h>
#import "TUIMultimediaStickerView.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIMultimediaPasterView : TUIMultimediaStickerView
@property(nullable, nonatomic) UIImage *paster;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import "TUIMultimediaPasterView.h"
#import "TUIMultimediaPlugin/TUIMultimediaCommon.h"
#import "TUIMultimediaPlugin/TUIMultimediaStickerView.h"
#define PASTER_INITIAL_WIDTH 150
@interface TUIMultimediaPasterView () {
UIImageView *_imgView;
}
@end
@implementation TUIMultimediaPasterView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self != nil) {
[self initUI];
}
return self;
}
- (void)initUI {
_imgView = [[UIImageView alloc] init];
_imgView.translatesAutoresizingMaskIntoConstraints = NO;
self.editButtonHidden = YES;
self.content = _imgView;
}
- (UIImage *)paster {
return _imgView.image;
}
- (void)setPaster:(UIImage *)paster {
self.content = nil;
_imgView.image = paster;
int width = MIN(PASTER_INITIAL_WIDTH, paster.size.width);
int height = width / paster.size.width * paster.size.height;
_imgView.bounds = CGRectMake(0, 0, width, height);
self.content = _imgView;
}
@end

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol TUIMultimediaStickerViewDelegate;
@interface TUIMultimediaStickerView : UIView
@property(nullable, nonatomic) UIView *content;
@property(nonatomic) BOOL selected;
@property(nonatomic) BOOL editButtonHidden;
@property(nonatomic, readonly) CGFloat rotation;
@property(nonatomic) CGFloat contentMargin;
// 旋转夹角小于此值时,吸附到水平和竖直方向
@property(nonatomic) CGFloat rotateAdsorptionLimitAngle;
@property(weak, nullable, nonatomic) id<TUIMultimediaStickerViewDelegate> delegate;
- (void)scale:(CGFloat) scale;
@end
@protocol TUIMultimediaStickerViewDelegate <NSObject>
- (void)onStickerViewSelected:(TUIMultimediaStickerView *)v;
- (void)onStickerViewShouldDelete:(TUIMultimediaStickerView *)v;
- (void)onStickerViewShouldEdit:(TUIMultimediaStickerView *)v;
- (void)onStickerViewSizeChanged:(TUIMultimediaStickerView *)v;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,255 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import "TUIMultimediaStickerView.h"
#import <TUICore/TUIThemeManager.h>
#import "Masonry/Masonry.h"
#import "TUIMultimediaPlugin/TUIMultimediaCommon.h"
#import "TUIMultimediaPlugin/TUIMultimediaGeometry.h"
#import "TUIMultimediaPlugin/TUIMultimediaImageUtil.h"
@interface TUIMultimediaStickerView () <UIGestureRecognizerDelegate> {
UIButton *_btnEdit;
UIButton *_btnTransform;
UIButton *_btnDelete;
UIView *_borderView;
BOOL _hideEditButton;
BOOL _active;
CGFloat _rawRotation;
UITapGestureRecognizer *_tapRec;
UITapGestureRecognizer *_doubleTapRec;
UIPanGestureRecognizer *_panRec;
UIPinchGestureRecognizer *_pinchRec;
UIRotationGestureRecognizer *_rotationRec;
UIPanGestureRecognizer *_btnTransformPanRec;
UIImpactFeedbackGenerator *_impactGen;
}
@end
@implementation TUIMultimediaStickerView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self != nil) {
_contentMargin = 5;
_rotateAdsorptionLimitAngle = M_PI / 180 * 5;
_impactGen = [[UIImpactFeedbackGenerator alloc] init];
[self frv_initUI];
}
return self;
}
- (void)frv_initUI {
_borderView = [[UIView alloc] init];
_borderView.translatesAutoresizingMaskIntoConstraints = NO;
_borderView.layer.borderWidth = 1;
_borderView.layer.borderColor = UIColor.whiteColor.CGColor;
_borderView.backgroundColor = [UIColor clearColor];
_borderView.hidden = YES;
[self addSubview:_borderView];
[_borderView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
_btnEdit = [self newCornerButtonWithImage:TUIMultimediaPluginBundleThemeImage(@"sticker_edit_img", @"round_edit") left:YES top:YES];
_btnTransform = [self newCornerButtonWithImage:[TUIMultimediaImageUtil rotateImage:TUIMultimediaPluginBundleThemeImage(@"sticker_transform_img", @"round_transform") angle:90.0f] left:NO top:NO];
_btnDelete = [self newCornerButtonWithImage:TUIMultimediaPluginBundleThemeImage(@"sticker_delete_img", @"round_close") left:NO top:YES];
[_btnEdit addTarget:self action:@selector(onBtnEditClicked) forControlEvents:UIControlEventTouchUpInside];
[_btnDelete addTarget:self action:@selector(onBtnDeleteClicked) forControlEvents:UIControlEventTouchUpInside];
_tapRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap)];
[self addGestureRecognizer:_tapRec];
_panRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onPan:)];
_panRec.delegate = self;
[self addGestureRecognizer:_panRec];
_pinchRec = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(onPinch:)];
_pinchRec.delegate = self;
[self addGestureRecognizer:_pinchRec];
_rotationRec = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(onGestureRotation:)];
_rotationRec.delegate = self;
[self addGestureRecognizer:_rotationRec];
_btnTransformPanRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onBtnTransformPan:)];
[_btnTransform addGestureRecognizer:_btnTransformPanRec];
_doubleTapRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onDoubleTap:)];
_doubleTapRec.delaysTouchesEnded = NO;
_doubleTapRec.numberOfTapsRequired = 2;
[self addGestureRecognizer:_doubleTapRec];
}
- (UIButton *)newCornerButtonWithImage:(UIImage *)image left:(BOOL)left top:(BOOL)top {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.hidden = YES;
[self addSubview:btn];
btn.translatesAutoresizingMaskIntoConstraints = NO;
[btn setImage:image forState:UIControlStateNormal];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.height.mas_equalTo(24);
make.centerX.equalTo(left ? self.mas_left : self.mas_right);
make.centerY.equalTo(top ? self.mas_top : self.mas_bottom);
}];
return btn;
}
- (void)layoutSubviews {
[super layoutSubviews];
[_delegate onStickerViewSizeChanged:self];
}
- (void)updateRotation {
// [0,2pi)
_rawRotation -= floor(_rawRotation / (M_PI * 2)) * M_PI * 2;
BOOL adsorped = NO;
// 0,90,180,270
for (int i = 0; i <= 3; i++) {
CGFloat diff = fabs(_rawRotation - i * M_PI_2);
CGFloat diff2 = fabs(_rawRotation - M_PI * 2 - i * M_PI_2);
if (diff < _rotateAdsorptionLimitAngle || diff2 < _rotateAdsorptionLimitAngle) {
adsorped = YES;
if (_rotation != i * M_PI_2) {
_rotation = i * M_PI_2;
[_impactGen impactOccurred];
}
break;
}
}
if (!adsorped) {
_rotation = _rawRotation;
}
self.transform = CGAffineTransformMakeRotation(_rotation);
NSLog(@"updateRotation %@",NSStringFromCGRect(self.frame));
}
#pragma mark - Actions
- (void)onTap {
if (!self.selected) {
self.selected = YES;
[_delegate onStickerViewSelected:self];
}
}
- (void)onDoubleTap:(UITapGestureRecognizer *)rec {
[_delegate onStickerViewShouldEdit:self];
}
- (void)onPan:(UIPanGestureRecognizer *)rec {
CGPoint offset = [rec translationInView:self.superview];
[rec setTranslation:CGPointZero inView:self.superview];
//
self.center = Vec2AddVector(self.center, offset);
NSLog(@"onPan %@",NSStringFromCGRect(self.frame));
}
- (void)onPinch:(UIPinchGestureRecognizer *)rec {
// pow/
CGFloat scale = pow(rec.scale, 1.5);
rec.scale = 1;
CGSize raw = self.bounds.size;
self.bounds = CGRectMake(0, 0, raw.width * scale, raw.height * scale);
NSLog(@"onPinch %@",NSStringFromCGRect(self.frame));
}
- (void)scale:(CGFloat) scale {
CGSize raw = self.bounds.size;
self.bounds = CGRectMake(0, 0, raw.width * scale, raw.height * scale);
self.center = CGPointMake(self.center.x * scale, self.center.y * scale);
}
- (void)onGestureRotation:(UIRotationGestureRecognizer *)rec {
_rawRotation += rec.rotation;
[self updateRotation];
rec.rotation = 0;
}
- (void)onBtnTransformPan:(UIPanGestureRecognizer *)rec {
CGPoint offset = [rec translationInView:self.superview];
[rec setTranslation:CGPointZero inView:self.superview];
CGPoint originSize = CGPointMake(self.bounds.size.width, self.bounds.size.height);
CGPoint originV = Vec2Rotate(Vec2Mul(originSize, 0.5), _rawRotation);
CGPoint newV = Vec2AddVector(originV, offset);
//
CGPoint newSize = Vec2Mul(originSize, Vec2Len(newV) / Vec2Len(originV));
self.bounds = CGRectMake(0, 0, newSize.x, newSize.y);
//
_rawRotation += Vec2Degree(originV, newV);
[self updateRotation];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(self.bounds, point)) {
return YES;
}
for (UIView *v in self.subviews) {
CGPoint localPoint = [v convertPoint:point fromView:self];
if (CGRectContainsPoint(v.bounds, localPoint)) {
return YES;
}
}
return NO;
}
- (void)onBtnEditClicked {
[_delegate onStickerViewShouldEdit:self];
}
- (void)onBtnDeleteClicked {
[_delegate onStickerViewShouldDelete:self];
}
#pragma mark - Properties
- (BOOL)editButtonHidden {
return _hideEditButton;
}
- (void)setEditButtonHidden:(BOOL)hideEditButton {
_hideEditButton = hideEditButton;
}
- (void)setContent:(UIView *)content {
[_content removeFromSuperview];
_content = content;
if (content == nil) {
return;
}
self.bounds = CGRectMake(0, 0, content.bounds.size.width + _contentMargin * 2, content.bounds.size.height + _contentMargin * 2);
// [self mas_makeConstraints:^(MASConstraintMaker *make) {
// make.center.equalTo(self.superview);
// }];
[self addSubview:content];
[self bringSubviewToFront:_btnEdit];
[self bringSubviewToFront:_btnTransform];
[self bringSubviewToFront:_btnDelete];
[content mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self).inset(_contentMargin);
}];
}
- (void)setSelected:(BOOL)selected {
_selected = selected;
_borderView.hidden = !selected;
_btnEdit.hidden = !selected || _hideEditButton;
_btnTransform.hidden = !selected;
_btnDelete.hidden = !selected;
}
#pragma mark - UIGestureRecognizerDelegate protocol
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)g2 {
NSArray<UIGestureRecognizer *> *recList = @[ _pinchRec, _rotationRec, _panRec ];
return [recList containsObject:g1] && [recList containsObject:g2];
}
@end

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import <UIKit/UIKit.h>
#import "TUIMultimediaPlugin/TUIMultimediaStickerView.h"
#import "TUIMultimediaPlugin/TUIMultimediaSubtitleInfo.h"
NS_ASSUME_NONNULL_BEGIN
@interface TUIMultimediaSubtitleView : TUIMultimediaStickerView
@property(nullable, nonatomic) TUIMultimediaSubtitleInfo *subtitleInfo;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,123 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import "TUIMultimediaSubtitleView.h"
#import "TUIMultimediaPlugin/TUIMultimediaCommon.h"
#import "TUIMultimediaPlugin/TUIMultimediaGeometry.h"
#define MIN_SUBTITLE_PASTER_WIDTH 100
#define MAX_SUBTITLE_PASTER_HEIGH 400
@interface TUIMultimediaSubtitleView () <TUIMultimediaStickerViewDelegate> {
id<TUIMultimediaStickerViewDelegate> _outerDelegate;
UILabel *_label;
BOOL _isSizeToFitingSubtitleInfo;
}
@end
@implementation TUIMultimediaSubtitleView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self != nil) {
[self initUI];
}
_isSizeToFitingSubtitleInfo = NO;
return self;
}
- (void)initUI {
[super setDelegate:self];
_label = [[UILabel alloc] init];
_label.font = [UIFont systemFontOfSize:20];
_label.numberOfLines = 0;
_label.lineBreakMode = NSLineBreakByClipping;
_label.translatesAutoresizingMaskIntoConstraints = NO;
}
#pragma mark - Getters & Setters
- (void)setSubtitleInfo:(TUIMultimediaSubtitleInfo *)subtitleInfo {
NSLog(@"TUIMultimediaSubtitleView setSubtitleInfo text = %@",subtitleInfo.wrappedText);
_subtitleInfo = subtitleInfo;
self.content = nil;
_label.text = subtitleInfo.wrappedText;
_label.textColor = subtitleInfo.color;
[self labeSizeToFit];
self.content = _label;
}
-(void)labeSizeToFit {
_isSizeToFitingSubtitleInfo = YES;
if (_subtitleInfo.wrappedText.length > 0) {
_label.text = [NSString stringWithFormat:@"%C", [_subtitleInfo.wrappedText characterAtIndex:0]];
} else {
NSLog(@"_subtitleInfo.wrappedText is empty or nil.");
_label.text = @"";
}
CGSize size = [_label sizeThatFits:CGSizeMake(MAXFLOAT, MAX_SUBTITLE_PASTER_HEIGH)];
CGFloat singleLineTextHeight = size.height;
_label.text = _subtitleInfo.wrappedText;
size = [_label sizeThatFits:CGSizeMake(MAXFLOAT, MAX_SUBTITLE_PASTER_HEIGH)];
CGFloat adjustedWidth = size.width;
if (size.height > singleLineTextHeight * 1.5) {
// multi line text
adjustedWidth = MAX(size.width, MIN_SUBTITLE_PASTER_WIDTH);
}
CGFloat adjustedHeight = MIN(size.height, MAX_SUBTITLE_PASTER_HEIGH);
_label.frame = CGRectMake(_label.frame.origin.x, _label.frame.origin.y, adjustedWidth, adjustedHeight);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self->_isSizeToFitingSubtitleInfo = NO;
});
}
- (id<TUIMultimediaStickerViewDelegate>)delegate {
return _outerDelegate;
}
- (void)setDelegate:(id<TUIMultimediaStickerViewDelegate>)delegate {
_outerDelegate = delegate;
}
#pragma mark - TUIMultimediaFloatingResizableDelegate protocol
- (void)onStickerViewShouldDelete:(TUIMultimediaStickerView *)v {
[_outerDelegate onStickerViewShouldDelete:self];
}
- (void)onStickerViewShouldEdit:(TUIMultimediaStickerView *)v {
[_outerDelegate onStickerViewShouldEdit:self];
}
- (void)onStickerViewSelected:(TUIMultimediaStickerView *)v {
[_outerDelegate onStickerViewSelected:self];
}
- (void)onStickerViewSizeChanged:(TUIMultimediaStickerView *)v {
[_outerDelegate onStickerViewSizeChanged:self];
if (_isSizeToFitingSubtitleInfo == NO) {
[self adjustFontSize];
}
}
- (void)adjustFontSize {
NSString *text = _label.text;
CGSize labelSize = _label.bounds.size;
UIFont *font = _label.font;
CGFloat minFontSize = 0;
CGFloat maxFontSize = 1000;
const CGFloat eps = 0.1;
// 14
while (maxFontSize - minFontSize > eps) {
CGFloat mid = (maxFontSize + minFontSize) / 2;
font = [font fontWithSize:mid];
CGSize size = [text sizeWithAttributes:@{NSFontAttributeName : font}];
if (size.width < labelSize.width && size.height < labelSize.height) {
minFontSize = mid;
} else {
maxFontSize = mid;
}
}
_label.font = [_label.font fontWithSize:minFontSize];
}
@end