10-下拉刷新 & 上拉加载

下拉刷新 & 上拉加载

课程目标

  • KVO的使用
  • UIScrollView使用

接口准备

  • 新浪微博下拉刷新与上拉加载需要有两个重要的参数
参数名 说明
since_id 返回ID比since_id大的微博(即比since_id时间晚的微博)
max_id 返回ID小于或等于max_id的微博

以上可知:

  1. 如果传入since_id,服务器会返回ID比since_id大的微博(即比since_id时间晚的微博),也就是最新的微博,所以这个参数可以用于下拉刷新.
  2. 传入max_id,服务器会返回ID小于或等于max_id的微博,id 越小时间越早,所以可以用作上拉加载。(特别注意:会返回ID小于或等于)
  • 更改微博数据加载的方法-> HMStatusListViewModelloadStatuses 方法添加参数
/// 加载微博数据的方法
func loadData(isPullUp isPullUp: Bool, completion: (isSuccessed: Bool)->()) {
    // 定义 url 与参数
    let urlString = "https://api.weibo.com/2/statuses/friends_timeline.json"

    let since_id = isPullUp ? 0 : (statuses?.first?.status?.id ?? 0)
    let max_id = isPullUp ? (statuses?.last?.status?.id ?? 0) : 0

    let params = [
        "access_token": HMUserAccountViewModel.sharedUserAccount.accessToken!,
        "since_id": since_id,
        "max_id": max_id
    ]
    ...
}

上拉加载

实现效果与思路

  • 当用户滚动到底部的时候,自动去加载更多数据
  • 可以在加载当前页面最后一个 cell 的时候去执行加载更多数据的方法
  • 给 tableView 添加一个 footerView(上拉显示控件),用作拉到最底部的友好显示

代码实现

  • 懒加载底部上拉显示控件
// 上拉加载控件
private lazy var pullupView: UIActivityIndicatorView = {
    let indicator = UIActivityIndicatorView()
    indicator.activityIndicatorViewStyle = .WhiteLarge
    indicator.color = UIColor.darkGrayColor()
    return indicator;
}()
  • 设置成 tableView 的footerView
// 设置上拉加载控件
tableView.tableFooterView = pullupView

运行测试,看不见任何东西。看不见控件的原因就是 UIActivityIndicatorView 控件默认不执行动画是看不见的

  • 开启执行动画
pullupView.startAnimating()

运行测试,已经可以看到,但是位置没有留出来,执行 sizeToFit 方法

  • 在将要加载最后一个 cell 的时候去加载更多数据
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    if indexPath.row == statusListViewModel.statuses!.count - 1 && pullupView.isAnimating() == false {
        // 加载更多
        pullupView.startAnimating()
        loadData()
    }
}

注意:需要在判断里面多加一个条件,就是底部控件没有执行动画的时候才去加载更多数据,防止重复加载

  • 更改 loadData() 方法逻辑
// MARK: - 加载数据
private func loadData(){
    statusListViewModel.loadData(isPullUp: pullupView.isAnimating()) { (isSuccessed, count) -> () in
        if isSuccessed {
            self.tableView.reloadData()
        }
    }
}

  • 更改 HMStatusListViewModelloadData 方法 -> 上拉加载与下拉刷新数据添加的位置不一样
if let array = res["statuses"] as? [[String: AnyObject]] {
    // 如果是字典
    // 判断数组是否为 nil
    if self.statuses == nil {
        self.statuses = [HMStatusViewModel]()
    }

    // 定义一个临时数组
    var tempStatuses = [HMStatusViewModel]()

    // 字典转模型
    for dic in array {
        tempStatuses.append(HMStatusViewModel(status: HMStatus(dictionary: dic)))
    }

    if isPullUp {
        // 代表是上拉加载,拼装数据到集合后面
        self.statuses! += tempArray
    }else{
        // 代表是下拉刷新,拼装数据到前面
        self.statuses! = tempArray + self.statuses!
    }
}

...

运行测试:发现只加载一次数据,下次再拖动就不去加载了,原因是加载完毕之后 pullupView 也一直在执行动画,下次就进入不到加载更多的判断逻辑里面去了,所以加载完毕需要将 pullupView 结束动画

  • 结束动画
/// 结束刷新
private func endRefresh(){
    pullupView.stopAnimating()
}

