(iOS)带你写一个类似MJRefresh的上下拉刷新控件

在iOS开发中, 上下拉加载的刷新动画大多数的APP都会采用基本相似的样式和动画, 当然还是有很多优秀的加载动画, 不过这些动画在国内的APP中真的是很少看到使用(感觉比较新颖的东西都很少是国人自己首先实现的...), 在使用oc的时候, 相信很多的开发者都会选择MJRefresh来集成上下拉刷新, 这个优秀的加载框架很方便的实现了常见的加载需求, 同时, 因为其是使用系统的UIImageView来实现gif图片的播放, 那么就可以很方便的直接利用设计给的gif动画图片来实现上下拉加载动画. 因为现在的笔者开发使用swift的时间比较多了, 很多的东西还是比较希望使用swift实现的. 像刷新控件, 也希望使用个swift的, 于是自己动手也实现了一个, 在使用上尽量是接近了MJRefresh的, 不过, 如果你去比较的话, 和MJRefresh的效果,灵活度等相似, 但是代码量相差很大, 笔者这个主要文件一个代码量不到400行, 如果你要借鉴的话, 很是方便. 然后需要说明的是, 在oc中提倡使用继承来实现很多东西, 不过swift提倡面向协议编程, 所以这次我也是用协议来实现的.Demo地址(这个是在草原旅行的路上坐车写的, 草原的风光最近真的不错)

使用效果:
refreshView.gif
refreshView1.gif
refreshView2.gif
refreshView3.gif
refreshView4.gif
实现原理:

其实仔细想想, 上下拉刷新的原理还是很简单的 ------>>> 首先把刷新控件添加到scrollView的头部或者底部, 然后监控到scrollView的滚动进度(底部刷新控件还需要监控scrollView的内容的改变, 每次改变后再次将控件调整到scrollView的底部), 根据不同的进度来设置刷新控件的相应的文字和图片动画等...

实现过程:
  • 首先写一个scrollView的分类, 在分类中给scrollView添加两个属性zj_refreshHeaderzj_refreshFooter用来存取header和footer刷新控件, 这里有两种方法可以实现
    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协议的类型
    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 ) {
}
  • 接着提供开启和结束刷新动画的方法
    /// 开启header刷新
    public func zj_startHeaderAnimation() {
        zj_refreshHeader?.canBegin = true
    }
    /// 结束header刷新
    public func zj_stopHeaderAnimation() {
        zj_refreshHeader?.canBegin = false
    }
    /// 开启footer刷新
    public func zj_startFooterAnimation() {
        zj_refreshFooter?.canBegin = true
    }
    /// 结束footer刷新
    public func zj_stopFooterAnimation() {
        zj_refreshFooter?.canBegin = false
    }
  • 然后是RefreshView的实现, 在笔者的实现中, RefreshView是添加到scrollView的顶部或者底部来作为真正的刷新控件的容器
  • 刷新控件的状态: 实际上控件有四种状态
public enum RefreshViewState {
    /// 正在加载状态
    case loading
    /// 正常状态
    case normal
    /// 下拉状态
    case pullToRefresh
    /// 松开手即进入刷新状态
    case releaseToFresh
}

  • 1, 正常状态, 即未开始和已经结束的状态.
  • 2, 拖拽状态, 这个时候拖拽的进度小于1, 如果继续拖拽直到拖拽进度等于(>)1的时候, 进入下一种状态.
  • 3, 松手即进入刷新的状态, 这个时候松开手才能进入下一个状态, 如果不松开手, 向反方向拖拽, 则拖拽进度会减小, 如果进度<1, 则会进入上一个状态 ...
  • 4, 加载动画状态, 这个时候进入加载状态, 知道收到 结束动画的指定, 才结束刷新动画进入正常状态等待
下拉刷新
  • 首先将刷新控件添加到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!)
        
    }
  • 然后需要监控scrollView的滚动(利用Cocoa强大的kvo机制)
    private func addObserverOf(scrollView: UIScrollView?) {
        scrollView?.addObserver(self, forKeyPath: ConstantValue.ScrollViewContentOffsetPath, options: .Initial, context: &ConstantValue.RefreshViewContext)
        
    }
Snip20160728_1.png
  • 在scrollView的滚动过程中, 根据滚动的偏移量来计算出拖拽的进度, 然后计算出对应的header的状态, 根据不同的状态来相应的调整不同的UI或者动画
        if scrollView.contentOffset.y > -scrollViewOriginalValue.contentInset.top {/**头部视图(隐藏)并且还没到显示的临界点*/ return }
        
        // 已经进入拖拽状态, 进行相关操作
        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)
  • 开始和停止动画的处理, 这个时候需要调整scrollView的contentInset ----> 注意这里需要了解scrollView的三大属性 contentInset, contentOffset, contentSize (这里就省略介绍了)

