提交
This commit is contained in:
34
TUIKit/TUIChat/CommonUI/Album/AlbumPicker.h
Normal file
34
TUIKit/TUIChat/CommonUI/Album/AlbumPicker.h
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// AlbumPicker.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by yiliangwang on 2024/10/30.
|
||||
// Copyright © 2024 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
typedef void (^IAlbumPickerCallback)(NSDictionary *param);
|
||||
@protocol IAlbumPicker <NSObject>
|
||||
- (void)pickMediaWithCaller:(UIViewController *)caller
|
||||
originalMediaPicked:(IAlbumPickerCallback)mediaPicked
|
||||
progressCallback:(IAlbumPickerCallback)progressCallback
|
||||
finishedCallback:(IAlbumPickerCallback)finishedCallback;
|
||||
@end
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AlbumPicker : NSObject
|
||||
|
||||
@property(nonatomic,strong) id<IAlbumPicker> advancedAlbumPicker;
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
+ (void)registerAdvancedAlbumPicker:(id<IAlbumPicker>)albumPicker;
|
||||
+ (void)pickMediaWithCaller:(UIViewController *)caller
|
||||
originalMediaPicked:(IAlbumPickerCallback)mediaPicked
|
||||
progressCallback:(IAlbumPickerCallback)progressCallback
|
||||
finishedCallback:(IAlbumPickerCallback)finishedCallback;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
46
TUIKit/TUIChat/CommonUI/Album/AlbumPicker.m
Normal file
46
TUIKit/TUIChat/CommonUI/Album/AlbumPicker.m
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// AlbumPicker.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by yiliangwang on 2024/10/30.
|
||||
// Copyright © 2024 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AlbumPicker.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AlbumPicker()
|
||||
|
||||
@end
|
||||
|
||||
@implementation AlbumPicker
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static AlbumPicker *sharedInstance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[self alloc] init];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
+ (void)registerAdvancedAlbumPicker:(id<IAlbumPicker>)albumPicker {
|
||||
[AlbumPicker sharedInstance].advancedAlbumPicker = albumPicker;
|
||||
}
|
||||
|
||||
+ (void)pickMediaWithCaller:(UIViewController *)caller
|
||||
originalMediaPicked:(IAlbumPickerCallback)mediaPicked
|
||||
progressCallback:(IAlbumPickerCallback)progressCallback
|
||||
finishedCallback:(IAlbumPickerCallback)finishedCallback {
|
||||
id<IAlbumPicker> albumPicker = nil;
|
||||
if ([AlbumPicker sharedInstance].advancedAlbumPicker) {
|
||||
albumPicker = [AlbumPicker sharedInstance].advancedAlbumPicker;
|
||||
}
|
||||
|
||||
if (albumPicker && [albumPicker respondsToSelector:@selector
|
||||
(pickMediaWithCaller:originalMediaPicked:progressCallback:finishedCallback:)]) {
|
||||
[albumPicker pickMediaWithCaller:caller originalMediaPicked:mediaPicked progressCallback:progressCallback finishedCallback:finishedCallback];
|
||||
}
|
||||
}
|
||||
@end
|
||||
42
TUIKit/TUIChat/CommonUI/Album/MultimediaRecorder.h
Normal file
42
TUIKit/TUIChat/CommonUI/Album/MultimediaRecorder.h
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// VideoRecorder.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by yiliangwang on 2024/10/30.
|
||||
// Copyright © 2024 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
typedef void (^VideoRecorderSuccessBlock)(NSURL *uri);
|
||||
typedef void (^VideoRecorderFailureBlock)(NSInteger errorCode, NSString *errorMessage);
|
||||
|
||||
@protocol IMultimediaRecorder <NSObject>
|
||||
- (void)recordVideoWithCaller:(UIViewController *)caller
|
||||
successBlock:(VideoRecorderSuccessBlock)successBlock
|
||||
failureBlock:(VideoRecorderFailureBlock)failureBlock;
|
||||
|
||||
-(void)takePhoneWithCaller:(UIViewController *)caller
|
||||
successBlock:(VideoRecorderSuccessBlock)successBlock
|
||||
failureBlock:(VideoRecorderFailureBlock)failureBlock;
|
||||
|
||||
@end
|
||||
|
||||
@interface MultimediaRecorder : NSObject
|
||||
@property (nonatomic, strong) id<IMultimediaRecorder> advancedVideoRecorder;
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
+ (void)registerAdvancedVideoRecorder:(id<IMultimediaRecorder>)videoRecorder;
|
||||
|
||||
- (void)recordVideoWithCaller:(UIViewController *)caller
|
||||
successBlock:(VideoRecorderSuccessBlock)successBlock
|
||||
failureBlock:(VideoRecorderFailureBlock)failureBlock;
|
||||
|
||||
-(void)takePhoneWithCaller:(UIViewController *)caller
|
||||
successBlock:(VideoRecorderSuccessBlock)successBlock
|
||||
failureBlock:(VideoRecorderFailureBlock)failureBlock;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
44
TUIKit/TUIChat/CommonUI/Album/MultimediaRecorder.m
Normal file
44
TUIKit/TUIChat/CommonUI/Album/MultimediaRecorder.m
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// VideoRecorder.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by yiliangwang on 2024/10/30.
|
||||
// Copyright © 2024 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MultimediaRecorder.h"
|
||||
|
||||
@interface MultimediaRecorder ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation MultimediaRecorder
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static MultimediaRecorder *sharedInstance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[self alloc] init];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
+ (void)registerAdvancedVideoRecorder:(id<IMultimediaRecorder>)videoRecorder {
|
||||
[MultimediaRecorder sharedInstance].advancedVideoRecorder = videoRecorder;
|
||||
}
|
||||
|
||||
- (void)recordVideoWithCaller:(UIViewController *)caller
|
||||
successBlock:(VideoRecorderSuccessBlock)successBlock
|
||||
failureBlock:(VideoRecorderFailureBlock)failureBlock {
|
||||
id<IMultimediaRecorder> videoRecorder = nil;
|
||||
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
|
||||
videoRecorder = [MultimediaRecorder sharedInstance].advancedVideoRecorder;
|
||||
}
|
||||
|
||||
if (videoRecorder && [videoRecorder respondsToSelector:@selector
|
||||
(recordVideoWithCaller:successBlock:failureBlock:)]) {
|
||||
[videoRecorder recordVideoWithCaller:caller successBlock:successBlock failureBlock:failureBlock];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
20
TUIKit/TUIChat/CommonUI/Camera/TUICameraMacro.h
Normal file
20
TUIKit/TUIChat/CommonUI/Camera/TUICameraMacro.h
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, TUICameraMediaType) {
|
||||
TUICameraMediaTypePhoto = 1,
|
||||
TUICameraMediaTypeVideo = 2,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, TUICameraViewAspectRatio) {
|
||||
TUICameraViewAspectRatio1x1,
|
||||
TUICameraViewAspectRatio16x9,
|
||||
TUICameraViewAspectRatio5x4,
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
32
TUIKit/TUIChat/CommonUI/Camera/TUICameraManager.h
Normal file
32
TUIKit/TUIChat/CommonUI/Camera/TUICameraManager.h
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUICameraManager : NSObject
|
||||
|
||||
- (AVCaptureDeviceInput *)switchCamera:(AVCaptureSession *)session old:(AVCaptureDeviceInput *)oldinput new:(AVCaptureDeviceInput *)newinput;
|
||||
|
||||
- (id)resetFocusAndExposure:(AVCaptureDevice *)device;
|
||||
|
||||
- (id)zoom:(AVCaptureDevice *)device factor:(CGFloat)factor;
|
||||
|
||||
- (id)focus:(AVCaptureDevice *)device point:(CGPoint)point;
|
||||
|
||||
- (id)expose:(AVCaptureDevice *)device point:(CGPoint)point;
|
||||
|
||||
- (id)changeFlash:(AVCaptureDevice *)device mode:(AVCaptureFlashMode)mode;
|
||||
|
||||
- (id)changeTorch:(AVCaptureDevice *)device model:(AVCaptureTorchMode)mode;
|
||||
|
||||
- (AVCaptureFlashMode)flashMode:(AVCaptureDevice *)device;
|
||||
|
||||
- (AVCaptureTorchMode)torchMode:(AVCaptureDevice *)device;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
174
TUIKit/TUIChat/CommonUI/Camera/TUICameraManager.m
Normal file
174
TUIKit/TUIChat/CommonUI/Camera/TUICameraManager.m
Normal file
@@ -0,0 +1,174 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUICameraManager.h"
|
||||
|
||||
@implementation TUICameraManager
|
||||
|
||||
#pragma mark - - Switch Camera
|
||||
- (AVCaptureDeviceInput *)switchCamera:(AVCaptureSession *)session old:(AVCaptureDeviceInput *)oldinput new:(AVCaptureDeviceInput *)newinput {
|
||||
[session beginConfiguration];
|
||||
[session removeInput:oldinput];
|
||||
if ([session canAddInput:newinput]) {
|
||||
[session addInput:newinput];
|
||||
[session commitConfiguration];
|
||||
return newinput;
|
||||
} else {
|
||||
[session addInput:oldinput];
|
||||
[session commitConfiguration];
|
||||
return oldinput;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - - Zoom
|
||||
- (id)zoom:(AVCaptureDevice *)device factor:(CGFloat)factor {
|
||||
if (device.activeFormat.videoMaxZoomFactor > factor && factor >= 1.0) {
|
||||
NSError *error;
|
||||
if ([device lockForConfiguration:&error]) {
|
||||
[device rampToVideoZoomFactor:factor withRate:4.0];
|
||||
[device unlockForConfiguration];
|
||||
}
|
||||
return error;
|
||||
}
|
||||
return [self error:@"Unsupported zoom factor" code:2000];
|
||||
}
|
||||
|
||||
#pragma mark - - Focus
|
||||
- (id)focus:(AVCaptureDevice *)device point:(CGPoint)point {
|
||||
BOOL supported = [device isFocusPointOfInterestSupported] && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus];
|
||||
if (supported) {
|
||||
NSError *error;
|
||||
if ([device lockForConfiguration:&error]) {
|
||||
device.focusPointOfInterest = point;
|
||||
device.focusMode = AVCaptureFocusModeAutoFocus;
|
||||
[device unlockForConfiguration];
|
||||
}
|
||||
return error;
|
||||
}
|
||||
return [self error:@"Device does not support focus" code:2001];
|
||||
}
|
||||
|
||||
#pragma mark - - Expose
|
||||
static const NSString *CameraAdjustingExposureContext;
|
||||
- (id)expose:(AVCaptureDevice *)device point:(CGPoint)point {
|
||||
BOOL supported = [device isExposurePointOfInterestSupported] && [device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure];
|
||||
if (supported) {
|
||||
NSError *error;
|
||||
if ([device lockForConfiguration:&error]) {
|
||||
device.exposurePointOfInterest = point;
|
||||
device.exposureMode = AVCaptureExposureModeContinuousAutoExposure;
|
||||
if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
|
||||
[device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:&CameraAdjustingExposureContext];
|
||||
}
|
||||
[device unlockForConfiguration];
|
||||
}
|
||||
return error;
|
||||
}
|
||||
return [self error:@"Device does not support exposure" code:2002];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
||||
if (context == &CameraAdjustingExposureContext) {
|
||||
AVCaptureDevice *device = (AVCaptureDevice *)object;
|
||||
if (!device.isAdjustingExposure && [device isExposureModeSupported:AVCaptureExposureModeLocked]) {
|
||||
[object removeObserver:self forKeyPath:@"adjustingExposure" context:&CameraAdjustingExposureContext];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSError *error;
|
||||
if ([device lockForConfiguration:&error]) {
|
||||
device.exposureMode = AVCaptureExposureModeLocked;
|
||||
[device unlockForConfiguration];
|
||||
} else {
|
||||
NSLog(@"%@", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - - Auto focus, exposure
|
||||
- (id)resetFocusAndExposure:(AVCaptureDevice *)device {
|
||||
AVCaptureFocusMode focusMode = AVCaptureFocusModeContinuousAutoFocus;
|
||||
AVCaptureExposureMode exposureMode = AVCaptureExposureModeContinuousAutoExposure;
|
||||
BOOL canResetFocus = [device isFocusPointOfInterestSupported] && [device isFocusModeSupported:focusMode];
|
||||
BOOL canResetExposure = [device isExposurePointOfInterestSupported] && [device isExposureModeSupported:exposureMode];
|
||||
CGPoint centerPoint = CGPointMake(0.5f, 0.5f);
|
||||
NSError *error;
|
||||
if ([device lockForConfiguration:&error]) {
|
||||
if (canResetFocus) {
|
||||
device.focusMode = focusMode;
|
||||
device.focusPointOfInterest = centerPoint;
|
||||
}
|
||||
if (canResetExposure) {
|
||||
device.exposureMode = exposureMode;
|
||||
device.exposurePointOfInterest = centerPoint;
|
||||
}
|
||||
[device unlockForConfiguration];
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
#pragma mark - - Flash
|
||||
- (AVCaptureFlashMode)flashMode:(AVCaptureDevice *)device {
|
||||
return [device flashMode];
|
||||
}
|
||||
|
||||
- (id)changeFlash:(AVCaptureDevice *)device mode:(AVCaptureFlashMode)mode {
|
||||
if (![device hasFlash]) {
|
||||
return [self error:@"Flash is not supported" code:2003];
|
||||
}
|
||||
if ([self torchMode:device] == AVCaptureTorchModeOn) {
|
||||
[self setTorch:device model:AVCaptureTorchModeOff];
|
||||
}
|
||||
return [self setFlash:device mode:mode];
|
||||
}
|
||||
|
||||
- (id)setFlash:(AVCaptureDevice *)device mode:(AVCaptureFlashMode)mode {
|
||||
if ([device isFlashModeSupported:mode]) {
|
||||
NSError *error;
|
||||
if ([device lockForConfiguration:&error]) {
|
||||
device.flashMode = mode;
|
||||
[device unlockForConfiguration];
|
||||
}
|
||||
return error;
|
||||
}
|
||||
return [self error:@"Flash is not supported" code:2003];
|
||||
}
|
||||
|
||||
#pragma mark - - Flashlight
|
||||
- (AVCaptureTorchMode)torchMode:(AVCaptureDevice *)device {
|
||||
return [device torchMode];
|
||||
}
|
||||
|
||||
- (id)changeTorch:(AVCaptureDevice *)device model:(AVCaptureTorchMode)mode {
|
||||
if (![device hasTorch]) {
|
||||
return [self error:@"Flashlight not supported" code:2004];
|
||||
}
|
||||
if ([self flashMode:device] == AVCaptureFlashModeOn) {
|
||||
[self setFlash:device mode:AVCaptureFlashModeOff];
|
||||
}
|
||||
return [self setTorch:device model:mode];
|
||||
}
|
||||
|
||||
- (id)setTorch:(AVCaptureDevice *)device model:(AVCaptureTorchMode)mode {
|
||||
if ([device isTorchModeSupported:mode]) {
|
||||
NSError *error;
|
||||
if ([device lockForConfiguration:&error]) {
|
||||
device.torchMode = mode;
|
||||
[device unlockForConfiguration];
|
||||
}
|
||||
return error;
|
||||
}
|
||||
return [self error:@"Flashlight not supported" code:2004];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
- (NSError *)error:(NSString *)text code:(NSInteger)code {
|
||||
NSDictionary *desc = @{NSLocalizedDescriptionKey : text};
|
||||
NSError *error = [NSError errorWithDomain:@"com.tui.camera" code:code userInfo:desc];
|
||||
return error;
|
||||
}
|
||||
|
||||
@end
|
||||
86
TUIKit/TUIChat/CommonUI/Camera/TUICameraView.h
Normal file
86
TUIKit/TUIChat/CommonUI/Camera/TUICameraView.h
Normal file
@@ -0,0 +1,86 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUICameraMacro.h"
|
||||
#import "TUICaptureVideoPreviewView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TUICameraView;
|
||||
@protocol TUICameraViewDelegate <NSObject>
|
||||
@optional
|
||||
|
||||
/**
|
||||
* Flash
|
||||
*/
|
||||
- (void)flashLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
|
||||
|
||||
/**
|
||||
* Fill light
|
||||
*/
|
||||
- (void)torchLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
|
||||
|
||||
/**
|
||||
*
|
||||
* Switch camera
|
||||
*/
|
||||
- (void)swicthCameraAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
|
||||
|
||||
/**
|
||||
*
|
||||
* Auto focus and exposure
|
||||
*/
|
||||
- (void)autoFocusAndExposureAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
|
||||
|
||||
/**
|
||||
*
|
||||
* Foucus
|
||||
*/
|
||||
- (void)focusAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *error))handle;
|
||||
|
||||
/**
|
||||
*
|
||||
* Expose
|
||||
*/
|
||||
- (void)exposAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *error))handle;
|
||||
|
||||
/**
|
||||
*
|
||||
* Zoom
|
||||
*/
|
||||
- (void)zoomAction:(TUICameraView *)cameraView factor:(CGFloat)factor;
|
||||
|
||||
- (void)cancelAction:(TUICameraView *)cameraView;
|
||||
|
||||
- (void)pictureLibAction:(TUICameraView *)cameraView;
|
||||
|
||||
- (void)takePhotoAction:(TUICameraView *)cameraView;
|
||||
|
||||
- (void)stopRecordVideoAction:(TUICameraView *)cameraView RecordDuration:(CGFloat)duration;
|
||||
|
||||
- (void)startRecordVideoAction:(TUICameraView *)cameraView;
|
||||
|
||||
- (void)didChangeTypeAction:(TUICameraView *)cameraView type:(TUICameraMediaType)type;
|
||||
|
||||
@end
|
||||
|
||||
@interface TUICameraView : UIView
|
||||
|
||||
@property(nonatomic, weak) id<TUICameraViewDelegate> delegate;
|
||||
|
||||
@property(nonatomic, readonly) TUICaptureVideoPreviewView *previewView;
|
||||
|
||||
/// default TUICameraMediaTypePhoto
|
||||
@property(nonatomic) TUICameraMediaType type;
|
||||
|
||||
/// default TUICameraViewAspectRatio16x9
|
||||
@property(nonatomic) TUICameraViewAspectRatio aspectRatio;
|
||||
|
||||
/// default 15s
|
||||
@property(nonatomic, assign) CGFloat maxVideoCaptureTimeLimit;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
434
TUIKit/TUIChat/CommonUI/Camera/TUICameraView.m
Normal file
434
TUIKit/TUIChat/CommonUI/Camera/TUICameraView.m
Normal file
@@ -0,0 +1,434 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUICameraView.h"
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import "TUICaptureTimer.h"
|
||||
|
||||
static CGFloat gPhotoBtnZoomInRatio = 1.125;
|
||||
static CGFloat gProgressLayerLineWidth = 5.0;
|
||||
|
||||
@interface TUICameraView ()
|
||||
|
||||
@property(nonatomic) UIView *contentView;
|
||||
@property(nonatomic) UIButton *switchCameraButton;
|
||||
@property(nonatomic) UIButton *closeButton;
|
||||
@property(nonatomic, strong) UIButton *pictureLibButton;
|
||||
@property(nonatomic) UIView *focusView;
|
||||
@property(nonatomic) UISlider *slider;
|
||||
@property(nonatomic) UIView *photoBtn;
|
||||
@property(nonatomic) UIView *photoStateView;
|
||||
@property(nonatomic) UILongPressGestureRecognizer *longPress;
|
||||
@property(nonatomic) CGRect lastRect;
|
||||
|
||||
@property(nonatomic) CAShapeLayer *progressLayer;
|
||||
|
||||
@property(nonatomic) TUICaptureTimer *timer;
|
||||
@property(nonatomic) BOOL isVideoRecording;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUICameraView
|
||||
|
||||
@synthesize previewView = _previewView;
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.type = TUICameraMediaTypePhoto;
|
||||
self.aspectRatio = TUICameraViewAspectRatio16x9;
|
||||
self.backgroundColor = [UIColor blackColor];
|
||||
self.maxVideoCaptureTimeLimit = 15.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupUI {
|
||||
[self addSubview:self.contentView];
|
||||
[self.contentView addSubview:self.previewView];
|
||||
[self.contentView addSubview:self.switchCameraButton];
|
||||
[self.contentView addSubview:self.photoBtn];
|
||||
[self.contentView addSubview:self.closeButton];
|
||||
[self.contentView addSubview:self.pictureLibButton];
|
||||
[self.previewView addSubview:self.focusView];
|
||||
[self.previewView addSubview:self.slider];
|
||||
|
||||
self.timer = ({
|
||||
TUICaptureTimer *timer = [TUICaptureTimer new];
|
||||
timer.maxCaptureTime = self.maxVideoCaptureTimeLimit;
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
timer.progressBlock = ^(CGFloat ratio, CGFloat recordTime) {
|
||||
weakSelf.progress = ratio;
|
||||
};
|
||||
timer.progressFinishBlock = ^(CGFloat ratio, CGFloat recordTime) {
|
||||
weakSelf.progress = 1;
|
||||
self.longPress.enabled = NO;
|
||||
[weakSelf endVideoRecordWithCaptureDuration:recordTime];
|
||||
self.longPress.enabled = YES;
|
||||
};
|
||||
timer.progressCancelBlock = ^{
|
||||
weakSelf.progress = 0;
|
||||
};
|
||||
timer;
|
||||
});
|
||||
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
|
||||
[self.previewView addGestureRecognizer:tap];
|
||||
|
||||
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchAction:)];
|
||||
[self.previewView addGestureRecognizer:pinch];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
if (!CGRectEqualToRect(self.lastRect, self.bounds)) {
|
||||
[self setupUI];
|
||||
|
||||
self.lastRect = self.bounds;
|
||||
self.contentView.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
|
||||
|
||||
CGFloat previewViewWidth = self.contentView.bounds.size.width;
|
||||
CGFloat previewViewHeight;
|
||||
switch (self.aspectRatio) {
|
||||
case TUICameraViewAspectRatio1x1:
|
||||
previewViewHeight = previewViewWidth;
|
||||
break;
|
||||
case TUICameraViewAspectRatio16x9:
|
||||
previewViewHeight = previewViewWidth * (16.0 / 9.0);
|
||||
break;
|
||||
case TUICameraViewAspectRatio5x4:
|
||||
previewViewHeight = previewViewWidth * (5.0 / 4.0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
CGFloat previewViewY = (self.contentView.bounds.size.height - previewViewHeight) / 2.0;
|
||||
self.previewView.frame = CGRectMake(0, previewViewY, self.contentView.bounds.size.width, previewViewHeight);
|
||||
|
||||
CGFloat switchCameraButtonWidth = 44.0;
|
||||
self.switchCameraButton.frame =
|
||||
CGRectMake(self.contentView.bounds.size.width - switchCameraButtonWidth - 16.0, 30.0, switchCameraButtonWidth, switchCameraButtonWidth);
|
||||
|
||||
if (isRTL()) {
|
||||
[self.switchCameraButton resetFrameToFitRTL];
|
||||
}
|
||||
|
||||
CGFloat photoBtnWidth = 100.0;
|
||||
self.photoBtn.frame = CGRectMake((self.contentView.bounds.size.width - photoBtnWidth) / 2.0, self.contentView.bounds.size.height - photoBtnWidth - 30,
|
||||
photoBtnWidth, photoBtnWidth);
|
||||
self.photoBtn.layer.cornerRadius = photoBtnWidth / 2.0;
|
||||
|
||||
CGFloat distanceToPhotoBtn = 10.0;
|
||||
CGFloat photoStateViewWidth = photoBtnWidth - 2 * distanceToPhotoBtn;
|
||||
self.photoStateView.frame = CGRectMake(distanceToPhotoBtn, distanceToPhotoBtn, photoStateViewWidth, photoStateViewWidth);
|
||||
self.photoStateView.layer.cornerRadius = photoStateViewWidth / 2.0;
|
||||
|
||||
if (self.type == TUICameraMediaTypeVideo) {
|
||||
self.progressLayer.frame = CGRectInset(self.photoBtn.bounds, gProgressLayerLineWidth / 2.0, gProgressLayerLineWidth / 2.0);
|
||||
|
||||
CGFloat radius = self.progressLayer.bounds.size.width / 2;
|
||||
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
|
||||
radius:radius
|
||||
startAngle:-M_PI_2
|
||||
endAngle:-M_PI_2 + M_PI * 2
|
||||
clockwise:YES];
|
||||
self.progressLayer.path = path.CGPath;
|
||||
[self.photoBtn.layer addSublayer:self.progressLayer];
|
||||
}
|
||||
|
||||
CGFloat closeButtonWidth = 30.0;
|
||||
CGFloat closeButtonX = (self.photoBtn.frame.origin.x - closeButtonWidth) / 2.0;
|
||||
CGFloat closeButtonY = self.photoBtn.center.y - closeButtonWidth / 2.0;
|
||||
self.closeButton.frame = CGRectMake(closeButtonX, closeButtonY, closeButtonWidth, closeButtonWidth);
|
||||
if (isRTL()) {
|
||||
[self.closeButton resetFrameToFitRTL];
|
||||
}
|
||||
CGFloat pictureButtonWidth = 30.0;
|
||||
|
||||
self.pictureLibButton.frame = CGRectMake(self.contentView.frame.size.width - closeButtonX, closeButtonY, pictureButtonWidth, pictureButtonWidth);
|
||||
if (isRTL()) {
|
||||
[self.pictureLibButton resetFrameToFitRTL];
|
||||
}
|
||||
self.slider.transform = CGAffineTransformMakeRotation(M_PI_2);
|
||||
self.slider.frame = CGRectMake(self.bounds.size.width - 50, 50, 15, 200);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)setProgress:(CGFloat)progress {
|
||||
if (progress < 0) {
|
||||
return;
|
||||
} else if (progress < 1.0) {
|
||||
self.progressLayer.strokeEnd = progress;
|
||||
}
|
||||
|
||||
if (progress >= 1.0) {
|
||||
self.progressLayer.strokeEnd = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Event Response
|
||||
- (void)tapAction:(UIGestureRecognizer *)tap {
|
||||
if ([_delegate respondsToSelector:@selector(focusAction:point:handle:)]) {
|
||||
CGPoint point = [tap locationInView:self.previewView];
|
||||
[self runFocusAnimation:self.focusView point:point];
|
||||
[_delegate focusAction:self
|
||||
point:[self.previewView captureDevicePointForPoint:point]
|
||||
handle:^(NSError *error) {
|
||||
if (error) NSAssert1(NO, @"%@", error); //[self showError:error];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)runFocusAnimation:(UIView *)view point:(CGPoint)point {
|
||||
view.center = point;
|
||||
view.hidden = NO;
|
||||
[UIView animateWithDuration:0.15f
|
||||
delay:0.0f
|
||||
options:UIViewAnimationOptionCurveEaseInOut
|
||||
animations:^{
|
||||
view.layer.transform = CATransform3DMakeScale(0.5, 0.5, 1.0);
|
||||
}
|
||||
completion:^(BOOL complete) {
|
||||
double delayInSeconds = 0.5f;
|
||||
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
|
||||
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
|
||||
view.hidden = YES;
|
||||
view.transform = CGAffineTransformIdentity;
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)pinchAction:(UIPinchGestureRecognizer *)pinch {
|
||||
if ([_delegate respondsToSelector:@selector(zoomAction:factor:)]) {
|
||||
if (pinch.state == UIGestureRecognizerStateBegan) {
|
||||
[UIView animateWithDuration:0.1
|
||||
animations:^{
|
||||
self->_slider.alpha = 1;
|
||||
}];
|
||||
} else if (pinch.state == UIGestureRecognizerStateChanged) {
|
||||
if (pinch.velocity > 0) {
|
||||
_slider.value += pinch.velocity / 100;
|
||||
} else {
|
||||
_slider.value += pinch.velocity / 20;
|
||||
}
|
||||
[_delegate zoomAction:self factor:powf(5, _slider.value)];
|
||||
} else {
|
||||
[UIView animateWithDuration:0.1
|
||||
animations:^{
|
||||
self->_slider.alpha = 0.0;
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)switchCameraButtonClick:(UIButton *)btn {
|
||||
if ([self.delegate respondsToSelector:@selector(swicthCameraAction:handle:)]) {
|
||||
[self.delegate swicthCameraAction:self
|
||||
handle:^(NSError *_Nonnull error){
|
||||
//
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeButtonClick:(UIButton *)btn {
|
||||
if ([self.delegate respondsToSelector:@selector(cancelAction:)]) {
|
||||
[self.delegate cancelAction:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pictureLibClick:(UIButton *)btn {
|
||||
if ([self.delegate respondsToSelector:@selector(pictureLibAction:)]) {
|
||||
[self.delegate pictureLibAction:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)longPressGesture:(UILongPressGestureRecognizer *)gesture {
|
||||
switch (gesture.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
[self beginVideoRecord];
|
||||
break;
|
||||
}
|
||||
case UIGestureRecognizerStateChanged: {
|
||||
break;
|
||||
}
|
||||
case UIGestureRecognizerStateEnded: {
|
||||
[self endVideoRecordWithCaptureDuration:self.timer.captureDuration];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)beginVideoRecord {
|
||||
if (self.isVideoRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.closeButton.hidden = YES;
|
||||
|
||||
self.isVideoRecording = YES;
|
||||
|
||||
self.pictureLibButton.hidden = YES;
|
||||
|
||||
[self.timer startTimer];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.progressLayer.strokeEnd = 0.0;
|
||||
[UIView animateWithDuration:0.2
|
||||
animations:^{
|
||||
self.photoStateView.transform = CGAffineTransformMakeScale(.5, .5);
|
||||
self.photoBtn.transform = CGAffineTransformMakeScale(gPhotoBtnZoomInRatio, gPhotoBtnZoomInRatio);
|
||||
}];
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(startRecordVideoAction:)]) {
|
||||
[self.delegate startRecordVideoAction:self];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)endVideoRecordWithCaptureDuration:(CGFloat)duration {
|
||||
if (self.isVideoRecording == NO) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.closeButton.hidden = NO;
|
||||
|
||||
self.isVideoRecording = NO;
|
||||
|
||||
self.pictureLibButton.hidden = NO;
|
||||
|
||||
[self.timer stopTimer];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[UIView animateWithDuration:0.2
|
||||
animations:^{
|
||||
self.photoStateView.transform = CGAffineTransformIdentity;
|
||||
self.photoBtn.transform = CGAffineTransformIdentity;
|
||||
}];
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(stopRecordVideoAction:RecordDuration:)]) {
|
||||
[self.delegate stopRecordVideoAction:self RecordDuration:duration];
|
||||
}
|
||||
|
||||
self.progressLayer.strokeEnd = 0.0;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)tapGesture:(UITapGestureRecognizer *)tapGesture {
|
||||
if ([_delegate respondsToSelector:@selector(takePhotoAction:)]) {
|
||||
[_delegate takePhotoAction:self];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Getters & Setters
|
||||
- (UIView *)contentView {
|
||||
if (!_contentView) {
|
||||
_contentView = [UIView new];
|
||||
}
|
||||
return _contentView;
|
||||
}
|
||||
|
||||
- (TUICaptureVideoPreviewView *)previewView {
|
||||
if (!_previewView) {
|
||||
_previewView = [[TUICaptureVideoPreviewView alloc] init];
|
||||
_previewView.userInteractionEnabled = YES;
|
||||
}
|
||||
return _previewView;
|
||||
}
|
||||
|
||||
- (UIButton *)switchCameraButton {
|
||||
if (!_switchCameraButton) {
|
||||
_switchCameraButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
UIImage *switchCameraButtonImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_switchCamera")];
|
||||
[_switchCameraButton setImage:switchCameraButtonImage forState:UIControlStateNormal];
|
||||
[_switchCameraButton addTarget:self action:@selector(switchCameraButtonClick:) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _switchCameraButton;
|
||||
}
|
||||
|
||||
- (UIButton *)closeButton {
|
||||
if (!_closeButton) {
|
||||
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
UIImage *closeButtonImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_back")];
|
||||
[_closeButton setBackgroundImage:[closeButtonImage rtl_imageFlippedForRightToLeftLayoutDirection] forState:UIControlStateNormal];
|
||||
[_closeButton addTarget:self action:@selector(closeButtonClick:) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _closeButton;
|
||||
}
|
||||
|
||||
- (UIButton *)pictureLibButton {
|
||||
if (!_pictureLibButton) {
|
||||
_pictureLibButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
UIImage *pictureImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"more_picture")];
|
||||
[_pictureLibButton setBackgroundImage:pictureImage forState:UIControlStateNormal];
|
||||
[_pictureLibButton addTarget:self action:@selector(pictureLibClick:) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _pictureLibButton;
|
||||
}
|
||||
|
||||
- (UIView *)photoBtn {
|
||||
if (!_photoBtn) {
|
||||
_photoBtn = [UIView new];
|
||||
[_photoBtn setBackgroundColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5]];
|
||||
if (self.type == TUICameraMediaTypeVideo) {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGesture:)];
|
||||
[_photoBtn addGestureRecognizer:longPress];
|
||||
_longPress = longPress;
|
||||
}
|
||||
if (self.type == TUICameraMediaTypePhoto) {
|
||||
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
|
||||
[_photoBtn addGestureRecognizer:tapGesture];
|
||||
}
|
||||
_photoBtn.userInteractionEnabled = YES;
|
||||
|
||||
_photoStateView = [UIView new];
|
||||
_photoStateView.backgroundColor = [UIColor whiteColor];
|
||||
[_photoBtn addSubview:_photoStateView];
|
||||
}
|
||||
return _photoBtn;
|
||||
}
|
||||
|
||||
- (CAShapeLayer *)progressLayer {
|
||||
if (!_progressLayer) {
|
||||
_progressLayer = [CAShapeLayer layer];
|
||||
_progressLayer.fillColor = [UIColor clearColor].CGColor;
|
||||
_progressLayer.lineWidth = gProgressLayerLineWidth;
|
||||
_progressLayer.strokeColor = [UIColor colorWithRed:0 green:204.0 / 255 blue:0 alpha:1].CGColor;
|
||||
_progressLayer.strokeStart = 0;
|
||||
_progressLayer.strokeEnd = 0;
|
||||
_progressLayer.lineCap = kCALineCapButt;
|
||||
}
|
||||
return _progressLayer;
|
||||
}
|
||||
|
||||
- (UIView *)focusView {
|
||||
if (_focusView == nil) {
|
||||
_focusView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 150, 150.0f)];
|
||||
_focusView.backgroundColor = [UIColor clearColor];
|
||||
_focusView.layer.borderColor = [UIColor colorWithRed:0 green:204.0 / 255 blue:0 alpha:1].CGColor;
|
||||
_focusView.layer.borderWidth = 3.0f;
|
||||
_focusView.hidden = YES;
|
||||
}
|
||||
return _focusView;
|
||||
}
|
||||
|
||||
- (UISlider *)slider {
|
||||
if (_slider == nil) {
|
||||
_slider = [[UISlider alloc] init];
|
||||
_slider.minimumValue = 0;
|
||||
_slider.maximumValue = 1;
|
||||
_slider.maximumTrackTintColor = [UIColor whiteColor];
|
||||
_slider.minimumTrackTintColor = [UIColor whiteColor];
|
||||
_slider.alpha = 0.0;
|
||||
_slider.hidden = YES;
|
||||
}
|
||||
return _slider;
|
||||
}
|
||||
|
||||
@end
|
||||
32
TUIKit/TUIChat/CommonUI/Camera/TUICameraViewController.h
Normal file
32
TUIKit/TUIChat/CommonUI/Camera/TUICameraViewController.h
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
@import UIKit;
|
||||
#import "TUICameraMacro.h"
|
||||
|
||||
@class TUICameraViewController;
|
||||
@protocol TUICameraViewControllerDelegate <NSObject>
|
||||
|
||||
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithVideoURL:(NSURL *)url;
|
||||
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithImageData:(NSData *)data;
|
||||
- (void)cameraViewControllerDidCancel:(TUICameraViewController *)controller;
|
||||
- (void)cameraViewControllerDidPictureLib:(TUICameraViewController *)controller finishCallback:(void (^)(void))callback;
|
||||
@end
|
||||
|
||||
@interface TUICameraViewController : UIViewController
|
||||
|
||||
@property(nonatomic, weak) id<TUICameraViewControllerDelegate> delegate;
|
||||
|
||||
/// default TUICameraMediaTypePhoto
|
||||
@property(nonatomic) TUICameraMediaType type;
|
||||
|
||||
/// default TUICameraViewAspectRatio16x9
|
||||
@property(nonatomic) TUICameraViewAspectRatio aspectRatio;
|
||||
|
||||
/// default 15s
|
||||
@property(nonatomic) NSTimeInterval videoMaximumDuration;
|
||||
/// default 3s
|
||||
@property(nonatomic) NSTimeInterval videoMinimumDuration;
|
||||
|
||||
@end
|
||||
413
TUIKit/TUIChat/CommonUI/Camera/TUICameraViewController.m
Normal file
413
TUIKit/TUIChat/CommonUI/Camera/TUICameraViewController.m
Normal file
@@ -0,0 +1,413 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUICameraViewController.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <AssetsLibrary/AssetsLibrary.h>
|
||||
#import <CoreMedia/CMMetadata.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
#import "TUICameraView.h"
|
||||
#import "TUICaptureImagePreviewController.h"
|
||||
#import "TUICaptureVideoPreviewViewController.h"
|
||||
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import "TUICameraManager.h"
|
||||
#import "TUICaptureTimer.h"
|
||||
#import "TUIMotionManager.h"
|
||||
#import "TUIMovieManager.h"
|
||||
|
||||
@interface TUICameraViewController () <AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate, TUICameraViewDelegate> {
|
||||
AVCaptureSession *_session;
|
||||
AVCaptureDeviceInput *_deviceInput;
|
||||
|
||||
AVCaptureConnection *_videoConnection;
|
||||
AVCaptureConnection *_audioConnection;
|
||||
AVCaptureVideoDataOutput *_videoOutput;
|
||||
AVCaptureStillImageOutput *_imageOutput;
|
||||
|
||||
BOOL _recording;
|
||||
}
|
||||
|
||||
@property(nonatomic, strong) TUICameraView *cameraView;
|
||||
@property(nonatomic, strong) TUIMovieManager *movieManager;
|
||||
@property(nonatomic, strong) TUICameraManager *cameraManager;
|
||||
@property(nonatomic, strong) TUIMotionManager *motionManager;
|
||||
@property(nonatomic, strong) AVCaptureDevice *activeCamera;
|
||||
@property(nonatomic, strong) AVCaptureDevice *inactiveCamera;
|
||||
|
||||
@property(nonatomic) BOOL isFirstShow;
|
||||
@property(nonatomic) BOOL lastPageBarHidden;
|
||||
@end
|
||||
|
||||
@implementation TUICameraViewController
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_motionManager = [[TUIMotionManager alloc] init];
|
||||
_cameraManager = [[TUICameraManager alloc] init];
|
||||
|
||||
_type = TUICameraMediaTypePhoto;
|
||||
_aspectRatio = TUICameraViewAspectRatio16x9;
|
||||
_videoMaximumDuration = 15.0;
|
||||
_videoMinimumDuration = 3.0;
|
||||
_isFirstShow = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.cameraView = [[TUICameraView alloc] initWithFrame:self.view.bounds];
|
||||
self.cameraView.type = self.type;
|
||||
self.cameraView.aspectRatio = self.aspectRatio;
|
||||
self.cameraView.delegate = self;
|
||||
self.cameraView.maxVideoCaptureTimeLimit = self.videoMaximumDuration;
|
||||
[self.view addSubview:self.cameraView];
|
||||
|
||||
NSError *error;
|
||||
[self setupSession:&error];
|
||||
if (!error) {
|
||||
[self.cameraView.previewView setCaptureSessionsion:_session];
|
||||
[self startCaptureSession];
|
||||
} else {
|
||||
// NSAssert1(NO, @"Camera Initialize Failed : %@", error.localizedDescription);
|
||||
// [self showErrorStr:error.localizedDescription];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self stopCaptureSession];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (self.isFirstShow) {
|
||||
self.isFirstShow = NO;
|
||||
self.lastPageBarHidden = self.navigationController.navigationBarHidden;
|
||||
}
|
||||
self.navigationController.navigationBarHidden = YES;
|
||||
}
|
||||
|
||||
- (void)willMoveToParentViewController:(UIViewController *)parent {
|
||||
[super willMoveToParentViewController:parent];
|
||||
if (!parent) {
|
||||
self.navigationController.navigationBarHidden = self.lastPageBarHidden;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - - Input Device
|
||||
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
|
||||
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
|
||||
for (AVCaptureDevice *device in devices) {
|
||||
if (device.position == position) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (AVCaptureDevice *)activeCamera {
|
||||
return _deviceInput.device;
|
||||
}
|
||||
|
||||
- (AVCaptureDevice *)inactiveCamera {
|
||||
AVCaptureDevice *device = nil;
|
||||
if ([[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count] > 1) {
|
||||
if ([self activeCamera].position == AVCaptureDevicePositionBack) {
|
||||
device = [self cameraWithPosition:AVCaptureDevicePositionFront];
|
||||
} else {
|
||||
device = [self cameraWithPosition:AVCaptureDevicePositionBack];
|
||||
}
|
||||
}
|
||||
return device;
|
||||
}
|
||||
|
||||
#pragma mark - - Configuration
|
||||
- (void)setupSession:(NSError **)error {
|
||||
_session = [[AVCaptureSession alloc] init];
|
||||
_session.sessionPreset = AVCaptureSessionPresetHigh;
|
||||
|
||||
[self setupSessionInputs:error];
|
||||
[self setupSessionOutputs:error];
|
||||
}
|
||||
|
||||
- (void)setupSessionInputs:(NSError **)error {
|
||||
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
||||
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
|
||||
if (videoInput) {
|
||||
if ([_session canAddInput:videoInput]) {
|
||||
[_session addInput:videoInput];
|
||||
}
|
||||
}
|
||||
_deviceInput = videoInput;
|
||||
if (_type == TUICameraMediaTypeVideo) {
|
||||
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
||||
AVCaptureDeviceInput *audioIn = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:error];
|
||||
if ([_session canAddInput:audioIn]) {
|
||||
[_session addInput:audioIn];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupSessionOutputs:(NSError **)error {
|
||||
dispatch_queue_t captureQueue = dispatch_queue_create("com.tui.captureQueue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
AVCaptureVideoDataOutput *videoOut = [[AVCaptureVideoDataOutput alloc] init];
|
||||
[videoOut setAlwaysDiscardsLateVideoFrames:YES];
|
||||
[videoOut setVideoSettings:@{(id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_32BGRA]}];
|
||||
[videoOut setSampleBufferDelegate:self queue:captureQueue];
|
||||
if ([_session canAddOutput:videoOut]) {
|
||||
[_session addOutput:videoOut];
|
||||
}
|
||||
_videoOutput = videoOut;
|
||||
_videoConnection = [videoOut connectionWithMediaType:AVMediaTypeVideo];
|
||||
|
||||
if (_type == TUICameraMediaTypeVideo) {
|
||||
AVCaptureAudioDataOutput *audioOut = [[AVCaptureAudioDataOutput alloc] init];
|
||||
[audioOut setSampleBufferDelegate:self queue:captureQueue];
|
||||
if ([_session canAddOutput:audioOut]) {
|
||||
[_session addOutput:audioOut];
|
||||
}
|
||||
_audioConnection = [audioOut connectionWithMediaType:AVMediaTypeAudio];
|
||||
}
|
||||
|
||||
AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init];
|
||||
imageOutput.outputSettings = @{AVVideoCodecKey : AVVideoCodecJPEG};
|
||||
if ([_session canAddOutput:imageOutput]) {
|
||||
[_session addOutput:imageOutput];
|
||||
}
|
||||
_imageOutput = imageOutput;
|
||||
}
|
||||
|
||||
#pragma mark - - Session Control
|
||||
- (void)startCaptureSession {
|
||||
if (!_session.isRunning) {
|
||||
[_session startRunning];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopCaptureSession {
|
||||
if (_session.isRunning) {
|
||||
[_session stopRunning];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - - Camera Operation
|
||||
- (void)zoomAction:(TUICameraView *)cameraView factor:(CGFloat)factor {
|
||||
NSError *error = [_cameraManager zoom:[self activeCamera] factor:factor];
|
||||
if (error) NSLog(@"%@", error);
|
||||
}
|
||||
|
||||
- (void)focusAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *))handle {
|
||||
NSError *error = [_cameraManager focus:[self activeCamera] point:point];
|
||||
handle(error);
|
||||
NSLog(@"%f", [self activeCamera].activeFormat.videoMaxZoomFactor);
|
||||
}
|
||||
|
||||
- (void)exposAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *))handle {
|
||||
NSError *error = [_cameraManager expose:[self activeCamera] point:point];
|
||||
handle(error);
|
||||
}
|
||||
|
||||
- (void)autoFocusAndExposureAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
|
||||
NSError *error = [_cameraManager resetFocusAndExposure:[self activeCamera]];
|
||||
handle(error);
|
||||
}
|
||||
|
||||
- (void)flashLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
|
||||
BOOL on = [_cameraManager flashMode:[self activeCamera]] == AVCaptureFlashModeOn;
|
||||
AVCaptureFlashMode mode = on ? AVCaptureFlashModeOff : AVCaptureFlashModeOn;
|
||||
NSError *error = [_cameraManager changeFlash:[self activeCamera] mode:mode];
|
||||
handle(error);
|
||||
}
|
||||
|
||||
- (void)torchLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
|
||||
BOOL on = [_cameraManager torchMode:[self activeCamera]] == AVCaptureTorchModeOn;
|
||||
AVCaptureTorchMode mode = on ? AVCaptureTorchModeOff : AVCaptureTorchModeOn;
|
||||
NSError *error = [_cameraManager changeTorch:[self activeCamera] model:mode];
|
||||
handle(error);
|
||||
}
|
||||
|
||||
- (void)swicthCameraAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
|
||||
NSError *error;
|
||||
AVCaptureDevice *videoDevice = [self inactiveCamera];
|
||||
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
|
||||
if (videoInput) {
|
||||
CATransition *animation = [CATransition animation];
|
||||
animation.type = @"oglFlip";
|
||||
animation.subtype = kCATransitionFromLeft;
|
||||
animation.duration = 0.5;
|
||||
[self.cameraView.previewView.layer addAnimation:animation forKey:@"flip"];
|
||||
|
||||
AVCaptureFlashMode mode = [_cameraManager flashMode:[self activeCamera]];
|
||||
|
||||
_deviceInput = [_cameraManager switchCamera:_session old:_deviceInput new:videoInput];
|
||||
|
||||
_videoConnection = [_videoOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
|
||||
[_cameraManager changeFlash:[self activeCamera] mode:mode];
|
||||
}
|
||||
handle(error);
|
||||
}
|
||||
|
||||
#pragma mark - - Taking Photo
|
||||
- (void)takePhotoAction:(TUICameraView *)cameraView {
|
||||
AVCaptureConnection *connection = [_imageOutput connectionWithMediaType:AVMediaTypeVideo];
|
||||
if (connection.isVideoOrientationSupported) {
|
||||
connection.videoOrientation = [self currentVideoOrientation];
|
||||
}
|
||||
[_imageOutput captureStillImageAsynchronouslyFromConnection:connection
|
||||
completionHandler:^(CMSampleBufferRef _Nullable imageDataSampleBuffer, NSError *_Nullable error) {
|
||||
if (error) {
|
||||
[self showErrorStr:error.localizedDescription];
|
||||
return;
|
||||
}
|
||||
NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
|
||||
UIImage *image = [[UIImage alloc] initWithData:imageData];
|
||||
TUICaptureImagePreviewController *vc = [[TUICaptureImagePreviewController alloc] initWithImage:image];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
vc.commitBlock = ^{
|
||||
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
||||
UIGraphicsBeginImageContext(CGSizeMake(image.size.width, image.size.height));
|
||||
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
|
||||
UIImage *convertToUpImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
NSData *data = UIImageJPEGRepresentation(convertToUpImage, 0.75);
|
||||
[strongSelf.delegate cameraViewController:strongSelf didFinishPickingMediaWithImageData:data];
|
||||
[strongSelf popViewControllerAnimated:YES];
|
||||
};
|
||||
vc.cancelBlock = ^{
|
||||
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
||||
[strongSelf.navigationController popViewControllerAnimated:YES];
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)cancelAction:(TUICameraView *)cameraView {
|
||||
[self.delegate cameraViewControllerDidCancel:self];
|
||||
|
||||
[self popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
- (void)pictureLibAction:(TUICameraView *)cameraView {
|
||||
@weakify(self);
|
||||
[self.delegate cameraViewControllerDidPictureLib:self
|
||||
finishCallback:^{
|
||||
@strongify(self);
|
||||
[self popViewControllerAnimated:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - - Record
|
||||
- (void)startRecordVideoAction:(TUICameraView *)cameraView {
|
||||
/**
|
||||
* Recreate each time to avoid Crash caused by unreleased previous information
|
||||
*/
|
||||
_movieManager = [[TUIMovieManager alloc] init];
|
||||
_recording = YES;
|
||||
_movieManager.currentDevice = [self activeCamera];
|
||||
_movieManager.currentOrientation = [self currentVideoOrientation];
|
||||
@weakify(self);
|
||||
[_movieManager start:^(NSError *_Nonnull error) {
|
||||
@strongify(self);
|
||||
@weakify(self);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@strongify(self);
|
||||
if (error) [self showErrorStr:error.localizedDescription];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)stopRecordVideoAction:(TUICameraView *)cameraView RecordDuration:(CGFloat)duration {
|
||||
_recording = NO;
|
||||
@weakify(self);
|
||||
[_movieManager stop:^(NSURL *_Nonnull url, NSError *_Nonnull error) {
|
||||
@strongify(self);
|
||||
@weakify(self);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@strongify(self);
|
||||
if (duration < self.videoMinimumDuration) {
|
||||
[self showErrorStr:TIMCommonLocalizableString(TUIKitMoreVideoCaptureDurationTip)];
|
||||
} else if (error) {
|
||||
[self showErrorStr:error.localizedDescription];
|
||||
} else {
|
||||
TUICaptureVideoPreviewViewController *videoPreviewController = [[TUICaptureVideoPreviewViewController alloc] initWithVideoURL:url];
|
||||
[self.navigationController pushViewController:videoPreviewController animated:YES];
|
||||
@weakify(self);
|
||||
videoPreviewController.commitBlock = ^{
|
||||
@strongify(self);
|
||||
[self.delegate cameraViewController:self didFinishPickingMediaWithVideoURL:url];
|
||||
[self popViewControllerAnimated:YES];
|
||||
};
|
||||
videoPreviewController.cancelBlock = ^{
|
||||
@strongify(self);
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
};
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - - AVCaptureVideoDataOutputSampleBufferDelegate
|
||||
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
|
||||
if (_recording) {
|
||||
[_movieManager writeData:connection video:_videoConnection audio:_audioConnection buffer:sampleBuffer];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - - Others
|
||||
- (AVCaptureVideoOrientation)currentVideoOrientation {
|
||||
AVCaptureVideoOrientation orientation;
|
||||
switch (self.motionManager.deviceOrientation) {
|
||||
case UIDeviceOrientationPortrait:
|
||||
orientation = AVCaptureVideoOrientationPortrait;
|
||||
break;
|
||||
case UIDeviceOrientationLandscapeLeft:
|
||||
orientation = AVCaptureVideoOrientationLandscapeRight;
|
||||
break;
|
||||
case UIDeviceOrientationLandscapeRight:
|
||||
orientation = AVCaptureVideoOrientationLandscapeLeft;
|
||||
break;
|
||||
case UIDeviceOrientationPortraitUpsideDown:
|
||||
orientation = AVCaptureVideoOrientationPortraitUpsideDown;
|
||||
break;
|
||||
default:
|
||||
orientation = AVCaptureVideoOrientationPortrait;
|
||||
break;
|
||||
}
|
||||
return orientation;
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
- (void)popViewControllerAnimated:(BOOL)animated {
|
||||
NSUInteger index = [self.navigationController.viewControllers indexOfObject:self];
|
||||
index--;
|
||||
UIViewController *lastVC = nil;
|
||||
if (index > 0 && index < self.navigationController.viewControllers.count) {
|
||||
lastVC = self.navigationController.viewControllers[index];
|
||||
}
|
||||
|
||||
self.navigationController.navigationBarHidden = self.lastPageBarHidden;
|
||||
|
||||
if (lastVC) {
|
||||
[self.navigationController popToViewController:lastVC animated:animated];
|
||||
} else {
|
||||
[self.navigationController popViewControllerAnimated:animated];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showErrorStr:(NSString *)errStr {
|
||||
[TUITool makeToast:errStr duration:1 position:CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
@import UIKit;
|
||||
|
||||
@interface TUICaptureImagePreviewController : UIViewController
|
||||
|
||||
@property(nonatomic) void (^commitBlock)(void);
|
||||
@property(nonatomic) void (^cancelBlock)(void);
|
||||
|
||||
- (instancetype)initWithImage:(UIImage *)image NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUICaptureImagePreviewController.h"
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
|
||||
@interface TUICaptureImagePreviewController () {
|
||||
UIImage *_image;
|
||||
}
|
||||
|
||||
@property(nonatomic) UIImageView *imageView;
|
||||
@property(nonatomic) UIButton *commitButton;
|
||||
@property(nonatomic) UIButton *cancelButton;
|
||||
@property(nonatomic) CGRect lastRect;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUICaptureImagePreviewController
|
||||
|
||||
- (instancetype)initWithImage:(UIImage *)image {
|
||||
if (self = [super initWithNibName:nil bundle:nil]) {
|
||||
_image = image;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor blackColor];
|
||||
|
||||
UIImageView *imageView = [[UIImageView alloc] initWithImage:_image];
|
||||
imageView.layer.masksToBounds = YES;
|
||||
imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
[self.view addSubview:imageView];
|
||||
self.imageView = imageView;
|
||||
NSLog(@"%ld--%ld", (long)_image.imageOrientation, UIImageOrientationUp);
|
||||
|
||||
self.commitButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
UIImage *commitImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commit")];
|
||||
[self.commitButton setImage:commitImage forState:UIControlStateNormal];
|
||||
UIImage *commitBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commitBg")];
|
||||
[self.commitButton setBackgroundImage:commitBGImage forState:UIControlStateNormal];
|
||||
[self.commitButton addTarget:self action:@selector(commitButtonClick:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:self.commitButton];
|
||||
|
||||
self.cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
UIImage *cancelButtonBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_cancel")];
|
||||
[self.cancelButton setBackgroundImage:cancelButtonBGImage forState:UIControlStateNormal];
|
||||
[self.cancelButton addTarget:self action:@selector(cancelButtonClick:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:self.cancelButton];
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
[super viewWillLayoutSubviews];
|
||||
|
||||
if (!CGRectEqualToRect(self.lastRect, self.view.bounds)) {
|
||||
self.lastRect = self.view.bounds;
|
||||
|
||||
self.imageView.frame = self.view.bounds;
|
||||
|
||||
CGFloat commitButtonWidth = 80.0;
|
||||
CGFloat buttonDistance = (self.view.bounds.size.width - 2 * commitButtonWidth) / 3.0;
|
||||
CGFloat commitButtonY = self.view.bounds.size.height - commitButtonWidth - 50.0;
|
||||
CGFloat commitButtonX = 2 * buttonDistance + commitButtonWidth;
|
||||
self.commitButton.frame = CGRectMake(commitButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
|
||||
|
||||
CGFloat cancelButtonX = commitButtonWidth;
|
||||
self.cancelButton.frame = CGRectMake(cancelButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
|
||||
|
||||
if (isRTL()) {
|
||||
[self.commitButton resetFrameToFitRTL];
|
||||
[self.cancelButton resetFrameToFitRTL];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)commitButtonClick:(UIButton *)btn {
|
||||
if (self.commitBlock) {
|
||||
self.commitBlock();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelButtonClick:(UIButton *)btn {
|
||||
if (self.cancelBlock) {
|
||||
self.cancelBlock();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
24
TUIKit/TUIChat/CommonUI/Camera/TUICaptureTimer.h
Normal file
24
TUIKit/TUIChat/CommonUI/Camera/TUICaptureTimer.h
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUICaptureTimer : NSObject
|
||||
@property(nonatomic, assign) CGFloat maxCaptureTime;
|
||||
@property(nonatomic, assign, readonly) CGFloat captureDuration;
|
||||
|
||||
@property(nonatomic, copy) void (^progressBlock)(CGFloat ratio, CGFloat recordTime);
|
||||
@property(nonatomic, copy) void (^progressCancelBlock)(void);
|
||||
@property(nonatomic, copy) void (^progressFinishBlock)(CGFloat ratio, CGFloat recordTime);
|
||||
|
||||
- (void)startTimer;
|
||||
- (void)stopTimer;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
77
TUIKit/TUIChat/CommonUI/Camera/TUICaptureTimer.m
Normal file
77
TUIKit/TUIChat/CommonUI/Camera/TUICaptureTimer.m
Normal file
@@ -0,0 +1,77 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUICaptureTimer.h"
|
||||
|
||||
@interface TUICaptureTimer ()
|
||||
@property(nonatomic, strong) dispatch_source_t gcdTimer;
|
||||
@property(nonatomic, assign) CGFloat captureDuration;
|
||||
@end
|
||||
|
||||
@implementation TUICaptureTimer
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.maxCaptureTime = 15.0f;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)startTimer {
|
||||
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
|
||||
|
||||
NSTimeInterval delayTime = 0.f;
|
||||
NSTimeInterval timeInterval = 0.1f;
|
||||
dispatch_time_t startDelayTime = dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(delayTime * NSEC_PER_SEC));
|
||||
dispatch_source_set_timer(self.gcdTimer, startDelayTime, timeInterval * NSEC_PER_SEC, timeInterval * NSEC_PER_SEC);
|
||||
|
||||
dispatch_source_set_event_handler(self.gcdTimer, ^{
|
||||
self.captureDuration += timeInterval;
|
||||
/**
|
||||
* Updating UI on the main thread
|
||||
*/
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.progressBlock) {
|
||||
self.progressBlock(self.captureDuration / self.maxCaptureTime, self.captureDuration);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Fnish
|
||||
*/
|
||||
if (self.captureDuration >= self.maxCaptureTime) {
|
||||
/**
|
||||
* Invalid timer
|
||||
*/
|
||||
CGFloat ratio = self.captureDuration / self.maxCaptureTime;
|
||||
CGFloat recordTime = self.captureDuration;
|
||||
[self cancel];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.progressFinishBlock) self.progressFinishBlock(ratio, recordTime);
|
||||
});
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Start the task. After the GCD timer is created, it needs to be started manually
|
||||
*/
|
||||
dispatch_resume(self.gcdTimer);
|
||||
}
|
||||
|
||||
- (void)stopTimer {
|
||||
[self cancel];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.progressCancelBlock) self.progressCancelBlock();
|
||||
});
|
||||
}
|
||||
|
||||
- (void)cancel {
|
||||
if (self.gcdTimer) {
|
||||
dispatch_source_cancel(self.gcdTimer);
|
||||
self.gcdTimer = nil;
|
||||
}
|
||||
self.captureDuration = 0;
|
||||
}
|
||||
|
||||
@end
|
||||
14
TUIKit/TUIChat/CommonUI/Camera/TUICaptureVideoPreviewView.h
Normal file
14
TUIKit/TUIChat/CommonUI/Camera/TUICaptureVideoPreviewView.h
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface TUICaptureVideoPreviewView : UIView
|
||||
|
||||
@property(strong, nonatomic) AVCaptureSession *captureSessionsion;
|
||||
|
||||
- (CGPoint)captureDevicePointForPoint:(CGPoint)point;
|
||||
|
||||
@end
|
||||
37
TUIKit/TUIChat/CommonUI/Camera/TUICaptureVideoPreviewView.m
Normal file
37
TUIKit/TUIChat/CommonUI/Camera/TUICaptureVideoPreviewView.m
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUICaptureVideoPreviewView.h"
|
||||
|
||||
@implementation TUICaptureVideoPreviewView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[(AVCaptureVideoPreviewLayer *)self.layer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (AVCaptureSession *)captureSessionsion {
|
||||
return [(AVCaptureVideoPreviewLayer *)self.layer session];
|
||||
}
|
||||
|
||||
- (void)setCaptureSessionsion:(AVCaptureSession *)session {
|
||||
[(AVCaptureVideoPreviewLayer *)self.layer setSession:session];
|
||||
}
|
||||
|
||||
- (CGPoint)captureDevicePointForPoint:(CGPoint)point {
|
||||
AVCaptureVideoPreviewLayer *layer = (AVCaptureVideoPreviewLayer *)self.layer;
|
||||
return [layer captureDevicePointOfInterestForPoint:point];
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the layer method of the view return the AVCaptureVideoPreviewLayer class object
|
||||
*/
|
||||
+ (Class)layerClass {
|
||||
return [AVCaptureVideoPreviewLayer class];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUICaptureVideoPreviewViewController : UIViewController
|
||||
|
||||
@property(nonatomic) void (^commitBlock)(void);
|
||||
@property(nonatomic) void (^cancelBlock)(void);
|
||||
|
||||
- (instancetype)initWithVideoURL:(NSURL *)url NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,136 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUICaptureVideoPreviewViewController.h"
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
@interface TUICaptureVideoPreviewViewController ()
|
||||
|
||||
@property(nonatomic) NSURL *fileURL;
|
||||
|
||||
@property(nonatomic) AVPlayer *player;
|
||||
@property(nonatomic) AVPlayerItem *item;
|
||||
@property(nonatomic) AVPlayerLayer *playerLayer;
|
||||
@property(nonatomic) UIButton *commitButton;
|
||||
@property(nonatomic) UIButton *cancelButton;
|
||||
|
||||
@property(nonatomic) CGRect lastRect;
|
||||
@property(nonatomic) BOOL onShow;
|
||||
@property(nonatomic) BOOL onReadyToPlay;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUICaptureVideoPreviewViewController
|
||||
|
||||
- (instancetype)initWithVideoURL:(NSURL *)url {
|
||||
self = [super initWithNibName:nil bundle:nil];
|
||||
if (self) {
|
||||
_fileURL = url;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
// Do any additional setup after loading the view.
|
||||
self.view.backgroundColor = [UIColor blackColor];
|
||||
|
||||
self.item = [[AVPlayerItem alloc] initWithURL:self.fileURL];
|
||||
self.player = [[AVPlayer alloc] initWithPlayerItem:self.item];
|
||||
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
|
||||
[self.view.layer addSublayer:self.playerLayer];
|
||||
|
||||
self.commitButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
UIImage *commitImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commit")];
|
||||
[self.commitButton setImage:commitImage forState:UIControlStateNormal];
|
||||
UIImage *commitBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commitBg")];
|
||||
[self.commitButton setBackgroundImage:commitBGImage forState:UIControlStateNormal];
|
||||
[self.commitButton addTarget:self action:@selector(commitButtonClick:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:self.commitButton];
|
||||
|
||||
self.cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
UIImage *cancelButtonBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_cancel")];
|
||||
[self.cancelButton setBackgroundImage:[cancelButtonBGImage rtl_imageFlippedForRightToLeftLayoutDirection] forState:UIControlStateNormal];
|
||||
[self.cancelButton addTarget:self action:@selector(cancelButtonClick:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:self.cancelButton];
|
||||
|
||||
[self.item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.item];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *, id> *)change context:(void *)context {
|
||||
if ([keyPath isEqualToString:@"status"]) {
|
||||
AVPlayerStatus status = [[change objectForKey:@"new"] intValue];
|
||||
if (status == AVPlayerStatusReadyToPlay) {
|
||||
self.onReadyToPlay = YES;
|
||||
[self playVideo];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)playVideo {
|
||||
[TUITool dispatchMainAsync:^{
|
||||
if (self.onShow && self.onReadyToPlay) {
|
||||
[self.player play];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
self.onShow = YES;
|
||||
|
||||
[self playVideo];
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
[super viewWillLayoutSubviews];
|
||||
|
||||
if (!CGRectEqualToRect(self.lastRect, self.view.bounds)) {
|
||||
self.lastRect = self.view.bounds;
|
||||
|
||||
self.playerLayer.frame = self.view.bounds;
|
||||
|
||||
CGFloat commitButtonWidth = 80.0;
|
||||
CGFloat buttonDistance = (self.view.bounds.size.width - 2 * commitButtonWidth) / 3.0;
|
||||
CGFloat commitButtonY = self.view.bounds.size.height - commitButtonWidth - 50.0;
|
||||
CGFloat commitButtonX = 2 * buttonDistance + commitButtonWidth;
|
||||
self.commitButton.frame = CGRectMake(commitButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
|
||||
|
||||
CGFloat cancelButtonX = commitButtonWidth;
|
||||
self.cancelButton.frame = CGRectMake(cancelButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
|
||||
if (isRTL()) {
|
||||
[self.commitButton resetFrameToFitRTL];
|
||||
[self.cancelButton resetFrameToFitRTL];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)commitButtonClick:(UIButton *)btn {
|
||||
if (self.commitBlock) {
|
||||
[self removeObserver];
|
||||
self.commitBlock();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelButtonClick:(UIButton *)btn {
|
||||
if (self.cancelBlock) {
|
||||
[self removeObserver];
|
||||
self.cancelBlock();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)playFinished:(NSNotification *)noti {
|
||||
[self.player seekToTime:CMTimeMake(0, 1)];
|
||||
[self.player play];
|
||||
}
|
||||
|
||||
- (void)removeObserver {
|
||||
[self.item removeObserver:self forKeyPath:@"status"];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
@end
|
||||
57
TUIKit/TUIChat/CommonUI/Camera/TUIChatMediaDataProvider.h
Normal file
57
TUIKit/TUIChat/CommonUI/Camera/TUIChatMediaDataProvider.h
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// TUIChatMediaDataProvider.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2022/12/20.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
#import "TUIChatMediaSendingManager.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^TUIChatMediaDataProviderResultCallback)(BOOL success, NSString *__nullable message, NSString *__nullable path);
|
||||
|
||||
@protocol TUIChatMediaDataProtocol <NSObject>
|
||||
|
||||
- (void)selectPhoto;
|
||||
- (void)takePicture;
|
||||
- (void)takeVideo;
|
||||
- (void)multimediaRecord;
|
||||
- (void)selectFile;
|
||||
|
||||
@end
|
||||
|
||||
@protocol TUIChatMediaDataListener <NSObject>
|
||||
|
||||
- (void)onProvideImage:(NSString *)imageUrl;
|
||||
- (void)onProvideImageError:(NSString *)errorMessage;
|
||||
|
||||
- (void)onProvideVideo:(NSString *)videoUrl
|
||||
snapshot:(NSString *)snapshotUrl
|
||||
duration:(NSInteger)duration
|
||||
placeHolderCellData:(TUIMessageCellData *)placeHolderCellData;
|
||||
- (void)onProvidePlaceholderVideoSnapshot:(NSString *)snapshotUrl
|
||||
SnapImage:(UIImage *)img
|
||||
Completion:(void (^__nullable)(BOOL finished, TUIMessageCellData *placeHolderCellData))completion;
|
||||
- (void)onProvideVideoError:(NSString *)errorMessage;
|
||||
- (void)onProvideFile:(NSString *)fileUrl filename:(NSString *)filename fileSize:(NSInteger)fileSize;
|
||||
- (void)onProvideFileError:(NSString *)errorMessage;
|
||||
|
||||
- (NSString *)currentConversationID;
|
||||
- (BOOL)isPageAppears;
|
||||
- (void)sendPlaceHolderUIMessage:(TUIMessageCellData *)cellData;
|
||||
- (void)sendMessage:(V2TIMMessage *)message placeHolderCellData:(TUIMessageCellData *)placeHolderCellData;
|
||||
@end
|
||||
|
||||
@interface TUIChatMediaDataProvider : NSObject <TUIChatMediaDataProtocol>
|
||||
|
||||
@property(nonatomic, weak) UIViewController *presentViewController;
|
||||
@property(nonatomic, weak) id<TUIChatMediaDataListener> listener;
|
||||
@property(nonatomic, copy) NSString *conversationID;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
898
TUIKit/TUIChat/CommonUI/Camera/TUIChatMediaDataProvider.m
Normal file
898
TUIKit/TUIChat/CommonUI/Camera/TUIChatMediaDataProvider.m
Normal file
@@ -0,0 +1,898 @@
|
||||
//
|
||||
// TUIChatMediaDataProvider.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by harvy on 2022/12/20.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIChatMediaDataProvider.h"
|
||||
|
||||
#import <AssetsLibrary/AssetsLibrary.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import <Photos/Photos.h>
|
||||
#import <PhotosUI/PhotosUI.h>
|
||||
#import <SDWebImage/SDWebImage.h>
|
||||
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TIMCommon/TUIUserAuthorizationCenter.h>
|
||||
#import <TIMCommon/NSTimer+TUISafe.h>
|
||||
#import <TUICore/TUITool.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import "TUICameraViewController.h"
|
||||
#import "TUIChatConfig.h"
|
||||
#import "AlbumPicker.h"
|
||||
#import "MultimediaRecorder.h"
|
||||
#define kTUIChatMediaSelectImageMax 9
|
||||
@interface TUIChatMediaDataProvider () <PHPickerViewControllerDelegate,
|
||||
UINavigationControllerDelegate,
|
||||
UIImagePickerControllerDelegate,
|
||||
UIDocumentPickerDelegate,
|
||||
TUICameraViewControllerDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIChatMediaDataProvider
|
||||
|
||||
#pragma mark - Public API
|
||||
- (void)selectPhoto {
|
||||
if ([AlbumPicker sharedInstance].advancedAlbumPicker) {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
__strong typeof(weakSelf.listener) strongListener = weakSelf.listener;
|
||||
[[AlbumPicker sharedInstance].advancedAlbumPicker pickMediaWithCaller:self.presentViewController originalMediaPicked:^(NSDictionary *param) {
|
||||
if (param) {
|
||||
NSString * type = param[@"type"];
|
||||
if ([type isEqualToString:@"image"]) {
|
||||
// image do nothing
|
||||
}
|
||||
else if ([type isEqualToString:@"video"]) {
|
||||
TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
|
||||
if ([strongListener respondsToSelector:@selector(sendPlaceHolderUIMessage:)]) {
|
||||
[strongListener sendPlaceHolderUIMessage:placeHolderCellData];
|
||||
}
|
||||
TUIChatMediaTask * task = [[TUIChatMediaTask alloc] init];
|
||||
task.placeHolderCellData = placeHolderCellData;
|
||||
task.msgID = placeHolderCellData.msgID;
|
||||
task.conversationID = weakSelf.conversationID;
|
||||
if (placeHolderCellData.msgID.length > 0) {
|
||||
[TUIChatMediaSendingManager.sharedInstance addMediaTask: task forKey:placeHolderCellData.msgID];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
} progressCallback:^(NSDictionary *param) {
|
||||
NSLog(@"%@,strongListener:%@",param,strongListener);
|
||||
} finishedCallback:^(NSDictionary *param) {
|
||||
if (param) {
|
||||
V2TIMMessage * message = param[@"message"];
|
||||
NSString * type = param[@"type"];
|
||||
if ([type isEqualToString:@"image"]) {
|
||||
if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
|
||||
[strongListener sendMessage:message placeHolderCellData:nil];
|
||||
}
|
||||
}
|
||||
else if ([type isEqualToString:@"video"]) {
|
||||
TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
|
||||
if (placeHolderCellData.msgID.length > 0) {
|
||||
[TUIChatMediaSendingManager.sharedInstance removeMediaTaskForKey:placeHolderCellData.msgID];
|
||||
}
|
||||
BOOL canSendByCurrentPage = NO;
|
||||
for (id<TUIChatMediaDataListener> currentVC in TUIChatMediaSendingManager.sharedInstance.mediaSendingControllers) {
|
||||
if ([currentVC.currentConversationID isEqualToString:self.conversationID]&&
|
||||
[currentVC respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
|
||||
if (currentVC.isPageAppears) {
|
||||
[currentVC sendMessage:message placeHolderCellData:placeHolderCellData];
|
||||
canSendByCurrentPage = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!canSendByCurrentPage) {
|
||||
if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
|
||||
[strongListener sendMessage:message placeHolderCellData:placeHolderCellData];
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
else {
|
||||
//defalut AlbumPicker
|
||||
[self _selectPhoto];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_selectPhoto {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (@available(iOS 14.0, *)) {
|
||||
PHPickerConfiguration *configuration = [[PHPickerConfiguration alloc] init];
|
||||
configuration.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[ [PHPickerFilter imagesFilter], [PHPickerFilter videosFilter] ]];
|
||||
configuration.selectionLimit = kTUIChatMediaSelectImageMax;
|
||||
PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:configuration];
|
||||
picker.delegate = self;
|
||||
picker.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
picker.view.backgroundColor = [UIColor whiteColor];
|
||||
[self.presentViewController presentViewController:picker animated:YES completion:nil];
|
||||
} else {
|
||||
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
|
||||
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
|
||||
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
|
||||
picker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
|
||||
picker.delegate = self;
|
||||
[self.presentViewController presentViewController:picker animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
- (void)takePicture {
|
||||
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
|
||||
[[MultimediaRecorder sharedInstance].advancedVideoRecorder takePhoneWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
|
||||
NSData *imageData = [NSData dataWithContentsOfURL:uri];
|
||||
UIImage *photo = [UIImage imageWithData:imageData];
|
||||
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
|
||||
[[NSFileManager defaultManager] createFileAtPath:path
|
||||
contents:UIImagePNGRepresentation(photo) attributes:nil];
|
||||
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
|
||||
[self.listener onProvideImage:path];
|
||||
}
|
||||
} failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
|
||||
|
||||
}];
|
||||
}
|
||||
else {
|
||||
//defalut PhotoCamera
|
||||
[self _takePicture];
|
||||
}
|
||||
}
|
||||
- (void)_takePicture {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
void (^actionBlock)(void) = ^(void) {
|
||||
TUICameraViewController *vc = [[TUICameraViewController alloc] init];
|
||||
vc.type = TUICameraMediaTypePhoto;
|
||||
vc.delegate = weakSelf;
|
||||
if (weakSelf.presentViewController.navigationController) {
|
||||
[weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
|
||||
} else {
|
||||
[weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
|
||||
}
|
||||
};
|
||||
if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
actionBlock();
|
||||
});
|
||||
} else {
|
||||
if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
[TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
actionBlock();
|
||||
});
|
||||
}];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
- (void)executeBlockWithMicroAndCameraAuth:(void(^)(void))block{
|
||||
if ([TUIUserAuthorizationCenter isEnableMicroAuthorization] && [TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
dispatch_async(dispatch_get_main_queue(), block);
|
||||
} else {
|
||||
if (![TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
|
||||
[TUIUserAuthorizationCenter microStateActionWithPopCompletion:^{
|
||||
if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
dispatch_async(dispatch_get_main_queue(), block);
|
||||
}
|
||||
}];
|
||||
}
|
||||
if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
|
||||
[TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
|
||||
if ([TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
|
||||
dispatch_async(dispatch_get_main_queue(), block);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)takeVideo {
|
||||
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
|
||||
[[MultimediaRecorder sharedInstance].advancedVideoRecorder recordVideoWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
|
||||
if (uri) {
|
||||
if ([uri.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
|
||||
[self handleVideoPick:YES message:nil videoUrl:uri];
|
||||
return;
|
||||
}
|
||||
else if ([self isImageURL:uri]){
|
||||
NSData *imageData = [NSData dataWithContentsOfURL:uri];
|
||||
UIImage *photo = [UIImage imageWithData:imageData];
|
||||
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
|
||||
[[NSFileManager defaultManager] createFileAtPath:path
|
||||
contents:UIImagePNGRepresentation(photo) attributes:nil];
|
||||
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
|
||||
[self.listener onProvideImage:path];
|
||||
}
|
||||
}
|
||||
else {
|
||||
[self transcodeIfNeed:YES message:nil videoUrl:uri];
|
||||
}
|
||||
}
|
||||
} failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
|
||||
|
||||
}];
|
||||
|
||||
return;
|
||||
}
|
||||
else {
|
||||
//defalut VideoRecorder
|
||||
[self _takeVideo];
|
||||
}
|
||||
}
|
||||
- (void)_takeVideo {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
void (^actionBlock)(void) = ^(void) {
|
||||
TUICameraViewController *vc = [[TUICameraViewController alloc] init];
|
||||
vc.type = TUICameraMediaTypeVideo;
|
||||
vc.videoMinimumDuration = 1.5;
|
||||
vc.delegate = weakSelf;
|
||||
if ([TUIChatConfig defaultConfig].maxVideoRecordDuration > 0) {
|
||||
vc.videoMaximumDuration = [TUIChatConfig defaultConfig].maxVideoRecordDuration;
|
||||
}
|
||||
if (weakSelf.presentViewController.navigationController) {
|
||||
[weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
|
||||
} else {
|
||||
[weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
|
||||
}
|
||||
};
|
||||
|
||||
[self executeBlockWithMicroAndCameraAuth:actionBlock];
|
||||
}
|
||||
|
||||
- (void)selectFile {
|
||||
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[ (NSString *)kUTTypeData ]
|
||||
inMode:UIDocumentPickerModeOpen];
|
||||
picker.delegate = self;
|
||||
[self.presentViewController presentViewController:picker animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (BOOL)isImageURL:(NSURL *)url {
|
||||
NSArray *imageExtensions = @[@"jpg", @"jpeg", @"png", @"gif", @"bmp", @"tiff", @"webp", @"heic"];
|
||||
NSString *pathExtension = url.pathExtension.lowercaseString;
|
||||
return [imageExtensions containsObject:pathExtension];
|
||||
}
|
||||
#pragma mark - Private Do task
|
||||
- (void)handleImagePick:(BOOL)succ message:(NSString *)message imageData:(NSData *)imageData {
|
||||
static NSDictionary *imageFormatExtensionMap = nil;
|
||||
if (imageFormatExtensionMap == nil) {
|
||||
imageFormatExtensionMap = @{
|
||||
@(SDImageFormatUndefined) : @"",
|
||||
@(SDImageFormatJPEG) : @"jpeg",
|
||||
@(SDImageFormatPNG) : @"png",
|
||||
@(SDImageFormatGIF) : @"gif",
|
||||
@(SDImageFormatTIFF) : @"tiff",
|
||||
@(SDImageFormatWebP) : @"webp",
|
||||
@(SDImageFormatHEIC) : @"heic",
|
||||
@(SDImageFormatHEIF) : @"heif",
|
||||
@(SDImageFormatPDF) : @"pdf",
|
||||
@(SDImageFormatSVG) : @"svg",
|
||||
@(SDImageFormatBMP) : @"bmp",
|
||||
@(SDImageFormatRAW) : @"raw"
|
||||
};
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (succ == NO || imageData == nil) {
|
||||
if ([self.listener respondsToSelector:@selector(onProvideImageError:)]) {
|
||||
[self.listener onProvideImageError:message];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithData:imageData];
|
||||
NSData *data = UIImageJPEGRepresentation(image, 1);
|
||||
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
|
||||
NSString *extenionName = [imageFormatExtensionMap objectForKey:@(image.sd_imageFormat)];
|
||||
if (extenionName.length > 0) {
|
||||
path = [path stringByAppendingPathExtension:extenionName];
|
||||
}
|
||||
|
||||
|
||||
int32_t imageFormatSizeMax = 28 * 1024 * 1024;
|
||||
|
||||
if (image.sd_imageFormat == SDImageFormatGIF) {
|
||||
imageFormatSizeMax = 10 * 1024 * 1024;
|
||||
}
|
||||
|
||||
if (imageData.length > imageFormatSizeMax) {
|
||||
if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
|
||||
[self.listener onProvideFileError:TIMCommonLocalizableString(TUIKitImageSizeCheckLimited)];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (image.sd_imageFormat != SDImageFormatGIF) {
|
||||
UIImage *newImage = image;
|
||||
UIImageOrientation imageOrientation = image.imageOrientation;
|
||||
CGFloat aspectRatio = MIN(1920 / image.size.width, 1920 / image.size.height);
|
||||
CGFloat aspectWidth = image.size.width * aspectRatio;
|
||||
CGFloat aspectHeight = image.size.height * aspectRatio;
|
||||
UIGraphicsBeginImageContext(CGSizeMake(aspectWidth, aspectHeight));
|
||||
[image drawInRect:CGRectMake(0, 0, aspectWidth, aspectHeight)];
|
||||
newImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
data = UIImageJPEGRepresentation(newImage, 0.75);
|
||||
}
|
||||
|
||||
[[NSFileManager defaultManager] createFileAtPath:path contents:data attributes:nil];
|
||||
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
|
||||
[self.listener onProvideImage:path];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url {
|
||||
if (succ == NO || url == nil) {
|
||||
[self handleVideoPick:NO message:message videoUrl:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
|
||||
[self handleVideoPick:succ message:message videoUrl:url];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSURL *urlName = [url URLByDeletingPathExtension];
|
||||
NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
if ([fileManager fileExistsAtPath:newUrl.path]) {
|
||||
NSError *error;
|
||||
BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
|
||||
if (!success || error) {
|
||||
NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// mov to mp4
|
||||
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
|
||||
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
|
||||
exportSession.outputURL = newUrl;
|
||||
exportSession.outputFileType = AVFileTypeMPEG4;
|
||||
exportSession.shouldOptimizeForNetworkUse = YES;
|
||||
|
||||
// intercept FirstTime VideoPicture
|
||||
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
|
||||
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
|
||||
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
|
||||
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
|
||||
gen.appliesPreferredTrackTransform = YES;
|
||||
gen.maximumSize = CGSizeMake(192, 192);
|
||||
NSError *error = nil;
|
||||
CMTime actualTime;
|
||||
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
|
||||
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
|
||||
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
|
||||
CGImageRelease(imageRef);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
|
||||
[self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:image Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
switch ([exportSession status]) {
|
||||
case AVAssetExportSessionStatusFailed:
|
||||
NSLog(@"Export session failed");
|
||||
break;
|
||||
case AVAssetExportSessionStatusCancelled:
|
||||
NSLog(@"Export canceled");
|
||||
break;
|
||||
case AVAssetExportSessionStatusCompleted: {
|
||||
// Video conversion finished
|
||||
NSLog(@"Successful!");
|
||||
[self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}];
|
||||
|
||||
[NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
if (exportSession.status == AVAssetExportSessionStatusExporting) {
|
||||
NSLog(@"exportSession.progress:%f",exportSession.progress);
|
||||
placeHolderCellData.videoTranscodingProgress = exportSession.progress;
|
||||
}
|
||||
}];
|
||||
|
||||
}];
|
||||
}
|
||||
else {
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
switch ([exportSession status]) {
|
||||
case AVAssetExportSessionStatusCompleted: {
|
||||
// Video conversion finished
|
||||
NSLog(@"Successful!");
|
||||
[self handleVideoPick:succ message:message videoUrl:newUrl];
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
- (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url placeHolderCellData:(TUIMessageCellData*)placeHolderCellData {
|
||||
if (succ == NO || url == nil) {
|
||||
[self handleVideoPick:NO message:message videoUrl:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
|
||||
[self handleVideoPick:succ message:message videoUrl:url];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSURL *urlName = [url URLByDeletingPathExtension];
|
||||
NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
if ([fileManager fileExistsAtPath:newUrl.path]) {
|
||||
NSError *error;
|
||||
BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
|
||||
if (!success || error) {
|
||||
NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// mov to mp4
|
||||
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
|
||||
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
|
||||
exportSession.outputURL = newUrl;
|
||||
exportSession.outputFileType = AVFileTypeMPEG4;
|
||||
exportSession.shouldOptimizeForNetworkUse = YES;
|
||||
|
||||
// intercept FirstTime VideoPicture
|
||||
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
|
||||
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
|
||||
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
|
||||
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
|
||||
gen.appliesPreferredTrackTransform = YES;
|
||||
gen.maximumSize = CGSizeMake(192, 192);
|
||||
NSError *error = nil;
|
||||
CMTime actualTime;
|
||||
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
|
||||
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
|
||||
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
|
||||
CGImageRelease(imageRef);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
switch ([exportSession status]) {
|
||||
case AVAssetExportSessionStatusFailed:
|
||||
NSLog(@"Export session failed");
|
||||
break;
|
||||
case AVAssetExportSessionStatusCancelled:
|
||||
NSLog(@"Export canceled");
|
||||
break;
|
||||
case AVAssetExportSessionStatusCompleted: {
|
||||
// Video conversion finished
|
||||
NSLog(@"Successful!");
|
||||
[self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}];
|
||||
|
||||
[NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
if (exportSession.status == AVAssetExportSessionStatusExporting) {
|
||||
NSLog(@"exportSession.progress:%f",exportSession.progress);
|
||||
placeHolderCellData.videoTranscodingProgress = exportSession.progress;
|
||||
}
|
||||
}];
|
||||
}
|
||||
else {
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
switch ([exportSession status]) {
|
||||
case AVAssetExportSessionStatusCompleted: {
|
||||
// Video conversion finished
|
||||
NSLog(@"Successful!");
|
||||
[self handleVideoPick:succ message:message videoUrl:newUrl];
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
- (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl {
|
||||
[self handleVideoPick:succ message:message videoUrl:videoUrl placeHolderCellData:nil];
|
||||
}
|
||||
- (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl placeHolderCellData:(TUIMessageCellData*)placeHolderCellData{
|
||||
if (succ == NO || videoUrl == nil) {
|
||||
if ([self.listener respondsToSelector:@selector(onProvideVideoError:)]) {
|
||||
[self.listener onProvideVideoError:message];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *videoData = [NSData dataWithContentsOfURL:videoUrl];
|
||||
NSString *videoPath = [NSString stringWithFormat:@"%@%@_%u.mp4", TUIKit_Video_Path, [TUITool genVideoName:nil],arc4random()];
|
||||
[[NSFileManager defaultManager] createFileAtPath:videoPath contents:videoData attributes:nil];
|
||||
|
||||
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
|
||||
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:videoUrl options:opts];
|
||||
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
|
||||
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
|
||||
gen.appliesPreferredTrackTransform = YES;
|
||||
gen.maximumSize = CGSizeMake(192, 192);
|
||||
NSError *error = nil;
|
||||
CMTime actualTime;
|
||||
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
|
||||
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
|
||||
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
|
||||
CGImageRelease(imageRef);
|
||||
|
||||
NSData *imageData = UIImagePNGRepresentation(image);
|
||||
NSString *imagePath = [TUIKit_Video_Path stringByAppendingFormat:@"%@_%u",[TUITool genSnapshotName:nil],arc4random()];
|
||||
[[NSFileManager defaultManager] createFileAtPath:imagePath contents:imageData attributes:nil];
|
||||
|
||||
if ([self.listener respondsToSelector:@selector(onProvideVideo:snapshot:duration:placeHolderCellData:)]) {
|
||||
[self.listener onProvideVideo:videoPath snapshot:imagePath duration:duration placeHolderCellData:placeHolderCellData];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - PHPickerViewControllerDelegate
|
||||
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14)) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[picker dismissViewControllerAnimated:YES completion:nil];
|
||||
[[[UIApplication sharedApplication] keyWindow] endEditing:YES];
|
||||
});
|
||||
|
||||
if (!results || results.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
PHPickerResult *result = [results firstObject];
|
||||
for (PHPickerResult *result in results) {
|
||||
[self _dealPHPickerResultFinishPicking:result];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_dealPHPickerResultFinishPicking:(PHPickerResult *)result API_AVAILABLE(ios(14)) {
|
||||
NSItemProvider *itemProvoider = result.itemProvider;
|
||||
__weak typeof(self) weakSelf = self;
|
||||
if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) {
|
||||
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeImage
|
||||
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
BOOL succ = YES;
|
||||
NSString *message = nil;
|
||||
if (error) {
|
||||
succ = NO;
|
||||
message = error.localizedDescription;
|
||||
}
|
||||
[weakSelf handleImagePick:succ message:message imageData:data];
|
||||
});
|
||||
}];
|
||||
} else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMPEG4]) {
|
||||
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
|
||||
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSString *fileName = @"temp.mp4";
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
|
||||
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
|
||||
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
|
||||
}
|
||||
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
|
||||
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
|
||||
[weakSelf transcodeIfNeed:flag message:flag ? nil : @"video not found" videoUrl:newUrl];
|
||||
});
|
||||
}];
|
||||
} else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie]) {
|
||||
// Mov type: screen first
|
||||
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
|
||||
[self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:nil Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
|
||||
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
|
||||
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Non-mp4 format video, temporarily use mov suffix, will be converted to mp4 format later
|
||||
NSDate *datenow = [NSDate date];
|
||||
NSString *timeSp = [NSString stringWithFormat:@"%ld", (long)([datenow timeIntervalSince1970]*1000)];
|
||||
NSString *fileName = [NSString stringWithFormat:@"%@_temp.mov",timeSp];
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
|
||||
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
|
||||
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
|
||||
}
|
||||
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
|
||||
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
|
||||
[weakSelf transcodeIfNeed:flag message:flag ? nil : @"movie not found" videoUrl:newUrl placeHolderCellData:placeHolderCellData];
|
||||
});
|
||||
}];
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
NSString *typeIdentifier = result.itemProvider.registeredTypeIdentifiers.firstObject;
|
||||
[itemProvoider loadFileRepresentationForTypeIdentifier:typeIdentifier
|
||||
completionHandler:^(NSURL *_Nullable url, NSError *_Nullable error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIImage *result;
|
||||
NSData *data = [NSData dataWithContentsOfURL:url];
|
||||
result = [UIImage imageWithData:data];
|
||||
|
||||
/**
|
||||
* Can't get url when typeIdentifier is public.jepg on emulator:
|
||||
* There is a separate JEPG transcoding issue that only affects the simulator (63426347), please refer to
|
||||
* https://developer.apple.com/forums/thread/658135 for more information.
|
||||
*/
|
||||
});
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIImagePickerController
|
||||
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
picker.delegate = nil;
|
||||
[picker dismissViewControllerAnimated:YES
|
||||
completion:^{
|
||||
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
|
||||
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
|
||||
NSURL *url = nil;
|
||||
if (@available(iOS 11.0, *)) {
|
||||
url = [info objectForKey:UIImagePickerControllerImageURL];
|
||||
} else {
|
||||
url = [info objectForKey:UIImagePickerControllerReferenceURL];
|
||||
}
|
||||
|
||||
BOOL succ = YES;
|
||||
NSData *imageData = nil;
|
||||
NSString *errorMessage = nil;
|
||||
if (url) {
|
||||
succ = YES;
|
||||
imageData = [NSData dataWithContentsOfURL:url];
|
||||
} else {
|
||||
succ = NO;
|
||||
errorMessage = @"image not found";
|
||||
}
|
||||
[weakSelf handleImagePick:succ message:errorMessage imageData:imageData];
|
||||
} else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
|
||||
NSURL *url = [info objectForKey:UIImagePickerControllerMediaURL];
|
||||
if (url) {
|
||||
[weakSelf transcodeIfNeed:YES message:nil videoUrl:url];
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases UIImagePickerControllerMediaURL may be empty, use UIImagePickerControllerPHAsset
|
||||
*/
|
||||
PHAsset *asset = nil;
|
||||
if (@available(iOS 11.0, *)) {
|
||||
asset = [info objectForKey:UIImagePickerControllerPHAsset];
|
||||
}
|
||||
if (asset) {
|
||||
[self originURLWithAsset:asset
|
||||
completion:^(BOOL success, NSURL *URL) {
|
||||
[weakSelf transcodeIfNeed:success
|
||||
message:success ? nil : @"origin url with asset not found"
|
||||
videoUrl:URL];
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* UIImagePickerControllerPHAsset may be empty, and other methods need to be used to obtain the original path of the video
|
||||
* file
|
||||
*/
|
||||
url = [info objectForKey:UIImagePickerControllerReferenceURL];
|
||||
if (url) {
|
||||
[weakSelf originURLWithRefrenceURL:url
|
||||
completion:^(BOOL success, NSURL *URL) {
|
||||
[weakSelf transcodeIfNeed:success
|
||||
message:success ? nil : @"origin url with asset not found"
|
||||
videoUrl:URL];
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
// not support the video
|
||||
[weakSelf transcodeIfNeed:NO message:@"not support the video" videoUrl:nil];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
||||
[picker dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original file path based on UIImagePickerControllerReferenceURL
|
||||
*/
|
||||
- (void)originURLWithRefrenceURL:(NSURL *)URL completion:(void (^)(BOOL success, NSURL *URL))completion {
|
||||
if (completion == nil) {
|
||||
return;
|
||||
}
|
||||
NSDictionary *queryInfo = [self dictionaryWithURLQuery:URL.query];
|
||||
NSString *fileName = @"temp.mp4";
|
||||
if ([queryInfo.allKeys containsObject:@"id"] && [queryInfo.allKeys containsObject:@"ext"]) {
|
||||
fileName = [NSString stringWithFormat:@"%@.%@", queryInfo[@"id"], [queryInfo[@"ext"] lowercaseString]];
|
||||
}
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
|
||||
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
|
||||
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
|
||||
}
|
||||
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
|
||||
ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init];
|
||||
[assetLibrary assetForURL:URL
|
||||
resultBlock:^(ALAsset *asset) {
|
||||
if (asset == nil) {
|
||||
completion(NO, nil);
|
||||
return;
|
||||
}
|
||||
ALAssetRepresentation *rep = [asset defaultRepresentation];
|
||||
Byte *buffer = (Byte *)malloc(rep.size);
|
||||
NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:rep.size error:nil];
|
||||
NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; // this is NSData may be what you want
|
||||
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
|
||||
completion(flag, newUrl);
|
||||
}
|
||||
failureBlock:^(NSError *err) {
|
||||
completion(NO, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)originURLWithAsset:(PHAsset *)asset completion:(void (^)(BOOL success, NSURL *URL))completion {
|
||||
if (completion == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
if (resources.count == 0) {
|
||||
completion(NO, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
PHAssetResourceRequestOptions *options = [[PHAssetResourceRequestOptions alloc] init];
|
||||
options.networkAccessAllowed = NO;
|
||||
__block BOOL invoked = NO;
|
||||
[PHAssetResourceManager.defaultManager requestDataForAssetResource:resources.firstObject
|
||||
options:options
|
||||
dataReceivedHandler:^(NSData *_Nonnull data) {
|
||||
/**
|
||||
*
|
||||
* There will be a problem of repeated callbacks here
|
||||
*/
|
||||
if (invoked) {
|
||||
return;
|
||||
}
|
||||
invoked = YES;
|
||||
if (data == nil) {
|
||||
completion(NO, nil);
|
||||
return;
|
||||
}
|
||||
NSString *fileName = @"temp.mp4";
|
||||
NSString *tempPath = NSTemporaryDirectory();
|
||||
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
|
||||
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
|
||||
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
|
||||
}
|
||||
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
|
||||
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
|
||||
completion(flag, newUrl);
|
||||
}
|
||||
completionHandler:^(NSError *_Nullable error) {
|
||||
completion(NO, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSDictionary *)dictionaryWithURLQuery:(NSString *)query {
|
||||
NSArray *components = [query componentsSeparatedByString:@"&"];
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
for (NSString *item in components) {
|
||||
NSArray *subs = [item componentsSeparatedByString:@"="];
|
||||
if (subs.count == 2) {
|
||||
[dict setObject:subs.lastObject forKey:subs.firstObject];
|
||||
}
|
||||
}
|
||||
return [NSDictionary dictionaryWithDictionary:dict];
|
||||
;
|
||||
}
|
||||
|
||||
#pragma mark - TUICameraViewControllerDelegate
|
||||
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithVideoURL:(NSURL *)url {
|
||||
[self transcodeIfNeed:YES message:nil videoUrl:url];
|
||||
}
|
||||
|
||||
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithImageData:(NSData *)data {
|
||||
[self handleImagePick:YES message:nil imageData:data];
|
||||
}
|
||||
|
||||
- (void)cameraViewControllerDidCancel:(TUICameraViewController *)controller {
|
||||
}
|
||||
|
||||
- (void)cameraViewControllerDidPictureLib:(TUICameraViewController *)controller finishCallback:(void (^)(void))callback {
|
||||
[self selectPhoto];
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIDocumentPickerDelegate
|
||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {
|
||||
[url startAccessingSecurityScopedResource];
|
||||
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init];
|
||||
NSError *error;
|
||||
@weakify(self);
|
||||
[coordinator
|
||||
coordinateReadingItemAtURL:url
|
||||
options:0
|
||||
error:&error
|
||||
byAccessor:^(NSURL *newURL) {
|
||||
@strongify(self);
|
||||
NSData *fileData = [NSData dataWithContentsOfURL:newURL options:NSDataReadingMappedIfSafe error:nil];
|
||||
NSString *fileName = [url lastPathComponent];
|
||||
NSString *filePath = [TUIKit_File_Path stringByAppendingString:fileName];
|
||||
if (fileData.length > 1e9 || fileData.length == 0) { // 1e9 bytes = 1GB
|
||||
UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitFileSizeCheckLimited) message:nil preferredStyle:UIAlertControllerStyleAlert];
|
||||
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Confirm) style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self.presentViewController presentViewController:ac animated:YES completion:nil];
|
||||
return;
|
||||
}
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:filePath]) {
|
||||
/**
|
||||
* If a file with the same name exists, increment the file name
|
||||
*/
|
||||
int i = 0;
|
||||
NSArray *arrayM = [NSFileManager.defaultManager subpathsAtPath:TUIKit_File_Path];
|
||||
for (NSString *sub in arrayM) {
|
||||
if ([sub.pathExtension isEqualToString:fileName.pathExtension] &&
|
||||
[sub.stringByDeletingPathExtension tui_containsString:fileName.stringByDeletingPathExtension]) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (i) {
|
||||
fileName = [fileName
|
||||
stringByReplacingOccurrencesOfString:fileName.stringByDeletingPathExtension
|
||||
withString:[NSString stringWithFormat:@"%@(%d)", fileName.stringByDeletingPathExtension, i]];
|
||||
filePath = [TUIKit_File_Path stringByAppendingString:fileName];
|
||||
}
|
||||
}
|
||||
|
||||
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
|
||||
unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] fileSize];
|
||||
if ([self.listener respondsToSelector:@selector(onProvideFile:filename:fileSize:)]) {
|
||||
[self.listener onProvideFile:filePath filename:fileName fileSize:fileSize];
|
||||
}
|
||||
} else {
|
||||
if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
|
||||
[self.listener onProvideFileError:@"file not found"];
|
||||
}
|
||||
}
|
||||
}];
|
||||
[url stopAccessingSecurityScopedResource];
|
||||
[controller dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
|
||||
[controller dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
14
TUIKit/TUIChat/CommonUI/Camera/TUIMotionManager.h
Normal file
14
TUIKit/TUIChat/CommonUI/Camera/TUIMotionManager.h
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
@import UIKit;
|
||||
@interface TUIMotionManager : NSObject
|
||||
|
||||
@property(nonatomic, assign) UIDeviceOrientation deviceOrientation;
|
||||
|
||||
@property(nonatomic, assign) AVCaptureVideoOrientation videoOrientation;
|
||||
|
||||
@end
|
||||
61
TUIKit/TUIChat/CommonUI/Camera/TUIMotionManager.m
Normal file
61
TUIKit/TUIChat/CommonUI/Camera/TUIMotionManager.m
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUIMotionManager.h"
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
@import UIKit;
|
||||
@interface TUIMotionManager ()
|
||||
|
||||
@property(nonatomic, strong) CMMotionManager *motionManager;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIMotionManager
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_motionManager = [[CMMotionManager alloc] init];
|
||||
_motionManager.deviceMotionUpdateInterval = 1 / 15.0;
|
||||
if (!_motionManager.deviceMotionAvailable) {
|
||||
_motionManager = nil;
|
||||
return self;
|
||||
}
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
[_motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
|
||||
withHandler:^(CMDeviceMotion *motion, NSError *error) {
|
||||
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
||||
[strongSelf performSelectorOnMainThread:@selector(handleDeviceMotion:) withObject:motion waitUntilDone:YES];
|
||||
}];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleDeviceMotion:(CMDeviceMotion *)deviceMotion {
|
||||
double x = deviceMotion.gravity.x;
|
||||
double y = deviceMotion.gravity.y;
|
||||
if (fabs(y) >= fabs(x)) {
|
||||
if (y >= 0) {
|
||||
_deviceOrientation = UIDeviceOrientationPortraitUpsideDown;
|
||||
_videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
|
||||
} else {
|
||||
_deviceOrientation = UIDeviceOrientationPortrait;
|
||||
_videoOrientation = AVCaptureVideoOrientationPortrait;
|
||||
}
|
||||
} else {
|
||||
if (x >= 0) {
|
||||
_deviceOrientation = UIDeviceOrientationLandscapeRight;
|
||||
_videoOrientation = AVCaptureVideoOrientationLandscapeRight;
|
||||
} else {
|
||||
_deviceOrientation = UIDeviceOrientationLandscapeLeft;
|
||||
_videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_motionManager stopDeviceMotionUpdates];
|
||||
}
|
||||
|
||||
@end
|
||||
26
TUIKit/TUIChat/CommonUI/Camera/TUIMovieManager.h
Normal file
26
TUIKit/TUIChat/CommonUI/Camera/TUIMovieManager.h
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIMovieManager : NSObject
|
||||
|
||||
@property(nonatomic, assign) AVCaptureVideoOrientation referenceOrientation;
|
||||
|
||||
@property(nonatomic, assign) AVCaptureVideoOrientation currentOrientation;
|
||||
|
||||
@property(nonatomic, strong) AVCaptureDevice *currentDevice;
|
||||
|
||||
- (void)start:(void (^)(NSError *error))handle;
|
||||
|
||||
- (void)stop:(void (^)(NSURL *url, NSError *error))handle;
|
||||
|
||||
- (void)writeData:(AVCaptureConnection *)connection video:(AVCaptureConnection *)video audio:(AVCaptureConnection *)audio buffer:(CMSampleBufferRef)buffer;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
235
TUIKit/TUIChat/CommonUI/Camera/TUIMovieManager.m
Normal file
235
TUIKit/TUIChat/CommonUI/Camera/TUIMovieManager.m
Normal file
@@ -0,0 +1,235 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
|
||||
#import "TUIMovieManager.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
|
||||
@interface TUIMovieManager () {
|
||||
BOOL _readyToRecordVideo;
|
||||
BOOL _readyToRecordAudio;
|
||||
dispatch_queue_t _movieWritingQueue;
|
||||
|
||||
NSURL *_movieURL;
|
||||
AVAssetWriter *_movieWriter;
|
||||
AVAssetWriterInput *_movieAudioInput;
|
||||
AVAssetWriterInput *_movieVideoInput;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIMovieManager
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_movieWritingQueue = dispatch_queue_create("com.tui.Movie.Writing.Queue", DISPATCH_QUEUE_SERIAL);
|
||||
_movieURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"TUICaptureTempMovie.mp4"]];
|
||||
_referenceOrientation = AVCaptureVideoOrientationPortrait;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)start:(void (^)(NSError *error))handle {
|
||||
@weakify(self);
|
||||
dispatch_async(_movieWritingQueue, ^{
|
||||
@strongify(self);
|
||||
[self removeFile:self->_movieURL];
|
||||
NSError *error;
|
||||
if (!self->_movieWriter) {
|
||||
self->_movieWriter = [[AVAssetWriter alloc] initWithURL:self->_movieURL fileType:AVFileTypeMPEG4 error:&error];
|
||||
}
|
||||
handle(error);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)stop:(void (^)(NSURL *url, NSError *error))handle {
|
||||
@weakify(self);
|
||||
dispatch_async(_movieWritingQueue, ^{
|
||||
@strongify(self);
|
||||
self->_readyToRecordVideo = NO;
|
||||
self->_readyToRecordAudio = NO;
|
||||
|
||||
if (self->_movieWriter && self->_movieWriter.status == AVAssetWriterStatusWriting) {
|
||||
@weakify(self);
|
||||
[self->_movieWriter finishWritingWithCompletionHandler:^() {
|
||||
@strongify(self);
|
||||
@weakify(self);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@strongify(self);
|
||||
if (self->_movieWriter.status == AVAssetWriterStatusCompleted) {
|
||||
handle(self->_movieURL, nil);
|
||||
} else {
|
||||
handle(nil, self->_movieWriter.error);
|
||||
}
|
||||
self->_movieWriter = nil;
|
||||
});
|
||||
}];
|
||||
} else {
|
||||
[self->_movieWriter cancelWriting];
|
||||
self->_movieWriter = nil;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
handle(nil, [NSError errorWithDomain:@"com.tui.Movie.Writing" code:0 userInfo:@{NSLocalizedDescriptionKey : @"AVAssetWriter status error"}]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)writeData:(AVCaptureConnection *)connection video:(AVCaptureConnection *)video audio:(AVCaptureConnection *)audio buffer:(CMSampleBufferRef)buffer {
|
||||
CFRetain(buffer);
|
||||
@weakify(self);
|
||||
dispatch_async(_movieWritingQueue, ^{
|
||||
@strongify(self);
|
||||
if (connection == video) {
|
||||
if (!self->_readyToRecordVideo) {
|
||||
self->_readyToRecordVideo = [self setupAssetWriterVideoInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
|
||||
}
|
||||
if ([self inputsReadyToRecord]) {
|
||||
[self writeSampleBuffer:buffer ofType:AVMediaTypeVideo];
|
||||
}
|
||||
} else if (connection == audio) {
|
||||
if (!self->_readyToRecordAudio) {
|
||||
self->_readyToRecordAudio = [self setupAssetWriterAudioInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
|
||||
}
|
||||
if ([self inputsReadyToRecord]) {
|
||||
[self writeSampleBuffer:buffer ofType:AVMediaTypeAudio];
|
||||
}
|
||||
}
|
||||
CFRelease(buffer);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)writeSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(NSString *)mediaType {
|
||||
if (_movieWriter.status == AVAssetWriterStatusUnknown) {
|
||||
if ([_movieWriter startWriting]) {
|
||||
[_movieWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
|
||||
} else {
|
||||
NSLog(@"%@", _movieWriter.error);
|
||||
}
|
||||
}
|
||||
if (_movieWriter.status == AVAssetWriterStatusWriting) {
|
||||
if (mediaType == AVMediaTypeVideo) {
|
||||
if (!_movieVideoInput.isReadyForMoreMediaData) {
|
||||
return;
|
||||
}
|
||||
if (![_movieVideoInput appendSampleBuffer:sampleBuffer]) {
|
||||
NSLog(@"%@", _movieWriter.error);
|
||||
}
|
||||
} else if (mediaType == AVMediaTypeAudio) {
|
||||
if (!_movieAudioInput.isReadyForMoreMediaData) {
|
||||
return;
|
||||
}
|
||||
if (![_movieAudioInput appendSampleBuffer:sampleBuffer]) {
|
||||
NSLog(@"%@", _movieWriter.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)inputsReadyToRecord {
|
||||
return _readyToRecordVideo && _readyToRecordAudio;
|
||||
}
|
||||
|
||||
- (NSError *)setupAssetWriterAudioInput:(CMFormatDescriptionRef)currentFormatDescription {
|
||||
size_t aclSize = 0;
|
||||
const AudioStreamBasicDescription *currentASBD = CMAudioFormatDescriptionGetStreamBasicDescription(currentFormatDescription);
|
||||
const AudioChannelLayout *channelLayout = CMAudioFormatDescriptionGetChannelLayout(currentFormatDescription, &aclSize);
|
||||
NSData *dataLayout = aclSize > 0 ? [NSData dataWithBytes:channelLayout length:aclSize] : [NSData data];
|
||||
NSDictionary *settings = @{
|
||||
AVFormatIDKey : [NSNumber numberWithInteger:kAudioFormatMPEG4AAC],
|
||||
AVSampleRateKey : [NSNumber numberWithFloat:currentASBD->mSampleRate],
|
||||
AVChannelLayoutKey : dataLayout,
|
||||
AVNumberOfChannelsKey : [NSNumber numberWithInteger:currentASBD->mChannelsPerFrame],
|
||||
AVEncoderBitRatePerChannelKey : [NSNumber numberWithInt:64000]
|
||||
};
|
||||
|
||||
if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeAudio]) {
|
||||
_movieAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:settings];
|
||||
_movieAudioInput.expectsMediaDataInRealTime = YES;
|
||||
if ([_movieWriter canAddInput:_movieAudioInput]) {
|
||||
[_movieWriter addInput:_movieAudioInput];
|
||||
} else {
|
||||
return _movieWriter.error;
|
||||
}
|
||||
} else {
|
||||
return _movieWriter.error;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSError *)setupAssetWriterVideoInput:(CMFormatDescriptionRef)currentFormatDescription {
|
||||
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(currentFormatDescription);
|
||||
NSUInteger numPixels = dimensions.width * dimensions.height;
|
||||
CGFloat bitsPerPixel = numPixels < (640 * 480) ? 4.05 : 11.0;
|
||||
NSDictionary *compression =
|
||||
@{AVVideoAverageBitRateKey : [NSNumber numberWithInteger:numPixels * bitsPerPixel], AVVideoMaxKeyFrameIntervalKey : [NSNumber numberWithInteger:30]};
|
||||
NSDictionary *settings = @{
|
||||
AVVideoCodecKey : AVVideoCodecH264,
|
||||
AVVideoWidthKey : [NSNumber numberWithInteger:dimensions.width],
|
||||
AVVideoHeightKey : [NSNumber numberWithInteger:dimensions.height],
|
||||
AVVideoCompressionPropertiesKey : compression
|
||||
};
|
||||
|
||||
if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeVideo]) {
|
||||
_movieVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:settings];
|
||||
_movieVideoInput.expectsMediaDataInRealTime = YES;
|
||||
_movieVideoInput.transform = [self transformFromCurrentVideoOrientationToOrientation:self.referenceOrientation];
|
||||
if ([_movieWriter canAddInput:_movieVideoInput]) {
|
||||
[_movieWriter addInput:_movieVideoInput];
|
||||
} else {
|
||||
return _movieWriter.error;
|
||||
}
|
||||
} else {
|
||||
return _movieWriter.error;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGAffineTransform)transformFromCurrentVideoOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
|
||||
CGFloat orientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:orientation];
|
||||
CGFloat videoOrientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:self.currentOrientation];
|
||||
CGFloat angleOffset;
|
||||
if (self.currentDevice.position == AVCaptureDevicePositionBack) {
|
||||
angleOffset = videoOrientationAngleOffset - orientationAngleOffset + M_PI_2;
|
||||
} else {
|
||||
angleOffset = orientationAngleOffset - videoOrientationAngleOffset + M_PI_2;
|
||||
}
|
||||
CGAffineTransform transform = CGAffineTransformMakeRotation(angleOffset);
|
||||
return transform;
|
||||
}
|
||||
|
||||
- (CGFloat)angleOffsetFromPortraitOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
|
||||
CGFloat angle = 0.0;
|
||||
switch (orientation) {
|
||||
case AVCaptureVideoOrientationPortrait:
|
||||
angle = 0.0;
|
||||
break;
|
||||
case AVCaptureVideoOrientationPortraitUpsideDown:
|
||||
angle = M_PI;
|
||||
break;
|
||||
case AVCaptureVideoOrientationLandscapeRight:
|
||||
angle = -M_PI_2;
|
||||
break;
|
||||
case AVCaptureVideoOrientationLandscapeLeft:
|
||||
angle = M_PI_2;
|
||||
break;
|
||||
}
|
||||
return angle;
|
||||
}
|
||||
|
||||
- (void)removeFile:(NSURL *)fileURL {
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSString *filePath = fileURL.path;
|
||||
if ([fileManager fileExistsAtPath:filePath]) {
|
||||
NSError *error;
|
||||
BOOL success = [fileManager removeItemAtPath:filePath error:&error];
|
||||
if (!success) {
|
||||
NSAssert(NO, error.localizedDescription);
|
||||
NSLog(@"Failed to delete file:%@", error);
|
||||
} else {
|
||||
NSLog(@"Succeed to delete file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
// Created by Tencent on 2023/06/09.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
/**
|
||||
*
|
||||
* This document declares the relevant modules for group request management.
|
||||
* You can manage users' group join requests through the TUIGroupPendencyController in this file.
|
||||
* Including browsing applicant information, processing applicant requests and other related operations.
|
||||
*/
|
||||
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TUIGroupPendencyDataProvider.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
*
|
||||
* 【Module name】 TUIGroupPendencyController
|
||||
* 【Function description】Group request controller.
|
||||
* This view is responsible for providing the group administrator with a controller for processing group addition applications when the group is set to
|
||||
* "Require Admin Approval" This control is implemented by UITableView by default, and the application for group membership is displayed through tableView. The
|
||||
* information for joining a group application includes: user avatar, user nickname, application introduction, and agree button. After clicking a specific
|
||||
* tableCell, you can enter the detailed interface corresponding to the application (the detailed page includes a reject button).
|
||||
*/
|
||||
@interface TUIGroupPendencyController : UITableViewController
|
||||
|
||||
@property TUIGroupPendencyDataProvider *viewModel;
|
||||
|
||||
@property(nonatomic, copy) void (^cellClickBlock)(TUIGroupPendencyCell *cell);
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// TUIGroupPendencyController.m
|
||||
// TXIMSDK_TUIKit_iOS
|
||||
//
|
||||
// Created by annidyfeng on 2019/6/18.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIGroupPendencyController.h"
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
|
||||
@interface TUIGroupPendencyController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIGroupPendencyController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self.tableView registerClass:[TUIGroupPendencyCell class] forCellReuseIdentifier:@"PendencyCell"];
|
||||
|
||||
self.tableView.tableFooterView = [UIView new];
|
||||
|
||||
self.title = TIMCommonLocalizableString(TUIKitGroupApplicant);
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.viewModel.dataList.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TUIGroupPendencyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PendencyCell" forIndexPath:indexPath];
|
||||
TUIGroupPendencyCellData *data = self.viewModel.dataList[indexPath.row];
|
||||
data.cselector = @selector(cellClick:);
|
||||
data.cbuttonSelector = @selector(btnClick:);
|
||||
[cell fillWithData:data];
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return 70;
|
||||
}
|
||||
|
||||
// Override to support editing the table view.
|
||||
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (editingStyle == UITableViewCellEditingStyleDelete) {
|
||||
// add code here for when you hit delete
|
||||
[self.tableView beginUpdates];
|
||||
TUIGroupPendencyCellData *data = self.viewModel.dataList[indexPath.row];
|
||||
[self.viewModel removeData:data];
|
||||
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationFade];
|
||||
[self.tableView endUpdates];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)btnClick:(TUIGroupPendencyCell *)cell {
|
||||
[self.viewModel acceptData:cell.pendencyData];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)cellClick:(TUIGroupPendencyCell *)cell {
|
||||
if (self.cellClickBlock) {
|
||||
self.cellClickBlock(cell);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
41
TUIKit/TUIChat/CommonUI/Pendency/TUIGroupPinCell.h
Normal file
41
TUIKit/TUIChat/CommonUI/Pendency/TUIGroupPinCell.h
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// TUIGroupPinCell.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by Tencent on 2024/05/20.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/NSDictionary+TUISafe.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUILogin.h>
|
||||
#import <TIMCommon/TUIBubbleMessageCellData.h>
|
||||
#import <TIMCommon/TUIMessageCellData.h>
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIGroupPinCellView : UIView
|
||||
@property (nonatomic, copy) void(^onClickRemove)(V2TIMMessage *originMessage);
|
||||
@property (nonatomic, copy) void(^onClickCellView)(V2TIMMessage *originMessage);
|
||||
@property (nonatomic, strong) TUIMessageCellData *cellData;
|
||||
@property (nonatomic, strong) UIImageView *leftIcon;
|
||||
@property (nonatomic, strong) UILabel * titleLabel;
|
||||
@property (nonatomic, strong) UILabel * content;
|
||||
@property (nonatomic, strong) UIButton * removeButton;
|
||||
@property (nonatomic, strong) UIView * multiAnimationView;
|
||||
@property (nonatomic, strong) UIView * bottomLine;
|
||||
@property (nonatomic, assign) BOOL isFirstPage;
|
||||
- (void)fillWithData:(TUIMessageCellData *)cellData;
|
||||
- (void)hiddenMultiAnimation;
|
||||
- (void)showMultiAnimation;
|
||||
@end
|
||||
|
||||
@interface TUIGroupPinCell : UITableViewCell
|
||||
@property (nonatomic,strong) TUIGroupPinCellView* cellView;
|
||||
- (void)fillWithData:(TUIMessageCellData *)cellData;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
297
TUIKit/TUIChat/CommonUI/Pendency/TUIGroupPinCell.m
Normal file
297
TUIKit/TUIChat/CommonUI/Pendency/TUIGroupPinCell.m
Normal file
@@ -0,0 +1,297 @@
|
||||
//
|
||||
// TUIGroupPinCell.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by Tencent on 2024/05/20.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIMessageDataProvider.h"
|
||||
#import "TUIGroupPinCell.h"
|
||||
|
||||
@implementation TUIGroupPinCellView
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
[self setupView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)fillWithData:(TUIMessageCellData *)cellData {
|
||||
self.cellData = cellData;
|
||||
self.titleLabel.text = [TUIMessageDataProvider getShowName:cellData.innerMessage];
|
||||
self.content.text = [TUIMessageDataProvider getDisplayString:cellData.innerMessage];
|
||||
|
||||
// tell constraints they need updating
|
||||
[self setNeedsUpdateConstraints];
|
||||
|
||||
// update constraints now so we can animate the change
|
||||
[self updateConstraintsIfNeeded];
|
||||
|
||||
[self layoutIfNeeded];
|
||||
}
|
||||
|
||||
+ (BOOL)requiresConstraintBasedLayout {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// this is Apple's recommended place for adding/updating constraints
|
||||
- (void)updateConstraints {
|
||||
|
||||
[super updateConstraints];
|
||||
[self.leftIcon mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(self);
|
||||
make.centerY.mas_equalTo(self);
|
||||
make.width.mas_equalTo(6);
|
||||
make.top.bottom.mas_equalTo(self);
|
||||
}];
|
||||
|
||||
[self.titleLabel sizeToFit];
|
||||
[self.titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(self.leftIcon.mas_trailing).mas_offset(8);
|
||||
make.trailing.mas_lessThanOrEqualTo(self.removeButton.mas_leading);
|
||||
make.width.mas_equalTo(self.titleLabel.frame.size.width);
|
||||
make.height.mas_equalTo(self.titleLabel.frame.size.height);
|
||||
make.top.mas_equalTo(self).mas_offset(9);
|
||||
}];
|
||||
|
||||
[self.content sizeToFit];
|
||||
[self.content mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(self.leftIcon.mas_trailing).mas_offset(8);
|
||||
make.trailing.mas_lessThanOrEqualTo(self.removeButton.mas_leading);
|
||||
make.width.mas_equalTo(self.content.frame.size.width);
|
||||
make.height.mas_equalTo(self.content.frame.size.height);
|
||||
make.bottom.mas_equalTo(self).mas_offset(-9);
|
||||
}];
|
||||
|
||||
[self.removeButton mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.trailing.mas_equalTo(-10);
|
||||
make.centerY.mas_equalTo(self);
|
||||
make.width.mas_equalTo(30);
|
||||
make.height.mas_equalTo(30);
|
||||
}];
|
||||
|
||||
[self.removeButton.imageView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.mas_equalTo(self.removeButton);
|
||||
make.width.mas_equalTo(14);
|
||||
make.height.mas_equalTo(14);
|
||||
}];
|
||||
|
||||
[self.multiAnimationView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(self.mas_leading);
|
||||
make.trailing.mas_equalTo(self.mas_trailing);
|
||||
make.width.mas_equalTo(self);
|
||||
make.height.mas_equalTo(20);
|
||||
make.top.mas_equalTo(self.mas_bottom);
|
||||
}];
|
||||
|
||||
[self.bottomLine mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.mas_equalTo(self);
|
||||
make.height.mas_equalTo(0.5);
|
||||
make.centerX.mas_equalTo(self);
|
||||
make.bottom.mas_equalTo(self);
|
||||
}];
|
||||
}
|
||||
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
CGPoint newP = [self convertPoint:point toView:self.multiAnimationView];
|
||||
if ([self.multiAnimationView pointInside:newP withEvent:event]) {
|
||||
return self.multiAnimationView;
|
||||
}
|
||||
return [super hitTest:point withEvent:event];
|
||||
|
||||
}
|
||||
- (void)setupView {
|
||||
self.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_back_color", @"#F9F9F9");
|
||||
[self addSubview:self.leftIcon];
|
||||
[self addSubview:self.titleLabel];
|
||||
[self addSubview:self.content];
|
||||
[self addSubview:self.removeButton];
|
||||
[self addSubview:self.multiAnimationView];
|
||||
[self addSubview:self.bottomLine];
|
||||
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
|
||||
[self addGestureRecognizer:tap];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
}
|
||||
|
||||
- (UIImageView *)leftIcon {
|
||||
if (!_leftIcon) {
|
||||
_leftIcon = [[UIImageView alloc] init];
|
||||
_leftIcon.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_left_color", @"#D9D9D9");
|
||||
}
|
||||
return _leftIcon;
|
||||
}
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.textColor = TUIChatDynamicColor(@"chat_pop_group_pin_title_color", @"#141516");
|
||||
_titleLabel.font = [UIFont systemFontOfSize:16.0];
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
- (UILabel *)content {
|
||||
if (!_content) {
|
||||
_content = [[UILabel alloc] init];
|
||||
_content.textColor = [TUIChatDynamicColor(@"chat_pop_group_pin_subtitle_color", @"#000000")
|
||||
colorWithAlphaComponent:0.6];
|
||||
_content.font = [UIFont systemFontOfSize:14.0];
|
||||
}
|
||||
return _content;
|
||||
}
|
||||
|
||||
- (UIButton *)removeButton {
|
||||
if (!_removeButton) {
|
||||
_removeButton = [UIButton buttonWithType:UIButtonTypeCustom];;
|
||||
[_removeButton setImage:[UIImage imageNamed:TUIChatImagePath(@"chat_group_del_icon")] forState:UIControlStateNormal];
|
||||
[_removeButton addTarget:self action:@selector(removeCurrentGroupPin) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
return _removeButton;
|
||||
}
|
||||
|
||||
|
||||
- (UIView *)multiAnimationView {
|
||||
if (!_multiAnimationView) {
|
||||
_multiAnimationView = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
_multiAnimationView.alpha = 0;
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
|
||||
_multiAnimationView.userInteractionEnabled = YES;
|
||||
[_multiAnimationView addGestureRecognizer:tap];
|
||||
UIView *arrowBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
arrowBackgroundView.backgroundColor = [UIColor clearColor];
|
||||
arrowBackgroundView.layer.cornerRadius = 5;
|
||||
[_multiAnimationView addSubview:arrowBackgroundView];
|
||||
_multiAnimationView.clipsToBounds = YES;
|
||||
UIImageView *arrow = [[UIImageView alloc] initWithFrame:CGRectZero];
|
||||
arrow.image = TUIChatBundleThemeImage(@"chat_pop_group_pin_down_arrow_img", @"chat_down_arrow_icon");
|
||||
[arrowBackgroundView addSubview:arrow];
|
||||
|
||||
UIView *bottomLine = [[UIView alloc] init];
|
||||
bottomLine.backgroundColor = [UIColor grayColor];
|
||||
[arrowBackgroundView addSubview:bottomLine];
|
||||
|
||||
[arrowBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.mas_equalTo(_multiAnimationView);
|
||||
make.size.mas_equalTo(CGSizeMake(20, 20));
|
||||
}];
|
||||
|
||||
[arrow mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.mas_equalTo(arrowBackgroundView);
|
||||
make.size.mas_equalTo(CGSizeMake(20, 20));
|
||||
}];
|
||||
|
||||
[bottomLine mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.width.mas_equalTo(_multiAnimationView);
|
||||
make.height.mas_equalTo(0.5);
|
||||
make.centerX.mas_equalTo(_multiAnimationView);
|
||||
make.bottom.mas_equalTo(_multiAnimationView);
|
||||
}];
|
||||
}
|
||||
return _multiAnimationView;
|
||||
}
|
||||
- (UIView *)bottomLine {
|
||||
if (!_bottomLine) {
|
||||
_bottomLine = [[UIView alloc] init];
|
||||
_bottomLine.backgroundColor = [UIColor grayColor];
|
||||
}
|
||||
return _bottomLine;
|
||||
}
|
||||
- (void)removeCurrentGroupPin {
|
||||
if (self.onClickRemove) {
|
||||
self.onClickRemove(self.cellData.innerMessage);
|
||||
}
|
||||
}
|
||||
- (void)onTap:(id)sender {
|
||||
if (self.onClickCellView) {
|
||||
self.onClickCellView(self.cellData.innerMessage);
|
||||
}
|
||||
}
|
||||
- (void)hiddenMultiAnimation {
|
||||
self.multiAnimationView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0];
|
||||
_multiAnimationView.alpha = 0;
|
||||
_bottomLine.alpha = 1;
|
||||
}
|
||||
|
||||
- (void)showMultiAnimation {
|
||||
self.multiAnimationView.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_back_color", @"#F9F9F9");
|
||||
_multiAnimationView.alpha = 1;
|
||||
_bottomLine.alpha = 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface TUIGroupPinCell ()
|
||||
@property (nonatomic, strong) UIView *separatorView;
|
||||
@end
|
||||
@implementation TUIGroupPinCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
|
||||
[self setupView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupView {
|
||||
[self.contentView addSubview:self.cellView];
|
||||
[self.contentView addSubview:self.separatorView];
|
||||
}
|
||||
|
||||
- (TUIGroupPinCellView *)cellView {
|
||||
if (!_cellView) {
|
||||
_cellView = [[TUIGroupPinCellView alloc] init];
|
||||
_cellView.isFirstPage = NO;
|
||||
}
|
||||
return _cellView;
|
||||
}
|
||||
- (UIView *)separatorView {
|
||||
if (!_separatorView) {
|
||||
_separatorView = [[UIView alloc] init];
|
||||
_separatorView.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_line_color", @"#DDDDDD");
|
||||
}
|
||||
return _separatorView;
|
||||
}
|
||||
|
||||
- (void)fillWithData:(TUIMessageCellData *)cellData {
|
||||
[self.cellView fillWithData:cellData];
|
||||
// tell constraints they need updating
|
||||
[self setNeedsUpdateConstraints];
|
||||
|
||||
// update constraints now so we can animate the change
|
||||
[self updateConstraintsIfNeeded];
|
||||
|
||||
[self layoutIfNeeded];
|
||||
}
|
||||
|
||||
+ (BOOL)requiresConstraintBasedLayout {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// this is Apple's recommended place for adding/updating constraints
|
||||
- (void)updateConstraints {
|
||||
|
||||
[super updateConstraints];
|
||||
|
||||
[self.cellView mas_remakeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(self);
|
||||
make.trailing.mas_equalTo(self);
|
||||
make.top.mas_equalTo(self);
|
||||
make.bottom.mas_equalTo(self);
|
||||
}];
|
||||
|
||||
[self.separatorView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.leading.mas_equalTo(self.contentView).mas_offset(6);
|
||||
make.trailing.mas_equalTo(self.contentView);
|
||||
make.bottom.mas_equalTo(self.contentView);
|
||||
make.height.mas_equalTo(0.5);
|
||||
}];
|
||||
|
||||
}
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// TUIGroupPinPageViewController.h
|
||||
// TUIChat
|
||||
//
|
||||
// Created by Tencent on 2024/05/20.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
#import <UIKit/UIKit.h>
|
||||
@import ImSDK_Plus;
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TUIGroupPinPageViewController : UIViewController
|
||||
|
||||
@property(nonatomic, strong) UITableView *tableview;
|
||||
@property(nonatomic, strong) UIView *customArrowView;
|
||||
@property(nonatomic, strong) NSArray *groupPinList;
|
||||
@property (nonatomic, copy) void(^onClickRemove)(V2TIMMessage *originMessage);
|
||||
@property (nonatomic, copy) void(^onClickCellView)(V2TIMMessage *originMessage);
|
||||
@property(nonatomic, assign) BOOL canRemove;
|
||||
@property (nonatomic,strong) UIView *bottomShadow;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
155
TUIKit/TUIChat/CommonUI/Pendency/TUIGroupPinPageViewController.m
Normal file
155
TUIKit/TUIChat/CommonUI/Pendency/TUIGroupPinPageViewController.m
Normal file
@@ -0,0 +1,155 @@
|
||||
//
|
||||
// TUIGroupPinPageViewController.m
|
||||
// TUIChat
|
||||
//
|
||||
// Created by Tencent on 2024/05/20.
|
||||
// Copyright © 2023 Tencent. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TUIGroupPinPageViewController.h"
|
||||
#import <TIMCommon/NSString+TUIEmoji.h>
|
||||
#import <TIMCommon/TIMCommonModel.h>
|
||||
#import <TIMCommon/TIMDefine.h>
|
||||
#import <TUICore/NSDictionary+TUISafe.h>
|
||||
#import <TUICore/TUICore.h>
|
||||
#import <TUICore/TUILogin.h>
|
||||
#import "TUIGroupPinCell.h"
|
||||
#import "TUIMessageDataProvider.h"
|
||||
|
||||
|
||||
@interface TUIGroupPinPageViewController() <UITableViewDelegate,UITableViewDataSource>
|
||||
|
||||
@end
|
||||
|
||||
@implementation TUIGroupPinPageViewController
|
||||
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.tableview.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_back_color", @"#F9F9F9");
|
||||
self.customArrowView.backgroundColor = TUIChatDynamicColor(@"chat_pop_group_pin_back_color", @"#F9F9F9");
|
||||
self.bottomShadow.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:.5];
|
||||
|
||||
[self addSingleTapGesture];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)addSingleTapGesture {
|
||||
// When clicking on the shadow, the page disappears
|
||||
self.view.userInteractionEnabled = YES;
|
||||
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap:)];
|
||||
[self.view addGestureRecognizer:singleTap];
|
||||
}
|
||||
|
||||
- (void)singleTap:(UITapGestureRecognizer *)tap {
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{
|
||||
self.tableview.frame = CGRectMake(0, self.tableview.frame.origin.y, self.view.frame.size.width, 60);
|
||||
self.customArrowView.frame = CGRectMake(0, CGRectGetMaxY(self.tableview.frame), self.view.frame.size.width, self.customArrowView.frame.size.height);
|
||||
self.bottomShadow.frame = CGRectMake(0, CGRectGetMaxY(self.customArrowView.frame), self.view.frame.size.width, 0);
|
||||
}completion:^(BOOL finished) {
|
||||
if (finished) {
|
||||
[self dismissViewControllerAnimated:NO completion:nil];
|
||||
}
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
- (UITableView *)tableview {
|
||||
if (!_tableview) {
|
||||
_tableview = [[UITableView alloc] init];
|
||||
_tableview.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
|
||||
_tableview.delegate = self;
|
||||
_tableview.dataSource = self;
|
||||
[_tableview setSeparatorStyle:UITableViewCellSeparatorStyleNone];
|
||||
_tableview.showsVerticalScrollIndicator = NO;
|
||||
_tableview.showsHorizontalScrollIndicator = NO;
|
||||
[self.view addSubview:_tableview];
|
||||
UIView *herderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 0)];
|
||||
_tableview.tableHeaderView = herderView;
|
||||
}
|
||||
return _tableview;
|
||||
}
|
||||
|
||||
- (UIView *)customArrowView {
|
||||
if (!_customArrowView) {
|
||||
_customArrowView = [[UIView alloc] initWithFrame:CGRectMake(0, _tableview.frame.size.height, _tableview.frame.size.width, 100)];
|
||||
[self.view addSubview:_customArrowView];
|
||||
UIView *arrowBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
arrowBackgroundView.backgroundColor = [UIColor clearColor];
|
||||
arrowBackgroundView.layer.cornerRadius = 5;
|
||||
[_customArrowView addSubview:arrowBackgroundView];
|
||||
_customArrowView.clipsToBounds = YES;
|
||||
UIImageView *arrow = [[UIImageView alloc] initWithFrame:CGRectZero];
|
||||
arrow.image = TUIChatBundleThemeImage(@"chat_pop_group_pin_up_arrow_img", @"chat_up_arrow_icon");
|
||||
[arrowBackgroundView addSubview:arrow];
|
||||
|
||||
[arrowBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.mas_equalTo(_customArrowView);
|
||||
make.size.mas_equalTo(CGSizeMake(20, 20));
|
||||
}];
|
||||
|
||||
[arrow mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.center.mas_equalTo(arrowBackgroundView);
|
||||
make.size.mas_equalTo(CGSizeMake(20, 20));
|
||||
}];
|
||||
|
||||
}
|
||||
return _customArrowView;
|
||||
}
|
||||
|
||||
- (UIView *)bottomShadow {
|
||||
if (!_bottomShadow) {
|
||||
_bottomShadow = [[UIView alloc] init];
|
||||
_bottomShadow.userInteractionEnabled = NO;
|
||||
[self.view addSubview:_bottomShadow];
|
||||
}
|
||||
return _bottomShadow;
|
||||
}
|
||||
#pragma mark - group pin
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
TUIGroupPinCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
|
||||
if (cell == nil) {
|
||||
cell = [[TUIGroupPinCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
|
||||
}
|
||||
cell.backgroundColor = [UIColor systemGroupedBackgroundColor];
|
||||
V2TIMMessage *msg = self.groupPinList[indexPath.row];
|
||||
TUIMessageCellData * cellData = [TUIMessageDataProvider getCellData:msg];
|
||||
[cell fillWithData:cellData];
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
cell.cellView.removeButton.hidden = !self.canRemove;
|
||||
cell.cellView.onClickRemove = ^(V2TIMMessage *originMessage) {
|
||||
if (weakSelf.onClickRemove) {
|
||||
weakSelf.onClickRemove(originMessage);
|
||||
}
|
||||
};
|
||||
|
||||
cell.cellView.onClickCellView = ^(V2TIMMessage *originMessage) {
|
||||
if (weakSelf.onClickCellView) {
|
||||
weakSelf.onClickCellView(originMessage);
|
||||
}
|
||||
[weakSelf singleTap:nil];
|
||||
};
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.groupPinList.count;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
CGFloat contentH = 62;
|
||||
CGFloat padding = 0;
|
||||
return contentH + padding;
|
||||
}
|
||||
|
||||
@end
|
||||
29
TUIKit/TUIChat/CommonUI/Pop/TUIChatFlexViewController.h
Normal file
29
TUIKit/TUIChat/CommonUI/Pop/TUIChatFlexViewController.h
Normal 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
|
||||
163
TUIKit/TUIChat/CommonUI/Pop/TUIChatFlexViewController.m
Normal file
163
TUIKit/TUIChat/CommonUI/Pop/TUIChatFlexViewController.m
Normal 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
|
||||
48
TUIKit/TUIChat/CommonUI/Pop/TUIChatPopContextController.h
Normal file
48
TUIKit/TUIChat/CommonUI/Pop/TUIChatPopContextController.h
Normal 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
|
||||
462
TUIKit/TUIChat/CommonUI/Pop/TUIChatPopContextController.m
Normal file
462
TUIKit/TUIChat/CommonUI/Pop/TUIChatPopContextController.m
Normal 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
|
||||
40
TUIKit/TUIChat/CommonUI/Pop/TUIChatPopContextExtionView.h
Normal file
40
TUIKit/TUIChat/CommonUI/Pop/TUIChatPopContextExtionView.h
Normal 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
|
||||
114
TUIKit/TUIChat/CommonUI/Pop/TUIChatPopContextExtionView.m
Normal file
114
TUIKit/TUIChat/CommonUI/Pop/TUIChatPopContextExtionView.m
Normal 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
|
||||
30
TUIKit/TUIChat/CommonUI/Pop/UIImage+ImageEffects.h
Normal file
30
TUIKit/TUIChat/CommonUI/Pop/UIImage+ImageEffects.h
Normal 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 you’ll 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
|
||||
202
TUIKit/TUIChat/CommonUI/Pop/UIImage+ImageEffects.m
Normal file
202
TUIKit/TUIChat/CommonUI/Pop/UIImage+ImageEffects.m
Normal 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
|
||||
Reference in New Issue
Block a user