/// 在数据请求成功,或者数据请求失败之后调用此方法
statusListViewModel.loadData(isPullUp: pullupView.isAnimating()) { (isSuccessed, count) -> () in
    if isSuccessed {
        self.tableView.reloadData()
    }
    self.endRefresh()
}

运行测试

下拉刷新

实现效果

  • 拖动 tableView,顶部显示 下拉刷新,箭头朝下
  • 拖动到一定程度的时候,顶部显示 释放更新,箭头朝上
  • 松手:
    • 到达一定程度松手,顶部显示 加载中…,隐藏箭头,显示菊花转
    • 未到达一定程度,直接回到最初状态

顶部的整个 View 会随着 tableView 的拖动而移动

示意图

下拉刷新示意.png.jpeg

实现思路

  • 给 tableView 添加一个自定义刷新控件(HMRefreshControl)
  • 这个刷新控件的 y 值是 自己的高度,以让其放在 tableView 的顶部以及可以跟随 tableView 滑动
  • 在刷新控件内部监听 tableView 的滑动
  • 当滑动到某种程度去改变子控件要显示的逻辑
  • 当用户松开手要刷新的时候,可以调整 tableView 的 contentInsettop 值以让刷新控件显示出来
  • 在刷新的时候调用外部提供的方法执行刷新的逻辑

实现代码

  • 自定义 HMRefreshControl
class HMRefreshControl: UIControl {

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupUI() {
        // 先设置默认宽度与高度
        self.frame.size.width = SCREENW
        self.frame.size.height = 44

        backgroundColor = RandomColor()
    }
}
  • 定义懒加载控件 & 添加到首页的 tableView 中去
// 下拉刷新控件
private lazy var hmRefreshControl: HMRefreshControl = HMRefreshControl()

...

// 添加头部视图
tableView.addSubview(hmRefreshControl)

运行测试

  • 抽取控件高度常量
private let HMRefreshControlH: CGFloat = 44
  • 更改 Y 值
private func setupUI() {
    // 先设置默认宽度与高度
    self.frame.size.width = SCREENW
    self.frame.size.height = HMRefreshControlH
    self.frame.origin.y = -HMRefreshControlH

    backgroundColor = RandomColor()
}
  • 定义 scrollView 属性
// 定义 scrollView,用于记录当前控件添加到哪一个 View 上的
var scrollView: UIScrollView?
  • HMRefreshView 中监听其添加到 tableView 的滚动
/// 当前 view 的父视图即将改变的时候会调用,可以在这个方法里面拿到父控件
override func willMoveToSuperview(newSuperview: UIView?) {
    super.willMoveToSuperview(newSuperview)
    // 如果父控件不为空,并且父控件是UIScrollView
    if let scrollView = newSuperview where scrollView.isKindOfClass(NSClassFromString("UIScrollView")!) {
        scrollView.addObserver(self, forKeyPath: "contentOffset", options: NSKeyValueObservingOptions.New, context: nil)
        // 记录当前 scrollView,以便在 `deinit` 方法里面移除监听
        self.scrollView = scrollView as? UIScrollView
    }
}

/// 当值改变之后回调的方法
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    printLog(change)
}

deinit{
    // 移除监听
    if let scrollView = self.scrollView {
        scrollView.removeObserver(self, forKeyPath: "contentOffset")
    }
}

注意:监听之后需要做两件事情:a.在合适的时候移除监听;b.一定要实现值改变之后的回调方法

  • 根据滚动,计算出 refreshView 完全展示出现的临界点值
// 取到顶部增加的可滑动的距离
let contentInsetTop = self.scrollView!.contentInset.top
// 取到当前 scrollView 的偏移 Y
let contentOffsetY = self.scrollView!.contentOffset.y

// printLog("contentInsetTop=\(contentInsetTop);contentOffsetY=\(contentOffsetY)")

// 通过分析可知:contentOffsetY 如果小于 (-contentInsetTop - 当前 View 高度),就代表当前 View 完全显示出来
// 而 (-contentInsetTop - 当前 View 高度) 这个值就代表临界值

// 临界值
let criticalValue = -contentInsetTop - self.height

