MJRefresh 源碼學(xué)習(xí)筆記

MJRefresh源碼學(xué)習(xí)筆記.png

1.前言

MJRefresh 是日常 iOS 開發(fā)中使用頻率比較高的一款下拉刷新/上拉加載更多的第三方控件,平時似乎沒有完整查看過源碼誓琼,此處就記錄一下探究源碼的過程吧爽醋。

注:本文已同步至 個人博客披泪。

2.使用示例

官方給的 Example 里邊提供了很多種刷新樣式 踪蹬,本文我們只以其中 2 種樣式(UITableView + 下拉刷新 動畫圖片UITableView + 上拉刷新動畫圖片 )為例展開討論。

示例1:UITableView + 下拉刷新 動畫圖片

- (void)exampleA
{
    // 1.設(shè)置 header
    self.tableView.mj_header = [MJChiBaoZiHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewData)];
    // 2.馬上進(jìn)入刷新狀態(tài)
    [self.tableView.mj_header beginRefreshing];
}

- (void)loadNewData {
    // 3.下載數(shù)據(jù)的操作
   __weak typeof(self) weakSelf = self;
   [self requestNetDataWithCompletionBlock:^(NSArray *result, BOOL isSuccess) {
      // 處理返回的數(shù)據(jù)(略)

      // 4.刷新表格恶迈,并結(jié)束刷新狀態(tài)
      [weakSelf.tableView reloadData];
      // 5.拿到當(dāng)前的下拉刷新控件涩金,
      [weakSelf.tableView.mj_header endRefreshing];
   }];
}

如上邊注釋所述,大概分 5 個步驟暇仲,其中 1步做、2、4奈附、5 都是 MJRefresh 的相關(guān)操作全度。

示例2:UITableView + 上拉刷新 動畫圖片

- (void)exampleB
{
    // 1.設(shè)置 footer
    self.tableView.mj_footer = [MJChiBaoZiFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreData)];
}

