浏览代码

Merge pull request #1432 from kinarobin/feature-trailer

Feature for trailer(UICollectionView 横拉)
Kinarobin 5 年之前
父节点
当前提交
c18eb2862c

+ 26 - 0
MJRefresh/Base/MJRefreshTrailer.h

@@ -0,0 +1,26 @@
+//
+//  MJRefreshTrailer.h
+//  MJRefresh
+//
+//  Created by kinarobin on 2020/5/3.
+//  Copyright © 2020 小码哥. All rights reserved.
+//
+
+#import <MJRefresh/MJRefresh.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MJRefreshTrailer : MJRefreshComponent
+
+/** 创建trailer*/
++ (instancetype)trailerWithRefreshingBlock:(MJRefreshComponentAction)refreshingBlock;
+/** 创建trailer */
++ (instancetype)trailerWithRefreshingTarget:(id)target refreshingAction:(SEL)action;
+
+/** 忽略多少scrollView的contentInset的right */
+@property (assign, nonatomic) CGFloat ignoredScrollViewContentInsetRight;
+
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 164 - 0
MJRefresh/Base/MJRefreshTrailer.m

@@ -0,0 +1,164 @@
+//
+//  MJRefreshTrailer.m
+//  MJRefresh
+//
+//  Created by kinarobin on 2020/5/3.
+//  Copyright © 2020 小码哥. All rights reserved.
+//
+
+#import "MJRefreshTrailer.h"
+
+@interface MJRefreshTrailer()
+@property (assign, nonatomic) NSInteger lastRefreshCount;
+@property (assign, nonatomic) CGFloat lastRightDelta;
+@end
+
+@implementation MJRefreshTrailer
+
+#pragma mark - 构造方法
++ (instancetype)trailerWithRefreshingBlock:(MJRefreshComponentAction)refreshingBlock {
+    MJRefreshTrailer *cmp = [[self alloc] init];
+    cmp.refreshingBlock = refreshingBlock;
+    return cmp;
+}
+
++ (instancetype)trailerWithRefreshingTarget:(id)target refreshingAction:(SEL)action {
+    MJRefreshTrailer *cmp = [[self alloc] init];
+    [cmp setRefreshingTarget:target refreshingAction:action];
+    return cmp;
+}
+
+- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change {
+    [super scrollViewContentOffsetDidChange:change];
+    
+    // 如果正在刷新,直接返回
+    if (self.state == MJRefreshStateRefreshing) return;
+    
+    _scrollViewOriginalInset = self.scrollView.mj_inset;
+    
+    // 当前的contentOffset
+    CGFloat currentOffsetX = self.scrollView.mj_offsetX;
+    // 尾部控件刚好出现的offsetX
+    CGFloat happenOffsetX = [self happenOffsetX];
+    // 如果是向右滚动到看不见右边控件,直接返回
+    if (currentOffsetX <= happenOffsetX) return;
+    
+    CGFloat pullingPercent = (currentOffsetX - happenOffsetX) / self.mj_w;
+    
+    // 如果已全部加载,仅设置pullingPercent,然后返回
+    if (self.state == MJRefreshStateNoMoreData) {
+        self.pullingPercent = pullingPercent;
+        return;
+    }
+    
+    if (self.scrollView.isDragging) {
+        self.pullingPercent = pullingPercent;
+        // 普通 和 即将刷新 的临界点
+        CGFloat normal2pullingOffsetX = happenOffsetX + self.mj_w;
+        
+        if (self.state == MJRefreshStateIdle && currentOffsetX > normal2pullingOffsetX) {
+            self.state = MJRefreshStatePulling;
+        } else if (self.state == MJRefreshStatePulling && currentOffsetX <= normal2pullingOffsetX) {
+            // 转为普通状态
+            self.state = MJRefreshStateIdle;
+        }
+    } else if (self.state == MJRefreshStatePulling) {// 即将刷新 && 手松开
+        // 开始刷新
+        [self beginRefreshing];
+    } else if (pullingPercent < 1) {
+        self.pullingPercent = pullingPercent;
+    }
+}
+
+- (void)setState:(MJRefreshState)state {
+    MJRefreshCheckState
+    // 根据状态来设置属性
+    if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
+        // 刷新完毕
+        if (MJRefreshStateRefreshing == oldState) {
+            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
+                if (self.endRefreshingAnimationBeginAction) {
+                    self.endRefreshingAnimationBeginAction();
+                }
+                
+                self.scrollView.mj_insetR -= self.lastRightDelta;
+                // 自动调整透明度
+                if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
+            } completion:^(BOOL finished) {
+                self.pullingPercent = 0.0;
+                
+                if (self.endRefreshingCompletionBlock) {
+                    self.endRefreshingCompletionBlock();
+                }
+            }];
+        }
+        
+        CGFloat deltaW = [self widthForContentBreakView];
+        // 刚刷新完毕
+        if (MJRefreshStateRefreshing == oldState && deltaW > 0 && self.scrollView.mj_totalDataCount != self.lastRefreshCount) {
+            self.scrollView.mj_offsetX = self.scrollView.mj_offsetX;
+        }
+    } else if (state == MJRefreshStateRefreshing) {
+        // 记录刷新前的数量
+        self.lastRefreshCount = self.scrollView.mj_totalDataCount;
+        
+        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
+            CGFloat right = self.mj_w + self.scrollViewOriginalInset.right;
+            CGFloat deltaW = [self widthForContentBreakView];
+            if (deltaW < 0) { // 如果内容宽度小于view的宽度
+                right -= deltaW;
+            }
+            self.lastRightDelta = right - self.scrollView.mj_insetR;
+            self.scrollView.mj_insetR = right;
+            
+            // 设置滚动位置
+            CGPoint offset = self.scrollView.contentOffset;
+            offset.x = [self happenOffsetX] + self.mj_w;
+            [self.scrollView setContentOffset:offset animated:NO];
+        } completion:^(BOOL finished) {
+            [self executeRefreshingCallback];
+        }];
+    }
+}
+
+- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
+    [super scrollViewContentSizeDidChange:change];
+    
+    // 内容的宽度
+    CGFloat contentWidth = self.scrollView.mj_contentW + self.ignoredScrollViewContentInsetRight;
+    // 表格的宽度
+    CGFloat scrollWidth = self.scrollView.mj_w - self.scrollViewOriginalInset.left - self.scrollViewOriginalInset.right + self.ignoredScrollViewContentInsetRight;
+    // 设置位置和尺寸
+    self.mj_x = MAX(contentWidth, scrollWidth);
+}
+
+- (void)willMoveToSuperview:(UIView *)newSuperview {
+    [super willMoveToSuperview:newSuperview];
+    
+    if (newSuperview) {
+        self.mj_h = _scrollView.mj_h;
+        // 设置自己的宽度
+        self.mj_w = MJRefreshTrailWidth;
+        
+        // 设置支持水平弹簧效果
+        _scrollView.alwaysBounceHorizontal = YES;
+    }
+}
+
+#pragma mark 刚好看到上拉刷新控件时的contentOffset.x
+- (CGFloat)happenOffsetX {
+    CGFloat deltaW = [self widthForContentBreakView];
+    if (deltaW > 0) {
+        return deltaW - self.scrollViewOriginalInset.left;
+    } else {
+        return - self.scrollViewOriginalInset.left;
+    }
+}
+
+#pragma mark 获得scrollView的内容 超出 view 的宽度
+- (CGFloat)widthForContentBreakView {
+    CGFloat w = self.scrollView.frame.size.width - self.scrollViewOriginalInset.right - self.scrollViewOriginalInset.left;
+    return self.scrollView.contentSize.width - w;
+}
+
+@end