// 在用户拖动的时候去判断临界值
if scrollView!.dragging {
    if contentOffsetY < criticalValue {
        printLog("完全显示出来啦")
    }else {
        printLog("没有完全显示出来/没有显示出来")
    }
}

  • 根据以上状态添加 state 枚举
enum HMRefreshControlStatus: Int {
    case Normal = 0 // 默认状态
    case Pulling = 1 // 松手就可以刷新的状态
    case Refreshing = 2 // 正在刷新的状态
}

  • 定义 state 属性

  • 根据滑动的位置设置当前 View 的状态
// 在用户拖动的时候去判断临界值
if scrollView!.dragging {
    if contentOffsetY < criticalValue {
        printLog("完全显示出来啦")
        self.status = .Pulling
    }else {
        printLog("没有完全显示出来/没有显示出来")
        self.status = .Normal
    }
}
  • 添加子控件 (箭头,提示文字label)
// MARK: - 懒加载控件
// 箭头图标
private lazy var arrowIcon: UIImageView = UIImageView(image: UIImage(named: "tableview_pull_refresh"))
// 显示文字的label
private lazy var messageLabel: UILabel = {
    let label = UILabel()
    label.text = "下拉刷新"
    label.textColor = UIColor.grayColor()
    label.font = UIFont.systemFontOfSize(12)
    return label
}()

...

// 添加子控件

private func setupUI(){
    ...

    // 添加控件
    addSubview(arrowIcon)
    addSubview(messageLabel)

    // 添加约束
    arrowIcon.snp_makeConstraints { (make) -> Void in
        make.centerX.equalTo(self.snp_centerX).offset(-30)
        make.centerY.equalTo(self.snp_centerY)
    }
    messageLabel.snp_makeConstraints { (make) -> Void in
        make.leading.equalTo(arrowIcon.snp_trailing)
        make.centerY.equalTo(arrowIcon.snp_centerY)
    }
}
  • 设置不同状态下执行不同的动画
// 定义当前控件的刷新状态
var status: HMRefreshControlStatus = .Normal {
    didSet{
        switch status {
        case .Pulling:
            UIView.animateWithDuration(0.25, animations: { () -> Void in
                self.arrowIcon.transform = CGAffineTransformMakeRotation(CGFloat(M_PI))
            })
            messageLabel.text = "释放更新"
        case .Normal:
            UIView.animateWithDuration(0.25, animations: { () -> Void in
                self.arrowIcon.transform = CGAffineTransformIdentity
            })
            messageLabel.text = "下拉刷新"
        default:
            break
        }
    }
}

运行测试

  • 监听用户松手进入刷新状态,满足两个条件
    • 用户松手
    • 当前状态是 Pulling 状态 (可以进入刷新的状态)
// 在用户拖动的时候去判断临界值
    if scrollView!.dragging {
        if contentOffsetY < criticalValue {
            printLog("完全显示出来啦")
            self.state = .Pulling
        }else {
            printLog("没有完全显示出来/没有显示出来")
            self.state = .Normal
        }
    }else{
        // 判断如果用户已经松手,并且当前状态是.Pulling,那么进入到 .Refreshing 状态
        if self.status == .Pulling {
            print("进入刷新状态")
            self.status = .Refreshing
        }
    }
  • 显示刷新状态的效果
// 1.懒加载控件
// 菊花转
private lazy var indecator: UIActivityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.Gray)

// 2.添加控件 & 设置约束

addSubview(indecator)
indecator.snp_makeConstraints { (make) -> Void in
    make.center.equalTo(arrowIcon.snp_center)
}


// 3.在 state 为 Refreshing 状态时显示效果
case .Refreshing:   // 显示刷新的效果
    // 添加顶部可以多滑动的距离
    UIView.animateWithDuration(0.25, animations: { () -> Void in
        var contentInset = self.scrollView!.contentInset
        contentInset.top += self.frame.height
        self.scrollView?.contentInset = contentInset
    })

    // 隐藏箭头
    arrowIcon.hidden = true
    // 开始菊花转
    indecator.startAnimating()
    // 显示 `加载中…`
    messageLabel.text = "加载中…"
  • 默认状态下显示箭头,隐藏菊花转
case .Normal:       // 置为默认的状态的效果
    UIView.animateWithDuration(0.25, animations: { () -> Void in
        self.arrowIcon.transform = CGAffineTransformIdentity
    })
    messageLabel.text = "下拉刷新"
    arrowIcon.hidden = false
    indecator.stopAnimating()