- (void)loadNewData {
    // 2.下載數(shù)據(jù)的操作
   __weak typeof(self) weakSelf = self;
   [self requestNetDataWithCompletionBlock:^(NSArray *result, BOOL isSuccess) {
      // 將返回?cái)?shù)據(jù)追加到表格的數(shù)據(jù)源(略)

      // 3.刷新表格,并結(jié)束刷新狀態(tài)
      [weakSelf.tableView reloadData];
      // 4.拿到當(dāng)前的上拉加載更多控件斥滤,
      [weakSelf.tableView.mj_footer endRefreshing];
   }
}

與下拉類似将鸵,這里的 1勉盅、3、4 是 MJRefresh 的相關(guān)操作咨堤。

3. 整體思路

在開始分析源碼之前菇篡,我覺得應(yīng)該先大概了解一下這個庫的基本實(shí)現(xiàn)思路漩符,這樣看源碼的時候才不至于暈頭轉(zhuǎn)向一喘,不知所云。下面以下拉刷新為例嗜暴,做一個簡單介紹凸克,先看下圖。

MJRefresh整體思路.png

首先闷沥,當(dāng)我們給 tableView.mj_header 賦值時萎战,實(shí)際在 tableView 上添加一個子視圖即刷新控件,但是并不是添加到 tableView 的 header 里邊舆逃,因此就不會占用 tableView 的 header蚂维。

然后,對 tableView 進(jìn)行監(jiān)聽 (KVO)路狮,當(dāng) tableView 的 contentOffset 發(fā)生變化時虫啥,刷新控件會截獲到這個時機(jī),根據(jù) contentOffset 的 y 值更新刷新控件的顯示及 tableView 的 contentInset.top奄妨,整個流程見上圖涂籽。下面說說這張圖吧 O(∩_∩)O:

① 剛把刷新控件加到 tableView 上的時候,設(shè)置刷新控件的 y 值為自身高度的負(fù)值砸抛,此時改控件會被導(dǎo)航擋住评雌,當(dāng)然也可以再設(shè)置其透明度為0。

② 下拉 tableView直焙,當(dāng)刷新控件完全顯示出來(臨界點(diǎn))之前景东,是一種狀態(tài),此時松手的話奔誓,會直接彈回去耐薯。

③ 過了臨界點(diǎn),再往下拉的時候丝里,更新控件的顯示曲初,此時松手的話就開始刷新。

④ 放手刷新的時候杯聚,控件會回彈臼婆,可以加動畫,不至于那么生硬幌绍。同時執(zhí)行調(diào)用方傳入的 block颁褂,一般是請求網(wǎng)絡(luò)數(shù)據(jù)的操作故响。

⑤ 刷新過程中,要顯示該刷新控件颁独,即不讓其彈回到導(dǎo)航后邊彩届,就給 tableView.contentInset.top 增加一個控件的高度,當(dāng)然是負(fù)值誓酒。

⑥ 當(dāng)調(diào)用方請求完數(shù)據(jù)后樟蠕,手動調(diào)用 刷新控件的 endRefreshing 方法,在這個方法類里邊更新控件 UI 至初始狀態(tài)靠柑,并將 tableView.contentInset.top 減少一個控件的高度寨辩,當(dāng)然也是負(fù)值,至此歼冰,刷新結(jié)束靡狞。

以上就基本實(shí)現(xiàn)邏輯,下面開始看源碼吧隔嫡。

4. 源碼分析

下面分別探究一下下拉刷新和上拉加載更多的源碼實(shí)現(xiàn)甸怕。

通過 示例1示例2 可以推測,這個框架可以大概分 2 部分腮恩,一部分是刷新控件的載體 (UIScrollView及其子類梢杭,即 tableView 和 collectionView),另一部分就是刷新控件本身庆揪,也就是所謂的 header 和 footer式曲。

4.1 刷新控件的載體

載體主要集中在下邊這幾個分類里邊

UIScrollView+MJExtension
UIScrollView+MJRefresh
UIView+MJExtension

UIView+MJExtension

UIView+MJExtension 只是為公共基類 UIView 的 frame 提供了便捷的訪問方式,包括刷新控件也會用到缸榛。

UIScrollView+MJRefresh

UIScrollView+MJRefresh 是列表基類的分類吝羞,這個文件里邊實(shí)際包含了 3 個分類,依次為

① NSObject (MJRefresh):分別提供了交換類方法和交換實(shí)例方法的工具方法:

/// 交換實(shí)例方法
+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{
    method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}
/// 交換類方法
+ (void)exchangeClassMethod1:(SEL)method1 method2:(SEL)method2
{
    method_exchangeImplementations(class_getClassMethod(self, method1), class_getClassMethod(self, method2));
}

② UIScrollView (MJRefresh):依次為基類 UIScrollView 添加了 header内颗、footer 和 mj_reloadDataBlock 這三個屬性钧排,并利用關(guān)聯(lián)對象添加了對應(yīng)的 setter 和 getter 實(shí)現(xiàn),關(guān)于在既有類中使用關(guān)聯(lián)對象存放自定義數(shù)據(jù)的方法均澳,可以查閱《Effective Objective-C 2.0》中第 10 條的介紹恨溜。

其中,在 mj_reloadDataBlocksetter 中設(shè)置關(guān)聯(lián)對象前后分別添加了 willChangeValueForKey:didChangeValueForKey: 這 2 個方法找前,意在可以添加 KVO 監(jiān)聽糟袁。

- (void)setMj_reloadDataBlock:(void (^)(NSInteger))mj_reloadDataBlock
{
    [self willChangeValueForKey:@"mj_reloadDataBlock"]; // KVO
    objc_setAssociatedObject(self, &MJRefreshReloadDataBlockKey, mj_reloadDataBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self didChangeValueForKey:@"mj_reloadDataBlock"]; // KVO
}

然后提供了一個執(zhí)行 mj_reloadDataBlock 的方法 executeReloadDataBlock:

- (void)executeReloadDataBlock
{
    !self.mj_reloadDataBlock ? : self.mj_reloadDataBlock(self.mj_totalDataCount);
}

即如果設(shè)置了 mj_reloadDataBlock ,就在此執(zhí)行這個 block躺盛,我們注意到這個參數(shù) mj_totalDataCount项戴,點(diǎn)開后發(fā)現(xiàn),原來它指的是 UITableView 或 UICollectionView 的總行數(shù)槽惫。

- (NSInteger)mj_totalDataCount
{
    NSInteger totalCount = 0;
    if ([self isKindOfClass:[UITableView class]]) {
        UITableView *tableView = (UITableView *)self;
        
        for (NSInteger section = 0; section<tableView.numberOfSections; section++) {
            totalCount += [tableView numberOfRowsInSection:section];
        }
    } else if ([self isKindOfClass:[UICollectionView class]]) {
        UICollectionView *collectionView = (UICollectionView *)self;
        
        for (NSInteger section = 0; section<collectionView.numberOfSections; section++) {
            totalCount += [collectionView numberOfItemsInSection:section];
        }
    }
    return totalCount;
}

③④ UITableView (MJRefresh) 和 UICollectionView (MJRefresh)周叮,他們都提供了下邊兩個方法:

+ (void)load
{
    [self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}

- (void)mj_reloadData
{
    [self mj_reloadData];
    [self executeReloadDataBlock];
}

也就是說辩撑,在程序啟動時執(zhí)行 load 方法時將列表的 reloadData 方法與自定義的 mj_reloadData 方法交換,在新方法中增加了一步操作 [self executeReloadDataBlock];仿耽,即執(zhí)行上文提到的 mj_reloadDataBlock合冀,這樣,當(dāng)我們執(zhí)行 tableView 的 reloadData 方法時项贺,實(shí)際執(zhí)行的就是 mj_reloadData 這個方法了君躺。

那么這個 block 是什么時候設(shè)置的呢,全局搜索了一下敬扛,發(fā)現(xiàn)只有在 MJRefreshFooterwillMoveToSuperview: 方法中設(shè)置過晰洒,也就是將 footer 添加到 tableView 上的時候朝抖。

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // 當(dāng)前視圖被添加到父視圖上時啥箭,設(shè)置后邊的 block,即當(dāng)列表(UICollectionView 或 UITableView)行數(shù)為 0 時治宣,隱藏當(dāng)前視圖急侥。
    if (newSuperview) {
        // 監(jiān)聽scrollView數(shù)據(jù)的變化
        if ([self.scrollView isKindOfClass:[UITableView class]] || [self.scrollView isKindOfClass:[UICollectionView class]]) {
            [self.scrollView setMj_reloadDataBlock:^(NSInteger totalDataCount) {
                if (self.isAutomaticallyHidden) {
                    self.hidden = (totalDataCount == 0);
                }
            }];
        }
    }
}

willMoveToSuperview: 是將視圖添加到父視圖或從父視圖中移除時調(diào)用的,if (newSuperview) { ... } 說明是將 footer 添加到父視圖上的時候設(shè)置這個 block 的侮邀,block 的具體實(shí)現(xiàn)是:如果需要自動隱藏坏怪,則當(dāng)數(shù)據(jù)的總條數(shù)為 0 時,隱藏 footer绊茧,否則展示铝宵。

UIScrollView+MJExtension

UIScrollView+MJExtension 是關(guān)于下邊幾個屬性的便捷訪問方式:

contentInset / adjustedContentInset
contentOffset
contentSize

其中,adjustedContentInset 是 iOS 11 新引入的一個 屬性华畏,在 iOS 11 中決定 tableView 的內(nèi)容與邊緣距離的是 adjustedContentInset 屬性鹏秋,而不是 contentInset。

4.2下拉刷新控件(refreshHeader)

先來看一張圖:

Header 繼承體系.png

上圖就是 header 的繼承關(guān)系亡笑,示例中的 MJChiBaoZiHeader 就是繼承自 MJRefreshGifHeader侣夷。為了描述更有條理,我們從基類開始討論吧仑乌。

MJRefreshComponent

MJRefreshComponent 是所有 header 和 footer 的基類百拓,這里定義了表示刷新狀態(tài)的枚舉 MJRefreshState 和 3 種不同的回調(diào)。

/** 刷新控件的狀態(tài) */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** 普通閑置狀態(tài) */
    MJRefreshStateIdle = 1,
    /** 松開就可以進(jìn)行刷新的狀態(tài) */
    MJRefreshStatePulling,
    /** 正在刷新中的狀態(tài) */
    MJRefreshStateRefreshing,
    /** 即將刷新的狀態(tài) */
    MJRefreshStateWillRefresh,
    /** 所有數(shù)據(jù)加載完畢晰甚,沒有更多的數(shù)據(jù)了 */
    MJRefreshStateNoMoreData
};