+ 19 - 0
MJRefresh/Custom/Trailer/MJRefreshNormalTrailer.h

@@ -0,0 +1,19 @@
+//
+//  MJRefreshNormalTrailer.h
+//  MJRefreshExample
+//
+//  Created by kinarobin on 2020/5/3.
+//  Copyright © 2020 小码哥. All rights reserved.
+//
+
+#import "MJRefreshStateTrailer.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MJRefreshNormalTrailer : MJRefreshStateTrailer
+
+@property (weak, nonatomic, readonly) UIImageView *arrowView;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 79 - 0
MJRefresh/Custom/Trailer/MJRefreshNormalTrailer.m

@@ -0,0 +1,79 @@
+//
+//  MJRefreshNormalTrailer.m
+//  MJRefreshExample
+//
+//  Created by kinarobin on 2020/5/3.
+//  Copyright © 2020 小码哥. All rights reserved.
+//
+
+#import "MJRefreshNormalTrailer.h"
+#import "NSBundle+MJRefresh.h"
+
+@interface MJRefreshNormalTrailer() {
+    __unsafe_unretained UIImageView *_arrowView;
+}
+@end
+
+@implementation MJRefreshNormalTrailer
+#pragma mark - 懒加载子控件
+- (UIImageView *)arrowView {
+    if (!_arrowView) {
+        UIImageView *arrowView = [[UIImageView alloc] initWithImage:[NSBundle mj_trailArrowImage]];
+        [self addSubview:_arrowView = arrowView];
+    }
+    return _arrowView;
+}
+
+- (void)placeSubviews {
+    [super placeSubviews];
+
+    CGSize arrowSize = self.arrowView.image.size;
+    // 箭头的中心点
+    CGPoint selfCenter = CGPointMake(self.mj_w * 0.5, self.mj_h * 0.5);
+    CGPoint arrowCenter = CGPointMake(arrowSize.width * 0.5 + 5, self.mj_h * 0.5);
+    BOOL stateHidden = self.stateLabel.isHidden;
+    
+    if (self.arrowView.constraints.count == 0) {
+        self.arrowView.mj_size = self.arrowView.image.size;
+        self.arrowView.center = stateHidden ?  selfCenter : arrowCenter ;
+    }
+    self.arrowView.tintColor = self.stateLabel.textColor;
+    
+    if (stateHidden) return;
+    
+    BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0;
+    CGFloat stateLabelW = ceil(self.stateLabel.font.pointSize);
+    // 状态
+    if (noConstrainsOnStatusLabel) {
+        BOOL arrowHidden = self.arrowView.isHidden;
+        CGFloat stateCenterX = (self.mj_w + arrowSize.width) * 0.5;
+        self.stateLabel.center = arrowHidden ? selfCenter : CGPointMake(stateCenterX, self.mj_h * 0.5);
+        self.stateLabel.mj_size = CGSizeMake(stateLabelW, self.mj_h) ;
+    }
+}
+
+- (void)setState:(MJRefreshState)state {
+    MJRefreshCheckState
+    // 根据状态做事情
+    if (state == MJRefreshStateIdle) {
+        if (oldState == MJRefreshStateRefreshing) {
+            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
+                self.arrowView.transform = CGAffineTransformMakeRotation(M_PI);
+            } completion:^(BOOL finished) {
+                self.arrowView.transform = CGAffineTransformIdentity;
+            }];
+        } else {
+            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
+                self.arrowView.transform = CGAffineTransformIdentity;
+            }];
+        }
+    } else if (state == MJRefreshStatePulling) {
+        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
+            self.arrowView.transform = CGAffineTransformMakeRotation(M_PI);
+        }];
+    }
+}
+
+
+
+@end

