增加换肤功能
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user