/** 進(jìn)入刷新狀態(tài)的回調(diào) */
typedef void (^MJRefreshComponentRefreshingBlock)(void);
/** 開始刷新后的回調(diào)(進(jìn)入刷新狀態(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(void);
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);

實(shí)際上這個類的文件里有兩個類衙传,除了自己之外還有一個 UILabel 的分類,提供了一個創(chuàng)建定制好的 Label 的類方法 mj_label 和獲取文本寬度的實(shí)例方法 mj_textWith厕九。

+ (instancetype)mj_label
{
    UILabel *label = [[self alloc] init];
    label.font = MJRefreshLabelFont;
    label.textColor = MJRefreshLabelTextColor;
    label.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    label.textAlignment = NSTextAlignmentCenter;
    label.backgroundColor = [UIColor clearColor];
    return label;
}

- (CGFloat)mj_textWith {
    CGFloat stringWidth = 0;
    CGSize size = CGSizeMake(MAXFLOAT, MAXFLOAT);
    if (self.text.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
        stringWidth =[self.text
                      boundingRectWithSize:size
                      options:NSStringDrawingUsesLineFragmentOrigin
                      attributes:@{NSFontAttributeName:self.font}
                      context:nil].size.width;
#else
        
        stringWidth = [self.text sizeWithFont:self.font
                            constrainedToSize:size
                                lineBreakMode:NSLineBreakByCharWrapping].width;
#endif
    }
    return stringWidth;
}

下面來看看 MJRefreshComponent 這個類吧蓖捶,依照慣例,從初始化方法開始:

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        // 準(zhǔn)備工作
        [self prepare];
        
        // 默認(rèn)是普通狀態(tài)
        self.state = MJRefreshStateIdle;
    }
    return self;
}

- (void)prepare
{
    // 基本屬性
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    self.backgroundColor = [UIColor clearColor];
}

初始化了幾個變量:

①初始狀態(tài)設(shè)置為普通狀態(tài)止剖,既沒有觸發(fā)刷新的情況腺阳;

prepare方法中設(shè)置了兩個基本屬性落君,backgroundColorautoresizingMask,autoresizingMask 的初值 UIViewAutoresizingFlexibleWidth 指的是:當(dāng)父視圖的 bounds 改變時亭引,子視圖 (即當(dāng)前視圖) 自動調(diào)整寬度绎速,以保證左、右邊距不變焙蚓。

關(guān)于布局纹冤,這里重寫了 layoutSubviews 方法,在調(diào)用 super 的方法之前增加了一步操作 placeSubviews购公,這個方法需要子類來實(shí)現(xiàn)萌京。

- (void)layoutSubviews
{
    [self placeSubviews];
    
    [super layoutSubviews];
}

- (void)placeSubviews {
    
}

重寫了 willMoveToSuperview: 這個方法用于將當(dāng)前視圖添加到父視圖或從父視圖中移除時添加一些額外操作。

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];
    
    // 如果不是UIScrollView宏浩,不做任何事情
    if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
    
    // 舊的父控件移除監(jiān)聽
    [self removeObservers];
    
    if (newSuperview) { // 新的父控件
        // 設(shè)置寬度
        self.mj_w = newSuperview.mj_w;
        // 設(shè)置位置
        self.mj_x = -_scrollView.mj_insetL;
        
        // 記錄UIScrollView
        _scrollView = (UIScrollView *)newSuperview;
        // 設(shè)置永遠(yuǎn)支持垂直彈簧效果
        _scrollView.alwaysBounceVertical = YES;
        // 記錄UIScrollView最開始的contentInset
        _scrollViewOriginalInset = _scrollView.mj_inset;
        
        // 添加監(jiān)聽
        [self addObservers];
    }
}

首先對 newSuperview 做了一層過濾知残,只有是 UIScrollView 及其子類才可以繼續(xù)往下走。

然后比庄,先將舊的監(jiān)聽移除求妹。

如果是移除當(dāng)前視圖的操作,則會跳過下邊的 if 代碼佳窑,結(jié)束這個方法的執(zhí)行制恍。如果是將當(dāng)前視圖添加父視圖上,即父視圖 newSuperview 存在時神凑,保存一些值净神,最后添加新的監(jiān)聽。

下面看看添加溉委、移除監(jiān)聽的這波操作:

- (void)addObservers
{
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];
    self.pan = self.scrollView.panGestureRecognizer;
    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
}

- (void)removeObservers
{
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset];
    [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];
    [self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
    self.pan = nil;
}

監(jiān)聽主要是針對 self.ScrollView 即父視圖的 contentOffset鹃唯、contentSize 和 父視圖的 panGestureRecognizer 的 state。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 遇到這些情況就直接返回:不能交互
    if (!self.userInteractionEnabled) return;
    
    // 這個就算看不見也需要處理
    if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
        [self scrollViewContentSizeDidChange:change];
    }
    
    // 看不見
    if (self.hidden) return;
    
    if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
        [self scrollViewContentOffsetDidChange:change];
    } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
        [self scrollViewPanStateDidChange:change];
    }
}

當(dāng)監(jiān)聽到變換時薛躬,會分別觸發(fā)對應(yīng)的處理方法(下邊 3 個)俯渤,其中 scrollViewContentOffsetDidChange: 在下拉刷新和上拉加載更多時都會用到,后邊兩個方法只在上拉加載更多時會用到型宝。

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}

接下來是一批公共方法:

① 設(shè)置回調(diào)對象和回調(diào)方法八匠,提供了一種內(nèi)部的響應(yīng)方式,即使用 target-Action 的方式趴酣,執(zhí)行刷新回調(diào) executeRefreshingCallback 時候用到梨树,見下邊的內(nèi)部方法。

- (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    self.refreshingTarget = target;
    self.refreshingAction = action;
}

#pragma mark - 內(nèi)部方法

- (void)executeRefreshingCallback
{
    MJRefreshDispatchAsyncOnMainQueue({
        
        if (self.refreshingBlock) {
            self.refreshingBlock();
        }
        
        // #define MJRefreshMsgSend(...) ((void (*)(void *, SEL, UIView *))objc_msgSend)(__VA_ARGS__)
        // #define MJRefreshMsgTarget(target) (__bridge void *)(target)
        if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) {
            MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self);
        }
        
        if (self.beginRefreshingCompletionBlock) {
            self.beginRefreshingCompletionBlock();
        }
    })
}

接下來是一個很重要的 setter岖寞,子類可以重寫該方法抡四,在狀態(tài)方法改變的時候,及時更新刷新控件。

- (void)setState:(MJRefreshState)state
{
    _state = state;
    
    // 加入主隊(duì)列的目的是等setState:方法調(diào)用完畢指巡、設(shè)置完文字后再去布局子控件
    MJRefreshDispatchAsyncOnMainQueue([self setNeedsLayout];)
}

② 進(jìn)入刷新及結(jié)束刷新的方法淑履,每種情況分別提供了一個帶 block 和一個不帶 block 的方法,后者保存了 block 之后藻雪,又調(diào)用了前者秘噪,這個 block 的作用是用來添加刷新結(jié)束后的附加操作的。

#pragma mark 進(jìn)入刷新狀態(tài)

- (void)beginRefreshing
{
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // 只要正在刷新勉耀,就完全顯示
    if (self.window) {
        self.state = MJRefreshStateRefreshing;
    } else {
        // 預(yù)防正在刷新中時指煎,調(diào)用本方法使得header inset回置失敗
        if (self.state != MJRefreshStateRefreshing) {
            self.state = MJRefreshStateWillRefresh;
            // 刷新(預(yù)防從另一個控制器回到這個控制器的情況,回來要重新刷新一下)
            [self setNeedsDisplay];
        }
    }
}

- (void)beginRefreshingWithCompletionBlock:(void (^)(void))completionBlock
{
    self.beginRefreshingCompletionBlock = completionBlock;
    
    [self beginRefreshing];
}

#pragma mark 結(jié)束刷新狀態(tài)

- (void)endRefreshing
{
    MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}

- (void)endRefreshingWithCompletionBlock:(void (^)(void))completionBlock
{
    self.endRefreshingCompletionBlock = completionBlock;
    
    [self endRefreshing];
}

③ 最后是根據(jù)拖拽進(jìn)度自動改變透明度的相關(guān)方法便斥,即如果需要自動改變透明度至壤,則會在拖拽過程中,將拖拽進(jìn)度時時賦值給 self.alpha枢纠。

#pragma mark 自動切換透明度

- (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha
{
    self.automaticallyChangeAlpha = autoChangeAlpha;
}

- (BOOL)isAutoChangeAlpha
{
    return self.isAutomaticallyChangeAlpha;
}

- (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha
{
    _automaticallyChangeAlpha = automaticallyChangeAlpha;
    
    if (self.isRefreshing) return;
    
    if (automaticallyChangeAlpha) {
        self.alpha = self.pullingPercent;
    } else {
        self.alpha = 1.0;
    }
}

#pragma mark 根據(jù)拖拽進(jìn)度設(shè)置透明度

- (void)setPullingPercent:(CGFloat)pullingPercent
{
    _pullingPercent = pullingPercent;
    
    // 不是正在刷新的狀態(tài)像街,而且要求自動改變透明度時,將 pullingPercent 的值給 alpha京郑,否則不再往下執(zhí)行宅广。
    
    if (self.isRefreshing) return;
    
    if (self.isAutomaticallyChangeAlpha) {
        self.alpha = pullingPercent;
    }
}

MJRefreshHeader

MJRefreshHeaderMJRefreshComponent 的子類葫掉,但還不是最終可以使用的類些举,還在為其子類做準(zhǔn)備。先來看看它提供的兩個構(gòu)造方法俭厚,在創(chuàng)建實(shí)例對象的同時户魏,保存了響應(yīng)的回調(diào),一個采用 block挪挤,另一個采用 target-action 的方式叼丑。

#pragma mark - 構(gòu)造方法

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshHeader *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}

+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    MJRefreshHeader *cmp = [[self alloc] init];
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}

下邊是重寫父類的方法,首先在 prepare 和 `` 方法中設(shè)置存取刷新時刻用的 key 扛门、自身高度 mj_h 及 自身的 y 坐標(biāo) mj_y鸠信。這里出現(xiàn)了一個 ignoredScrollViewContentInsetTop,推測是一個預(yù)留的 refreshHeader 和 tableView 之間的間隙值论寨,默認(rèn)為 0星立,需要用戶設(shè)置才會有值。

- (void)prepare
{
    [super prepare];
    
    // 設(shè)置key
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    
    // 設(shè)置高度
    self.mj_h = MJRefreshHeaderHeight;
}

- (void)placeSubviews
{
    [super placeSubviews];
    
    // 設(shè)置y值(當(dāng)自己的高度發(fā)生改變了葬凳,肯定要重新調(diào)整Y值绰垂,所以放到placeSubviews方法中設(shè)置y值)
    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}

然后是 2 個重要的方法,scrollViewContentOffsetDidChange:setState:火焰。

scrollViewContentOffsetDidChange: 方法主要是根據(jù)當(dāng)前狀態(tài) (self.state) 和 contentOffset 更新 self.state劲装,之所以要考慮當(dāng)前狀態(tài),是為了避免頻繁的更新 state 的值,詳見代碼注釋占业。

setState: 方法針對 進(jìn)入刷新狀態(tài)從刷新恢復(fù)正常狀態(tài) 分別進(jìn)行處理绒怨,前者話,將 scrollView 的 contentInset.top 加上一個 refreshHeader 的高度谦疾,對于后者窖逗,又需要將之前加上的高度減掉,以此來控制刷新控件的懸浮狀態(tài)餐蔬。另外碎紊, 從刷新恢復(fù)正常狀態(tài) 時,保存了當(dāng)前時刻樊诺,這個是為了顯示上一次刷新時間用的仗考。

最后是 2 個公共方法,一個用來獲取保存在本地的上次刷新時間词爬,另一個是 ignoredScrollViewContentInsetTop 的setter秃嗜,同時更新了刷新控件的 y 值。

MJRefreshStateHeader

MJRefreshStateHeader 也屬于這個繼承體系中的一員顿膨,繼承自 MJRefreshHeader锅锨,這里開始就到實(shí)用階段了:

  • 在 prepare 方法中將三種狀態(tài)對應(yīng)的標(biāo)簽保存到一個可變字典(stateTitles)中,以備后邊展示恋沃。
- (void)prepare {
    [super prepare];

    // 初始化間距
    self.labelLeftInset = MJRefreshLabelLeftInset;

    // 初始化文字
    // 初始未觸發(fā)刷新的狀態(tài)
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
    // 拖拽狀態(tài)
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
    // 刷新狀態(tài)
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
}

- (void)setTitle:(NSString *)title forState:(MJRefreshState)state {
    if (title == nil) return;
    self.stateTitles[@(state)] = title;
    self.stateLabel.text = self.stateTitles[@(self.state)];
}
  • 用懶加載的方式為 header 添加了兩個標(biāo)簽必搞,分別用于展示狀態(tài)提示文案和上次刷新的時間;
/** 顯示刷新狀態(tài)的label */
- (UILabel *)stateLabel {
    if (!_stateLabel) {
        [self addSubview:_stateLabel = [UILabel mj_label]];
    }
    return _stateLabel;
}