开始动画的时候, 因为刷新控件是添加到scrollView的头部或者底部的, 在滚动的时候因为scrollView的bounces的原因, 松开手之后, 刷新控件是会回到原来的位置的, 这个时候, 我们希望加载动画的时候, 刷新控件停在我们的实现之内, 所以需要调整scrollView的contentInset(会自动调整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不同导致不同的结果
                    /// 所有内容高度>屏幕上显示的内容高度
                    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复原为动画开始之前, 以便于不影响页面的其他布局

  • 对于上拉刷新而言, 只是要多一个监控scrollView的contentSize, 在其改变的时候再次将刷新控件调整到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 三个必须实现的代理方法
    
    /// 开始进入刷新(loading)状态, 这个时候应该开启自定义的(动画)刷新
    func refreshDidBegin(refreshView: RefreshView, refreshViewType: RefreshViewType)
    
    /// 刷新结束状态, 这个时候应该关闭自定义的(动画)刷新
    func refreshDidEnd(refreshView: RefreshView, refreshViewType: RefreshViewType)
    
    /// 刷新状态变为新的状态, 这个时候可以自定义设置各个状态对应的属性
    func refreshDidChangeState(refreshView: RefreshView, fromState: RefreshViewState, toState: RefreshViewState, refreshViewType: RefreshViewType)
    
    /// optional 两个可选的实现方法
    /// 允许在控件添加到scrollView之前的准备
    func refreshViewDidPrepare(refreshView: RefreshView, refreshType: RefreshViewType)
    
    /// 拖拽的进度, 可用于自定义实现拖拽过程中的动画
    func refreshDidChangeProgress(refreshView: RefreshView, progress: CGFloat, refreshViewType: RefreshViewType)
    
}
  • 最后是自己继承 RefreshViewDelegate实现自定义的加载, 这里, 笔者提供了两种使用实例(代码布局和xib), 这两种能够完成MJRefresh提供的使用效果, 当然, 更灵活的自定义方式, 你可以自己随意实现, 具体的你可以参见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
    /// 这个属性是协议定义的, 当写在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 = "正常状态"
                case .loading:
                    descriptionLabel.text = "加载数据中..."
                case .pullToRefresh:
                    if type == .header {
                        descriptionLabel.text = "继续下拉刷新"
                    } else {
                        descriptionLabel.text = "继续上拉刷新"
                    }
                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 "继续下下下下"
//
//                } else {
//                    return "继续上上上上"
//                }
//            case .releaseToFresh:
//                return "放开我"
//            };
//        }
        
        /// 自定义时间显示
//        normal.setupLastFreshTime { (date) -> String in
//            return ...
//        }

        tableView.zj_addRefreshHeader(normal, refreshHandler: {[weak self] in
            /// 多线程中不要使用 [unowned self]
            /// 注意这里的gcd是为了模拟网络加载的过程, 在实际的使用中, 不需要这段gcd代码, 直接在这里进行网络请求, 在请求完毕后, 调用分类方法, 结束刷新
            dispatch_async(dispatch_get_global_queue(0, 0), { 
                for i in 0...50000 {
                    if i <= 10 {
                        self?.data.append(i)

                    }
                    /// 延时
                    print("加载数据中")
                }
                dispatch_async(dispatch_get_main_queue(), {
                    self?.tableView.reloadData()
                    /// 刷新完毕, 停止动画
                    self?.tableView.zj_stopHeaderAnimation()

                })
            })
            
        })

  • GifAnimator的使用
/// 设置高度
let gifAnimatorHeader = GifAnimator.gifAnimatorWithHeight(100.0)
        gifAnimatorHeader.lastRefreshTimeKey = "exampleHeader4"
        
        /// 为不同的state设置不同的图片
        /// 闭包需要返回一个元组: 图片数组和gif动画每一帧的执行时间
        /// 一般需要设置loading状态的图片(必须), 作为加载的gif
        /// 和pullToRefresh状态的图片数组(可选择设置), 作为拖拽时的加载动画
        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是为了模拟网络加载的过程, 在实际的使用中, 不需要这段gcd代码, 直接在这里进行网络请求, 在请求完毕后, 调用分类方法, 结束刷新
            dispatch_async(dispatch_get_global_queue(0, 0), { 
                for i in 0...50000 {
                    if i <= 10 {
                        self?.data.append(i)

                    }
                    /// 延时
                    print("加载数据中")
                }
                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 "继续下下下下"
                    
                } else {
                    return "继续上上上上"
                }
            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("加载数据中")
                }
                dispatch_async(dispatch_get_main_queue(), {
                    self?.tableView.reloadData()
                    self?.tableView.zj_stopFooterAnimation()

                })
            })
        }

总的来说, 简单写一个刷新控件还是很简单的, 但是在实现的过程中有很多的细节需要调整, 比如刷新的时候要处理sectionHeader的悬停问题... (这里直接借鉴了MJRefresh中的处理了), Demo地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 在梦里,我梦见一个甜美的声音在我的耳边响起:“主人,您好!”我吓得直冒冷汗。我揉了揉眼睛,在我面前出现了一...
    麻丹阳阅读 273评论 0 1
  • 天气忽冷忽热,温差十分大,我这里是南京,不知道你那呢是否也这样? 我是一个慢热的人,也非常没有耐心,人也比较直白。...
    梦幻_少女阅读 287评论 0 0
  • 前几天,在爱豆上认识了一个姐姐,因为粉的是同一个偶像,所以聊的时候也更投机,经常聊到很晚,各种话题随意切换...
    木子心韵阅读 366评论 0 0