iOS中UIScrollView、UIWebView、UICollectionView实现商品详情页图文混排

 

实现思路

  1. 将文本和图片拼接为HTML代码。

  2. 使用JavaScript添加点击事件。

  3. 使用MagicWebViewWebP提供UIWebView加载webp格式图片支持。

  4. 使用UIWebView加载HTML代码。

  5. 使用UIWebView代理方法,拦截页面发出的请求,获取selectIndex。

实现效果

组件 描述 说明
UIScrollView 根容器 高度自适应(KVO处理UIWebView + UICollectionView高度)
UIWebView 图文混排展示 加载HTML代码
UICollectionView 更多推荐展示
 

123.gif

实现效果

问题汇总

1、如何实现JavaScript与Objective-C间传值?

点击Webview中的图片,放大,需要JavaScript和Objective-C传值,获取到具体需要放大哪张图片。

本方案中,不需要引入WebViewJavascriptBridge,而是通过【控制Webview重定向方法,拦截发出的请求】来实现。

示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// 每个添加点击事件(window.location.href),其中selectIndex为图片标识// webview发起请求拦截

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{

   

    // 获取img标识index

    NSString *url = request.URL.absoluteString;

    NSRange range = [url rangeOfString:@"selectIndex="];

    if (range.location != NSNotFound) {

        NSInteger begin = range.location + range.length;

        NSString *index = [url substringFromIndex:begin];

        NSLog(@"img: %@", index);

        return NO;

    }

    return YES;

   

}

2、如何实现UIWebView高度自适应?

UIWebView自适应高度的方案有很多,选择一个较为科学的方式,显得尤为重要。

本方案中,通过【KVO监听Webview的contentSize】来实现,需要注意KVO的添加、移除,稍有不慎有Crash风险。

示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

// 添加监听

[self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];

- (UIWebView *)webView

{

    if (!_webView) {

        _webView = [[UIWebView alloc] initWithFrame:CGRectMake(00, self.frame.size.width, self.frame.size.height)];

        _webView.delegate = self;

        _webView.scrollView.bounces = NO;

        _webView.scrollView.showsHorizontalScrollIndicator = NO;

        _webView.scrollView.scrollEnabled = NO;

        _webView.scalesPageToFit = YES;

    }

    return _webView;

}

// 修改webview的frame

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context

{

    if ([keyPath isEqualToString:@"contentSize"]) {

        CGSize resize = [self.webView sizeThatFits:CGSizeZero];

        self.webView.frame =  CGRectMake(00, CGRectGetWidth(self.frame), resize.height);

    }

}

// 移除监听

-(void)dealloc