/** 顯示上一次刷新時間的label */
- (UILabel *)lastUpdatedTimeLabel {
    if (!_lastUpdatedTimeLabel) {
        [self addSubview:_lastUpdatedTimeLabel = [UILabel mj_label]];
    }
    return _lastUpdatedTimeLabel;
}
  • setState: 方法中為 2 個標(biāo)簽分別賦值囊咏,stateLabel 根據(jù) state 從之前保存的可變字典(stateTitles)中取值(這里作者做了本地化處理)恕洲,lastUpdatedTimeLabel 的賦值有點(diǎn)特別,他是在 setLastUpdatedTimeKey: 方法中對時間進(jìn)行格式化等相關(guān)處理后賦值給 lastUpdatedTimeLabel梅割,所以每次更新 state 的時候要調(diào)用用一次 self.lastUpdatedTimeKey = self.lastUpdatedTimeKey霜第。
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    // 設(shè)置狀態(tài)文字
    self.stateLabel.text = self.stateTitles[@(state)];
    // 重新設(shè)置key(重新顯示時間)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}

MJRefreshGifHeader

MJRefreshGifHeader 繼承自上一個類,并增加了一個用于展示動畫圖片的 imageView 及用于保存各種狀態(tài)對應(yīng)的動畫圖片和動畫時間字典户辞。

