(iOS)帶你寫一個類似MJRefresh的上下拉刷新控件

在iOS開發(fā)中, 上下拉加載的刷新動畫大多數(shù)的APP都會采用基本相似的樣式和動畫, 當然還是有很多優(yōu)秀的加載動畫, 不過這些動畫在國內(nèi)的APP中真的是很少看到使用(感覺比較新穎的東西都很少是國人自己首先實現(xiàn)的...), 在使用oc的時候, 相信很多的開發(fā)者都會選擇MJRefresh來集成上下拉刷新, 這個優(yōu)秀的加載框架很方便的實現(xiàn)了常見的加載需求, 同時, 因為其是使用系統(tǒng)的UIImageView來實現(xiàn)gif圖片的播放, 那么就可以很方便的直接利用設計給的gif動畫圖片來實現(xiàn)上下拉加載動畫. 因為現(xiàn)在的筆者開發(fā)使用swift的時間比較多了, 很多的東西還是比較希望使用swift實現(xiàn)的. 像刷新控件, 也希望使用個swift的, 于是自己動手也實現(xiàn)了一個, 在使用上盡量是接近了MJRefresh的, 不過, 如果你去比較的話, 和MJRefresh的效果,靈活度等相似, 但是代碼量相差很大, 筆者這個主要文件一個代碼量不到400行, 如果你要借鑒的話, 很是方便. 然后需要說明的是, 在oc中提倡使用繼承來實現(xiàn)很多東西, 不過swift提倡面向協(xié)議編程, 所以這次我也是用協(xié)議來實現(xiàn)的.Demo地址(這個是在草原旅行的路上坐車寫的, 草原的風光最近真的不錯)

使用效果:
refreshView.gif
refreshView1.gif
refreshView2.gif
refreshView3.gif
refreshView4.gif
實現(xiàn)原理:

其實仔細想想, 上下拉刷新的原理還是很簡單的 ------>>> 首先把刷新控件添加到scrollView的頭部或者底部, 然后監(jiān)控到scrollView的滾動進度(底部刷新控件還需要監(jiān)控scrollView的內(nèi)容的改變, 每次改變后再次將控件調(diào)整到scrollView的底部), 根據(jù)不同的進度來設置刷新控件的相應的文字和圖片動畫等...

實現(xiàn)過程:
  • 首先寫一個scrollView的分類, 在分類中給scrollView添加兩個屬性zj_refreshHeaderzj_refreshFooter用來存取header和footer刷新控件, 這里有兩種方法可以實現(xiàn)
    1, 使用運行時
private var ZJHeaderKey: UInt8 = 0
private var ZJFooterKey: UInt8 = 0

extension UIScrollView {
    