{

    [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize"];

}

3、如何实现UIWebView显示webp格式图片?

UIWebView、WKWebview本身都不支持webp格式图片,需要额外扩展。

可以直接访问我的GitHub,下载MagicWebViewWebP,将【MagicWebViewWebP.framework】直接导入工程。

参考: UIWebView、WKWebView支持WebP图片显示

示例:

1

2

3

4

5

6

7

8

9

// 导入头文件

#import

// 注册 MagicURLProtocol

[[MagicWebViewWebPManager shareManager] registerMagicURLProtocolWebView:self.webView];

// 销毁 MagicURLProtocol

-(void)dealloc

{

    [[MagicWebViewWebPManager shareManager] unregisterMagicURLProtocolWebView:self.webView];

}

4、如何实现图文混排 + UIKit组件?

使用UIWebView加载自定义HTML代码的方式,实现图文混排。

点击图片,放大,function()跳转链接,携带selectIndex标识,通过拦截UIWebView的请求来获取selectIndex标识。

通过KVO获取到WebView高度,重新设置webView.frame,collectionView.frame,scrollView.contentSize

本方案中,图文混排+UIKit组件,具体逻辑如下:

1.png

5、如何自定义HTML代码?

本方案中,以纯图片为例,处理后的HTML如下:

1

                                    ......

6、如何实现并发执行多个网络请求,统一处理?

本方案中,利用GCD创建队列组,提交多个任务到队列组,多个任务同时执行,监听队列组执行完毕,在主线程刷新UI。

注意: dispatch_group_enter() 、 dispatch_group_leave()将队列组中的任务未执行完毕的任务数目加减1(两个函数要配合使用)

参考: 玩转GCD

示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

- (void)exampleMoreNetwork{

     

    dispatch_group_t group = dispatch_group_create();

    dispatch_queue_t serialQueue = dispatch_queue_create("magic_gcd_group", DISPATCH_QUEUE_SERIAL);

     

    // 网络请求1

    dispatch_group_enter(group);

    dispatch_group_async(group, serialQueue, ^{

        [[MagicNetworkManager shareManager] GET:@"网络请求1" Parameters:nil Success:^(NSURLResponse *response, id responseObject) {

            dispatch_group_leave(group);

        } Failure:^(NSURLResponse *response, id error) {

            dispatch_group_leave(group);

        }];

    });

     

    // 网络请求2

    dispatch_group_enter(group);

    dispatch_group_async(group, serialQueue, ^{

        [[MagicNetworkManager shareManager] GET:@"网络请求2" Parameters:nil Success:^(NSURLResponse *response, id responseObject) {

            dispatch_group_leave(group);

        } Failure:^(NSURLResponse *response, id error) {

            dispatch_group_leave(group);

        }];

    });

     

    // 所有网络请求结束

    dispatch_group_notify(group, serialQueue, ^{

        dispatch_async(dispatch_get_global_queue(00), ^{

            dispatch_async(dispatch_get_main_queue(), ^{

                // 主线程刷新UI

            });

        });

    });

     

}

图文混排——核心

1.png

目录结构

实现代理方法,放大图片,跳转商品,置顶。

实现针对showjoy.com域名,图片url拼接.webp。

实现UIScrollView作为根容器,自适应内容高度。

实现UIWebView支持webp格式图片。

实现自定义HTML代码,图片居中,window.location.href事件传递selectIndex,UIWebView代理拦截selectIndex。

通过HTML,JavaScript,还可以实现更多功能。。。。。。

ProductLoadMorePicTextView.h

1

2

3

4

5

6

7

8

9

10

11

12

13

#import

#import "ProductDetailModel.h"

#import "ProductLoadMorePicTextModel.h"

@protocol ProductLoadMorePicTextViewDelegate

- (void)productLoadMorePicTextViewZoomImageWithIndex:(NSInteger)index;

- (void)productLoadMorePicTextViewPushProductWithSkuId:(NSString *)skuId;

- (void)productLoadMorePicTextViewGoTop;

@end

@interface ProductLoadMorePicTextView : UIView

@property (nonatomic, weak) iddelegate;

- (instancetype)initWithFrame:(CGRect)frame productDetailModel:(ProductDetailModel *)productDetailModel picTextModel:(ProductLoadMorePicTextModel *)picTextModel;

- (void)reload;

@end

ProductLoadMorePicTextView.m

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

#import "ProductLoadMorePicTextView.h"

#import "ProductLoadMorePicTextCollectionViewCell.h"

#import "MagicScrollPageRefreshHeader.h"

#import

static const CGFloat recommendViewHeight = 170.0;

static const CGFloat recommendViewSpace = 10.0;

static const CGFloat recommendItemWidth = 105.0;

static const CGFloat recommendItemSpace = 5.0;

static const CGFloat recommendTitleHeight = 40.0;

@interface ProductLoadMorePicTextView ()

@property (nonatomic, strong) UIScrollView *scrollView;

@property (nonatomic, strong) UIWebView *webView;

@property (nonatomic, strong) UICollectionView *collectionView;

@property (nonatomic, strong) UILabel *recommendLabel;

@property (nonatomic, strong) NSMutableArray *recommendDataArray;

@property (nonatomic, strong) NSMutableArray *picTextDataArray;

@end

@implementation ProductLoadMorePicTextView

- (instancetype)initWithFrame:(CGRect)frame productDetailModel:(ProductDetailModel *)productDetailModel picTextModel:(ProductLoadMorePicTextModel *)picTextModel

{

    self = [super initWithFrame:frame];

    if (self) {

        self.recommendDataArray = [NSMutableArray arrayWithArray:productDetailModel.recommend];

        self.picTextDataArray = [NSMutableArray arrayWithArray:picTextModel.itemPic.packageImages];

        [self createSubViewsWithPicTextModel:picTextModel];

    }

    return self;

}

- (void)createSubViewsWithPicTextModel:(ProductLoadMorePicTextModel *)picTextModel

{

    [self addSubview:self.scrollView];

    [[MagicWebViewWebPManager shareManager] registerMagicURLProtocolWebView:self.webView];

    [self.scrollView addSubview:self.webView];

    [self.scrollView addSubview:self.recommendLabel];

    [self.scrollView addSubview:self.collectionView];

    [self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];

     

    MC_SELF_WEAK(self)

    MagicScrollPageRefreshHeader *header = [MagicScrollPageRefreshHeader headerWithRefreshingBlock:^{

        [weakself.scrollView.mj_header endRefreshing];

        [weakself executeProductLoadMorePicTextViewGoTop];

    }];

    self.scrollView.mj_header = header;

}

#pragma mark -Lazy

- (UIScrollView *)scrollView

{

    if (!_scrollView) {

        _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(00, self.frame.size.width, self.frame.size.height)];

        _scrollView.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.00];

    }

    return _scrollView;

}