__unsafe_unretained UIImageView *_gifView;

/** 所有狀態(tài)對應(yīng)的動畫圖片 */
@property (strong, nonatomic) NSMutableDictionary *stateImages;
/** 所有狀態(tài)對應(yīng)的動畫時間 */
@property (strong, nonatomic) NSMutableDictionary *stateDurations;

為了獲取這些動畫圖片和時間泌类,并與對應(yīng)的狀態(tài)關(guān)聯(lián)起來,提供了 2 個供外界調(diào)用的方法,圖片必須提供,時間可以不傳(第二種方法)舔亭,會去默認(rèn)值 images.count * 0.1

- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{ 
    if (images == nil) return; 
    
    self.stateImages[@(state)] = images; 
    self.stateDurations[@(state)] = @(duration); 
    
    /* 根據(jù)圖片設(shè)置控件的高度 */ 
    UIImage *image = [images firstObject]; 
    if (image.size.height > self.mj_h) { 
        self.mj_h = image.size.height; 
    } 
}

- (void)setImages:(NSArray *)images forState:(MJRefreshState)state 
{ 
    [self setImages:images duration:images.count * 0.1 forState:state]; 
}

這里重寫了父類的 setState: 方法喇澡,當(dāng)拖拽或刷新的時候才會去設(shè)置動畫圖片,首先停止之前的動畫殊校,然后再設(shè)置新值晴玖,如果是單張圖片,直接展示,多張情況才需要展示動畫; 如果 state 是正常狀態(tài)呕屎,則停止動畫让簿。

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // 根據(jù)狀態(tài)做事情
    if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
        NSArray *images = self.stateImages[@(state)];
        if (images.count == 0) return;
        
        [self.gifView stopAnimating];
        if (images.count == 1) { // 單張圖片
            self.gifView.image = [images lastObject];
        } else { // 多張圖片
            self.gifView.animationImages = images;
            self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
            [self.gifView startAnimating];
        }
    } else if (state == MJRefreshStateIdle) {
        [self.gifView stopAnimating];
    }
}