+ 24 - 0
MJRefresh/Custom/Trailer/MJRefreshStateTrailer.h

@@ -0,0 +1,24 @@
+//
+//  MJRefreshStateTrailer.h
+//  MJRefreshExample
+//
+//  Created by kinarobin on 2020/5/3.
+//  Copyright © 2020 小码哥. All rights reserved.
+//
+
+#import "MJRefreshTrailer.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+
+@interface MJRefreshStateTrailer : MJRefreshTrailer
+
+#pragma mark - 状态相关
+/** 显示刷新状态的label */
+@property (weak, nonatomic, readonly) UILabel *stateLabel;
+/** 设置state状态下的文字 */
+- (void)setTitle:(NSString *)title forState:(MJRefreshState)state;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 73 - 0
MJRefresh/Custom/Trailer/MJRefreshStateTrailer.m

@@ -0,0 +1,73 @@
+//
+//  MJRefreshStateTrailer.m
+//  MJRefreshExample
+//
+//  Created by kinarobin on 2020/5/3.
+//  Copyright © 2020 小码哥. All rights reserved.
+//
+
+#import "MJRefreshStateTrailer.h"
+
+@interface MJRefreshStateTrailer() {
+    /** 显示刷新状态的label */
+    __unsafe_unretained UILabel *_stateLabel;
+}
+/** 所有状态对应的文字 */
+@property (strong, nonatomic) NSMutableDictionary *stateTitles;
+@end
+
+@implementation MJRefreshStateTrailer
+#pragma mark - 懒加载
+- (NSMutableDictionary *)stateTitles {
+    if (!_stateTitles) {
+        self.stateTitles = [NSMutableDictionary dictionary];
+    }
+    return _stateTitles;
+}
+
+- (UILabel *)stateLabel {
+    if (!_stateLabel) {
+        UILabel *stateLabel = [UILabel mj_label];
+        stateLabel.numberOfLines = 0;
+        [self addSubview:_stateLabel = stateLabel];
+    }
+    return _stateLabel;
+}
+
+#pragma mark - 公共方法
+- (void)setTitle:(NSString *)title forState:(MJRefreshState)state {
+    if (title == nil) return;
+    self.stateTitles[@(state)] = title;
+}
+
+#pragma mark - 覆盖父类的方法
+- (void)prepare {
+    [super prepare];
+    
+    // 初始化文字
+    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshTrailerIdleText] forState:MJRefreshStateIdle];
+    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshTrailerPullingText] forState:MJRefreshStatePulling];
+    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshTrailerPullingText] forState:MJRefreshStateRefreshing];
+}
+
+- (void)setState:(MJRefreshState)state {
+    MJRefreshCheckState
+    // 设置状态文字
+    self.stateLabel.text = self.stateTitles[@(state)];
+}
+
+- (void)placeSubviews {
+    [super placeSubviews];
+    
+    if (self.stateLabel.hidden) return;
+    
+    BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0;
+    CGFloat stateLabelW = ceil(self.stateLabel.font.pointSize);
+    // 状态
+    if (noConstrainsOnStatusLabel) {
+        self.stateLabel.center = CGPointMake(self.mj_w * 0.5, self.mj_h * 0.5);
+        self.stateLabel.mj_size = CGSizeMake(stateLabelW, self.mj_h) ;
+    }
+}
+
+@end