- (UIWebView *)webView

{

    if (!_webView) {

        _webView = [[UIWebView alloc] initWithFrame:CGRectMake(00, self.frame.size.width, self.frame.size.height)];

        _webView.delegate = self;

        _webView.scrollView.bounces = NO;

        _webView.scrollView.showsHorizontalScrollIndicator = NO;

        _webView.scrollView.scrollEnabled = NO;

        _webView.scalesPageToFit = YES;

    }

    return _webView;

}

- (UILabel *)recommendLabel{

    if (!_recommendLabel) {

        _recommendLabel = [[UILabel alloc] init];

        _recommendLabel.text = @"   更多推荐";

        _recommendLabel.textColor = [UIColor colorWithRed:0.30 green:0.30 blue:0.30 alpha:1.00];

        _recommendLabel.font = [UIFont systemFontOfSize:12];

        _recommendLabel.backgroundColor = [UIColor whiteColor];

    }

    return _recommendLabel;

}

- (UICollectionView *)collectionView

{

    if (!_collectionView) {

        UICollectionViewFlowLayout *flowLayout = [UICollectionViewFlowLayout new];

        flowLayout.sectionInset = UIEdgeInsetsMake(000, recommendItemSpace);

        flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;

        flowLayout.itemSize = CGSizeMake(recommendItemWidth, recommendViewHeight);

        _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout];

        _collectionView.backgroundColor = [UIColor whiteColor];

        _collectionView.delegate = self;

        _collectionView.dataSource = self;

        [_collectionView registerClass:[ProductLoadMorePicTextCollectionViewCell class] forCellWithReuseIdentifier:@"cell"];

    }

    return _collectionView;

}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context

{

    if ([keyPath isEqualToString:@"contentSize"]) {

        CGSize resize = [self.webView sizeThatFits:CGSizeZero];

        self.webView.frame =  CGRectMake(00, CGRectGetWidth(self.frame), resize.height);

        self.recommendLabel.frame = CGRectMake(0, CGRectGetMaxY(self.webView.frame) + recommendViewSpace, CGRectGetWidth(self.frame), recommendTitleHeight);

        self.collectionView.frame = CGRectMake(0, CGRectGetMaxY(self.recommendLabel.frame), CGRectGetWidth(self.frame), recommendViewHeight);

        self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.frame), CGRectGetMaxY(self.collectionView.frame) + recommendViewSpace);

    }

}

-(void)dealloc