MJChiBaoZiHeader

先來看看 prepare 的實(shí)現(xiàn),只重寫了父類的一個方法:

- (void)prepare
{
    // 0.執(zhí)行父類的 prepare 方法
    [super prepare];
    
    // 1.設(shè)置普通狀態(tài)的動畫圖片
    NSMutableArray *idleImages = [NSMutableArray array];
    for (NSUInteger i = 1; i<=60; i++) {
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_anim__000%zd", i]];
        [idleImages addObject:image];
    }
     [self setImages:idleImages forState:MJRefreshStateIdle];
    
    // 2.設(shè)置即將刷新狀態(tài)的動畫圖片(一松開就會刷新的狀態(tài))
    NSMutableArray *refreshingImages = [NSMutableArray array];
    for (NSUInteger i = 1; i<=3; i++) {
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", i]];
        [refreshingImages addObject:image];
    }
    [self setImages:refreshingImages forState:MJRefreshStatePulling];
    
    // 3.設(shè)置正在刷新狀態(tài)的動畫圖片
    [self setImages:refreshingImages forState:MJRefreshStateRefreshing];
}

具體實(shí)現(xiàn)見上邊的代碼注釋秀睛,其中 preparesetImages: forState: 兩個方法均來自基類尔当,上邊已經(jīng)介紹過了。

4.3 上拉加載更多控件(refreshFooter)

與 header 類似蹂安,先看一下類的繼承關(guān)系:

Footer繼承體系.png

既然都是繼承自 MJRefreshComponent椭迎,這里就直接從 MJRefreshFooter 開始討論,然后準(zhǔn)者繼承體系一直講到 MJChiBaoZiFooter田盈,即示例 2 用到的 footer畜号。

MJRefreshFooter

觀察這個類的源碼就會發(fā)現(xiàn),他和 MJRefreshHeader 有許多相似之處允瞧,比如都提供了兩個構(gòu)造方法简软,一個用 block ,一個用 target-Action述暂。下面主要說下不一樣的地方痹升。

有一個自動根據(jù)有無數(shù)據(jù)來顯示和隱藏 footer 的屬性 automaticallyHidden,不過作者不建議使用畦韭,而且后期可能會移除疼蛾。不過,還是假名單介紹一下吧廊驼。

@property (assign, nonatomic, getter=isAutomaticallyHidden) BOOL automaticallyHidden

viewWillMoveToSuperView: 中設(shè)置 footer 隱藏與否的時候會用到這個屬性据过,詳見前邊 4.1刷新控件的載體 的介紹,這里不再復(fù)述妒挎。

最后看 2 個公共方法,廢棄的那個就不列出來了O(∩_∩)O:

/** 提示沒有更多的數(shù)據(jù) */
- (void)endRefreshingWithNoMoreData {
    MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateNoMoreData;)
}

/** 重置沒有更多的數(shù)據(jù)(消除沒有更多數(shù)據(jù)的狀態(tài)) */
- (void)resetNoMoreData {
    MJRefreshDispatchAsyncOnMainQueue(self.state = MJRefreshStateIdle;)
}

MJRefreshAutoFooter

MJRefreshAutoFooterMJRefreshFooter 的直接子類西饵,并不是可以直接使用的類酝掩,還是在為子類提供方便,這里需要重點(diǎn)介紹一下眷柔。

  • 提供了幾個公開的屬性期虾,其作用見下方注釋。
#pragma mark - 公開的屬性

/** 是否自動刷新(默認(rèn)為YES驯嘱,即達(dá)到一定的觸發(fā)條件就會自動開始刷新) */
@property (assign, nonatomic, getter=isAutomaticallyRefresh) BOOL automaticallyRefresh;

/** 當(dāng)?shù)撞靠丶霈F(xiàn)多少時就自動刷新(默認(rèn)為1.0镶苞,也就是底部控件完全出現(xiàn)時,才會自動刷新) */
@property (assign, nonatomic) CGFloat triggerAutomaticallyRefreshPercent;

/** 是否每一次拖拽只發(fā)一次請求鞠评,手沒有離開屏幕的情況下反復(fù)拖拽的話茂蚓,不會觸發(fā)多次刷新 */
@property (assign, nonatomic, getter=isOnlyRefreshPerDrag) BOOL onlyRefreshPerDrag;

#pragma mark - 私有的屬性

