diff --git a/android/src/main/java/com/finogeeks/mop/api/mop/AppletHandlerModule.java b/android/src/main/java/com/finogeeks/mop/api/mop/AppletHandlerModule.java index d2b2c55..88dab1d 100644 --- a/android/src/main/java/com/finogeeks/mop/api/mop/AppletHandlerModule.java +++ b/android/src/main/java/com/finogeeks/mop/api/mop/AppletHandlerModule.java @@ -153,6 +153,7 @@ public class AppletHandlerModule extends BaseApi { @Nullable @Override + @SuppressWarnings("unchecked") public List getRegisteredMoreMenuItems(@NotNull String s) { CountDownLatch latch = new CountDownLatch(1); List moreMenuItems = new ArrayList<>(); @@ -162,9 +163,8 @@ public class AppletHandlerModule extends BaseApi { channel.invokeMethod("extensionApi:getCustomMenus", params, new MethodChannel.Result() { @Override public void success(Object result) { - List> ret = (List>) result; - FinAppTrace.d(TAG, "getCustomMenus success : " + ret + " size : " + ret.size()); - if (ret != null) { + if (result instanceof List) { + List> ret = (List>) result; for (Map map : ret) { String type = (String) map.get("type"); MoreMenuType moreMenuType; @@ -173,7 +173,28 @@ public class AppletHandlerModule extends BaseApi { } else { moreMenuType = MoreMenuType.ON_MINI_PROGRAM; } - moreMenuItems.add(new MoreMenuItem((String) map.get("menuId"), (String) map.get("title"), moreMenuType)); + String menuId = (String) map.get("menuId"); + if (menuId == null) { + menuId = ""; + } + String title = (String) map.get("title"); + if (title == null) { + title = ""; + } + String image = (String) map.get("image"); + if (image == null) { + image = ""; + } + moreMenuItems.add( + new MoreMenuItem( + menuId, + title, + image, + -1, + moreMenuType, + true + ) + ); } } latch.countDown(); diff --git a/example/pubspec.lock b/example/pubspec.lock index f69158c..86b7e85 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -108,7 +108,7 @@ packages: path: ".." relative: true source: path - version: "2.37.13" + version: "2.39.1" path: dependency: transitive description: diff --git a/ios/Classes/Api/MOPAppletDelegate.m b/ios/Classes/Api/MOPAppletDelegate.m index 6381e36..8947600 100644 --- a/ios/Classes/Api/MOPAppletDelegate.m +++ b/ios/Classes/Api/MOPAppletDelegate.m @@ -9,6 +9,7 @@ #import "MopPlugin.h" #import "MopCustomMenuModel.h" #import +#import @interface NSString (FATEncode) - (NSString *)fat_encodeString; @@ -73,7 +74,14 @@ MopCustomMenuModel *model = [[MopCustomMenuModel alloc] init]; model.menuId = data[@"menuId"]; model.menuTitle = data[@"title"]; - model.menuIconImage = [UIImage imageNamed:data[@"image"]]; + NSString *imageUrl = data[@"image"]; + if ([imageUrl hasPrefix:@"http"]) { + // 需要异步加载,待优化! + NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]; + model.menuIconImage = [UIImage imageWithData:data]; + } else { + model.menuIconImage = [UIImage imageNamed:imageUrl]; + } NSString *typeString = data[@"type"]; if (typeString) { FATAppletMenuStyle style = [typeString isEqualToString:@"onMiniProgram"] ? FATAppletMenuStyleOnMiniProgram : FATAppletMenuStyleCommon; @@ -86,11 +94,16 @@ } - (void)clickCustomItemMenuWithInfo:(NSDictionary *)contentInfo inApplet:(FATAppletInfo *)appletInfo completion:(void (^)(FATExtensionCode code, NSDictionary *result))completion { + NSError *parseError = nil; + NSMutableDictionary *shareDic = [[NSMutableDictionary alloc] initWithDictionary:[self dictionaryRepresentation:appletInfo]]; + [shareDic setValue:@{@"desc" : shareDic[@"originalInfo"][@"customData"][@"detailDescription"]} forKey:@"params"]; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:shareDic options:NSJSONWritingPrettyPrinted error:&parseError]; + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; NSDictionary *arguments = @{ @"appId": contentInfo[@"appId"], @"path": contentInfo[@"path"], @"menuId": contentInfo[@"menuId"], - @"appInfo": appletInfo.description + @"appInfo": jsonString }; FlutterMethodChannel *channel = [[MopPlugin instance] methodChannel]; [channel invokeMethod:@"extensionApi:onCustomMenuClick" arguments:arguments result:^(id _Nullable result) { @@ -102,6 +115,21 @@ } } +- (NSDictionary *)dictionaryRepresentation:(FATAppletInfo *)object { + unsigned int count; + objc_property_t *properties = class_copyPropertyList([object class], &count); + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:count]; + for (int i = 0; i < count; i++) { + NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])]; + id value = [object valueForKey:key]; + if (key && value) { + [dict setObject:value forKey:key]; + } + } + free(properties); + return dict; +} + - (void)applet:(NSString *)appletId didOpenCompletion:(NSError *)error { if (!appletId) { return; diff --git a/ios/Classes/MopPlugin.m b/ios/Classes/MopPlugin.m index dc760ea..0417baf 100644 --- a/ios/Classes/MopPlugin.m +++ b/ios/Classes/MopPlugin.m @@ -3,6 +3,9 @@ #import "MOPApiRequest.h" #import "MOPApiConverter.h" #import "MOPAppletDelegate.h" +#import +#import "MopShareView.h" +#import @implementation MopEventStream { FlutterEventSink _eventSink; @@ -55,6 +58,12 @@ static MopPlugin *_instance; binaryMessenger:[registrar messenger]]; [registrar addMethodCallDelegate:_instance channel:appletChannel]; _instance.appletMethodChannel = appletChannel; + + FlutterMethodChannel* appletShareChannel = [FlutterMethodChannel + methodChannelWithName:@"plugins.finosprite.finogeeks.com/share_applet" + binaryMessenger:[registrar messenger]]; + [registrar addMethodCallDelegate:_instance channel:appletShareChannel]; + _instance.shareMethodChannel = appletShareChannel; } @@ -77,13 +86,54 @@ static MopPlugin *_instance; result(dict); } else if ([@"copyFileAsFinFile" isEqualToString:call.method]) { - NSString *appId = call.arguments[@"appId"]; +// NSString *appId = call.arguments[@"appId"]; NSString *path = call.arguments[@"path"]; NSString *fileName = [path componentsSeparatedByString:@"/"].lastObject; NSMutableDictionary *dict = [NSMutableDictionary dictionary]; dict[@"path"] = [[FATClient sharedClient] saveFile:[NSData dataWithContentsOfFile:path] fileName:fileName]; result(dict); } + else if ([@"showShareAppletDialog" isEqualToString:call.method]) { +// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + UIImage *image = [MOPTools getCurrentPageImage]; + MopShareView *view = [MopShareView viewWithData:call.arguments]; + view.image = image; + [view show]; + [view setDidSelcetTypeBlock:^(NSString *type) { + result(type); + }]; +// }); + } + else if ([@"showLoading" isEqualToString:call.method]) { + UIViewController *currentVC = [MOPTools topViewController]; + [currentVC.view fatMakeToastActivity:CSToastPositionCenter]; + result([self appInfoDictWithAppId:call.arguments[@"appId"]]); + } + else if ([@"hideLoading" isEqualToString:call.method]) { + UIViewController *currentVC = [MOPTools topViewController]; + [currentVC.view fatHideToastActivity]; + [currentVC.view fatHideAllToasts]; + result([self appInfoDictWithAppId:call.arguments[@"appId"]]); + } + else if ([@"showToast" isEqualToString:call.method]) { + UIViewController *currentVC = [MOPTools topViewController]; + [currentVC.view fatMakeToast:call.arguments[@"msg"] + duration:2.0 + position:CSToastPositionCenter]; + result([self appInfoDictWithAppId:call.arguments[@"appId"]]); + } + else if ([@"getScreenshot" isEqualToString:call.method]) { + UIViewController *currentVC = [MOPTools topViewController]; +// [currentVC.view fatHideToastActivity]; +// [currentVC.view fatHideAllToasts]; +// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ +// UIImage *image = [[FATClient sharedClient] getDefaultCurrentAppletImage:0.0f]; + UIImage *image = [MOPTools getCurrentPageImage]; + NSString *filePtah = [[FATClient sharedClient] saveFile:UIImagePNGRepresentation(image) fileName:[NSString stringWithFormat:@"%@",call.arguments[@"appId"]]]; + filePtah = [[FATClient sharedClient] fat_absolutePathWithPath:filePtah]; + result(filePtah); +// }); + } else if ([@"getPhoneNumberResult" isEqualToString:call.method]) { if ([MOPAppletDelegate instance].bindGetPhoneNumbers) { NSDictionary *dic = [[NSDictionary alloc] initWithDictionary:call.arguments]; @@ -199,4 +249,5 @@ static MopPlugin *_instance; } return nil;; } + @end diff --git a/ios/Classes/Utils/MOPTools.h b/ios/Classes/Utils/MOPTools.h index 37195b2..f9f048a 100644 --- a/ios/Classes/Utils/MOPTools.h +++ b/ios/Classes/Utils/MOPTools.h @@ -18,6 +18,22 @@ NS_ASSUME_NONNULL_BEGIN + (UIColor *)fat_colorWithHexString:(NSString *)hexColor; + (BOOL)fat_currentLanguageIsEn; + +/// 设置颜色( + 暗黑模式):UIDynamicProviderColor(会随着暗黑模式/明亮模式切换自动变化颜色) +/// @param lightHexString (明亮模式 的颜色值) +/// @param darkHexString (暗黑模式 的颜色值) ++ (UIColor *)fat_dynamicColorWithLightHexString:(NSString *)lightHexString darkHexString:(NSString *)darkHexString; + +/// 生成image对象 +/// @param view 指定的view ++ (UIImage *)snapshotWithView:(UIView *)view; + +/// 根据链接生成二维码图片 +/// @param string 二维码的内容 ++ (UIImage *)makeQRCodeForString:(NSString *)string; + ++ (UIImage *)getCurrentPageImage; + @end NS_ASSUME_NONNULL_END diff --git a/ios/Classes/Utils/MOPTools.m b/ios/Classes/Utils/MOPTools.m index d9a2baa..a2a9e9a 100644 --- a/ios/Classes/Utils/MOPTools.m +++ b/ios/Classes/Utils/MOPTools.m @@ -6,6 +6,7 @@ // #import "MOPTools.h" +#import @implementation MOPTools + (UIViewController *)topViewController{ @@ -120,4 +121,147 @@ return YES; } +/// 设置颜色( + 暗黑模式) +/// @param lightHexString (明亮模式 的颜色值) +/// @param darkHexString (暗黑模式 的颜色值) ++ (UIColor *)fat_dynamicColorWithLightHexString:(NSString *)lightHexString darkHexString:(NSString *)darkHexString { + return [self fat_dynamicColorWithLight:[UIColor fat_colorWithRGBAHexString:lightHexString] darkColor:[UIColor fat_colorWithRGBAHexString:darkHexString]]; +} + ++ (UIColor *)fat_dynamicColorWithLight:(UIColor *)lightColor darkColor:(UIColor *)darkColor { + if (!darkColor) { + return lightColor; + } + BOOL autoAdaptDarkMode = [FATClient sharedClient].uiConfig.autoAdaptDarkMode; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunsupported-availability-guard" +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + if (@available(iOS 13.0, *) && autoAdaptDarkMode) { + return [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(UITraitCollection *_Nonnull traitCollection) { + if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { +#pragma clang diagnostic pop + return darkColor; + } else { + return lightColor; + } + }]; + } else { + return lightColor; + } +} + ++ (UIImage *)snapshotWithView:(UIView *)view { + UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, [UIScreen mainScreen].scale); + [view.layer renderInContext:UIGraphicsGetCurrentContext()]; + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + + + ++ (UIImage *)makeQRCodeForString:(NSString *)string { + NSString *text = string; + NSData *stringData = [text dataUsingEncoding: NSUTF8StringEncoding]; + //生成 + CIFilter *qrFilter = [CIFilter filterWithName:@"CIQRCodeGenerator"]; + [qrFilter setValue:stringData forKey:@"inputMessage"]; + [qrFilter setValue:@"M" forKey:@"inputCorrectionLevel"]; + //二维码颜色 + UIColor *onColor = [UIColor whiteColor]; + UIColor *offColor = [UIColor blackColor]; + //上色,如果只要白底黑块的QRCode可以跳过这一步 + CIFilter *colorFilter = [CIFilter filterWithName:@"CIFalseColor" + keysAndValues: + @"inputImage",qrFilter.outputImage, + @"inputColor0",[CIColor colorWithCGColor:onColor.CGColor], + @"inputColor1",[CIColor colorWithCGColor:offColor.CGColor], + nil]; + //绘制 + CIImage *qrImage = colorFilter.outputImage; + CGSize size = CGSizeMake(300, 300); + CGImageRef cgImage = [[CIContext contextWithOptions:nil] createCGImage:qrImage fromRect:qrImage.extent]; + UIGraphicsBeginImageContext(size); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetInterpolationQuality(context, kCGInterpolationNone); + CGContextScaleCTM(context, 1.0, -1.0);//生成的QRCode就是上下颠倒的,需要翻转一下 + CGContextDrawImage(context, CGContextGetClipBoundingBox(context), cgImage); + UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + CGImageRelease(cgImage); + + return [UIImage imageWithCIImage:qrImage]; +} + + +/// 顶部状态栏高度(包括安全区) ++ (CGFloat)getStatusHeight { + if (@available(iOS 13.0, *)) { + NSSet *set = [UIApplication sharedApplication].connectedScenes; + UIWindowScene *windowScene = [set anyObject]; + UIStatusBarManager *statusBarManager = windowScene.statusBarManager; + return statusBarManager.statusBarFrame.size.height; + } else { + return [UIApplication sharedApplication].statusBarFrame.size.height; + } +} + ++ (UIImage *)getCurrentPageImage { + UIViewController *currentVC = [MOPTools topViewController]; + + UIGraphicsBeginImageContextWithOptions(CGSizeMake(currentVC.view.frame.size.width, 440), NO, [UIScreen mainScreen].scale); + [currentVC.view.layer renderInContext:UIGraphicsGetCurrentContext()]; + UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return snapshotImage; +} + +// 截取当前屏幕 ,返回截取到的图片 +//+ (UIImage *)getCurrentPageImage { +// +// +// CGSize imageSize = CGSizeZero; +// +// +// +// +// UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; +// if (UIInterfaceOrientationIsPortrait(orientation)) { +// imageSize = [UIScreen mainScreen].bounds.size; +// } else { +// imageSize = CGSizeMake([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width); +// } +// // 绘制上下文 +// UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0); +// CGContextRef context = UIGraphicsGetCurrentContext(); +// for (UIWindow *window in [[UIApplication sharedApplication] windows]) { +// CGContextSaveGState(context); +// CGContextTranslateCTM(context, window.center.x, window.center.y); +// CGContextConcatCTM(context, window.transform); +// CGContextTranslateCTM(context, -window.bounds.size.width * window.layer.anchorPoint.x, -window.bounds.size.height * window.layer.anchorPoint.y); +// if (orientation == UIInterfaceOrientationLandscapeLeft) { +// CGContextRotateCTM(context, M_PI_2); +// CGContextTranslateCTM(context, 0, -imageSize.width); +// } else if (orientation == UIInterfaceOrientationLandscapeRight) { +// CGContextRotateCTM(context, -M_PI_2); +// CGContextTranslateCTM(context, -imageSize.height, 0); +// } else if (orientation == UIInterfaceOrientationPortraitUpsideDown) { +// CGContextRotateCTM(context, M_PI); +// CGContextTranslateCTM(context, -imageSize.width, -imageSize.height); +// } +// if ([window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) { +// [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES]; +// } else { +// [window.layer renderInContext:context]; +// } +// CGContextRestoreGState(context); +// } +// UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); +// UIGraphicsEndImageContext(); +// NSData *imageData = UIImagePNGRepresentation(image); +// return [UIImage imageWithData:imageData]; +//} + + + @end diff --git a/ios/Classes/Utils/MopShareView.h b/ios/Classes/Utils/MopShareView.h new file mode 100644 index 0000000..0df5bdc --- /dev/null +++ b/ios/Classes/Utils/MopShareView.h @@ -0,0 +1,39 @@ +// +// MopShareView.h +// mop +// +// Created by 王兆耀 on 2023/1/2. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^MOPshareBottomViewTypeBlock)(NSString *type); + +@interface MopShareView : UIView +@property (nonatomic, copy) MOPshareBottomViewTypeBlock didSelcetTypeBlock; +@property (nonatomic, strong) UIImage *image; +@property (nonatomic, strong) NSDictionary *dataDic; ++ (instancetype)viewWithData:(NSDictionary *)data; +- (void)show; +- (void)dismiss; +@end + + +@class MOPshareBottomViewCell; + +@protocol MOPCellClickDelegate +- (void)iconBtnDidClick:(MOPshareBottomViewCell *)cell; +@end + +@interface MOPshareBottomViewCell : UICollectionViewCell +@property (nonatomic, strong) NSString *type; +@property (nonatomic, strong) UIButton *imageButton; +@property (nonatomic, strong) UILabel *label; +@property (nonatomic, weak) id delegate; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Classes/Utils/MopShareView.m b/ios/Classes/Utils/MopShareView.m new file mode 100644 index 0000000..fe55b73 --- /dev/null +++ b/ios/Classes/Utils/MopShareView.m @@ -0,0 +1,317 @@ +// +// MopShareView.m +// mop +// +// Created by 王兆耀 on 2023/1/2. +// + +#import "MopShareView.h" +#import "MopPlugin.h" +#import "MOPTools.h" +#import +#import + +//获取安全区域距离 +#define kFinoSafeAreaTop kFinoWindowSafeAreaInset.top +#define kFinoSafeAreaBottom kFinoWindowSafeAreaInset.bottom + +#define kFinoWindowSafeAreaInset \ +({\ +UIEdgeInsets returnInsets = UIEdgeInsetsMake([UIApplication sharedApplication].statusBarFrame.size.height, 0, 0, 0);\ +UIWindow * keyWindow = [UIApplication sharedApplication].keyWindow;\ +if ([keyWindow respondsToSelector:NSSelectorFromString(@"safeAreaInsets")]) {\ +UIEdgeInsets inset = [[keyWindow valueForKeyPath:@"safeAreaInsets"] UIEdgeInsetsValue];\ +if (inset.top < [UIApplication sharedApplication].statusBarFrame.size.height) {\ +inset.top = [UIApplication sharedApplication].statusBarFrame.size.height;\ +}\ +returnInsets = inset;\ +}\ +(returnInsets);\ +})\ + +@interface MopShareView () + + +@property (nonatomic, strong) UIView *shareView; +@property (nonatomic, strong) UIImageView *appletImageView; + +@property (nonatomic, strong) UIView *contentView; +@property (nonatomic, strong) UIButton *cancelButton; +@property(nonatomic, strong) UICollectionView *collectionView; +@property(nonatomic, strong) NSArray *dataArray; +@property (nonatomic, strong) UIImageView *qrCodeImageView; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *descLabel; + +@end + +@implementation MopShareView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.dataArray = @[@{@"lightImage":@"share_wechat", @"title":@"微信好友", @"type":@"wechat"}, + @{@"lightImage":@"share_moments",@"title":@"朋友圈", @"type":@"moments"}, + @{@"lightImage":@"share_link",@"title":@"复制链接", @"type":@"links"}]; + self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4f]; + + self.shareView = [[UIView alloc] initWithFrame:CGRectMake(52.5 , 46 + kFinoSafeAreaTop, 270, 380)]; + self.shareView.layer.cornerRadius = 6; + self.shareView.backgroundColor = UIColor.whiteColor; + + UIImageView *appletImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 270, 300)]; + appletImageView.contentMode = UIViewContentModeScaleToFill; + self.appletImageView = appletImageView; + [self.shareView addSubview:appletImageView]; + + float bottomY = appletImageView.frame.size.height + appletImageView.frame.origin.y; + UIView *line0 = [[UIView alloc] initWithFrame:CGRectMake(0, bottomY, self.shareView.frame.size.width, 0.5)]; + line0.backgroundColor = [MOPTools fat_dynamicColorWithLightHexString:@"#eeeeee" darkHexString:@"#eeeeee"]; + [self.shareView addSubview:line0]; + + + UILabel *descLabel = [[UILabel alloc] init]; + descLabel.frame = CGRectMake(14, bottomY + 12, 168, 21); + descLabel.font = [UIFont fontWithName:@"PingFangSC-Semibold" size:15]; + descLabel.textAlignment = NSTextAlignmentLeft; + descLabel.textColor = [MOPTools fat_dynamicColorWithLightHexString:@"#222222" darkHexString:@"#222222"]; + self.titleLabel = descLabel; + [self.shareView addSubview:descLabel]; + + UILabel *detailLabel = [[UILabel alloc] init]; + detailLabel.frame = CGRectMake(14, self.titleLabel.frame.size.height + self.titleLabel.frame.origin.y - 3, 168, 44); + detailLabel.font = [UIFont fontWithName:@"PingFangSC-Regular" size:11]; + detailLabel.textAlignment = NSTextAlignmentLeft; + detailLabel.numberOfLines = 0; + detailLabel.textColor = [MOPTools fat_dynamicColorWithLightHexString:@"#666666" darkHexString:@"#666666"]; + self.descLabel = detailLabel; + [self.shareView addSubview:detailLabel]; + + self.qrCodeImageView = [[UIImageView alloc] initWithFrame:CGRectMake(196, bottomY + 8, 64, 64)]; + [self.shareView addSubview:self.qrCodeImageView]; + + [self addSubview:self.shareView]; + + + self.contentView = [[UIView alloc] initWithFrame:CGRectMake(0,self.frame.size.height - (221 + kFinoSafeAreaBottom) , self.frame.size.width, (221 + kFinoSafeAreaBottom))]; + self.contentView.backgroundColor = [MOPTools fat_dynamicColorWithLightHexString:@"#F0F0F0" darkHexString:@"#1A1A1A"]; + [self addSubview:self.contentView]; + + UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, 58 )]; + titleLabel.text = @"分享至"; + titleLabel.textColor = [MOPTools fat_dynamicColorWithLightHexString:@"#222222" darkHexString:@"#D0D0D0"]; + titleLabel.font = [UIFont fontWithName:@"PingFangSC-Regular" size:16]; + titleLabel.textAlignment = NSTextAlignmentCenter; + [self.contentView addSubview:titleLabel]; + + UIView *line1 = [[UIView alloc] initWithFrame:CGRectMake(0, 58 , self.contentView.frame.size.width, 0.5)]; + line1.backgroundColor = [MOPTools fat_dynamicColorWithLightHexString:@"#E0E0E0" darkHexString:@"#E0E0E0"]; + [self.contentView addSubview:line1]; + + self.cancelButton = [UIButton buttonWithType:UIButtonTypeCustom]; + NSString *cancel = [MOPTools fat_currentLanguageIsEn] ? @"Cancel" : @"取消"; + self.cancelButton.titleLabel.font = [UIFont fontWithName:@"PingFangSC-Regular" size:17]; + [self.cancelButton setTitle:cancel forState:UIControlStateNormal]; + [self.cancelButton setTitleColor:[MOPTools fat_dynamicColorWithLightHexString:@"#222222" darkHexString:@"#D0D0D0"] forState:UIControlStateNormal]; + [self.contentView addSubview:self.cancelButton]; + self.cancelButton.frame = CGRectMake(0, self.contentView.frame.size.height - kFinoSafeAreaBottom - 56, self.contentView.frame.size.width, 56 ); + [self.cancelButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside]; + + [self.contentView addSubview:self.collectionView]; + self.collectionView.frame = CGRectMake(0, 58.5, frame.size.width, 107); + + UIView *line2 = [[UIView alloc] initWithFrame:CGRectMake(0, 165.5, self.collectionView.frame.size.width, 0.5)]; + line2.backgroundColor = [MOPTools fat_dynamicColorWithLightHexString:@"#E0E0E0" darkHexString:@"#2E2E2E"]; + [self.contentView addSubview:line2]; + UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.contentView.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(6.0, 6.0)]; + CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init]; + maskLayer.frame = self.contentView.bounds; + maskLayer.path = maskPath.CGPath; + self.contentView.layer.mask = maskLayer; + + UIBezierPath *appletImageViewMask = [UIBezierPath bezierPathWithRoundedRect:self.appletImageView.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(6.0, 6.0)]; + CAShapeLayer *appletImageMaskLayer = [[CAShapeLayer alloc] init]; + appletImageMaskLayer.frame = self.appletImageView.bounds; + appletImageMaskLayer.path = appletImageViewMask.CGPath; + self.appletImageView.layer.mask = appletImageMaskLayer; + + self.shareView.frame = CGRectMake(52.5, self.contentView.frame.origin.y - 400 , 270 , 380); + + UIButton *saveButton = [[UIButton alloc] initWithFrame:CGRectMake(278.5, self.shareView.frame.origin.y + 12, 36, 36)]; + [saveButton addTarget:self action:@selector(saveOnClick) forControlEvents:UIControlEventTouchUpInside]; + [saveButton setImage:[UIImage imageNamed:@"share_download"] forState:UIControlStateNormal]; + [self addSubview:saveButton]; + [self p_addNotifications]; + } + return self; +} + ++ (instancetype)viewWithData:(NSDictionary *)data { + MopShareView *view = [[self alloc] initWithFrame:[UIScreen mainScreen].bounds]; + view.dataDic = data; + return view; +} + +- (void)setDataDic:(NSDictionary *)dataDic { + dispatch_async(dispatch_get_main_queue(), ^{ + self.qrCodeImageView.image = [MOPTools makeQRCodeForString:dataDic[@"shareUrl"]]; + }); + self.titleLabel.text = dataDic[@"miniAppName"]; + self.descLabel.text = dataDic[@"miniAppDesc"]; +} + +- (void)saveOnClick { + // 把view生成图片并保存 + UIImage *image = [MOPTools snapshotWithView:self.shareView]; + UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), (__bridge void *)self); +} + +- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo { + if (!error) { + // 出错 + [self fatMakeToast:@"保存成功" duration:1.5 position:CSToastPositionCenter]; + } else { + [self fatMakeToast:[NSString stringWithFormat:@"保存失败,%@", error.description] duration:1.5 position:CSToastPositionCenter]; + + } +} + + +- (void)p_addNotifications { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceOrientationDidChange) name:UIDeviceOrientationDidChangeNotification object:nil]; +} + +- (void)deviceOrientationDidChange { + CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; + CGFloat currentWidth = self.frame.size.width; + if (screenWidth != currentWidth) { + [self removeFromSuperview]; + } +} + +- (void)show { + self.appletImageView.image = self.image; + if (self.superview) { + return; + } + [UIView animateWithDuration:0.3 animations:^{ + UIWindow * window = [UIApplication sharedApplication].keyWindow; + [window addSubview:self]; + } completion:^(BOOL finished) { + }]; +} + +- (void)dismiss { + [UIView animateWithDuration:0.25 animations:^{ + self.contentView.transform = CGAffineTransformMakeTranslation(0, self.contentView.frame.size.height); + self.alpha = 0.5f; + } completion:^(BOOL finished) { + [self removeFromSuperview]; + }]; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { +// [self dismiss]; +} + +#pragma mark colletionview ------------------------------ +static NSString *cellID = @"cellid"; +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + MOPshareBottomViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath]; + [cell.imageButton setImage:[UIImage imageNamed:self.dataArray[indexPath.row][@"lightImage"]] forState:UIControlStateNormal]; + cell.label.text = self.dataArray[indexPath.row][@"title"]; + cell.type = self.dataArray[indexPath.row][@"type"]; + cell.delegate = self; + return cell; +} + +- (void)iconBtnDidClick:(MOPshareBottomViewCell *)cell { + if (self.didSelcetTypeBlock) { + NSString *typeString = cell.type; + self.didSelcetTypeBlock(typeString); + } + [self dismiss]; +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + if (self.didSelcetTypeBlock) { + NSString *typeString = self.dataArray[indexPath.row][@"type"]; + self.didSelcetTypeBlock(typeString); + } + [self dismiss]; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return 1; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return self.dataArray.count; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + return CGSizeMake(66, self.collectionView.frame.size.height); +} + +- (UICollectionViewFlowLayout *)layout { + UICollectionViewFlowLayout *collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; + collectionViewLayout.minimumLineSpacing = 0; + collectionViewLayout.minimumInteritemSpacing = 0; + collectionViewLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + return collectionViewLayout; +} + +- (UICollectionView *)collectionView { + if (!_collectionView) { + _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[self layout]]; + _collectionView.delegate = self; + _collectionView.dataSource = self; + [_collectionView registerClass:[MOPshareBottomViewCell class] forCellWithReuseIdentifier:cellID]; + _collectionView.pagingEnabled = YES; + _collectionView.showsVerticalScrollIndicator = NO; + _collectionView.showsHorizontalScrollIndicator = NO; + _collectionView.backgroundColor = [MOPTools fat_dynamicColorWithLightHexString:@"#F0F0F0" darkHexString:@"#1A1A1A"]; + if (@available(iOS 11.0, *)) { + _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + } + return _collectionView;; +} +@end + +@implementation MOPshareBottomViewCell +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.backgroundColor = [MOPTools fat_dynamicColorWithLightHexString:@"#F0F0F0" darkHexString:@"#1A1A1A"]; + + UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(18, 20, 48, 48)]; + button.backgroundColor = [MOPTools fat_dynamicColorWithLightHexString:@"#FFFFFF " darkHexString:@"#2C2C2C"]; + [self.contentView addSubview:button]; + button.layer.masksToBounds = YES; + button.layer.cornerRadius = 6; + UIImage *highBgImage = [UIImage fat_imageWithColor:[MOPTools fat_dynamicColorWithLightHexString:@"#D7D7D7 " darkHexString:@"#4A4A4A"]]; + [button setBackgroundImage:highBgImage forState:UIControlStateHighlighted]; + [button addTarget:self action:@selector(iconBtnClick:) forControlEvents:UIControlEventTouchUpInside]; + self.imageButton = button; + // self.label = [[UILabel alloc] initWithFrame:CGRectMake((frame.size.width-50)/2, 65, frame.size.width, 30)]; + self.label = [[UILabel alloc] initWithFrame:CGRectMake(18, 75, 48, 12)]; + self.label.font = [UIFont systemFontOfSize:10]; + self.label.textColor = [UIColor grayColor]; + self.label.textAlignment = NSTextAlignmentCenter; + [self.contentView addSubview:self.label]; + + } + return self; +} + +- (void)iconBtnClick:(UIButton *)btn { + if (self.delegate && [self.delegate respondsToSelector:@selector(iconBtnDidClick:)]) { + [self.delegate iconBtnDidClick:self]; + } +} + +@end + diff --git a/ios/Classes/Utils/UIView+MOPFATToast.h b/ios/Classes/Utils/UIView+MOPFATToast.h new file mode 100644 index 0000000..3a2fea7 --- /dev/null +++ b/ios/Classes/Utils/UIView+MOPFATToast.h @@ -0,0 +1,421 @@ +#import + +extern const NSString * CSToastPositionTop; +extern const NSString * CSToastPositionCenter; +extern const NSString * CSToastPositionBottom; + +@class CSToastStyle; + +/** + Toast is an Objective-C category that adds toast notifications to the UIView + object class. It is intended to be simple, lightweight, and easy to use. Most + toast notifications can be triggered with a single line of code. + + The `makeToast:` methods create a new view and then display it as toast. + + The `showToast:` methods display any view as toast. + + */ +@interface UIView (MOPFATToast) + +/** + Creates and presents a new toast view with a message and displays it with the + default duration and position. Styled using the shared style. + + @param message The message to be displayed + */ +- (void)fatMakeToast:(NSString *)message; + +/** + Creates and presents a new toast view with a message. Duration and position + can be set explicitly. Styled using the shared style. + + @param message The message to be displayed + @param duration The toast duration + @param position The toast's center point. Can be one of the predefined CSToastPosition + constants or a `CGPoint` wrapped in an `NSValue` object. + */ +- (void)fatMakeToast:(NSString *)message + duration:(NSTimeInterval)duration + position:(id)position; + +/** + Creates and presents a new toast view with a message. Duration, position, and + style can be set explicitly. + + @param message The message to be displayed + @param duration The toast duration + @param position The toast's center point. Can be one of the predefined CSToastPosition + constants or a `CGPoint` wrapped in an `NSValue` object. + @param style The style. The shared style will be used when nil + */ +- (void)fatMakeToast:(NSString *)message + duration:(NSTimeInterval)duration + position:(id)position + style:(CSToastStyle *)style; + +/** + Creates and presents a new toast view with a message, title, and image. Duration, + position, and style can be set explicitly. The completion block executes when the + toast view completes. `didTap` will be `YES` if the toast view was dismissed from + a tap. + + @param message The message to be displayed + @param duration The toast duration + @param position The toast's center point. Can be one of the predefined CSToastPosition + constants or a `CGPoint` wrapped in an `NSValue` object. + @param title The title + @param image The image + @param style The style. The shared style will be used when nil + @param completion The completion block, executed after the toast view disappears. + didTap will be `YES` if the toast view was dismissed from a tap. + */ +- (void)fatMakeToast:(NSString *)message + duration:(NSTimeInterval)duration + position:(id)position + title:(NSString *)title + image:(UIImage *)image + style:(CSToastStyle *)style + completion:(void(^)(BOOL didTap))completion; + +/** + Creates a new toast view with any combination of message, title, and image. + The look and feel is configured via the style. Unlike the `makeToast:` methods, + this method does not present the toast view automatically. One of the showToast: + methods must be used to present the resulting view. + + @warning if message, title, and image are all nil, this method will return nil. + + @param message The message to be displayed + @param title The title + @param image The image + @param style The style. The shared style will be used when nil + @return The newly created toast view + */ +- (UIView *)fatToastViewForMessage:(NSString *)message + title:(NSString *)title + image:(UIImage *)image + style:(CSToastStyle *)style; + +/** + Hides the active toast. If there are multiple toasts active in a view, this method + hides the oldest toast (the first of the toasts to have been presented). + + @see `hideAllToasts` to remove all active toasts from a view. + + @warning This method has no effect on activity toasts. Use `hideToastActivity` to + hide activity toasts. + */ +- (void)fatHideToast; + +/** + Hides an active toast. + + @param toast The active toast view to dismiss. Any toast that is currently being displayed + on the screen is considered active. + + @warning this does not clear a toast view that is currently waiting in the queue. + */ +- (void)fatHideToast:(UIView *)toast; + +/** + Hides all active toast views and clears the queue. + */ +- (void)fatHideAllToasts; + +/** + Hides all active toast views, with options to hide activity and clear the queue. + + @param includeActivity If `true`, toast activity will also be hidden. Default is `false`. + @param clearQueue If `true`, removes all toast views from the queue. Default is `true`. + */ +- (void)fatHideAllToasts:(BOOL)includeActivity clearQueue:(BOOL)clearQueue; + +/** + Removes all toast views from the queue. This has no effect on toast views that are + active. Use `hideAllToasts` to hide the active toasts views and clear the queue. + */ +- (void)fatClearToastQueue; + +/** + Creates and displays a new toast activity indicator view at a specified position. + + @warning Only one toast activity indicator view can be presented per superview. Subsequent + calls to `makeToastActivity:` will be ignored until hideToastActivity is called. + + @warning `makeToastActivity:` works independently of the showToast: methods. Toast activity + views can be presented and dismissed while toast views are being displayed. `makeToastActivity:` + has no effect on the queueing behavior of the showToast: methods. + + @param position The toast's center point. Can be one of the predefined CSToastPosition + constants or a `CGPoint` wrapped in an `NSValue` object. + */ +- (void)fatMakeToastActivity:(id)position; + +/** + Dismisses the active toast activity indicator view. + */ +- (void)fatHideToastActivity; + +/** + Displays any view as toast using the default duration and position. + + @param toast The view to be displayed as toast + */ +- (void)fatShowToast:(UIView *)toast; + +/** + Displays any view as toast at a provided position and duration. The completion block + executes when the toast view completes. `didTap` will be `YES` if the toast view was + dismissed from a tap. + + @param toast The view to be displayed as toast + @param duration The notification duration + @param position The toast's center point. Can be one of the predefined CSToastPosition + constants or a `CGPoint` wrapped in an `NSValue` object. + @param completion The completion block, executed after the toast view disappears. + didTap will be `YES` if the toast view was dismissed from a tap. + */ +- (void)fatShowToast:(UIView *)toast + duration:(NSTimeInterval)duration + position:(id)position + completion:(void(^)(BOOL didTap))completion; + +@end + +/** + `CSToastStyle` instances define the look and feel for toast views created via the + `makeToast:` methods as well for toast views created directly with + `toastViewForMessage:title:image:style:`. + + @warning `CSToastStyle` offers relatively simple styling options for the default + toast view. If you require a toast view with more complex UI, it probably makes more + sense to create your own custom UIView subclass and present it with the `showToast:` + methods. + */ +@interface CSToastStyle : NSObject + +/** + The background color. Default is `[UIColor blackColor]` at 80% opacity. + */ +@property (strong, nonatomic) UIColor *backgroundColor; + +/** + The title color. Default is `[UIColor whiteColor]`. + */ +@property (strong, nonatomic) UIColor *titleColor; + +/** + The message color. Default is `[UIColor whiteColor]`. + */ +@property (strong, nonatomic) UIColor *messageColor; + +/** + A percentage value from 0.0 to 1.0, representing the maximum width of the toast + view relative to it's superview. Default is 0.8 (80% of the superview's width). + */ +@property (assign, nonatomic) CGFloat maxWidthPercentage; + +/** + A percentage value from 0.0 to 1.0, representing the maximum height of the toast + view relative to it's superview. Default is 0.8 (80% of the superview's height). + */ +@property (assign, nonatomic) CGFloat maxHeightPercentage; + +/** + The spacing from the horizontal edge of the toast view to the content. When an image + is present, this is also used as the padding between the image and the text. + Default is 10.0. + */ +@property (assign, nonatomic) CGFloat horizontalPadding; + +/** + The spacing from the vertical edge of the toast view to the content. When a title + is present, this is also used as the padding between the title and the message. + Default is 10.0. + */ +@property (assign, nonatomic) CGFloat verticalPadding; + +/** + The corner radius. Default is 10.0. + */ +@property (assign, nonatomic) CGFloat cornerRadius; + +/** + The title font. Default is `[UIFont boldSystemFontOfSize:16.0]`. + */ +@property (strong, nonatomic) UIFont *titleFont; + +/** + The message font. Default is `[UIFont systemFontOfSize:16.0]`. + */ +@property (strong, nonatomic) UIFont *messageFont; + +/** + The title text alignment. Default is `NSTextAlignmentLeft`. + */ +@property (assign, nonatomic) NSTextAlignment titleAlignment; + +/** + The message text alignment. Default is `NSTextAlignmentLeft`. + */ +@property (assign, nonatomic) NSTextAlignment messageAlignment; + +/** + The maximum number of lines for the title. The default is 0 (no limit). + */ +@property (assign, nonatomic) NSInteger titleNumberOfLines; + +/** + The maximum number of lines for the message. The default is 0 (no limit). + */ +@property (assign, nonatomic) NSInteger messageNumberOfLines; + +/** + Enable or disable a shadow on the toast view. Default is `NO`. + */ +@property (assign, nonatomic) BOOL displayShadow; + +/** + The shadow color. Default is `[UIColor blackColor]`. + */ +@property (strong, nonatomic) UIColor *shadowColor; + +/** + A value from 0.0 to 1.0, representing the opacity of the shadow. + Default is 0.8 (80% opacity). + */ +@property (assign, nonatomic) CGFloat shadowOpacity; + +/** + The shadow radius. Default is 6.0. + */ +@property (assign, nonatomic) CGFloat shadowRadius; + +/** + The shadow offset. The default is `CGSizeMake(4.0, 4.0)`. + */ +@property (assign, nonatomic) CGSize shadowOffset; + +/** + The image size. The default is `CGSizeMake(80.0, 80.0)`. + */ +@property (assign, nonatomic) CGSize imageSize; + +/** + The size of the toast activity view when `makeToastActivity:` is called. + Default is `CGSizeMake(100.0, 100.0)`. + */ +@property (assign, nonatomic) CGSize activitySize; + +/** + The fade in/out animation duration. Default is 0.2. + */ +@property (assign, nonatomic) NSTimeInterval fadeDuration; + +/** + Creates a new instance of `CSToastStyle` with all the default values set. + */ +- (instancetype)initWithDefaultStyle NS_DESIGNATED_INITIALIZER; + +/** + @warning Only the designated initializer should be used to create + an instance of `CSToastStyle`. + */ +- (instancetype)init NS_UNAVAILABLE; + +@end + +/** + `CSToastManager` provides general configuration options for all toast + notifications. Backed by a singleton instance. + */ +@interface CSToastManager : NSObject + +/** + Sets the shared style on the singleton. The shared style is used whenever + a `makeToast:` method (or `toastViewForMessage:title:image:style:`) is called + with with a nil style. By default, this is set to `CSToastStyle`'s default + style. + + @param sharedStyle the shared style + */ ++ (void)setSharedStyle:(CSToastStyle *)sharedStyle; + +/** + Gets the shared style from the singlton. By default, this is + `CSToastStyle`'s default style. + + @return the shared style + */ ++ (CSToastStyle *)sharedStyle; + +/** + Enables or disables tap to dismiss on toast views. Default is `YES`. + + @param tapToDismissEnabled YES or NO + */ ++ (void)setTapToDismissEnabled:(BOOL)tapToDismissEnabled; + +/** + Returns `YES` if tap to dismiss is enabled, otherwise `NO`. + Default is `YES`. + + @return BOOL YES or NO + */ ++ (BOOL)isTapToDismissEnabled; + +/** + Enables or disables queueing behavior for toast views. When `YES`, + toast views will appear one after the other. When `NO`, multiple Toast + views will appear at the same time (potentially overlapping depending + on their positions). This has no effect on the toast activity view, + which operates independently of normal toast views. Default is `NO`. + + @param queueEnabled YES or NO + */ ++ (void)setQueueEnabled:(BOOL)queueEnabled; + +/** + Returns `YES` if the queue is enabled, otherwise `NO`. + Default is `NO`. + + @return BOOL + */ ++ (BOOL)isQueueEnabled; + +/** + Sets the default duration. Used for the `makeToast:` and + `showToast:` methods that don't require an explicit duration. + Default is 3.0. + + @param duration The toast duration + */ ++ (void)setDefaultDuration:(NSTimeInterval)duration; + +/** + Returns the default duration. Default is 3.0. + + @return duration The toast duration +*/ ++ (NSTimeInterval)defaultDuration; + +/** + Sets the default position. Used for the `makeToast:` and + `showToast:` methods that don't require an explicit position. + Default is `CSToastPositionBottom`. + + @param position The default center point. Can be one of the predefined + CSToastPosition constants or a `CGPoint` wrapped in an `NSValue` object. + */ ++ (void)setDefaultPosition:(id)position; + +/** + Returns the default toast position. Default is `CSToastPositionBottom`. + + @return position The default center point. Will be one of the predefined + CSToastPosition constants or a `CGPoint` wrapped in an `NSValue` object. + */ ++ (id)defaultPosition; + +@end diff --git a/ios/Classes/Utils/UIView+MOPFATToast.m b/ios/Classes/Utils/UIView+MOPFATToast.m new file mode 100644 index 0000000..110cd62 --- /dev/null +++ b/ios/Classes/Utils/UIView+MOPFATToast.m @@ -0,0 +1,561 @@ +#import "UIView+MOPFATToast.h" +#import +#import + +// Positions +NSString * CSToastPositionTop = @"CSToastPositionTop"; +NSString * CSToastPositionCenter = @"CSToastPositionCenter"; +NSString * CSToastPositionBottom = @"CSToastPositionBottom"; + +// Keys for values associated with toast views +static const NSString * CSToastTimerKey = @"CSToastTimerKey"; +static const NSString * CSToastDurationKey = @"CSToastDurationKey"; +static const NSString * CSToastPositionKey = @"CSToastPositionKey"; +static const NSString * CSToastCompletionKey = @"CSToastCompletionKey"; + +// Keys for values associated with self +static const NSString * CSToastActiveKey = @"CSToastActiveKey"; +static const NSString * CSToastActivityViewKey = @"CSToastActivityViewKey"; +static const NSString * CSToastQueueKey = @"CSToastQueueKey"; + +@interface UIView (ToastPrivate) + +/** + These private methods are being prefixed with "cs_" to reduce the likelihood of non-obvious + naming conflicts with other UIView methods. + + @discussion Should the public API also use the cs_ prefix? Technically it should, but it + results in code that is less legible. The current public method names seem unlikely to cause + conflicts so I think we should favor the cleaner API for now. + */ +- (void)cs_showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)position; +- (void)cs_hideToast:(UIView *)toast; +- (void)cs_hideToast:(UIView *)toast fromTap:(BOOL)fromTap; +- (void)cs_toastTimerDidFinish:(NSTimer *)timer; +- (void)cs_handleToastTapped:(UITapGestureRecognizer *)recognizer; +- (CGPoint)cs_centerPointForPosition:(id)position withToast:(UIView *)toast; +- (NSMutableArray *)cs_toastQueue; + +@end + +@implementation UIView (MOPFATToast) + +#pragma mark - Make Toast Methods + +- (void)fatMakeToast:(NSString *)message { + [self fatMakeToast:message duration:[CSToastManager defaultDuration] position:[CSToastManager defaultPosition] style:nil]; +} + +- (void)fatMakeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position { + [self fatMakeToast:message duration:duration position:position style:nil]; +} + +- (void)fatMakeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position style:(CSToastStyle *)style { + UIView *toast = [self fatToastViewForMessage:message title:nil image:nil style:style]; + [self fatShowToast:toast duration:duration position:position completion:nil]; +} + +- (void)fatMakeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position title:(NSString *)title image:(UIImage *)image style:(CSToastStyle *)style completion:(void(^)(BOOL didTap))completion { + UIView *toast = [self fatToastViewForMessage:message title:title image:image style:style]; + [self fatShowToast:toast duration:duration position:position completion:completion]; +} + +#pragma mark - Show Toast Methods + +- (void)fatShowToast:(UIView *)toast { + [self fatShowToast:toast duration:[CSToastManager defaultDuration] position:[CSToastManager defaultPosition] completion:nil]; +} + +- (void)fatShowToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)position completion:(void(^)(BOOL didTap))completion { + // sanity + if (toast == nil) return; + + // store the completion block on the toast view + objc_setAssociatedObject(toast, &CSToastCompletionKey, completion, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if ([CSToastManager isQueueEnabled] && [self.cs_activeToasts count] > 0) { + // we're about to queue this toast view so we need to store the duration and position as well + objc_setAssociatedObject(toast, &CSToastDurationKey, @(duration), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + objc_setAssociatedObject(toast, &CSToastPositionKey, position, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // enqueue + [self.cs_toastQueue addObject:toast]; + } else { + // present + [self cs_showToast:toast duration:duration position:position]; + } +} + +#pragma mark - Hide Toast Methods + +- (void)fatHideToast { + [self fatHideToast:[[self cs_activeToasts] firstObject]]; +} + +- (void)fatHideToast:(UIView *)toast { + // sanity + if (!toast || ![[self cs_activeToasts] containsObject:toast]) return; + + [self cs_hideToast:toast]; +} + +- (void)fatHideAllToasts { + [self fatHideAllToasts:NO clearQueue:YES]; +} + +- (void)fatHideAllToasts:(BOOL)includeActivity clearQueue:(BOOL)clearQueue { + if (clearQueue) { + [self fatClearToastQueue]; + } + + for (UIView *toast in [self cs_activeToasts]) { + [self fatHideToast:toast]; + } + + if (includeActivity) { + [self fatHideToastActivity]; + } +} + +- (void)fatClearToastQueue { + [[self cs_toastQueue] removeAllObjects]; +} + +#pragma mark - Private Show/Hide Methods + +- (void)cs_showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)position { + toast.center = [self cs_centerPointForPosition:position withToast:toast]; + toast.alpha = 0.0; + + if ([CSToastManager isTapToDismissEnabled]) { + UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cs_handleToastTapped:)]; + [toast addGestureRecognizer:recognizer]; + toast.userInteractionEnabled = YES; + toast.exclusiveTouch = YES; + } + + [[self cs_activeToasts] addObject:toast]; + + [self addSubview:toast]; + + [UIView animateWithDuration:[[CSToastManager sharedStyle] fadeDuration] + delay:0.0 + options:(UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction) + animations:^{ + toast.alpha = 1.0; + } completion:^(BOOL finished) { + NSTimer *timer = [NSTimer timerWithTimeInterval:duration target:self selector:@selector(cs_toastTimerDidFinish:) userInfo:toast repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; + objc_setAssociatedObject(toast, &CSToastTimerKey, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + }]; +} + +- (void)cs_hideToast:(UIView *)toast { + [self cs_hideToast:toast fromTap:NO]; +} + +- (void)cs_hideToast:(UIView *)toast fromTap:(BOOL)fromTap { + NSTimer *timer = (NSTimer *)objc_getAssociatedObject(toast, &CSToastTimerKey); + [timer invalidate]; + + [UIView animateWithDuration:[[CSToastManager sharedStyle] fadeDuration] + delay:0.0 + options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState) + animations:^{ + toast.alpha = 0.0; + } completion:^(BOOL finished) { + [toast removeFromSuperview]; + + // remove + [[self cs_activeToasts] removeObject:toast]; + + // execute the completion block, if necessary + void (^completion)(BOOL didTap) = objc_getAssociatedObject(toast, &CSToastCompletionKey); + if (completion) { + completion(fromTap); + } + + if ([self.cs_toastQueue count] > 0) { + // dequeue + UIView *nextToast = [[self cs_toastQueue] firstObject]; + [[self cs_toastQueue] removeObjectAtIndex:0]; + + // present the next toast + NSTimeInterval duration = [objc_getAssociatedObject(nextToast, &CSToastDurationKey) doubleValue]; + id position = objc_getAssociatedObject(nextToast, &CSToastPositionKey); + [self cs_showToast:nextToast duration:duration position:position]; + } + }]; +} + +#pragma mark - View Construction + +- (UIView *)fatToastViewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image style:(CSToastStyle *)style { + // sanity + if (message == nil && title == nil && image == nil) return nil; + + // default to the shared style + if (style == nil) { + style = [CSToastManager sharedStyle]; + } + + // dynamically build a toast view with any combination of message, title, & image + UILabel *messageLabel = nil; + UILabel *titleLabel = nil; + UIImageView *imageView = nil; + + UIView *wrapperView = [[UIView alloc] init]; + wrapperView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin); + wrapperView.layer.cornerRadius = style.cornerRadius; + + if (style.displayShadow) { + wrapperView.layer.shadowColor = style.shadowColor.CGColor; + wrapperView.layer.shadowOpacity = style.shadowOpacity; + wrapperView.layer.shadowRadius = style.shadowRadius; + wrapperView.layer.shadowOffset = style.shadowOffset; + } + + wrapperView.backgroundColor = style.backgroundColor; + + if(image != nil) { + imageView = [[UIImageView alloc] initWithImage:image]; + imageView.contentMode = UIViewContentModeScaleAspectFit; + imageView.frame = CGRectMake(style.horizontalPadding, style.verticalPadding, style.imageSize.width, style.imageSize.height); + } + + CGRect imageRect = CGRectZero; + + if(imageView != nil) { + imageRect.origin.x = style.horizontalPadding; + imageRect.origin.y = style.verticalPadding; + imageRect.size.width = imageView.bounds.size.width; + imageRect.size.height = imageView.bounds.size.height; + } + + if (title != nil) { + titleLabel = [[UILabel alloc] init]; + titleLabel.numberOfLines = style.titleNumberOfLines; + titleLabel.font = style.titleFont; + titleLabel.textAlignment = style.titleAlignment; + titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + titleLabel.textColor = style.titleColor; + titleLabel.backgroundColor = [UIColor clearColor]; + titleLabel.alpha = 1.0; + titleLabel.text = title; + + // size the title label according to the length of the text + CGSize maxSizeTitle = CGSizeMake((self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, self.bounds.size.height * style.maxHeightPercentage); + CGSize expectedSizeTitle = [titleLabel sizeThatFits:maxSizeTitle]; + // UILabel can return a size larger than the max size when the number of lines is 1 + expectedSizeTitle = CGSizeMake(MIN(maxSizeTitle.width, expectedSizeTitle.width), MIN(maxSizeTitle.height, expectedSizeTitle.height)); + titleLabel.frame = CGRectMake(0.0, 0.0, expectedSizeTitle.width, expectedSizeTitle.height); + } + + if (message != nil) { + messageLabel = [[UILabel alloc] init]; + messageLabel.numberOfLines = style.messageNumberOfLines; + messageLabel.font = style.messageFont; + messageLabel.textAlignment = style.messageAlignment; + messageLabel.lineBreakMode = NSLineBreakByTruncatingTail; + messageLabel.textColor = style.messageColor; + messageLabel.backgroundColor = [UIColor clearColor]; + messageLabel.alpha = 1.0; + messageLabel.text = message; + + CGSize maxSizeMessage = CGSizeMake((self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, self.bounds.size.height * style.maxHeightPercentage); + CGSize expectedSizeMessage = [messageLabel sizeThatFits:maxSizeMessage]; + // UILabel can return a size larger than the max size when the number of lines is 1 + expectedSizeMessage = CGSizeMake(MIN(maxSizeMessage.width, expectedSizeMessage.width), MIN(maxSizeMessage.height, expectedSizeMessage.height)); + messageLabel.frame = CGRectMake(0.0, 0.0, expectedSizeMessage.width, expectedSizeMessage.height); + } + + CGRect titleRect = CGRectZero; + + if(titleLabel != nil) { + titleRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding; + titleRect.origin.y = style.verticalPadding; + titleRect.size.width = titleLabel.bounds.size.width; + titleRect.size.height = titleLabel.bounds.size.height; + } + + CGRect messageRect = CGRectZero; + + if(messageLabel != nil) { + messageRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding; + messageRect.origin.y = titleRect.origin.y + titleRect.size.height + style.verticalPadding; + messageRect.size.width = messageLabel.bounds.size.width; + messageRect.size.height = messageLabel.bounds.size.height; + } + + CGFloat longerWidth = MAX(titleRect.size.width, messageRect.size.width); + CGFloat longerX = MAX(titleRect.origin.x, messageRect.origin.x); + + // Wrapper width uses the longerWidth or the image width, whatever is larger. Same logic applies to the wrapper height. + CGFloat wrapperWidth = MAX((imageRect.size.width + (style.horizontalPadding * 2.0)), (longerX + longerWidth + style.horizontalPadding)); + CGFloat wrapperHeight = MAX((messageRect.origin.y + messageRect.size.height + style.verticalPadding), (imageRect.size.height + (style.verticalPadding * 2.0))); + + wrapperView.frame = CGRectMake(0.0, 0.0, wrapperWidth, wrapperHeight); + + if(titleLabel != nil) { + titleLabel.frame = titleRect; + [wrapperView addSubview:titleLabel]; + } + + if(messageLabel != nil) { + messageLabel.frame = messageRect; + [wrapperView addSubview:messageLabel]; + } + + if(imageView != nil) { + [wrapperView addSubview:imageView]; + } + + return wrapperView; +} + +#pragma mark - Storage + +- (NSMutableArray *)cs_activeToasts { + NSMutableArray *cs_activeToasts = objc_getAssociatedObject(self, &CSToastActiveKey); + if (cs_activeToasts == nil) { + cs_activeToasts = [[NSMutableArray alloc] init]; + objc_setAssociatedObject(self, &CSToastActiveKey, cs_activeToasts, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return cs_activeToasts; +} + +- (NSMutableArray *)cs_toastQueue { + NSMutableArray *cs_toastQueue = objc_getAssociatedObject(self, &CSToastQueueKey); + if (cs_toastQueue == nil) { + cs_toastQueue = [[NSMutableArray alloc] init]; + objc_setAssociatedObject(self, &CSToastQueueKey, cs_toastQueue, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return cs_toastQueue; +} + +#pragma mark - Events + +- (void)cs_toastTimerDidFinish:(NSTimer *)timer { + [self cs_hideToast:(UIView *)timer.userInfo]; +} + +- (void)cs_handleToastTapped:(UITapGestureRecognizer *)recognizer { + UIView *toast = recognizer.view; + NSTimer *timer = (NSTimer *)objc_getAssociatedObject(toast, &CSToastTimerKey); + [timer invalidate]; + + [self cs_hideToast:toast fromTap:YES]; +} + +#pragma mark - Activity Methods + +- (void)fatMakeToastActivity:(id)position { + // sanity + UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey); + if (existingActivityView != nil) return; + + CSToastStyle *style = [CSToastManager sharedStyle]; + + UIView *activityView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, style.activitySize.width, style.activitySize.height)]; + activityView.center = [self cs_centerPointForPosition:position withToast:activityView]; + activityView.backgroundColor = style.backgroundColor; + activityView.alpha = 0.0; + activityView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin); + activityView.layer.cornerRadius = style.cornerRadius; + + if (style.displayShadow) { + activityView.layer.shadowColor = style.shadowColor.CGColor; + activityView.layer.shadowOpacity = style.shadowOpacity; + activityView.layer.shadowRadius = style.shadowRadius; + activityView.layer.shadowOffset = style.shadowOffset; + } + + UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + activityIndicatorView.center = CGPointMake(activityView.bounds.size.width / 2, activityView.bounds.size.height / 2); + [activityView addSubview:activityIndicatorView]; + [activityIndicatorView startAnimating]; + + // associate the activity view with self + objc_setAssociatedObject (self, &CSToastActivityViewKey, activityView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + [self addSubview:activityView]; + + [UIView animateWithDuration:style.fadeDuration + delay:0.0 + options:UIViewAnimationOptionCurveEaseOut + animations:^{ + activityView.alpha = 1.0; + } completion:nil]; +} + +- (void)fatHideToastActivity { + UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey); + if (existingActivityView != nil) { + [UIView animateWithDuration:[[CSToastManager sharedStyle] fadeDuration] + delay:0.0 + options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState) + animations:^{ + existingActivityView.alpha = 0.0; + } completion:^(BOOL finished) { + [existingActivityView removeFromSuperview]; + objc_setAssociatedObject (self, &CSToastActivityViewKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + }]; + } +} + +#pragma mark - Helpers + +- (CGPoint)cs_centerPointForPosition:(id)point withToast:(UIView *)toast { + CSToastStyle *style = [CSToastManager sharedStyle]; + + UIEdgeInsets safeInsets = UIEdgeInsetsZero; + if (@available(iOS 11.0, *)) { + safeInsets = self.safeAreaInsets; + } + + CGFloat topPadding = style.verticalPadding + safeInsets.top; + CGFloat bottomPadding = style.verticalPadding + safeInsets.bottom; + + if([point isKindOfClass:[NSString class]]) { + if([point caseInsensitiveCompare:CSToastPositionTop] == NSOrderedSame) { + return CGPointMake(self.bounds.size.width / 2.0, (toast.frame.size.height / 2.0) + topPadding); + } else if([point caseInsensitiveCompare:CSToastPositionCenter] == NSOrderedSame) { + return CGPointMake(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0); + } + } else if ([point isKindOfClass:[NSValue class]]) { + return [point CGPointValue]; + } + + // default to bottom + return CGPointMake(self.bounds.size.width / 2.0, (self.bounds.size.height - (toast.frame.size.height / 2.0)) - bottomPadding); +} + +@end + +@implementation CSToastStyle + +#pragma mark - Constructors + +- (instancetype)initWithDefaultStyle { + self = [super init]; + if (self) { + self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.8]; + self.titleColor = [UIColor whiteColor]; + self.messageColor = [UIColor whiteColor]; + self.maxWidthPercentage = 0.8; + self.maxHeightPercentage = 0.8; + self.horizontalPadding = 10.0; + self.verticalPadding = 10.0; + self.cornerRadius = 10.0; + self.titleFont = [UIFont boldSystemFontOfSize:16.0]; + self.messageFont = [UIFont systemFontOfSize:16.0]; + self.titleAlignment = NSTextAlignmentLeft; + self.messageAlignment = NSTextAlignmentLeft; + self.titleNumberOfLines = 0; + self.messageNumberOfLines = 0; + self.displayShadow = NO; + self.shadowOpacity = 0.8; + self.shadowRadius = 6.0; + self.shadowOffset = CGSizeMake(4.0, 4.0); + self.imageSize = CGSizeMake(80.0, 80.0); + self.activitySize = CGSizeMake(100.0, 100.0); + self.fadeDuration = 0.2; + } + return self; +} + +- (void)setMaxWidthPercentage:(CGFloat)maxWidthPercentage { + _maxWidthPercentage = MAX(MIN(maxWidthPercentage, 1.0), 0.0); +} + +- (void)setMaxHeightPercentage:(CGFloat)maxHeightPercentage { + _maxHeightPercentage = MAX(MIN(maxHeightPercentage, 1.0), 0.0); +} + +- (instancetype)init NS_UNAVAILABLE { + return nil; +} + +@end + +@interface CSToastManager () + +@property (strong, nonatomic) CSToastStyle *sharedStyle; +@property (assign, nonatomic, getter=isTapToDismissEnabled) BOOL tapToDismissEnabled; +@property (assign, nonatomic, getter=isQueueEnabled) BOOL queueEnabled; +@property (assign, nonatomic) NSTimeInterval defaultDuration; +@property (strong, nonatomic) id defaultPosition; + +@end + +@implementation CSToastManager + +#pragma mark - Constructors + ++ (instancetype)sharedManager { + static CSToastManager *_sharedManager = nil; + static dispatch_once_t oncePredicate; + dispatch_once(&oncePredicate, ^{ + _sharedManager = [[self alloc] init]; + }); + + return _sharedManager; +} + +- (instancetype)init { + self = [super init]; + if (self) { + self.sharedStyle = [[CSToastStyle alloc] initWithDefaultStyle]; + self.tapToDismissEnabled = YES; + self.queueEnabled = NO; + self.defaultDuration = 3.0; + self.defaultPosition = CSToastPositionBottom; + } + return self; +} + +#pragma mark - Singleton Methods + ++ (void)setSharedStyle:(CSToastStyle *)sharedStyle { + [[self sharedManager] setSharedStyle:sharedStyle]; +} + ++ (CSToastStyle *)sharedStyle { + return [[self sharedManager] sharedStyle]; +} + ++ (void)setTapToDismissEnabled:(BOOL)tapToDismissEnabled { + [[self sharedManager] setTapToDismissEnabled:tapToDismissEnabled]; +} + ++ (BOOL)isTapToDismissEnabled { + return [[self sharedManager] isTapToDismissEnabled]; +} + ++ (void)setQueueEnabled:(BOOL)queueEnabled { + [[self sharedManager] setQueueEnabled:queueEnabled]; +} + ++ (BOOL)isQueueEnabled { + return [[self sharedManager] isQueueEnabled]; +} + ++ (void)setDefaultDuration:(NSTimeInterval)duration { + [[self sharedManager] setDefaultDuration:duration]; +} + ++ (NSTimeInterval)defaultDuration { + return [[self sharedManager] defaultDuration]; +} + ++ (void)setDefaultPosition:(id)position { + if ([position isKindOfClass:[NSString class]] || [position isKindOfClass:[NSValue class]]) { + [[self sharedManager] setDefaultPosition:position]; + } +} + ++ (id)defaultPosition { + return [[self sharedManager] defaultPosition]; +} + +@end diff --git a/ios/mop.podspec b/ios/mop.podspec index 16dc9c9..b94531f 100644 --- a/ios/mop.podspec +++ b/ios/mop.podspec @@ -16,7 +16,6 @@ A finclip miniprogram flutter sdk. s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.ios.deployment_target = '9.0' - s.dependency 'FinApplet' , '2.39.1' s.dependency 'FinAppletExt' , '2.39.1' end