{

    [[MagicWebViewWebPManager shareManager] unregisterMagicURLProtocolWebView:self.webView];

    [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize"];

    self.scrollView = nil;

    self.webView = nil;

    self.collectionView = nil;

    self.recommendDataArray = nil;

    self.picTextDataArray = nil;

}

#pragma mark - UIWebViewDelegate

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{

    return [self handleWebviewEventWithRequest:request];

}

- (void)webViewDidStartLoad:(UIWebView *)webView

{

     

}

- (void)webViewDidFinishLoad:(UIWebView *)webView

{

     

}

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error

{

    NSLog(@"商品详情web错误 %@", error);

}

- (BOOL)handleWebviewEventWithRequest:(NSURLRequest *)request

{

    NSString *url = request.URL.absoluteString;

    NSRange range = [url rangeOfString:@"selectIndex="];

    if (range.location != NSNotFound) {

        NSInteger begin = range.location + range.length;

        NSString *index = [url substringFromIndex:begin];

        [self executeProductLoadMorePicTextViewZoomImageWithIndexString:index];

        return NO;

    }

    return YES;

}

#pragma mark - CustomHTML

- (void)loadWebViewCustomHTMLWithImageUrls:(NSArray *)imageUrls

{

    NSMutableString *html = [NSMutableString string];

    [html appendString:@""];

    [html appendString:@""];

    [html appendString:@""];

    [html appendString:@""];

    [html appendString:[self settingWebViewBodyWithImageUrlArray:imageUrls]];

    [html appendString:@""];

    [html appendString:@""];

    [self.webView loadHTMLString:html baseURL:nil];

}

- (NSString *)settingWebViewBodyWithImageUrlArray:(NSArray *)imageUrlArray

{

    NSMutableString *body = [NSMutableString string];

    for (NSInteger i = 0; i < imageUrlArray.count; i++) {

        NSString *imgUrl = [NSString stringWithFormat:@"%@", [imageUrlArray objectAtIndex:i]];

        imgUrl = [self handlerImgUrlString:imgUrl];

        NSMutableString *html = [NSMutableString string];

        [html appendString:@"

"];

        NSString *onload = [NSString stringWithFormat:@"this.onclick = function() {window.location.href = 'selectIndex=' + %ld;}", i];

        [html appendFormat:@"", onload, imgUrl];

        [html appendString:@""];

        [body appendString:html];

    }

    return body;

}

#pragma mark -UICollectionViewDataSource

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{

    return self.recommendDataArray.count;

}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{

    ProductLoadMorePicTextCollectionViewCell *cell = (ProductLoadMorePicTextCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];

    cell.productRecommendModel = [self.recommendDataArray objectAtIndex:indexPath.row];

    return cell;

}

#pragma mark -UICollectionViewDelegate

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{

   ProductRecommend *productRecommendModel = [self.recommendDataArray objectAtIndex:indexPath.row];

   [self executeProductLoadMorePicTextViewPushProductWithSkuId:productRecommendModel.ID];

}

#pragma mark -ProductLoadMoreViewDelegate

- (void)executeProductLoadMorePicTextViewZoomImageWithIndexString:(NSString *)indexString

{

    if ([self.delegate respondsToSelector:@selector(productLoadMorePicTextViewZoomImageWithIndex:)]) {

        [self.delegate productLoadMorePicTextViewZoomImageWithIndex:[indexString integerValue]];

    }

}

- (void)executeProductLoadMorePicTextViewPushProductWithSkuId:(NSInteger)skuId

{

    if ([self.delegate respondsToSelector:@selector(productLoadMorePicTextViewPushProductWithSkuId:)]) {

        [self.delegate productLoadMorePicTextViewPushProductWithSkuId:[NSString stringWithFormat:@"%ld", skuId]];

    }

}

- (void)executeProductLoadMorePicTextViewGoTop

{

    if ([self.delegate respondsToSelector:@selector(productLoadMorePicTextViewGoTop)]) {

        [self.delegate productLoadMorePicTextViewGoTop];

    }

}

#pragma mark - Reload

- (void)reload{

    [self loadWebViewCustomHTMLWithImageUrls:self.picTextDataArray];

    [self.collectionView reloadData];

}

#pragma mark - IMGURL

- (NSString *)handlerImgUrlString:(NSString *)imgUrlString

{

    NSString *result = [NetworkManager httpsSchemeHandler:imgUrlString];

    // webp

    if ([result containsString:@"showjoy.com"] && ![result hasSuffix:@".webp"]) {

        result = [result stringByAppendingString:@".webp"];

    }

    return result;

}

@end

图文混排——使用

ProductLoadMoreViewController中,保证两个接口都请求完成后,刷新ProductLoadMorePicTextView。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

#import "ProductLoadMoreViewController.h"

#import "MagicNetworkManager.h"

#import "ProductLoadMorePicTextView.h"

static NSString * const SJProductAPI = @"https://shopappserver.showjoy.com/api/shop/sku";

static NSString * const SJProductPicTextAPI = @"https://shopappserver.showjoy.com/api/shop/item/pictext";

static NSString * const SJProductSkuId = @"146931";

@interface ProductLoadMoreViewController ()

@end

@implementation ProductLoadMoreViewController{

    ProductDetailModel *_productModel;

    ProductLoadMorePicTextModel *_productPicTextModel;

    ProductLoadMorePicTextView *_picTextView;

}

- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view.

    self.view.backgroundColor = [UIColor grayColor];

    [self networkRequestData];

}

#pragma mark - Network

- (void)networkRequestData

{

   [QuicklyHUD showWindowsProgressHUDText:@"加载中..."];

    dispatch_group_t group = dispatch_group_create();

    dispatch_queue_t serialQueue = dispatch_queue_create("product_group", DISPATCH_QUEUE_SERIAL);

     

    // 商品信息

    dispatch_group_enter(group);

    dispatch_group_async(group, serialQueue, ^{

        [[MagicNetworkManager shareManager] GET:SJProductAPI Parameters:@{@"skuId" : SJProductSkuId} Success:^(NSURLResponse *response, id responseObject) {

            [ProductDetailModel mj_setupObjectClassInArray:^NSDictionary *{

                return @{@"shop" : [ProductShop class],

                         @"skuList" : [ProductSkuList class],

                         @"value" : [ProductValue class],

                         @"saleInfo" : [ProductSaleInfo class],

                         @"recommend" : [ProductRecommend class],

                         @"skuCommission" : [ProductSkuCommission class],

                         @"item" : [ProductItem class],

                         @"tagSkus" : [ProductTagSkus class],

                         @"tagMap" : [ProductTagMap class],

                         @"skuEnsures" : [ProductSkuEnsures class],

                         @"salesPromotion" : [ProductSalesPromotion class]};

            }];

            _productModel = [ProductDetailModel mj_objectWithKeyValues:[responseObject valueForKey:@"data"]];

            dispatch_group_leave(group);

        } Failure:^(NSURLResponse *response, id error) {

            dispatch_group_leave(group);

        }];

    });

     

    // 图文信息

    dispatch_group_enter(group);

    dispatch_group_async(group, serialQueue, ^{

        [[MagicNetworkManager shareManager] GET:SJProductPicTextAPI Parameters:@{@"skuId" : SJProductSkuId} Success:^(NSURLResponse *response, id responseObject) {

            [ProductLoadMorePicTextModel mj_setupObjectClassInArray:^NSDictionary *{

                return @{@"item" : [PicTextItem class],

                         @"itemPic" : [PicTextItemPic class],

                         @"spu" : [PicTextSpu class]};

            }];

            _productPicTextModel = [ProductLoadMorePicTextModel mj_objectWithKeyValues:[responseObject valueForKey:@"data"]];

            dispatch_group_leave(group);

        } Failure:^(NSURLResponse *response, id error) {

            dispatch_group_leave(group);

        }];

    });

     

    // 主线程刷新UI

    dispatch_group_notify(group, serialQueue, ^{

        dispatch_async(dispatch_get_global_queue(00), ^{

            dispatch_async(dispatch_get_main_queue(), ^{

                [QuicklyHUD hiddenMBProgressHUDForView:MC_APP_WINDOW];

                [self reloadPicTextView];

            });

        });

    }); 

}

#pragma mark - Reload

- (void)reloadPicTextView

{

    if (_picTextView) {

        [_picTextView removeFromSuperview];

        _picTextView.delegate = nil;

        _picTextView = nil;

    }

    CGFloat border = 20.0f;

    _picTextView = [[ProductLoadMorePicTextView alloc] initWithFrame:CGRectMake(border, border, MC_SCREEN_W - 2 * border, MC_SCREEN_H - MC_NAVIGATION_BAR_H - MC_STATUS_BAR_H - 2 * border) productDetailModel:_productModel picTextModel:_productPicTextModel];

    _picTextView.delegate = self;

    [self.view addSubview:_picTextView];

    [_picTextView reload];

}

#pragma mark - ProductLoadMorePicTextViewDelegate

- (void)productLoadMorePicTextViewGoTop

{

    [QuicklyHUD showWindowsOnlyTextHUDText:@"Go Top"];

}

- (void)productLoadMorePicTextViewZoomImageWithIndex:(NSInteger)index

{

    [QuicklyHUD showWindowsOnlyTextHUDText:[NSString stringWithFormat:@"img: %ld", index]];

}

- (void)productLoadMorePicTextViewPushProductWithSkuId:(NSString *)skuId

{

    [QuicklyHUD showWindowsOnlyTextHUDText:[NSString stringWithFormat:@"skuId: %@", skuId]];

}

@end

Demo

https://github.com/Luis-X/MagicCubeKit

猜你喜欢

转载自blog.csdn.net/Keep_Moving31038/article/details/81361559