二进制
MJRefresh/MJRefresh.bundle/trail_arrow@2x.png


二进制
MJRefresh/MJRefresh.bundle/zh-Hans.lproj/Localizable.strings


+ 3 - 0
MJRefresh/MJRefresh.bundle/zh-Hant.lproj/Localizable.strings

@@ -2,6 +2,9 @@
 "MJRefreshHeaderPullingText" = "鬆開立即刷新";
 "MJRefreshHeaderRefreshingText" = "正在刷新數據中...";
 
+"MJRefreshTrailerIdleText" = "滑動查看圖文詳情";
+"MJRefreshTrailerPullingText" = "釋放查看圖文詳情";
+
 "MJRefreshAutoFooterIdleText" = "點擊或上拉加載更多";
 "MJRefreshAutoFooterRefreshingText" = "正在加載更多的數據...";
 "MJRefreshAutoFooterNoMoreDataText" = "已經全部加載完畢";

+ 4 - 0
MJRefresh/MJRefreshConst.h

@@ -33,6 +33,7 @@
 UIKIT_EXTERN const CGFloat MJRefreshLabelLeftInset;
 UIKIT_EXTERN const CGFloat MJRefreshHeaderHeight;
 UIKIT_EXTERN const CGFloat MJRefreshFooterHeight;
+UIKIT_EXTERN const CGFloat MJRefreshTrailWidth;
 UIKIT_EXTERN const CGFloat MJRefreshFastAnimationDuration;
 UIKIT_EXTERN const CGFloat MJRefreshSlowAnimationDuration;
 
@@ -47,6 +48,9 @@ UIKIT_EXTERN NSString *const MJRefreshHeaderIdleText;
 UIKIT_EXTERN NSString *const MJRefreshHeaderPullingText;
 UIKIT_EXTERN NSString *const MJRefreshHeaderRefreshingText;
 
+UIKIT_EXTERN NSString *const MJRefreshTrailerIdleText;
+UIKIT_EXTERN NSString *const MJRefreshTrailerPullingText;
+
 UIKIT_EXTERN NSString *const MJRefreshAutoFooterIdleText;
 UIKIT_EXTERN NSString *const MJRefreshAutoFooterRefreshingText;
 UIKIT_EXTERN NSString *const MJRefreshAutoFooterNoMoreDataText;

+ 5 - 1
MJRefresh/MJRefreshConst.m

@@ -5,6 +5,7 @@
 const CGFloat MJRefreshLabelLeftInset = 25;
 const CGFloat MJRefreshHeaderHeight = 54.0;
 const CGFloat MJRefreshFooterHeight = 44.0;
+const CGFloat MJRefreshTrailWidth = 60.0;
 const CGFloat MJRefreshFastAnimationDuration = 0.25;
 const CGFloat MJRefreshSlowAnimationDuration = 0.4;
 
@@ -19,6 +20,9 @@
 NSString *const MJRefreshHeaderPullingText = @"MJRefreshHeaderPullingText";
 NSString *const MJRefreshHeaderRefreshingText = @"MJRefreshHeaderRefreshingText";
 