运行:测试发现当松手刷新的时候,显示的效果能出来,但是当一滑动的时候状态就发会了改变,而 Refreshing 的状态改变是由数据刷新完成之后去重置,所以更改滑动时候的判断逻辑

  • 更改滑动时的判断逻辑,以防止正在刷新中的时候的状态异常改变
// 在用户拖动的时候去判断临界值
if scrollView!.dragging {
    if state == .Normal && contentOffsetY < criticalValue {
        printLog("完全显示出来啦")
        self.status = .Pulling
    }else if status == .Pulling && contentOffsetY >= criticalValue {
        printLog("没有完全显示出来/没有显示出来")
        self.status = .Normal
    }
}else{
    // 判断如果用户已经松手,并且当前状态是.Pulling,那么进入到 .Refreshing 状态
    if self.status == .Pulling {
        self.status = .Refreshing
    }
}
  • 模拟 5 秒后结束刷新
UIView.animateWithDuration(0.25, animations: { () -> Void in
        var contentInset = self.scrollView!.contentInset
        contentInset.top += self.height
        self.scrollView?.contentInset = contentInset
    }, completion: { (finish) -> Void in

        // 模似 5 秒之后约束刷新
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(5 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in
            // 设置状态为 默认状态
            self.status = .Normal
            // 重置contentInsetTop
            UIView.animateWithDuration(0.25, animations: { () -> Void in
                var contentInset = self.scrollView!.contentInset
                contentInset.top -= self.frame.height
                self.scrollView?.contentInset = contentInset
            })
        }
    })

运行测试

  • 添加要执行刷新的方法
hmRefreshControl.addTarget(self, action: "loadData", forControlEvents: UIControlEvents.ValueChanged)
  • 在刷新的时候执行方法
case .Refreshing:   // 显示刷新的效果
    ...
    // 调用刷新的方法
    sendActionsForControlEvents(.ValueChanged)
  • 去掉上面模拟5秒结束刷新的逻辑,添加结束刷新方法
func endRefreshing(){
    // 重置contentInsetTop
    UIView.animateWithDuration(0.25, animations: { () -> Void in
        var contentInset = self.scrollView!.contentInset
        contentInset.top -= self.frame.height
        self.scrollView?.contentInset = contentInset
    })
    // 设置状态为 默认状态
    self.status = .Normal
}
  • 在刷新完毕之后调用 HMRefreshControlendRefreshing() 方法
/// 结束刷新
private func endRefresh(){
    pullupView.stopAnimating()
    hmRefreshControl.endRefreshing()
}

运行测试:第一次启动的时候,刷新完毕,出现contentInset.top值递减问题,所以要判断如果之前状态是刷新状态,结束刷新才去更改contentInset.top

  • 增加保存上一次状态的逻辑

// 定义旧状态属性,保存上一次状态
var oldStatus: HMRefreshControlStatus?

// 在 `state` 的 `didSet` 方法末尾记录状态
// 定义当前控件的刷新状态
var status: HMRefreshState = .Normal {
    didSet{
        switch status {
        case .Pulling:      // 松手就可以刷新的状态
            ...
        case .Normal:       // 置为默认的状态的效果
            ...
        case .Refreshing:   // 显示刷新的效果
            ...
        }
        // 记录本次状态
        oldStatus = status
    }
}
  • 在结束刷新的时候判断如果是从刷新状态进入到默认状态就递减contentInset.top
/// 结束刷新
func endRefreshing(){
    if oldStatus == .Refreshing {
        // 重置contentInsetTop
        UIView.animateWithDuration(0.25, animations: { () -> Void in
            var contentInset = self.scrollView!.contentInset
            contentInset.top -= self.frame.height
            self.scrollView?.contentInset = contentInset
        })
    }
    // 设置状态为 默认状态
    self.state = .Normal
}
  • 部分代码抽取