    private var zj_refreshHeader: RefreshView? {
        set {
            objc_setAssociatedObject(self, &ZJHeaderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
        
        get {
            return objc_getAssociatedObject(self, &ZJHeaderKey) as? RefreshView
        }
    }
    private var zj_refreshFooter: RefreshView? {
        set {
            objc_setAssociatedObject(self, &ZJFooterKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
        
        get {
            return objc_getAssociatedObject(self, &ZJFooterKey) as? RefreshView
        }
    }
}

2, 使用tag來存取

private var ZJHeaderTag = 1994
private var ZJFooterTag = 1995
extension UIScrollView {
    
    private var zj_refreshHeader: RefreshView? {
        set {
            if let header = newValue {
                header.tag = ZJHeaderTag
                addSubview(header)
            }
        }
        
        get {
            return viewWithTag(ZJHeaderTag) as? RefreshView
        }
    }
    private var zj_refreshFooter: RefreshView? {
        set {
            if let footer = newValue {
                footer.tag = ZJFooterTag
                addSubview(footer)
            }
        }
        
        get {
            return viewWithTag(ZJFooterTag) as? RefreshView
        }
    }
 }
  • 然后在分類中給出使用header和footer的方法, 注意看, 這里我使用了一點swift中強大的泛型和類型約束, <Animator where Animator: UIView, Animator: RefreshViewDelegate> 這個就是約束Animator必須是UIView并且遵守RefreshViewDelegate協(xié)議的類型
    public func zj_addRefreshHeader<Animator where Animator: UIView, Animator: RefreshViewDelegate>(headerAnimator: Animator, refreshHandler: RefreshHandler ) {
}
    public func zj_addRefreshFooter<Animator where Animator: UIView, Animator: RefreshViewDelegate>(footerAnimator: Animator, refreshHandler: RefreshHandler ) {
}
  • 接著提供開啟和結(jié)束刷新動畫的方法
    /// 開啟header刷新
    public func zj_startHeaderAnimation() {
        zj_refreshHeader?.canBegin = true
    }
    /// 結(jié)束header刷新
    public func zj_stopHeaderAnimation() {
        zj_refreshHeader?.canBegin = false
    }
    /// 開啟footer刷新
    public func zj_startFooterAnimation() {
        zj_refreshFooter?.canBegin = true
    }
    /// 結(jié)束footer刷新
    public func zj_stopFooterAnimation() {
        zj_refreshFooter?.canBegin = false
    }
  • 然后是RefreshView的實現(xiàn), 在筆者的實現(xiàn)中, RefreshView是添加到scrollView的頂部或者底部來作為真正的刷新控件的容器
  • 刷新控件的狀態(tài): 實際上控件有四種狀態(tài)
public enum RefreshViewState {
    /// 正在加載狀態(tài)
    case loading
    /// 正常狀態(tài)
    case normal
    /// 下拉狀態(tài)
    case pullToRefresh
    /// 松開手即進入刷新狀態(tài)
    case releaseToFresh
}

  • 1, 正常狀態(tài), 即未開始和已經(jīng)結(jié)束的狀態(tài).
  • 2, 拖拽狀態(tài), 這個時候拖拽的進度小于1, 如果繼續(xù)拖拽直到拖拽進度等于(>)1的時候, 進入下一種狀態(tài).
  • 3, 松手即進入刷新的狀態(tài), 這個時候松開手才能進入下一個狀態(tài), 如果不松開手, 向反方向拖拽, 則拖拽進度會減小, 如果進度<1, 則會進入上一個狀態(tài) ...
  • 4, 加載動畫狀態(tài), 這個時候進入加載狀態(tài), 知道收到 結(jié)束動畫的指定, 才結(jié)束刷新動畫進入正常狀態(tài)等待
下拉刷新
  • 首先將刷新控件添加到scrollView的頂部(在scrollView的分類方法中添加)
    ///
    public func zj_addRefreshHeader<Animator where Animator: UIView, Animator: RefreshViewDelegate>(headerAnimator: Animator, refreshHandler: RefreshHandler ) {
        if let header = zj_refreshHeader {
            header.removeFromSuperview()
        }
        ///
        let frame = CGRect(x: 0.0, y: -headerAnimator.bounds.height, width: bounds.width, height: headerAnimator.bounds.height)
        zj_refreshHeader = RefreshView(frame: frame, refreshType: .header, refreshAnimator: headerAnimator, refreshHandler: refreshHandler)
        addSubview(zj_refreshHeader!)
        
    }
  • 然后需要監(jiān)控scrollView的滾動(利用Cocoa強大的kvo機制)
    private func addObserverOf(scrollView: UIScrollView?) {
        scrollView?.addObserver(self, forKeyPath: ConstantValue.ScrollViewContentOffsetPath, options: .Initial, context: &ConstantValue.RefreshViewContext)
        
    }
Snip20160728_1.png
  • 在scrollView的滾動過程中, 根據(jù)滾動的偏移量來計算出拖拽的進度, 然后計算出對應的header的狀態(tài), 根據(jù)不同的狀態(tài)來相應的調(diào)整不同的UI或者動畫
        if scrollView.contentOffset.y > -scrollViewOriginalValue.contentInset.top {/**頭部視圖(隱藏)并且還沒到顯示的臨界點*/ return }
        
        // 已經(jīng)進入拖拽狀態(tài), 進行相關操作
        let progress = (-scrollViewOriginalValue.contentInset.top - scrollView.contentOffset.y) / self.bounds.height
                
        if scrollView.tracking {
            
            if progress >= 1.0 {
                refreshViewState = .releaseToFresh
                
            } else if progress <= 0.0 {
                refreshViewState = .normal
            } else {
                refreshViewState = .pullToRefresh
            }
            
        }
        else if refreshViewState == .releaseToFresh {// releaseToFreah 2 refresh
            canBegin = true// begin refresh
        }
        else {// release
            if progress <= 0.0 {
                refreshViewState = .normal
            }
            
        }
        
        var actualProgress = min(1.0, progress)
        actualProgress = max(0.0, actualProgress)
        refreshAnimator.refreshDidChangeProgress(self, progress: actualProgress, refreshViewType: refreshViewType)
  • 開始和停止動畫的處理, 這個時候需要調(diào)整scrollView的contentInset ----> 注意這里需要了解scrollView的三大屬性 contentInset, contentOffset, contentSize (這里就省略介紹了)

開始動畫的時候, 因為刷新控件是添加到scrollView的頭部或者底部的, 在滾動的時候因為scrollView的bounces的原因, 松開手之后, 刷新控件是會回到原來的位置的, 這個時候, 我們希望加載動畫的時候, 刷新控件停在我們的實現(xiàn)之內(nèi), 所以需要調(diào)整scrollView的contentInset(會自動調(diào)整contentOffset), 比如下拉刷新需要將contentInset的top加上刷新控件的高度, 上拉刷新的時候需要將contentInset的bottom加上刷新控件的高度

    private func startAnimation() {
        guard let validScrollView = scrollView else { return }
        validScrollView.bounces = false
        /// may update UI
        dispatch_async(dispatch_get_main_queue(), {[weak self] in
            guard let validSelf = self else { return }
            
            UIView.animateWithDuration(0.25, animations: {
                if validSelf.refreshViewType == .header {
                    validScrollView.contentInset.top = validSelf.scrollViewOriginalValue.contentInset.top + validSelf.bounds.height
                } else {
                    let offPartHeight = validScrollView.contentSize.height - validSelf.heightOfContentOnScreenOfScrollView(validScrollView)
                    /// contentSize改變的時候設置的self.y不同導致不同的結(jié)果
                    /// 所有內(nèi)容高度>屏幕上顯示的內(nèi)容高度
                    let notSureBottom = validSelf.scrollViewOriginalValue.contentInset.bottom + validSelf.bounds.height
                    validScrollView.contentInset.bottom = offPartHeight>=0 ? notSureBottom : notSureBottom - offPartHeight // 加上
                    
                }
                
                }, completion: { (_) in
                    /// 這個時候才正式刷新
                    validScrollView.bounces = true
                    validSelf.refreshViewState = .loading
                    validSelf.refreshHandler()
            })
            
            })
        
    }

停止動畫的時候, 需要將scrollView的contentInset復原為動畫開始之前, 以便于不影響頁面的其他布局

  • 對于上拉刷新而言, 只是要多一個監(jiān)控scrollView的contentSize, 在其改變的時候再次將刷新控件調(diào)整到scrollView的contentSize的底部

  • RefreshViewDelegate的定義

public protocol RefreshViewDelegate {
    /// 你應該為每一個header或者footer設置一個不同的key來保存時間, 否則將公用同一個key使用相同的時間
    var lastRefreshTimeKey: String? { get }
    /// 是否刷新完成后自動隱藏 默認為false
    var isAutomaticlyHidden: Bool { get }
    /// 上次刷新時間, 有默認賦值和返回
    var lastRefreshTime: NSDate? { get set }
    /// repuired 三個必須實現(xiàn)的代理方法
    
    /// 開始進入刷新(loading)狀態(tài), 這個時候應該開啟自定義的(動畫)刷新
    func refreshDidBegin(refreshView: RefreshView, refreshViewType: RefreshViewType)
    
    /// 刷新結(jié)束狀態(tài), 這個時候應該關閉自定義的(動畫)刷新
    func refreshDidEnd(refreshView: RefreshView, refreshViewType: RefreshViewType)
    
    /// 刷新狀態(tài)變?yōu)樾碌臓顟B(tài), 這個時候可以自定義設置各個狀態(tài)對應的屬性
    func refreshDidChangeState(refreshView: RefreshView, fromState: RefreshViewState, toState: RefreshViewState, refreshViewType: RefreshViewType)
    
    /// optional 兩個可選的實現(xiàn)方法
    /// 允許在控件添加到scrollView之前的準備
    func refreshViewDidPrepare(refreshView: RefreshView, refreshType: RefreshViewType)
    
    /// 拖拽的進度, 可用于自定義實現(xiàn)拖拽過程中的動畫
    func refreshDidChangeProgress(refreshView: RefreshView, progress: CGFloat, refreshViewType: RefreshViewType)
    
}
  • 最后是自己繼承 RefreshViewDelegate實現(xiàn)自定義的加載, 這里, 筆者提供了兩種使用實例(代碼布局和xib), 這兩種能夠完成MJRefresh提供的使用效果, 當然, 更靈活的自定義方式, 你可以自己隨意實現(xiàn), 具體的你可以參見demo中的示例, 這里只貼一點代碼出來
public class NormalAnimator: UIView {
    /// 設置imageView
    @IBOutlet private(set) weak var imageView: UIImageView!
    @IBOutlet private(set) weak var indicatorView: UIActivityIndicatorView!
    /// 設置state描述
    @IBOutlet private(set) weak var descriptionLabel: UILabel!
    /// 上次刷新時間label footer 默認為hidden, 可設置hidden=false開啟
    @IBOutlet private(set) weak var lastTimelabel: UILabel!
    
    public typealias SetDescriptionClosure = (refreshState: RefreshViewState, refreshType: RefreshViewType) -> String
    public typealias SetLastTimeClosure = (date: NSDate) -> String


    /// 是否刷新完成后自動隱藏 默認為false
    /// 這個屬性是協(xié)議定義的, 當寫在class里面可以供外界修改, 如果寫在extension里面只能是可讀的
    public var isAutomaticlyHidden: Bool = false
    
    private var setupDesctiptionClosure: SetDescriptionClosure?
    private var setupLastTimeClosure: SetLastTimeClosure?
    /// 耗時
    private lazy var formatter: NSDateFormatter = {
       let formatter = NSDateFormatter()
        formatter.dateStyle = .ShortStyle
        return formatter
    }()
    /// 耗時
    private lazy var calendar: NSCalendar = NSCalendar.currentCalendar()

    public class func normalAnimator() -> NormalAnimator {
        return NSBundle.mainBundle().loadNibNamed(String(NormalAnimator), owner: nil, options: nil).first as! NormalAnimator
    }
    
    
    public func setupDescriptionForState(closure: SetDescriptionClosure) {
        setupDesctiptionClosure = closure
    }
    
    public func setupLastFreshTime(closure: SetLastTimeClosure) {
        setupLastTimeClosure = closure
    }

    override public func awakeFromNib() {
        super.awakeFromNib()
        indicatorView.hidden = true
        indicatorView.hidesWhenStopped = true
    }
    
//    public override func layoutSubviews() {
//        super.layoutSubviews()
//        print("layout--------------------------------------------")
//    }
}

extension NormalAnimator: RefreshViewDelegate {
    
    public func refreshViewDidPrepare(refreshView: RefreshView, refreshType: RefreshViewType) {
        if refreshType == .header {
        } else {
            lastTimelabel.hidden = true
            rotateArrowToUpAnimated(false)
        }
        setupLastTime()

    }
    
    public func refreshDidBegin(refreshView: RefreshView, refreshViewType: RefreshViewType) {
        indicatorView.hidden = false
        indicatorView.startAnimating()
    }
    public func refreshDidEnd(refreshView: RefreshView, refreshViewType: RefreshViewType) {
        indicatorView.stopAnimating()
    }
    public func refreshDidChangeProgress(refreshView: RefreshView, progress: CGFloat, refreshViewType: RefreshViewType) {
        //        print(progress)
        
    }
    
    public func refreshDidChangeState(refreshView: RefreshView, fromState: RefreshViewState, toState: RefreshViewState, refreshViewType: RefreshViewType) {
        print(toState)
        
        setupDescriptionForState(toState, type: refreshViewType)
        switch toState {
        case .loading:
            imageView.hidden = true
        case .normal:

            setupLastTime()
            imageView.hidden = false
            ///恢復
            if refreshViewType == .header {
                rotateArrowToDownAnimated(false)
                
            } else {
                rotateArrowToUpAnimated(false)
            }
            
        case .pullToRefresh:
            if refreshViewType == .header {

                if fromState == .releaseToFresh {
                    rotateArrowToDownAnimated(true)
                }
                
            } else {

                if fromState == .releaseToFresh {
                    rotateArrowToUpAnimated(true)
                }
            }
            imageView.hidden = false
            
        case .releaseToFresh:

            imageView.hidden = false
            if refreshViewType == .header {
                rotateArrowToUpAnimated(true)
            } else {
                rotateArrowToDownAnimated(true)
            }
        }
    }
    
    private func setupDescriptionForState(state: RefreshViewState, type: RefreshViewType) {
        if descriptionLabel.hidden {
            descriptionLabel.text = ""
        } else {
            if let closure = setupDesctiptionClosure {
                descriptionLabel.text = closure(refreshState: state, refreshType: type)
            } else {
                switch state {
                case .normal:
                    descriptionLabel.text = "正常狀態(tài)"
                case .loading:
                    descriptionLabel.text = "加載數(shù)據(jù)中..."
                case .pullToRefresh:
                    if type == .header {
                        descriptionLabel.text = "繼續(xù)下拉刷新"
                    } else {
                        descriptionLabel.text = "繼續(xù)上拉刷新"
                    }
                case .releaseToFresh:
                    descriptionLabel.text = "松開手刷新"
                    
                }
            }
        }
    }
 }
  • 使用方法
    NormalAnimator
        let normal = NormalAnimator.normalAnimator()
                /// 指定存儲刷新時間的key, 如果不指定或設置為nil, 那么將會和其他未指定的使用相同的key(記錄的時間相同, MJRefresh是所有的控件使用相同的時間的)
        normal.lastRefreshTimeKey = "DemoKey1"
        
        /// 隱藏時間顯示
//        normal.lastTimelabel.hidden = true

        
        /// 自定義提示文字
//        normal.setupDescriptionForState { (refreshState,refreshType) -> String in
//            switch refreshState {
//            case .loading:
//                return "努力加載中"
//            case .normal:
//                return "休息中"
//            case .pullToRefresh:
//                if refreshType == .header {
//                    return "繼續(xù)下下下下"
//
//                } else {
//                    return "繼續(xù)上上上上"
//                }
//            case .releaseToFresh:
//                return "放開我"
//            };
//        }
        
        /// 自定義時間顯示
//        normal.setupLastFreshTime { (date) -> String in
//            return ...
//        }

        tableView.zj_addRefreshHeader(normal, refreshHandler: {[weak self] in
            /// 多線程中不要使用 [unowned self]
            /// 注意這里的gcd是為了模擬網(wǎng)絡加載的過程, 在實際的使用中, 不需要這段gcd代碼, 直接在這里進行網(wǎng)絡請求, 在請求完畢后, 調(diào)用分類方法, 結(jié)束刷新
            dispatch_async(dispatch_get_global_queue(0, 0), { 
                for i in 0...50000 {
                    if i <= 10 {
                        self?.data.append(i)

                    }
                    /// 延時
                    print("加載數(shù)據(jù)中")
                }
                dispatch_async(dispatch_get_main_queue(), {
                    self?.tableView.reloadData()
                    /// 刷新完畢, 停止動畫
                    self?.tableView.zj_stopHeaderAnimation()

                })
            })
            
        })

  • GifAnimator的使用
/// 設置高度
let gifAnimatorHeader = GifAnimator.gifAnimatorWithHeight(100.0)
        gifAnimatorHeader.lastRefreshTimeKey = "exampleHeader4"
        
        /// 為不同的state設置不同的圖片
        /// 閉包需要返回一個元組: 圖片數(shù)組和gif動畫每一幀的執(zhí)行時間
        /// 一般需要設置loading狀態(tài)的圖片(必須), 作為加載的gif
        /// 和pullToRefresh狀態(tài)的圖片數(shù)組(可選擇設置), 作為拖拽時的加載動畫
        gifAnimatorHeader.setupImagesForRefreshstate { (refreshState) -> (images: [UIImage], duration: Double)? in
            if refreshState == .loading {
                var images = [UIImage]()
                for index in 1...47 {
                    let image = UIImage(named: "loading\\(index)")!
                    images.append(image)
                }
                return (images, 1.0)
            }
            else if  refreshState == .pullToRefresh {
                var images = [UIImage]()
                for index in 1...47 {
                    let image = UIImage(named: "loading\\(index)")!
                    images.append(image)
                }
                return (images, 0.25)
            }
            return nil
        }

        tableView.zj_addRefreshHeader(gif, refreshHandler: {[weak self] in
            /// 多線程中不要使用 [unowned self]
            /// 注意這里的gcd是為了模擬網(wǎng)絡加載的過程, 在實際的使用中, 不需要這段gcd代碼, 直接在這里進行網(wǎng)絡請求, 在請求完畢后, 調(diào)用分類方法, 結(jié)束刷新
            dispatch_async(dispatch_get_global_queue(0, 0), { 
                for i in 0...50000 {
                    if i <= 10 {
                        self?.data.append(i)

                    }
                    /// 延時
                    print("加載數(shù)據(jù)中")
                }
                dispatch_async(dispatch_get_main_queue(), {
                    self?.tableView.reloadData()
                    /// 刷新完畢, 停止動畫
                    self?.tableView.zj_stopHeaderAnimation()

                })
            })
            
        })
  • 或者你可以將這些自定義的設置移到另外新建的class中, 例如
class TestNormal {
    class func normal() -> NormalAnimator {
        let normal = NormalAnimator.normalAnimator()
                /// 隱藏時間顯示
//        normal.lastTimelabel.hidden = true
        /// 指定存儲刷新時間的key, 如果不指定或設置為nil, 那么將會和其他未指定的使用相同的key(記錄的時間相同, MJRefresh是所有的控件使用相同的時間的)
        normal.lastRefreshTimeKey = "DemoKey1"
        normal.setupDescriptionForState({ (refreshState ,refreshType) -> String in
            switch refreshState {
            case .loading:
                return "努力加載中"
            case .normal:
                return "休息中"
            case .pullToRefresh:
                if refreshType == .header {
                    return "繼續(xù)下下下下"
                    
                } else {
                    return "繼續(xù)上上上上"
                }
            case .releaseToFresh:
                return "放開我"
            }
        })
        return normal
    }
}


/// 使用方法
        let footer = TestNormal.normal()
        tableView.zj_addRefreshFooter(footer) {[weak self] in

            dispatch_async(dispatch_get_global_queue(0, 0), {
                for i in 0...50000 {
                    if i <= 10 {
                        self?.data.append(i)

                    }
                    /// 延時
                    print("加載數(shù)據(jù)中")
                }
                dispatch_async(dispatch_get_main_queue(), {
                    self?.tableView.reloadData()
                    self?.tableView.zj_stopFooterAnimation()

                })
            })
        }

總的來說, 簡單寫一個刷新控件還是很簡單的, 但是在實現(xiàn)的過程中有很多的細節(jié)需要調(diào)整, 比如刷新的時候要處理sectionHeader的懸停問題... (這里直接借鑒了MJRefresh中的處理了), Demo地址

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末骆莹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子铃岔,更是在濱河造成了極大的恐慌汪疮,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毁习,死亡現(xiàn)場離奇詭異智嚷,居然都是意外死亡,警方通過查閱死者的電腦和手機纺且,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門盏道,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人载碌,你說我怎么就攤上這事猜嘱⌒品悖” “怎么了?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵朗伶,是天一觀的道長弦撩。 經(jīng)常有香客問我,道長论皆,這世上最難降的妖魔是什么益楼? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮点晴,結(jié)果婚禮上感凤,老公的妹妹穿的比我還像新娘。我一直安慰自己粒督,他們只是感情好陪竿,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著屠橄,像睡著了一般族跛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仇矾,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天庸蔼,我揣著相機與錄音解总,去河邊找鬼贮匕。 笑死,一個胖子當著我的面吹牛花枫,可吹牛的內(nèi)容都是我干的刻盐。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼劳翰,長吁一口氣:“原來是場噩夢啊……” “哼敦锌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起佳簸,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤乙墙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后生均,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體听想,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡怎炊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年勒虾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片邻悬。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡佩脊,死狀恐怖蛙粘,靈堂內(nèi)的尸體忽然破棺而出垫卤,到底是詐尸還是另有隱情,我是刑警寧澤出牧,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布穴肘,位于F島的核電站,受9級特大地震影響舔痕,放射性物質(zhì)發(fā)生泄漏梢褐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一赵讯、第九天 我趴在偏房一處隱蔽的房頂上張望盈咳。 院中可真熱鬧,春花似錦边翼、人聲如沸鱼响。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丈积。三九已至,卻和暖如春债鸡,著一層夾襖步出監(jiān)牢的瞬間江滨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工厌均, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留唬滑,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓棺弊,卻偏偏與公主長得像晶密,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子模她,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,796評論 25 707
  • 發(fā)現(xiàn) 關注 消息 iOS 第三方庫稻艰、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,066評論 4 62
  • 在夢里侈净,我夢見一個甜美的聲音在我的耳邊響起:“主人尊勿,您好!”我嚇得直冒冷汗畜侦。我揉了揉眼睛元扔,在我面前出現(xiàn)了一...
    麻丹陽閱讀 278評論 0 1
  • 天氣忽冷忽熱,溫差十分大夏伊,我這里是南京摇展,不知道你那呢是否也這樣? 我是一個慢熱的人,也非常沒有耐心咏连,人也比較直白盯孙。...
    夢幻_少女閱讀 289評論 0 0
  • 前幾天振惰,在愛豆上認識了一個姐姐,因為粉的是同一個偶像垄懂,所以聊的時候也更投機骑晶,經(jīng)常聊到很晚,各種話題隨意切換...
    木子心韻閱讀 369評論 0 0