增加换肤功能

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,18 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIMultimediaEffectCell : UICollectionViewCell
@property(nonatomic) UIImage *image;
@property(nonatomic) NSString *text;
@property(nonatomic) BOOL effectSelected;
@property(nonatomic) CGSize iconSize;
- (instancetype)initWithFrame:(CGRect)frame;
+ (NSString *)reuseIdentifier;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,120 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import "TUIMultimediaEffectCell.h"
#import <Masonry/Masonry.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIMultimediaPlugin/TUIMultimediaCommon.h"
#import "TUIMultimediaPlugin/TUIMultimediaConfig.h"
const CGFloat MaskBorderWidth = 2;
const CGFloat EffectCellRadius = 10;
const CGFloat EffectCellWidth = 60;
const CGFloat LabelFontSize = 12;
const CGFloat LabelHeight = 18;
const CGFloat LabelInsectToBottom = 5;
@interface TUIMultimediaEffectCell () {
UIView *_imgContainerView;
UIView *_highlightView;
UIImageView *_imgView;
UILabel *_label;
NSString *_text;
}
@end
@implementation TUIMultimediaEffectCell
@dynamic image;
@dynamic text;
+ (NSString *)reuseIdentifier {
return NSStringFromClass([self class]);
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self != nil) {
[self initUI];
}
return self;
}
- (void)initUI {
_imgContainerView = [[UIView alloc] init];
[self.contentView addSubview:_imgContainerView];
_imgContainerView.layer.cornerRadius = EffectCellRadius;
_imgContainerView.clipsToBounds = YES;
_imgView = [[UIImageView alloc] init];
[_imgContainerView addSubview:_imgView];
_imgView.contentMode = UIViewContentModeScaleAspectFit;
[_imgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(_iconSize);
make.center.equalTo(_imgContainerView);
}];
_highlightView = [[UIView alloc] init];
[self.contentView addSubview:_highlightView];
_highlightView.hidden = YES;
_highlightView.backgroundColor = [[TUIMultimediaConfig sharedInstance] getThemeColor];
_label = [[UILabel alloc] init];
_label.font = [UIFont systemFontOfSize:LabelFontSize];
_label.textColor = UIColor.whiteColor;
_label.textAlignment = NSTextAlignmentCenter;
[self.contentView addSubview:_label];
[_highlightView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
[_imgContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.equalTo(self.contentView).inset(MaskBorderWidth);
make.height.equalTo(_imgContainerView.mas_width);
}];
[_label mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.contentView);
make.bottom.equalTo(self.contentView);
make.top.equalTo(_imgContainerView.mas_bottom);
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self addMaskToHighlightView];
}
- (void)addMaskToHighlightView {
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:_highlightView.bounds cornerRadius:EffectCellRadius];
[maskPath appendPath:[UIBezierPath bezierPathWithRoundedRect:_imgContainerView.frame cornerRadius:EffectCellRadius].bezierPathByReversingPath];
CAShapeLayer *layer = [[CAShapeLayer alloc] init];
layer.path = maskPath.CGPath;
_highlightView.layer.mask = layer;
}
- (void)setIconSize:(CGSize)iconSize {
_iconSize = iconSize;
[_imgView mas_updateConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(_iconSize);
}];
}
- (UIImage *)image {
return _imgView.image;
}
- (void)setImage:(UIImage *)image {
_imgView.image = image;
}
- (NSString *)text {
return _text;
}
- (void)setText:(NSString *)text {
_text = text;
_label.text = text;
}
- (void)setEffectSelected:(BOOL)effectSelected {
_effectSelected = effectSelected;
_highlightView.hidden = !effectSelected;
}
@end

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import <UIKit/UIKit.h>
#import "TUIMultimediaEffectCell.h"
#import "TUIMultimediaPlugin/TUIMultimediaEffectItem.h"
NS_ASSUME_NONNULL_BEGIN
@protocol TUIMultimediaEffectPanelDelegate;
/**
美颜和滤镜设置界面
*/
@interface TUIMultimediaEffectPanel : UIView
@property(nonatomic) NSArray<TUIMultimediaEffectItem *> *items;
@property(nonatomic) NSInteger selectedIndex;
@property(nonatomic) CGSize iconSize;
@property(weak, nullable, nonatomic) id<TUIMultimediaEffectPanelDelegate> delegate;
- (id)initWithFrame:(CGRect)frame;
@end
@protocol TUIMultimediaEffectPanelDelegate <NSObject>
- (void)effectPanelSelectionChanged:(TUIMultimediaEffectPanel *)panel;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,100 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import "TUIMultimediaEffectPanel.h"
#import <Masonry/Masonry.h>
#import "TUIMultimediaEffectCell.h"
#import "TUIMultimediaPlugin/TUIMultimediaBeautifySettings.h"
#import "TUIMultimediaPlugin/TUIMultimediaCommon.h"
static const CGSize EffectItemSize = TUIMultimediaConstCGSize(60, 86);
@interface TUIMultimediaEffectPanel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout> {
NSArray<TUIMultimediaEffectItem *> *_items;
UICollectionView *_collectionView;
}
@end
@implementation TUIMultimediaEffectPanel
@dynamic items;
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self != nil) {
_selectedIndex = -1;
_items = @[];
[self initUI];
}
return self;
}
- (void)initUI {
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
layout.minimumInteritemSpacing = 10;
layout.itemSize = EffectItemSize;
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
_collectionView.backgroundColor = UIColor.clearColor;
_collectionView.showsHorizontalScrollIndicator = NO;
_collectionView.delegate = self;
_collectionView.dataSource = self;
[_collectionView registerClass:TUIMultimediaEffectCell.class forCellWithReuseIdentifier:TUIMultimediaEffectCell.reuseIdentifier];
[self addSubview:_collectionView];
[_collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(UIViewNoIntrinsicMetric, EffectItemSize.height /* + self.safeAreaInsets.bottom*/);
}
#pragma mark - Properties
- (NSArray<TUIMultimediaEffectItem *> *)items {
return _items;
}
- (void)setItems:(NSArray<TUIMultimediaEffectItem *> *)value {
_items = value;
_selectedIndex = -1;
[_collectionView reloadData];
}
- (void)setSelectedIndex:(NSInteger)selectedIndex {
_selectedIndex = selectedIndex;
[_collectionView reloadData];
}
- (void)setIconSize:(CGSize)iconSize {
_iconSize = iconSize;
[_collectionView reloadData];
}
#pragma mark - UICollectionViewDelegateFlowLayout protocol
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
TUIMultimediaEffectCell *cell = (TUIMultimediaEffectCell *)[collectionView dequeueReusableCellWithReuseIdentifier:TUIMultimediaEffectCell.reuseIdentifier
forIndexPath:indexPath];
cell.image = _items[indexPath.item].iconImage;
cell.text = _items[indexPath.item].name;
cell.effectSelected = (indexPath.item == _selectedIndex);
cell.iconSize = _iconSize;
return cell;
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _items.count;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
// cell使CollectionView
[collectionView deselectItemAtIndexPath:indexPath animated:NO];
_selectedIndex = indexPath.item;
[collectionView reloadData];
[_delegate effectPanelSelectionChanged:self];
}
@end

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import <UIKit/UIKit.h>
#import "TUIMultimediaPlugin/TUIMultimediaBeautifySettings.h"
NS_ASSUME_NONNULL_BEGIN
@protocol TUIMultimediaBeautifyViewDelegate;
@interface TUIMultimediaBeautifyView : UIView
@property(nonatomic) TUIMultimediaBeautifySettings *settings;
@property(weak, nullable, nonatomic) id<TUIMultimediaBeautifyViewDelegate> delegate;
- (instancetype)initWithFrame:(CGRect)frame;
- (instancetype)initWithFrame:(CGRect)frame settings:(nullable TUIMultimediaBeautifySettings *)settings;
@end
@protocol TUIMultimediaBeautifyViewDelegate <NSObject>
- (void)beautifyView:(TUIMultimediaBeautifyView *)beautifyView onSettingsChange:(TUIMultimediaBeautifySettings *)settings;
@end
@interface TUIMultimediaMarker : UIView
@property(nonatomic) NSString *text;
- (void)showForDuration:(CGFloat)showSeconds withHideAnimeDuration:(CGFloat)hideAnimeSeconds;
- (void)hide;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,259 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import "TUIMultimediaBeautifyView.h"
#import <Foundation/Foundation.h>
#import <Masonry/Masonry.h>
#import <ReactiveObjC/RACEXTScope.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIMultimediaPlugin/TUIMultimediaCommon.h"
#import "TUIMultimediaPlugin/TUIMultimediaEffectCell.h"
#import "TUIMultimediaPlugin/TUIMultimediaEffectPanel.h"
#import "TUIMultimediaPlugin/TUIMultimediaImageUtil.h"
#import "TUIMultimediaPlugin/TUIMultimediaTabPanel.h"
#import "TUIMultimediaPlugin/TUIMultimediaConfig.h"
#pragma mark - TUIMultimediaBeautifyView
@interface TUIMultimediaBeautifyView () <TUIMultimediaEffectPanelDelegate, TUIMultimediaTabPanelDelegate> {
UIView *_topPanel;
UILabel *_lbStrength;
UIImageView *_imgViewCompare;
UISlider *_slider;
TUIMultimediaTabPanel *_tabPanel;
TUIMultimediaEffectPanel *_effectPanelBeauty;
TUIMultimediaEffectPanel *_effectPanelFilter;
TUIMultimediaEffectItem *_selectedItem;
TUIMultimediaMarker *_marker;
}
@end
@implementation TUIMultimediaBeautifyView
- (instancetype)initWithFrame:(CGRect)frame {
return [self initWithFrame:frame settings:nil];
}
- (instancetype)initWithFrame:(CGRect)frame settings:(TUIMultimediaBeautifySettings *)settings {
self = [super initWithFrame:frame];
_settings = settings;
if (_settings == nil) {
_settings = [[TUIMultimediaBeautifySettings alloc] init];
}
[self initUI];
return self;
}
- (void)initUI {
self.opaque = NO;
self.backgroundColor = UIColor.clearColor;
_topPanel = [[UIView alloc] init];
_topPanel.hidden = YES;
[self addSubview:_topPanel];
[_topPanel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.equalTo(self);
make.height.mas_equalTo(52);
}];
_marker = [[TUIMultimediaMarker alloc] init];
[_topPanel addSubview:_marker];
_lbStrength = [[UILabel alloc] init];
[_topPanel addSubview:_lbStrength];
_lbStrength.text = [TUIMultimediaCommon localizedStringForKey:@"strength"];
_lbStrength.textColor = UIColor.whiteColor;
_lbStrength.font = [UIFont systemFontOfSize:16];
_imgViewCompare = [[UIImageView alloc] init];
[_topPanel addSubview:_imgViewCompare];
_imgViewCompare.image = TUIMultimediaPluginBundleThemeImage(@"beautify_effect_compare_img", @"effect_compare");
UIColor* themeColor = [[TUIMultimediaConfig sharedInstance] getThemeColor];
_slider = [[UISlider alloc] init];
[_topPanel addSubview:_slider];
//_slider.thumbTintColor = TUIMultimediaPluginDynamicColor(@"theme_accent_dark_color", @"#006CFF");
_slider.minimumValue = TUIMultimediaEffectSliderMin;
_slider.maximumValue = TUIMultimediaEffectSliderMax;
_slider.minimumTrackTintColor = themeColor;//TUIMultimediaPluginDynamicColor(@"theme_accent_dark_color", @"#006CFF");
_slider.maximumTrackTintColor = TUIMultimediaPluginDynamicColor(@"beautify_slider_track_bg_color", @"#F4F5F9");
UIImage *thumbImg = [TUIMultimediaImageUtil createBlueCircleWithWhiteBorder:CGSizeMake(24, 24) withColor:themeColor];
[_slider setThumbImage:thumbImg forState:UIControlStateNormal];
[_slider setThumbImage:thumbImg forState:UIControlStateHighlighted];
[_slider addTarget:self action:@selector(onSliderValueChange) forControlEvents:UIControlEventValueChanged];
[_lbStrength mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(_topPanel).inset(16);
make.centerY.equalTo(_topPanel);
}];
[_imgViewCompare mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(_topPanel).inset(16);
make.centerY.equalTo(_topPanel);
make.width.height.mas_equalTo(30);
}];
[_slider mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(_lbStrength.mas_right).inset(8);
make.right.equalTo(_imgViewCompare.mas_left).inset(8);
make.centerY.equalTo(_topPanel);
make.height.mas_equalTo(30);
}];
_effectPanelBeauty = [[TUIMultimediaEffectPanel alloc] init];
_effectPanelBeauty.iconSize = CGSizeMake(32, 32);
_effectPanelBeauty.delegate = self;
_effectPanelBeauty.items = _settings.beautifyItems;
_effectPanelFilter = [[TUIMultimediaEffectPanel alloc] init];
_effectPanelFilter.iconSize = CGSizeMake(56, 56);
_effectPanelFilter.delegate = self;
_effectPanelFilter.items = _settings.filterItems;
_effectPanelFilter.selectedIndex = 0;
UIView *bottomPanel = [[UIView alloc] init];
[self addSubview:bottomPanel];
bottomPanel.backgroundColor = UIColor.blackColor;
_tabPanel = [[TUIMultimediaTabPanel alloc] initWithFrame:self.bounds];
_tabPanel.delegate = self;
_tabPanel.backgroundColor = UIColor.blackColor;
_tabPanel.tabs = @[
[[TUIMultimediaTabPanelTab alloc] initWithName:[TUIMultimediaCommon localizedStringForKey:@"beautify"] icon:nil view:_effectPanelBeauty],
[[TUIMultimediaTabPanelTab alloc] initWithName:[TUIMultimediaCommon localizedStringForKey:@"filter"] icon:nil view:_effectPanelFilter],
];
[bottomPanel addSubview:_tabPanel];
[bottomPanel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_topPanel.mas_bottom);
make.left.right.bottom.equalTo(self);
}];
[_tabPanel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.equalTo(bottomPanel);
make.bottom.equalTo(bottomPanel.mas_safeAreaLayoutGuideBottom);
}];
}
- (void)effectPanelSelectionChanged:(TUIMultimediaEffectPanel *)panel {
[_marker hide];
if (panel.selectedIndex == -1 || panel.items[panel.selectedIndex].tag == TUIMultimediaEffectItemTagNone) {
panel.selectedIndex = -1;
_selectedItem = nil;
_topPanel.hidden = YES;
if (panel == _effectPanelFilter) {
_settings.activeFilterIndex = -1;
} else {
for (TUIMultimediaEffectItem *item in _settings.beautifyItems) {
item.strength = 0;
}
}
[_delegate beautifyView:self onSettingsChange:_settings];
return;
}
_selectedItem = panel.items[panel.selectedIndex];
if (panel == _effectPanelFilter) {
_settings.activeFilterIndex = panel.selectedIndex;
_topPanel.hidden = NO;
_slider.value = _selectedItem.strength;
[_delegate beautifyView:self onSettingsChange:_settings];
} else {
_topPanel.hidden = NO;
_slider.value = _selectedItem.strength;
if ([@[ @(TUIMultimediaEffectItemTagSmooth), @(TUIMultimediaEffectItemTagNatural), @(TUIMultimediaEffectItemTagPitu) ] containsObject:@(_selectedItem.tag)]) {
_settings.activeBeautifyTag = _selectedItem.tag;
[_delegate beautifyView:self onSettingsChange:_settings];
}
}
}
- (void)tabPanel:(TUIMultimediaTabPanel *)panel selectedIndexChanged:(NSInteger)selectedIndex {
TUIMultimediaEffectPanel *effectPanel = (TUIMultimediaEffectPanel *)panel.tabs[selectedIndex].view;
[self effectPanelSelectionChanged:effectPanel];
}
- (void)onSliderValueChange {
_selectedItem.strength = _slider.value;
_marker.text = [NSString stringWithFormat:@"%d", (int)_slider.value];
CGRect trackRect = [_slider trackRectForBounds:_slider.bounds];
CGRect thumbRect = [_slider thumbRectForBounds:_slider.bounds trackRect:trackRect value:_slider.value];
CGRect rect = [_topPanel convertRect:thumbRect fromView:_slider];
[_marker mas_remakeConstraints:^(MASConstraintMaker *make) {
make.width.height.mas_equalTo(34);
make.centerX.equalTo(_topPanel.mas_left).offset(CGRectGetMidX(rect));
make.bottom.equalTo(_topPanel.mas_top).offset(rect.origin.y - 4);
}];
[_marker showForDuration:0.5 withHideAnimeDuration:0.5];
[_delegate beautifyView:self onSettingsChange:_settings];
}
- (void)setSettings:(TUIMultimediaBeautifySettings *)settings {
_effectPanelBeauty.items = _settings.beautifyItems;
_effectPanelFilter.items = _settings.filterItems;
_effectPanelFilter.selectedIndex = 0;
}
@end
#pragma mark - TUIMultimediaMarker
@interface TUIMultimediaMarker () {
UIImageView *_imgView;
UILabel *_label;
dispatch_block_t hideBlock;
}
@end
@implementation TUIMultimediaMarker
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
[self initUI];
return self;
}
- (void)initUI {
_imgView = [[UIImageView alloc] init];
[self addSubview:_imgView];
_imgView.image = TUIMultimediaPluginBundleThemeImage(@"beautify_marker_img", @"marker");
[_imgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
_label = [[UILabel alloc] init];
[self addSubview:_label];
_label.textAlignment = NSTextAlignmentCenter;
_label.textColor = UIColor.blackColor;
_label.font = [UIFont systemFontOfSize:12];
[_label mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self);
make.centerY.equalTo(self);
}];
}
- (NSString *)text {
return _label.text;
}
- (void)setText:(NSString *)text {
_label.text = text;
}
- (void)showForDuration:(CGFloat)showSeconds withHideAnimeDuration:(CGFloat)hideAnimeSeconds {
self.alpha = 1;
self.hidden = NO;
if (hideBlock != nil) {
dispatch_block_cancel(hideBlock);
}
@weakify(self);
hideBlock = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{
@strongify(self);
[UIView animateWithDuration:hideAnimeSeconds
animations:^{
self.alpha = 0;
}
completion:^(BOOL finished) {
self.alpha = 1;
self.hidden = YES;
}];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(showSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), hideBlock);
}
- (void)hide {
if (hideBlock != nil) {
dispatch_block_cancel(hideBlock);
}
self.alpha = 1;
self.hidden = YES;
}
@end

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol TUIMultimediaRecordButtonDelegate;
/**
视频录制按钮
*/
@interface TUIMultimediaRecordButton : UIView
@property(nonatomic) float progress;
@property(nonatomic) float dotSizeNormal;
@property(nonatomic) float progressSizeNormal;
@property(nonatomic) float dotSizePressed;
@property(nonatomic) float progressSizePressed;
@property(weak, nullable, nonatomic) id<TUIMultimediaRecordButtonDelegate> delegate;
@property(nonatomic) BOOL isOnlySupportTakePhoto;
- (instancetype)initWithFrame:(CGRect)frame;
@end
@protocol TUIMultimediaRecordButtonDelegate <NSObject>
- (void)onRecordButtonTap:(TUIMultimediaRecordButton *)btn;
- (void)onRecordButtonLongPressBegan:(TUIMultimediaRecordButton *)btn;
- (void)onRecordButtonLongPressEnded:(TUIMultimediaRecordButton *)btn;
- (void)onRecordButtonLongPressCancelled:(TUIMultimediaRecordButton *)btn;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,127 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import "TUIMultimediaRecordButton.h"
#import <Foundation/Foundation.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIMultimediaPlugin/TUIMultimediaCircleProgressView.h"
#import "TUIMultimediaPlugin/TUIMultimediaCommon.h"
#import "TUIMultimediaPlugin/TUIMultimediaConfig.h"
static const CGFloat RecordAnimeDuration = 0.3;
#pragma mark - TUIMultimediaRecordButton
@interface TUIMultimediaRecordButton () {
TUIMultimediaCircleProgressView *_progressView;
UIView *_dotView;
BOOL _pressed;
UILongPressGestureRecognizer *_longPressRec;
}
@end
@implementation TUIMultimediaRecordButton
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self != nil) {
[self initUI];
}
return self;
}
- (void)initUI {
_progressView = [[TUIMultimediaCircleProgressView alloc] init];
_progressView.lineCap = kCALineCapButt;
_progressView.progressBgColor = TUIMultimediaPluginDynamicColor(@"record_btn_progress_bg_color", @"#FFFFFF");
_progressView.progressColor = [[TUIMultimediaConfig sharedInstance] getThemeColor];
_progressView.width = 2;
[self addSubview:_progressView];
_dotView = [[UIView alloc] init];
_dotView.backgroundColor = TUIMultimediaPluginDynamicColor(@"record_btn_dot_color", @"#FFFFFF");
[self addSubview:_dotView];
UITapGestureRecognizer *tapRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
tapRec.cancelsTouchesInView = NO;
[self addGestureRecognizer:tapRec];
_longPressRec = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPress:)];
_longPressRec.cancelsTouchesInView = NO;
_longPressRec.minimumPressDuration = RecordAnimeDuration;
[self addGestureRecognizer:_longPressRec];
[self layoutSubviews];
}
- (void)layoutSubviews {
CGFloat dotSize = _pressed ? _dotSizePressed : _dotSizeNormal;
CGFloat progressSize = _pressed ? _progressSizePressed : _progressSizeNormal;
if (_isOnlySupportTakePhoto) {
progressSize = _progressSizeNormal;
}
CGPoint center = CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2);
CGFloat len = MIN(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds));
_progressView.center = center;
_progressView.bounds = CGRectMake(0, 0, len, len);
_progressView.transform = CGAffineTransformMakeScale(progressSize / len, progressSize / len);
_dotView.center = center;
_dotView.bounds = CGRectMake(0, 0, dotSize, dotSize);
_dotView.layer.cornerRadius = dotSize / 2;
}
- (void)animeStartRecord {
_progressView.width = 4;
[UIView animateWithDuration:RecordAnimeDuration
animations:^{
self->_pressed = YES;
[self layoutSubviews];
}];
}
- (void)animeStopRecord {
_progressView.width = 2;
[UIView animateWithDuration:RecordAnimeDuration
animations:^{
self->_pressed = NO;
[self layoutSubviews];
}];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self animeStartRecord];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
[self animeStopRecord];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self animeStopRecord];
}
- (void)onTap:(UITapGestureRecognizer *)rec {
[_delegate onRecordButtonTap:self];
}
- (void)onLongPress:(UILongPressGestureRecognizer *)rec {
switch (rec.state) {
case UIGestureRecognizerStateBegan: {
[_delegate onRecordButtonLongPressBegan:self];
break;
}
case UIGestureRecognizerStateEnded: {
[_delegate onRecordButtonLongPressEnded:self];
break;
}
case UIGestureRecognizerStateCancelled: {
[self animeStopRecord];
[_delegate onRecordButtonLongPressCancelled:self];
}
default:;
}
}
- (void)setProgress:(float)progress {
[_progressView setProgress:progress animated:YES];
}
@end

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import <UIKit/UIKit.h>
#import "TUIMultimediaPlugin/TUIMultimediaBeautifySettings.h"
NS_ASSUME_NONNULL_BEGIN
@class TUIMultimediaRecordControlView;
typedef NS_ENUM(NSInteger, TUIMultimediaRecordAspectRatio) {
TUIMultimediaRecordAspectRatio1_1,
TUIMultimediaRecordAspectRatio3_4,
TUIMultimediaRecordAspectRatio4_3,
TUIMultimediaRecordAspectRatio9_16,
TUIMultimediaRecordAspectRatio16_9,
};
typedef void (^TUIMultimediaRecordControlCallback)(TUIMultimediaRecordControlView *);
@protocol TUIMultimediaRecordControlViewDelegate <NSObject>
- (void)recordControlViewOnRecordStart;
- (void)recordControlViewOnRecordFinish;
- (void)recordControlViewPhoto;
- (void)recordControlViewOnFlashStateChange:(BOOL)flashState;
- (void)recordControlViewOnAspectChange:(TUIMultimediaRecordAspectRatio)aspect;
- (void)recordControlViewOnExit;
- (void)recordControlViewOnCameraSwicth:(BOOL)isUsingFrontCamera;
- (void)recordControlViewOnBeautify;
@end
/**
包含视频录制页面的各种控制按钮
*/
@interface TUIMultimediaRecordControlView : UIView
@property(nonatomic) BOOL flashState;
@property(nonatomic) TUIMultimediaRecordAspectRatio aspectRatio;
@property(nonatomic) BOOL isUsingFrontCamera;
@property(readonly, nonatomic) UIView *previewView;
@property(nonatomic) TUIMultimediaBeautifySettings *beautifySettings;
@property(nonatomic) BOOL recordTipHidden;
@property(weak, nullable, nonatomic) id<TUIMultimediaRecordControlViewDelegate> delegate;
- (instancetype)initWithFrame:(CGRect)frame isOnlySupportTakePhoto:(BOOL)isOnlySupportTakePhoto;
- (instancetype)initWithFrame:(CGRect)frame isOnlySupportTakePhoto:(BOOL)isOnlySupportTakePhoto beautifySettings:(TUIMultimediaBeautifySettings *)beautifySettings;
- (void)setProgress:(float)progress duration:(float)duration;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,408 @@
// Copyright (c) 2024 Tencent. All rights reserved.
// Author: eddardliu
#import "TUIMultimediaRecordControlView.h"
#import <Masonry/Masonry.h>
#import <TUICore/NSDictionary+TUISafe.h>
#import <TUICore/TUICore.h>
#import <TUICore/TUIThemeManager.h>
#import "TUIMultimediaPlugin/TUIMultimediaCommon.h"
#import "TUIMultimediaPlugin/TUIMultimediaIconLabelButton.h"
#import "TUIMultimediaPlugin/TUIMultimediaRecordButton.h"
#import "TUIMultimediaPlugin/TUIMultimediaConfig.h"
#import "TUIMultimediaPlugin/TUIMultimediaAuthorizationPrompter.h"
#pragma mark - UI relative constants
const static CGFloat BtnStartRecordSize = 72;
const static CGFloat BtnStartRecordGapBottom = 15;
const static CGFloat BtnStartRecordDotSizeNormal = 56;
const static CGFloat BtnStartRecordDotSizePressed = 20;
const static CGFloat BtnStartRecordProgressSizeNormal = 72;
const static CGFloat BtnStartRecordProgressSizePressed = 80;
const static CGSize BtnExitRecordSize = TUIMultimediaConstCGSize(28, 28);
const static CGFloat BtnExitRecordGapToStartRecord = 55;
const static CGSize BtnCameraSwitch = TUIMultimediaConstCGSize(28, 28);
const static CGFloat BtnCameraSwitchGapToStartRecord = 55;
const static CGFloat FunctionBtnToToTop = 64;
const static CGFloat FunctionBtnToRight = 16;
const static CGSize BtnExtendFunctionIconSize = TUIMultimediaConstCGSize(28, 28);
const static CGFloat BtnExtendFunctionIconTextGap = 4;
const static CGFloat BtnExtendFunctionGap = 24;
const static BOOL ShowDurationLabel = YES;
#pragma mark - TUIMultimediaRecordControlView
@interface TUIMultimediaRecordControlView () <TUIMultimediaRecordButtonDelegate> {
TUIMultimediaRecordButton *_btnRecord;
UIButton *_btnExitRecord;
UIButton *_btnCameraSwitch;
UIButton *_btnFlash;
UIButton *_btnBeautify;
UIButton *_btnAspect;
UIButton *_lastFuncitonBtn;
UILabel *_lbDuration;
UILabel *_lbTip;
BOOL _isOnlySupportTakePhoto;
}
@end
@implementation TUIMultimediaRecordControlView
- (instancetype)initWithFrame:(CGRect)frame isOnlySupportTakePhoto:(BOOL)isOnlySupportTakePhoto {
return [self initWithFrame:frame isOnlySupportTakePhoto:isOnlySupportTakePhoto beautifySettings:nil];
}
- (instancetype)initWithFrame:(CGRect)frame isOnlySupportTakePhoto:(BOOL) isOnlySupportTakePhoto beautifySettings:(TUIMultimediaBeautifySettings *)beautifySettings {
self = [super initWithFrame:frame];
_isUsingFrontCamera = NO;
_flashState = NO;
_aspectRatio = TUIMultimediaRecordAspectRatio9_16;
_beautifySettings = beautifySettings;
_isOnlySupportTakePhoto = isOnlySupportTakePhoto;
if (_beautifySettings == nil) {
_beautifySettings = [[TUIMultimediaBeautifySettings alloc] init];
}
[self initUI];
return self;
}
- (void)setProgress:(float)progress duration:(float)duration {
[_btnRecord setProgress:progress];
int m = floor(duration / 60);
int s = floor(duration - m * 60);
_lbDuration.text = [NSString stringWithFormat:@"%02d:%02d", m, s];
}
#pragma mark - UI Init
- (void)initUI {
_previewView = [[UIView alloc] init];
[self addSubview:_previewView];
[_previewView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self);
make.width.equalTo(self);
make.height.equalTo(_previewView.mas_width).multipliedBy(16.0 / 9.0);
}];
// _lbDuration _previewView
UIView *_bottomMaskView = [[UIView alloc] init];
[self addSubview:_bottomMaskView];
_bottomMaskView.backgroundColor = UIColor.blackColor;
[self initControlButtons];
[self initFunctionButtons];
_lbDuration = [[UILabel alloc] init];
[self addSubview:_lbDuration];
_lbDuration.hidden = YES;
_lbDuration.text = @"00:00";
_lbDuration.font = [UIFont monospacedSystemFontOfSize:18 weight:UIFontWeightMedium];
_lbDuration.textColor = UIColor.whiteColor;
[_lbDuration mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(_btnRecord.mas_top).inset(8);
make.centerX.equalTo(_btnRecord);
}];
[_bottomMaskView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(self);
make.top.equalTo(_lbDuration.mas_top).offset(-8);
}];
_lbTip = [[UILabel alloc] init];
[self addSubview:_lbTip];
if (_isOnlySupportTakePhoto) {
_lbTip.text = [TUIMultimediaCommon localizedStringForKey:@"record_photo_tip"];
} else {
_lbTip.text = [TUIMultimediaCommon localizedStringForKey:@"record_tip"];
}
_lbTip.font = [UIFont systemFontOfSize:16];
_lbTip.textColor = UIColor.whiteColor;
[_lbTip mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.lessThanOrEqualTo(_previewView).inset(16);
make.bottom.lessThanOrEqualTo(_bottomMaskView.mas_top).offset(-16);
make.centerX.equalTo(self);
}];
}
//
- (void)initControlButtons {
_btnRecord = [[TUIMultimediaRecordButton alloc] init];
[self addSubview:_btnRecord];
_btnRecord.dotSizeNormal = BtnStartRecordDotSizeNormal;
_btnRecord.dotSizePressed = BtnStartRecordDotSizePressed;
_btnRecord.progressSizeNormal = BtnStartRecordProgressSizeNormal;
_btnRecord.progressSizePressed = BtnStartRecordProgressSizePressed;
_btnRecord.isOnlySupportTakePhoto = _isOnlySupportTakePhoto;
_btnRecord.delegate = self;
_btnExitRecord = [self newCustomButtonWithImage:TUIMultimediaPluginBundleThemeImage(@"record_exit_img", @"cross") onTouchUpInside:@selector(onBtnExitClick)];
_btnCameraSwitch = [self newCustomButtonWithImage:TUIMultimediaPluginBundleThemeImage(@"record_camera_switch_img", @"camera_switch")
onTouchUpInside:@selector(onBtnCameraSwitchClick)];
[_btnRecord mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.height.mas_equalTo(BtnStartRecordSize);
make.centerX.equalTo(self);
make.bottom.equalTo(self.mas_safeAreaLayoutGuideBottom).inset(BtnStartRecordGapBottom);
}];
[_btnExitRecord mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(BtnExitRecordSize);
make.centerY.equalTo(_btnRecord);
make.right.equalTo(_btnRecord.mas_left).inset(BtnExitRecordGapToStartRecord);
}];
[_btnCameraSwitch mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(BtnCameraSwitch);
make.centerY.equalTo(_btnRecord);
make.left.equalTo(_btnRecord.mas_right).inset(BtnCameraSwitchGapToStartRecord);
}];
}
//
- (void)initFunctionButtons {
[self initTorchView];
[self initBeautyView];
[self initAspectView];
}
- (void) initTorchView {
if (![[TUIMultimediaConfig sharedInstance] isSupportRecordTorch]) {
return;
}
_btnFlash = [self newFunctionButtonWithImage:TUIMultimediaPluginBundleThemeImage(@"record_flash_close_img", @"flash_close")
title:[TUIMultimediaCommon localizedStringForKey:@"flash"]
onTouchUpInside:@selector(onBtnFlashClick)];
[_btnFlash mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self).inset(FunctionBtnToRight);
make.top.equalTo(self).inset(FunctionBtnToToTop);
}];
_lastFuncitonBtn = _btnFlash;
}
- (void) initBeautyView {
if (![[TUIMultimediaConfig sharedInstance] isSupportRecordBeauty]) {
return;
}
_btnBeautify = [self newFunctionButtonWithImage:TUIMultimediaPluginBundleThemeImage(@"record_beautify_img", @"beauty_record")
title:[TUIMultimediaCommon localizedStringForKey:@"beautify"]
onTouchUpInside:@selector(onBtnBeautifyClick)];
[_btnBeautify mas_makeConstraints:^(MASConstraintMaker *make) {
if (_lastFuncitonBtn == nil) {
make.top.equalTo(self).inset(FunctionBtnToToTop);
make.right.equalTo(self).inset(FunctionBtnToRight);
} else {
make.centerX.equalTo(_lastFuncitonBtn);
make.top.equalTo(_lastFuncitonBtn.mas_bottom).inset(BtnExtendFunctionGap);
}
_lastFuncitonBtn = _btnBeautify;
}];
}
- (void) initAspectView {
if (![[TUIMultimediaConfig sharedInstance] isSupportRecordAspect]) {
return;
}
_btnAspect = [self newFunctionButtonWithImage:TUIMultimediaPluginBundleThemeImage(@"record_aspect_9_16_img", @"record_aspect_9_16")
title:[TUIMultimediaCommon localizedStringForKey:@"aspect"]
onTouchUpInside:@selector(onBtnAspectClick)];
[_btnAspect mas_makeConstraints:^(MASConstraintMaker *make) {
if (_lastFuncitonBtn == nil) {
make.top.equalTo(self).inset(FunctionBtnToToTop);
make.right.equalTo(self).inset(FunctionBtnToRight);
} else {
make.centerX.equalTo(_lastFuncitonBtn);
make.top.equalTo(_lastFuncitonBtn.mas_bottom).inset(BtnExtendFunctionGap);
}
_lastFuncitonBtn = _btnBeautify;
}];
}
- (UIButton *)newFunctionButtonWithImage:(UIImage *)img title:(NSString *)title onTouchUpInside:(SEL)sel {
TUIMultimediaIconLabelButton *btn = [TUIMultimediaIconLabelButton buttonWithType:UIButtonTypeCustom];
[btn setImage:img forState:UIControlStateNormal];
[btn setAttributedTitle:[[NSAttributedString alloc]
initWithString:title
attributes:@{
NSFontAttributeName : [UIFont systemFontOfSize:12],
NSForegroundColorAttributeName : TUIMultimediaPluginDynamicColor(@"record_func_btn_text_color", @"#FFFFFF"),
}]
forState:UIControlStateNormal];
[btn addTarget:self action:sel forControlEvents:UIControlEventTouchUpInside];
btn.iconSize = BtnExtendFunctionIconSize;
btn.iconLabelGap = BtnExtendFunctionIconTextGap;
[self addSubview:btn];
return btn;
}
- (UIButton *)newCustomButtonWithImage:(nullable UIImage *)image onTouchUpInside:(nullable SEL)sel {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setImage:image forState:UIControlStateNormal];
if (sel != nil) {
[btn addTarget:self action:sel forControlEvents:UIControlEventTouchUpInside];
}
[self addSubview:btn];
return btn;
}
#pragma mark - Actions
- (void)onBtnExitClick {
[_delegate recordControlViewOnExit];
}
- (void)onBtnCameraSwitchClick {
if (_isUsingFrontCamera) {
_isUsingFrontCamera = NO;
_btnFlash.enabled = YES;
} else {
_isUsingFrontCamera = YES;
_btnFlash.enabled = NO;
[self setFlashState:NO];
}
[_delegate recordControlViewOnCameraSwicth:_isUsingFrontCamera];
}
- (void)onBtnFlashClick {
if (_isUsingFrontCamera) {
return;
}
[self setFlashState:!_flashState];
[_delegate recordControlViewOnFlashStateChange:_flashState];
}
- (void)onBtnAspectClick {
if (_aspectRatio == TUIMultimediaRecordAspectRatio9_16) {
_aspectRatio = TUIMultimediaRecordAspectRatio3_4;
[_btnAspect setImage:TUIMultimediaPluginBundleThemeImage(@"record_aspect_3_4_img", @"record_aspect_3_4") forState:UIControlStateNormal];
[_previewView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
make.width.equalTo(self);
make.height.equalTo(_previewView.mas_width).multipliedBy(4.0 / 3.0);
}];
} else {
_aspectRatio = TUIMultimediaRecordAspectRatio9_16;
[_btnAspect setImage:TUIMultimediaPluginBundleThemeImage(@"record_aspect_9_16_img", @"record_aspect_9_16") forState:UIControlStateNormal];
[_previewView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self);
make.width.equalTo(self);
make.height.equalTo(_previewView.mas_width).multipliedBy(16.0 / 9.0);
}];
}
[_delegate recordControlViewOnAspectChange:_aspectRatio];
}
- (void)onBtnBeautifyClick {
[_delegate recordControlViewOnBeautify];
}
#pragma mark - TUIMultimediaRecordButtonDelegate protocol
- (void)onRecordButtonLongPressBegan:(TUIMultimediaRecordButton *)btn {
if (![TUIMultimediaAuthorizationPrompter verifyPermissionGranted:self.delegate]) {
return;
}
if (!_isOnlySupportTakePhoto) {
[_delegate recordControlViewOnRecordStart];
_lbDuration.hidden = !ShowDurationLabel;
} else {
_lbDuration.hidden = YES;
}
_lbTip.hidden = YES;
_btnExitRecord.hidden = YES;
_btnCameraSwitch.hidden = YES;
if (_btnFlash != nil) {
_btnFlash.hidden = YES;
}
if (_btnBeautify != nil) {
_btnBeautify.hidden = YES;
}
if (_btnAspect != nil) {
_btnAspect.hidden = YES;
}
}
- (void)onRecordButtonLongPressEnded:(TUIMultimediaRecordButton *)btn {
if (![TUIMultimediaAuthorizationPrompter verifyPermissionGranted:nil]) {
return;
}
if (!_isOnlySupportTakePhoto) {
[_delegate recordControlViewOnRecordFinish];
} else {
[_delegate recordControlViewPhoto];
}
_lbDuration.hidden = YES;
_lbTip.hidden = NO;
_btnExitRecord.hidden = NO;
_btnCameraSwitch.hidden = NO;
if (_btnFlash != nil) {
_btnFlash.hidden = NO;
}
if (_btnBeautify != nil) {
_btnBeautify.hidden = NO;
}
if (_btnAspect != nil) {
_btnAspect.hidden = NO;
}
}
- (void)onRecordButtonLongPressCancelled:(TUIMultimediaRecordButton *)btn {
if (![TUIMultimediaAuthorizationPrompter verifyPermissionGranted:nil]) {
return;
}
if (!_isOnlySupportTakePhoto) {
[_delegate recordControlViewOnRecordFinish];
}
_lbDuration.hidden = YES;
_lbTip.hidden = NO;
_btnExitRecord.hidden = NO;
_btnCameraSwitch.hidden = NO;
if (_btnFlash != nil) {
_btnFlash.hidden = NO;
}
if (_btnBeautify != nil) {
_btnBeautify.hidden = NO;
}
if (_btnAspect != nil) {
_btnAspect.hidden = NO;
}
}
- (void)onRecordButtonTap:(TUIMultimediaRecordButton *)btn {
[_delegate recordControlViewPhoto];
}
#pragma mark - Properties
- (void)setFlashState:(BOOL)flashState {
_flashState = flashState;
if (_flashState) {
[_btnFlash setImage:TUIMultimediaPluginBundleThemeImage(@"record_flash_open_img", @"flash_open") forState:UIControlStateNormal];
} else {
[_btnFlash setImage:TUIMultimediaPluginBundleThemeImage(@"record_flash_close_img", @"flash_close") forState:UIControlStateNormal];
}
}
- (BOOL)recordTipHidden {
return _lbTip.hidden;
}
- (void)setRecordTipHidden:(BOOL)recordTipHidden {
_lbTip.hidden = recordTipHidden;
}
@end