// 把结束刷新的逻辑,移动到 state 的 didSet 的 case .Normal 中
switch state {
    case .Pulling:      // 松手就可以刷新的状态
        ...
    case .Normal:       // 置为默认的状态的效果
        ...
        // 如果之前状态是刷新状态,需要递减 contentInset.top
        if oldStatus == .Refreshing {
            // 重置contentInsetTop
            UIView.animateWithDuration(0.25, animations: { () -> Void in
                var contentInset = self.scrollView!.contentInset
                contentInset.top -= self.frame.height
                self.scrollView?.contentInset = contentInset
            })
        }
    case .Refreshing:   // 显示刷新的效果
        ...
    }
...

// 抽取之后的方法
func endRefreshing(){
    // 设置状态为 默认状态
    self.state = .Normal
}

运行测试

下拉刷新提示

  • 修改 loadStatuses ,回调加载成功数据条数
/// 加载微博数据的方法
    func loadData(isPullUp isPullUp: Bool, completion: (isSuccessed: Bool, count: Int)->()) {
}
  • 懒加载提示控件
/// 下拉刷新提示的label
// 提示控件
private lazy var pullDownTipLabel: UILabel = {
    let label = UILabel(textColor: UIColor.whiteColor(), fontSize: 12)
    // 设置文字居中、背景颜色
    label.textAlignment = NSTextAlignment.Center
    label.backgroundColor = UIColor.orangeColor()

    // 设置大小
    label.frame.size = CGSizeMake(SCREENW, 35)
    return label
}()
  • 增加 showPullDownTips 方法,测试添加位置
/// 显示下拉刷新提示
private func showPullDownTips(count: Int){
    pullDownTipLabel.y = 35
    navigationController?.view.insertSubview(pullDownTipLabel, belowSubview: navigationController!.navigationBar)
}
  • 在下拉刷新完成之后调用此方法
@objc private func loadData(){
    statusListViewModel.loadData(isPullUp: pullupView.isAnimating()) { (isSuccessed, count) -> () in
        if isSuccessed {
            self.tableView.reloadData()
        }
        if self.pullupView.isAnimating() == false {
            self.showPullDownTips(count)
        }
        self.endRefresh()
    }
}
  • 更改懒加载代码
/// 下拉刷新提示的label
private lazy var pullDownTipLabel: UILabel = {
    let label = UILabel()

    // 设置文字颜色、文字大小、居中、背景颜色
    label.textColor = UIColor.whiteColor()
    label.font = UIFont.systemFontOfSize(12)
    label.textAlignment = NSTextAlignment.Center
    label.backgroundColor = UIColor.orangeColor()

    // 设置大小
    label.size = CGSizeMake(SCREENW, 35)

    // 默认是隐藏状态
    label.hidden = true
    // 添加控件
    if let navigationController = self.navigationController {
        navigationController.view.insertSubview(label, belowSubview: navigationController.navigationBar)
    }
    return label
}()
  • 完成显示逻辑
/// 显示下拉刷新提示
private func showPullDownTips(count: Int){

    // 如果当前控件处于显示状态,直接返回
    if !pullDownTipLabel.hidden {
        return
    }
    /// 提示文字信息
    let tipStr = count==0 ? "没有微博数据": "\(count)条新微博"
    let height = pullDownTipLabel.frame.height
    pullDownTipLabel.frame.origin.y = CGRectGetMaxY(self.navigationController!.navigationBar.frame) - height
    // 设置文字并将其显示
    pullDownTipLabel.text = tipStr
    pullDownTipLabel.hidden = false
    //执行动画
    UIView.animateWithDuration(1, animations: { () -> Void in
        self.pullDownTipLabel.transform = CGAffineTransformMakeTranslation(0, height)
        }) { (finish) -> Void in
            UIView.animateWithDuration(1, delay: 1, options: [], animations: { () -> Void in
                self.pullDownTipLabel.transform = CGAffineTransformIdentity

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,943评论 4 60
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,047评论 25 707
  • 20170720第10拆 片段10 《拆出你的沟通力》 R: 片段10《拆出你的沟通力》 与有情绪的人沟通时...
    LiLi1113阅读 301评论 0 2
  • 去年在参加QCon时,就明显感受到各个创业者都是『通吃』型,身怀百技。同时,在招聘或者寻找合作伙伴时,也个个都是能...
    闷骚乔巴阅读 4,969评论 0 13
  • 富记菜馆020-29897628 失眠的人,睡不着觉是个好麻烦的事情。失眠往往是有几种因素:一种因素是因为心火很亢...
    琥珀工坊阅读 189评论 0 1