/** 是否是一個新的拖拽 */
@property (assign, nonatomic, getter=isOneNewPan) BOOL oneNewPan;
  • 重寫了 willMoveToSuperView 方法,當(dāng)添加到父控件上時,給 scrollView.contentInset.bottom 添加一個 footer 本身的高度聋涨,反之晾浴,從父控件上移除時,又要將之前加上的再減掉牍白。
- (void)willMoveToSuperview:(UIView *)newSuperview {
    [super willMoveToSuperview:newSuperview];
    
    if (newSuperview) { // 添加到父控件上
        
        if (self.hidden == NO) {
            self.scrollView.mj_insetB += self.mj_h;
        }
        self.mj_y = _scrollView.mj_contentH; // 設(shè)置位置
        
    } else {            // 被移除了
        
        if (self.hidden == NO) {
            self.scrollView.mj_insetB -= self.mj_h;
        }
    }
}
  • 在準(zhǔn)備數(shù)據(jù)階段設(shè)置了這么幾個初值:
- (void)prepare {
    [super prepare];
    
    // 默認(rèn)底部控件100%出現(xiàn)時才會自動刷新
    self.triggerAutomaticallyRefreshPercent = 1.0;
    
    // 設(shè)置為默認(rèn)狀態(tài)
    self.automaticallyRefresh = YES;
    
    // 默認(rèn)是當(dāng)offset達(dá)到條件就發(fā)送請求(可連續(xù))
    self.onlyRefreshPerDrag = NO;
}
  • 下面是對 scrollView 的監(jiān)聽觸發(fā)的 3 個事件:

當(dāng) scrollView 的 contentSize 發(fā)生變化的時候脊凰,計(jì)時更新 footer 的 y 值,保證它一直貼著 scrollView 的下邊沿茂腥。

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
    [super scrollViewContentSizeDidChange:change];
    
    // 設(shè)置位置
    self.mj_y = self.scrollView.mj_contentH;
}

當(dāng)滑動 scrollView 產(chǎn)生 contentOffset 的時候狸涌,控制當(dāng)?shù)撞克⑿驴丶耆霈F(xiàn)的時候,才能刷新最岗。

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return;
    
    if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) { // 內(nèi)容超過一個屏幕
        // 這里的_scrollView.mj_contentH替換掉self.mj_y更為合理
        if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) {
            // 防止手松開時連續(xù)調(diào)用
            CGPoint old = [change[@"old"] CGPointValue];
            CGPoint new = [change[@"new"] CGPointValue];
            if (new.y <= old.y) return;
            
            // 當(dāng)?shù)撞克⑿驴丶耆霈F(xiàn)時杈抢,才刷新
            [self beginRefreshing];
        }
    }
}

當(dāng)手勢的狀態(tài)發(fā)生變化的時候,針對 UIGestureRecognizerStateEndedUIGestureRecognizerStateBegan 兩種狀態(tài)進(jìn)行處理仑性,對于前者惶楼,根據(jù) contentOffset.y 決定開始刷新的時機(jī),后者的話诊杆,就認(rèn)為是一個新的手勢開始了歼捐。

- (void)scrollViewPanStateDidChange:(NSDictionary *)change
{
    [super scrollViewPanStateDidChange:change];
    
    if (self.state != MJRefreshStateIdle) return;
    
    UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state;
    if (panState == UIGestureRecognizerStateEnded) {// 手松開
        if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) {  // 不夠一個屏幕
            if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽
                [self beginRefreshing];
            }
        } else { // 超出一個屏幕
            if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) {
                [self beginRefreshing];
            }
        }
    } else if (panState == UIGestureRecognizerStateBegan) {
        self.oneNewPan = YES;
    }
}
  • 當(dāng)然也會重寫 setState 方法,如果是刷新狀態(tài)晨汹,就執(zhí)行刷新的回調(diào)豹储;如果是從刷新狀態(tài)變成沒有更多數(shù)據(jù)或停止刷新的狀態(tài),則執(zhí)行停止刷新完成的 block淘这。
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    if (state == MJRefreshStateRefreshing) {
        
        // 刷新狀態(tài)剥扣,執(zhí)行刷新的回調(diào)
        [self executeRefreshingCallback];
        
    } else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) {
        
        // 從 刷新狀態(tài) 進(jìn)入 沒有更多數(shù)據(jù)或者正常狀態(tài) 時,如果有完成后的回調(diào)铝穷,則執(zhí)行之
        if (MJRefreshStateRefreshing == oldState) {
            if (self.endRefreshingCompletionBlock) {
                self.endRefreshingCompletionBlock();
            }
        }
    }
}
  • 最后是 setHidden: 方法钠怯,根據(jù)顯示隱藏的變化,調(diào)整 contentInset 曙聂、 stateself.frame.origin.y晦炊。
- (void)setHidden:(BOOL)hidden
{
    BOOL lastHidden = self.isHidden;
    
    [super setHidden:hidden];
    
    // 從顯示變成隱藏狀態(tài)
    if (!lastHidden && hidden) {
        
        self.state = MJRefreshStateIdle;
        self.scrollView.mj_insetB -= self.mj_h;
        
    } else if (lastHidden && !hidden) {
        
        // 從隱藏變成顯示狀態(tài)
        
        self.scrollView.mj_insetB += self.mj_h;
        // 設(shè)置位置
        self.mj_y = _scrollView.mj_contentH;
    }
}

MJRefreshAutoStateFooter

MJRefreshAutoStateFooter 繼承自 MJRefreshAutoFooterMJRefreshStateHeader 類似,從這里開始介入具體的 UI宁脊,既可以直接使用了断国,這里只介紹與 MJRefreshStateHeader 不同的地方。

  • 只有一個顯示刷新狀態(tài)的 stateLabel 和 保存不同狀態(tài)下文案的可變字典 stateTitles