+NSString *const MJRefreshTrailerIdleText = @"MJRefreshTrailerIdleText";
+NSString *const MJRefreshTrailerPullingText = @"MJRefreshTrailerPullingText";
+
 NSString *const MJRefreshAutoFooterIdleText = @"MJRefreshAutoFooterIdleText";
 NSString *const MJRefreshAutoFooterRefreshingText = @"MJRefreshAutoFooterRefreshingText";
 NSString *const MJRefreshAutoFooterNoMoreDataText = @"MJRefreshAutoFooterNoMoreDataText";
@@ -30,4 +34,4 @@
 
 NSString *const MJRefreshHeaderLastTimeText = @"MJRefreshHeaderLastTimeText";
 NSString *const MJRefreshHeaderDateTodayText = @"MJRefreshHeaderDateTodayText";
-NSString *const MJRefreshHeaderNoneLastDateText = @"MJRefreshHeaderNoneLastDateText";
+NSString *const MJRefreshHeaderNoneLastDateText = @"MJRefreshHeaderNoneLastDateText";

+ 1 - 0
MJRefresh/NSBundle+MJRefresh.h

@@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
 @interface NSBundle (MJRefresh)
 + (instancetype)mj_refreshBundle;
 + (UIImage *)mj_arrowImage;
++ (UIImage *)mj_trailArrowImage;
 + (NSString *)mj_localizedStringForKey:(NSString *)key value:(nullable NSString *)value;
 + (NSString *)mj_localizedStringForKey:(NSString *)key;
 @end

+ 8 - 0
MJRefresh/NSBundle+MJRefresh.m

@@ -30,6 +30,14 @@ + (UIImage *)mj_arrowImage
     return arrowImage;
 }
 
++ (UIImage *)mj_trailArrowImage {
+    static UIImage *arrowImage = nil;
+    if (arrowImage == nil) {
+        arrowImage = [[UIImage imageWithContentsOfFile:[[self mj_refreshBundle] pathForResource:@"trail_arrow@2x" ofType:@"png"]] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+    }
+    return arrowImage;
+}
+
 + (NSString *)mj_localizedStringForKey:(NSString *)key
 {
     return [self mj_localizedStringForKey:key value:nil];

+ 5 - 2
MJRefresh/UIScrollView+MJRefresh.h

@@ -5,12 +5,12 @@
 //
 //  Created by MJ Lee on 15/3/4.
 //  Copyright (c) 2015年 小码哥. All rights reserved.
-//  给ScrollView增加下拉刷新、上拉刷新的功能
+//  给ScrollView增加下拉刷新、上拉刷新、 左滑刷新的功能
 
 #import <UIKit/UIKit.h>
 #import "MJRefreshConst.h"
 
-@class MJRefreshHeader, MJRefreshFooter;
+@class MJRefreshHeader, MJRefreshFooter, MJRefreshTrailer;
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -22,6 +22,9 @@ NS_ASSUME_NONNULL_BEGIN
 @property (strong, nonatomic, nullable) MJRefreshFooter *mj_footer;
 @property (strong, nonatomic, nullable) MJRefreshFooter *footer MJRefreshDeprecated("使用mj_footer");
 
+/** 左滑刷新控件 */
+@property (strong, nonatomic, nullable) MJRefreshTrailer *mj_trailer;
+
 #pragma mark - other
 - (NSInteger)mj_totalDataCount;
 

+ 19 - 0
MJRefresh/UIScrollView+MJRefresh.m

@@ -10,6 +10,7 @@
 #import "UIScrollView+MJRefresh.h"
 #import "MJRefreshHeader.h"
 #import "MJRefreshFooter.h"
+#import "MJRefreshTrailer.h"
 #import <objc/runtime.h>
 
 @implementation UIScrollView (MJRefresh)
@@ -54,6 +55,24 @@ - (MJRefreshFooter *)mj_footer
     return objc_getAssociatedObject(self, &MJRefreshFooterKey);
 }
 
+#pragma mark - footer
+static const char MJRefreshTrailerKey = '\0';
+- (void)setMj_trailer:(MJRefreshTrailer *)mj_trailer {
+    if (mj_trailer != self.mj_trailer) {
+        // 删除旧的,添加新的
+        [self.mj_trailer removeFromSuperview];
+        [self insertSubview:mj_trailer atIndex:0];
+        
+        // 存储新的
+        objc_setAssociatedObject(self, &MJRefreshTrailerKey,
+                                 mj_trailer, OBJC_ASSOCIATION_RETAIN);
+    }
+}
+
+- (MJRefreshTrailer *)mj_trailer {
+    return objc_getAssociatedObject(self, &MJRefreshTrailerKey);
+}
+
 #pragma mark - 过期
 - (void)setFooter:(MJRefreshFooter *)footer
 {