/** 顯示刷新狀態(tài)的label */
__unsafe_unretained UILabel *_stateLabel;

/** 所有狀態(tài)對應(yīng)的文字 */
@property (strong, nonatomic) NSMutableDictionary *stateTitles;
  • 重寫父類 prepare 方法時榆苞,除了保存各種狀態(tài)的本地化文案外稳衬,還給 stateLabel 添加了點(diǎn)擊手勢。
- (void)prepare
{
    [super prepare];
    
    // 初始化間距
    self.labelLeftInset = MJRefreshLabelLeftInset;
    
    // 初始化文字
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterIdleText] forState:MJRefreshStateIdle];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterRefreshingText] forState:MJRefreshStateRefreshing];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterNoMoreDataText] forState:MJRefreshStateNoMoreData];
    
    // 監(jiān)聽label
    self.stateLabel.userInteractionEnabled = YES;
    [self.stateLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(stateLabelClick)]];
}

點(diǎn)擊 stateLabel 的時候坐漏,如果是正常未刷新的狀態(tài)薄疚,則開始刷新碧信。

- (void)stateLabelClick {
    if (self.state == MJRefreshStateIdle) {
        [self beginRefreshing];
    }
}
  • 重寫 setState: 方法的時候,增加刷新過程中對 stateLabel 顯示與隱藏的控制输涕。
- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    if (self.isRefreshingTitleHidden && state == MJRefreshStateRefreshing) {
        self.stateLabel.text = nil;
    } else {
        self.stateLabel.text = self.stateTitles[@(state)];
    }
}

MJRefreshAutoGifFooter

MJRefreshAutoGifFooter 繼承自 MJRefreshAutoStateFooter音婶,仔細(xì)查看其實(shí)現(xiàn)代碼,就會發(fā)現(xiàn)與 MJRefreshGifHeader 非常類似莱坎,都是在父類基礎(chǔ)上加了一個 gifView(UIImageView) 用于展示動畫圖片衣式,其他操作也基本類似,只是增加了沒有更多數(shù)據(jù)的狀態(tài) MJRefreshStateNoMoreData 以及 gifViewstateLabel 的顯隱控制檐什。

MJChiBaoZiFooter

MJChiBaoZiFooter 里邊也重寫了父類的 prepare 方法碴卧,調(diào)用了父類的 setImages: forState: 方法用于設(shè)置創(chuàng)新狀態(tài)時的動畫圖片,至于其方法實(shí)現(xiàn)乃正,見父類住册。

- (void)prepare
{
    [super prepare];
    
    // 設(shè)置正在刷新狀態(tài)的動畫圖片
    NSMutableArray *refreshingImages = [NSMutableArray array];

    for (NSUInteger i = 1; i<=3; i++) {
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", i]];
        [refreshingImages addObject:image];
    }

    [self setImages:refreshingImages forState:MJRefreshStateRefreshing];
}

5.小結(jié)

本文只是對 MJRefresh 源碼的一個簡單討論,很多細(xì)節(jié)還沒有講的很透瓮具,后期會及時更新這部分內(nèi)容荧飞。

6.參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市名党,隨后出現(xiàn)的幾起案子叹阔,更是在濱河造成了極大的恐慌,老刑警劉巖传睹,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耳幢,死亡現(xiàn)場離奇詭異,居然都是意外死亡欧啤,警方通過查閱死者的電腦和手機(jī)睛藻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來邢隧,“玉大人店印,你說我怎么就攤上這事「颍” “怎么了吱窝?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長迫靖。 經(jīng)常有香客問我,道長兴使,這世上最難降的妖魔是什么系宜? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮发魄,結(jié)果婚禮上盹牧,老公的妹妹穿的比我還像新娘俩垃。我一直安慰自己,他們只是感情好汰寓,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布口柳。 她就那樣靜靜地躺著,像睡著了一般有滑。 火紅的嫁衣襯著肌膚如雪跃闹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天毛好,我揣著相機(jī)與錄音望艺,去河邊找鬼。 笑死肌访,一個胖子當(dāng)著我的面吹牛找默,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吼驶,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼惩激,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蟹演?” 一聲冷哼從身側(cè)響起风钻,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎轨帜,沒想到半個月后魄咕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蚌父,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年哮兰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苟弛。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡喝滞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出膏秫,到底是詐尸還是另有隱情右遭,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布缤削,位于F島的核電站窘哈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏亭敢。R本人自食惡果不足惜滚婉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望帅刀。 院中可真熱鬧让腹,春花似錦远剩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至腹纳,卻和暖如春痢掠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背只估。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工志群, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛔钙。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓锌云,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吁脱。 傳聞我的和親對象是個殘疾皇子桑涎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件兼贡、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,120評論 4 61
  • MJRefresh 已經(jīng)很久沒有寫技術(shù)文章了攻冷,之前一段時間確實(shí)也是很忙,當(dāng)然這也是一個借口遍希,自己不思進(jìn)取的成分也有...
    雨雪傳奇閱讀 1,136評論 0 4
  • MJRefresh是李明杰老師的作品等曼,到現(xiàn)在已經(jīng)有9800多顆star了,是一個簡單實(shí)用凿蒜,功能強(qiáng)大的iOS下拉刷新...
    Style_mao閱讀 661評論 1 2
  • 2017年11月11日 星期六 晴 天越來越冷禁谦,出門時需要穿羽絨服戴帽子,在外面玩的時間也縮短了不少废封,我珍惜在...
    格子記閱讀 265評論 1 0
  • 《英倫對決》21 日在北京舉行「重返好萊塢」發(fā)布會州泊,「功夫之王」與「特工之王007」的巔峰對決,更是讓觀眾充滿了無...
    老金博客閱讀 258評論 0 1