bawn

bawn 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织 bawn.github.io/ 编辑
编辑

欢迎技术交流

个人动态

bawn 发布了文章 · 2019-08-02

关于图片的一些知识点

如果说之前的项目中哪个 bug 让我记忆犹新,我会毫不犹豫的说是内存溢出(OOM),因为当时无论从 dSYM 还是第三方的报错信息中我都找不出问题是所在,而且开发过程中也极少遇到,现在知道当时遇到的是高分辨率的图片集中渲染导致的 OOM。

内存溢出从字面上就很好理解,传统意义上的 OOM 就是当前使用的 App 达到了 “high water mark”,也就是达到了系统对单个 App 的内存限制,系统会把这个应用杀掉(由 Jetsam 执行)。简单的说完关于 OOM 的知识点,接下来是本文要探讨一些关于图片渲染的一些知识点。

现在大家都应该知道图片从读取到最终渲染都会经历解压的过程,大致过程如下(图片来自于Image and Graphics Best Practices

AboutImage-8

Decode 过程简单说就是把图片转化成 Bitmap,那么 Bitmap 具体是什么?

Bitmap

Wikipedia 有这么一段解释

In computing, a bitmap is a mapping from some domain (for example, a range of integers) to bits. It is also called a bit array or bitmap index.The more general term pix-map refers to a map of pixels,

通俗点讲 bitmap 就是像素图,通过以下方法我们可以得到一张图片的 bitmap 信息

extension UIImage {
    
    var decodeData: Data? {
        guard let cgimage = cgImage
            , let dataProvider = cgimage.dataProvider
            , let rawData = dataProvider.data as Data? else {
                return nil
        }
        return rawData
    }
}

拿下面这张 48 * 48 的图片为例

image-1

通过上面提供的方法最终获取到的 bitmap 信息前一小段是这样的(这样子分割开来的十六进制数据一共有 48 * 48 个),事实上这些数据对应的就是各个像素上要显示的颜色信息

ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffbf9ff ffd5c8ff ffb19bff ff9b7eff ff8765ff ff7e59ff ff7750ff ff7750ff ff7e59ff ff8765ff ff9b7eff ffb19bff ffd5c8ff fffbf9ff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fff6f3ff ffcbbcff ff9f84ff ff7953ff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff734bff ff7953ff ff9f84ff ffcbbcff fff6f3ff ffffffff ffffffff ffffffff

验证一下,前十七个像素点都是白色,这符合预期,因为左上角都是白色区域,到了第十八个像素点的时候变成了 fffbf9ff,我们把图片放大后对比下

AboutImage-2

第十八个像素点是fffbf9ff,具体的,这个色值就是就是上面所显示的浅红色,最后两位 ff 代表的 alpha 值,到现在算是搞清楚了 bitmap 到底是什么。

内存占用

回到文章开头提到的高分辨率图片的解压导致 OOM 问题,首先一个问题是:一张图片解压需要消耗多少内存?答案是 水平象素 × 垂直象素 × 4 单位是 byte。我们来验证一下,这次换一张稍大的彩色 PNG 图片,它的分辨率是 400 350,所以理论上解压需要的内存大小是 400 350 * 4 = 547k,测试设备是 iPhone X 12.3.0,通过 Instruments 的 Allocations 检测到结果是 560k 这非常接近于理论值

AboutImage-3

为什么要强调彩色图片呢,或许你已经猜到了解压一张彩色图片和解压一张只有黑白组成的照片是所消耗的性能是不一样的,对的,比如下面这张图片同样是 400 * 350 的 PNG 图片

AboutImage-4

解压消耗的内存只要288k,计算方法 400 350 2 = 273k

AboutImage-5

这时候可能有些小伙伴会想,这样子也可以的话,那是不是纯红,纯绿或者纯蓝消耗的内存大小也一样的,抱歉,不是的,要解释这个问题需要引入颜色空间的概念,苹果目前支持的颜色空间有下面这种方式

  • Gray spaces, used for grayscale display and printing; see Gray Spaces
  • RGB-based color spaces, used mainly for displays and scanners; see RGB-Based Color Spaces
  • CMYK-based color spaces, used mainly for color printing; see CMY-Based Color Spaces
  • Device-independent color spaces, such as Lab, used mainly for color comparisons, color differences, and color conversion; see Device-Independent Color Spaces
  • Named color spaces, used mainly for printing and graphic design; see Named Color Spaces
  • Heterogeneous HiFi color spaces, also referred to as multichannel color spaces, primarily used in new printing processes involving the use of red-orange, green and blue, and also for spot coloring, such as gold and silver metallics; see Color-Component Values, Color Values, and Color

具体到 iOS 有这几个方式

CSPixel format and bitmap information constantAvailability
Null8 bpp, 8 bpc, kCGImageAlphaOnlyMac OS X, iOS
Gray8 bpp, 8 bpc, kCGImageAlphaNoneMac OS X, iOS
Gray8 bpp, 8 bpc, kCGImageAlphaOnlyMac OS X, iOS
RGB16 bpp, 5 bpc, kCGImageAlphaNoneSkipFirstMac OS X, iOS
RGB32 bpp, 8 bpc, kCGImageAlphaNoneSkipFirstMac OS X, iOS
RGB32 bpp, 8 bpc, kCGImageAlphaNoneSkipLastMac OS X, iOS
RGB32 bpp, 8 bpc, kCGImageAlphaPremultipliedFirstMac OS X, iOS
RGB32 bpp, 8 bpc, kCGImageAlphaPremultipliedLastMac OS X, iOS

回到前面的计算方式,在表格中可以看到每个颜色空间(CS)对应的像素格式(Pixel format)和一些常量(bitmap information constant),包括每种颜色空间对应的每像素总 bit 数(bpp)等 ,对于彩色图片来说,解压它所需要用到的必然是 RGB ,对应的就是 32 bpp(关于 16 bpp 后面会提到), 32 bits / 8 = 4 bytes,所以计算方式就是 水平象素 × 垂直象素 × 4

那么对于只有黑白组成的图片,对应的就是灰度颜色空间(Gray),官方解释

Gray spaces typically have a single component, ranging from black to white, as shown in Figure 2-1. Gray spaces are used for black-and-white and grayscale display and printing. A properly plotted gray space should have a fifty percent value as its midpoint.

image

所以内存占用计算方式就是 水平象素 × 垂直象素 × 2 (为什么表格给的是 8 bpp),这种解释方式同样适用于 UILabel 的渲染,不信可以试试红色文字的显示和黑白文字的显示需要占用的内存。

另外,或许你会想到 UIColor 的有个通过 HSB 颜色空间初始化的方法,貌似违背以上的说法

public init(hue: CGFloat, saturation: CGFloat, brightness: CGFloat, alpha: CGFloat)

HSB 确实是一种颜色空间,但是也是基于 RGB,在维基百科有相应的解释,以及官方文档也提到最终还是 RGB。

RGB

前面提到的 RGB 颜色空间,每像素拥有的总 bit 数并不一定都是 32 bits,也有一种特殊情况是 16 bits。首先 32 bpp 的意思就是,在 R G B 三个颜色上都用 8 bits 去表示,比如 Red 颜色有 2^8 个数去表示,也就是 0 - 255 个数值,当然剩下的 8 bits 留给了 alpha。那么 16 bpp 就是在 R G B 上分别使用 5 6 5 位去去表示,至于为什么 G 分到 6 位而不是其他的,据说是因为人类的眼睛对绿色比较敏感。所以它们还有另外一种表述形式叫做 RGB888 和 RGB565。

RGB888 示意图

AboutImage-6

RGB565 示意图

AboutImage-7

比如我们需要创建一个 bitmap 来表示 RGB565,以十六进制 0x001f 表示其中的一个像素,转化为二进制就是 11111,这时候它表示并不是红色,而是蓝色,因为如果高位不够就会用 0 来补,左边 -> 右边 就是高位 -> 低位,所以最终其实是 0000000000011111 来表示一个像素的颜色,也就是蓝色。知道这些后我们可以动手创建一个RGB565 的 bitmap

    static func makeData() -> UnsafeMutablePointer<UInt16> {
        let capacity = 200 * 200 * 2
        let imageBuffer = UnsafeMutablePointer<UInt16>.allocate(capacity: capacity)
        for row in 0..<200 {
            let color: UInt16 = row >= 100 ? 0x001f : 0x7e0
            for col in 0..<200 {
                imageBuffer[row * 200 + col] = color
            }
        }
        free(imageBuffer)
        return imageBuffer
    }

这里设置 200 * 200 分辨率的图片上半部分是 0x7e0 也就是 11111100000 绿色,下半部分是 0x001f 就是刚才说的蓝色,然后通过 CGImage 生成图片。

static var RGB565Image: UIImage? {
        let width = 200
        let height = 200
        let rawData = makeData()
        guard let data = Data(bytes: rawData, count: width * height * 2) as CFData?
            , let provider = CGDataProvider(data: data) else {
                return nil
        }
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo =  CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder16Little.rawValue)
        let imageRef = CGImage(width: width,
                               height: height,
                               bitsPerComponent: 5,
                               bitsPerPixel: 16,
                               bytesPerRow: width * 2,
                               space: colorSpace,
                               bitmapInfo: bitmapInfo,
                               provider: provider,
                               decode: nil,
                               shouldInterpolate: false,
                               intent: CGColorRenderingIntent.defaultIntent
        )
        
        guard let cgImage = imageRef else {
            return nil
        }
        
        let finalImage = UIImage(cgImage: cgImage)
        return finalImage
    }

注意 CGBitmapInfo 的参数,不需要 alpha 通道 noneSkipFirst,以及 16 位小端模式 byteOrder16Little,这里不做过多介绍,有兴趣的可以搜一搜。

未完待续

查看原文

赞 0 收藏 0 评论 0

bawn 发布了文章 · 2019-03-06

关于埋点

本文主要介绍 火球买手 项目上的埋点方案(基于神策),以及一些心得。事实上在项目早期,我们的埋点完全依赖于第三方的全埋点技术,客户端开发人员只需要做一些简单的工作就能满足 BI 部门对数据的需求。但随着业务增长,对数据的准确性和精细化的要求越来越高,之后不得不转向手动埋点,当然这个也是基于第三方的。

目前 BI 部门对埋点数据要求可以总结为一句话:『从哪里来到哪里去』,比如在 Timeline 中点击一篇文章进入详情页,那么 Timeline 就是『从哪里来』,详情页就是『到哪里去』,当然实际项目中『从哪里来』不只需要一个维度定位,有时候需要两三个维度才能定位。

下面具体来说下 火球买手 项目上是如何埋点的,首先『频道主页』是项目中比较常见的页面,它对应的 Model 是 Channel,然后当任何点击进入的频道主页的事件触发后都需要上报以下数据

{
    module_name
    page_name
    channel_name
    channel_id
}

page_name指的是当前的 ViewController 名称,module_name主要用于区分同一个页面内的不同入口,这样子就能确定『从哪里来』,channel_namechannel_id 数据来自于 Channel,至于『到哪里去』这里就用埋点的 key 来表明,比如是 ChannelClick。

频道主页在 APP 中入口众多,即使在不考虑埋点的情况下,一个统一的入口也是必要的

extension UIViewController {
    func pushToChannelDetailController(_ id: String?) {
        // ...
    }
}

显然这样的方法根本无法满足埋点上的需求,改造一下:

extension UIViewController {
    func pushToChannelDetailController(_ model: Channel?) {
        // ...
        let value = [
            "module_name" : model.module_name,
            "channel_id" : model._id,
            "channel_name": model.name,
            "page_name": self.pageName
        ]
        SensorsAnalyticsSDK.sharedInstance()?.track(key, withProperties: value)
    }
}

需要注意的是大多数情况下入口函数只接受一个具象参数是行不通的,因为随着项目的开发业务的迭代总有一些其他的模型被加入,它们同样带有能够跳转至频道主页的 id 属性。还有 pageName 映射:

extension UIViewController {
    var pageName: String {
        switch self {
            case is ChannelDetailController:
                return "频道主页"
        }
    }
}

最后在具体的跳转处设置 module_name

@objc func buttonAction(_ sender: Any) {
    model.module_name = "Header"
    pushToChannelDetailController(model)
}

整体看下来虽然可以应付点击进入频道主页的埋点,但是还是存在以下问题

入口函数不够抽象

在实际开发中接收不同的数据模型跳转到同一个页面的情况应该不少见,并且入口函数也是因为埋点把参数从相对抽象的 String 替换成了具象的 Channel,所以抽象 model 是首先要做的。无论接受什么类型参数,传给 ChannelDetail 还是 id,那么让一个只有带有 id 属性的 protocol 去约束模型再合适不过了。

protocol CommonModelType {
    var id: String { get }
}

然后让 Channle 遵守这个协议,利用 extension 是为了看起来更解耦

extension Channel: CommonModelType{}

这时候入口函数就是这样

func pushToChannellDetail(_ model: CommonModelType?)

可以接受任何有 id 属性的模型。为了更抽象,甚至可以让 String 也遵守这个协议

extension String: CommonModelType {
    var id: String {
        return self
    }
}

当然不同其他可以用来跳转到频道主页的模型都可以这样约束。

数据提供方式不够优雅

埋点数据除了 pageName 不属于 model 以外,其他都属于 model 本身的属性(module_name 属于额外添加),所以和参数一样,同样用 protocol 约束 Channel ,让 Channel 拥有一个直接用于提供数据的属性。

protocol AnalyticsModelType {
    var analytics: [String: Any] { get }
}

让 Channel 同时遵守这两个协议,并且添加 analytics 属性

extension Channel: CommonModelType, AnalyticsModelType {
    var analytics: [String : Any] {
        let value = [
            "module_name" : module_name,
            "channel_id" : id,
            "channel_name": name
        ]
        return value
    }
}

最后完整的入口函数是这样的

extension UIViewController {

    func pushToChannellDetail(_ model: CommonModelType?) {
        guard let model = model else {
            return
        }
        let viewController = ChannelDetailViewController()
        viewController.id = model.id
        navigationController?.pushViewController(viewController, animated: true)
        
        guard let value = model as? AnalyticsModelType else {
            return
        }
        var properties = value.analytics
        properties["page_name"] = pageName
        SensorsAnalyticsSDK.sharedInstance.track(key: "ChannelClick", properties: properties)
    }
}

总的来说入口函数够抽象,无论后期增加多少种模型只要它遵循CommonModelType即可,甚至对于不熟悉项目的人来说直接传入 id 也是可以正常跳转的。埋点的细节也被隐藏到了入口函数内,而需要上报的数据又由相应的模型负责提供只要它遵循AnalyticsModelType即可。

未完待续...

查看原文

赞 0 收藏 0 评论 0

bawn 发布了文章 · 2019-03-06

嵌套滚动效果实现讨论

本文要讨论的是类似于即刻淘票票首页,抖音简书个人主页这样的嵌套滚动效果,事实上网上已经有很多的相关的文章,比如:

而且绝大多数的文章都是从如何解决手势冲突出发给出相应的解决方案,原因是他们大多数都采用了三级 Scrollview 的解决方案,如下图

image1

  • 蓝色视图:一级 ScrollView
  • 红色视图:HeaderView
  • 绿色视图:MenuView
  • 橘色视图:二级 ScrollView
  • 黑色、深黑、浅黑:三级 ScrollView

可以看到三级 ScrollView 和 一级 ScrollView都需要在纵向滚动,所以重点要解决的就是这里的滚动冲突,具体的细节我就不再赘述,大家还可以参考HGPersonalCenter这个项目,里面有详细的注释。下面的视图结构是淘票票首页,可以比较清楚看到采用的是三级 ScrollView 的形式

image2

  • 上层的 MVNestTableView:一级 ScrollView
  • 中间的 UIScrollView:二级 ScrollView
  • 下层的 MVNestTableView:三级 ScrollView

之所以在前面给出了四个例子,除了淘票票和简书采用的三级 ScrollView 方案以外还有抖音和即刻采用的二级 ScrollView 方案,并且即刻在体验上更完美,这个后面会讲到。二级 ScrollView 方案的大致结构如下

image-4

  • 蓝色视图:一级 ScrollView
  • 黑色、深黑、浅黑:二级 ScrollView
  • 红色视图:HeaderView
  • 绿色视图:MenuView

下面是 5.x 版本即刻首页的结构,可以清楚的看到即刻采用的是二级 ScrollView 的方案

image3

当然通过点击状态栏看也可以粗略判断实现方式,比如淘票票在点击状态栏后视图只会滚动到子 ScrollView 的顶部而不是最外面 ScrollView 的,简书虽然滚动到最外层的顶部但效果明显不够自然,原因就是三级 ScrollView 在纵向没有延伸到顶部,抖音和即刻在点击状态栏返回到顶部的效果则非常自然。

从整体结构上来看即刻只有二级 ScrollView,所以在纵向上 ChildScrollView 会完全接管手势,横向滚动时又由 MainScrollView 控制,这样子带来的好处在于无需关心手势冲突问题,但要实现前面提到的效果还必须处理是以下问题:

  • HeaderView 和 MenuView 的位置需要根据 ChildScrollView 的滚动而改变
  • 在切换的 Tab 的时候需要同步下一个 ChildScrollView 的 offset
  • ChildScrollView 必须在顶部留出 HeaderView 和 MenuView 高度总和的空白区域
  • HeaderView 不能拦截滚动手势

在这里就不给出具体的实现细节,文章后面最后有通过两种方案实现的开源库,欢迎 Star。虽然即刻和抖音采用的都是这种二级 ScrollView 的方案,但即刻在体验上更好,比如抖音的个人主页如果手指开始滚动的地方有可交互的控件(Tab栏),那么这时候滑动是会失效的,还有在切换Tab后将视图下拉滚动到顶部然后返回到之前的Tab页,抖音是直接返回到了原始的位置而即刻还是能保留之前进度。

头部滚动失效解决方案

即刻为了达到完美的效果,在每个 ChildScrollView 顶部都添加了 HeaderView 和 MenuView,这样子作为一个整体,即使开始触摸的地方有可交互控件也可以上下滚动。然后在左右滑动的时又让ChildScrollView 内的 HeaderView 和 MenuView 隐藏,当停止滚动的时让原本在外层 ScrollView 内的 HeaderView 和 MenuView 显示。

保留进度解决方案

关于保留进度首先要做的就是判断当前 ChildScrollView 是不是处于一种特殊状态,这种状态就是 offset.y的值是否大于 HeaderView 的偏移量,然后再通过判断 ChildScrollView 当前的滚动方向,来决定是否要调整 HeaderView 和 MenuView 的位置。

对比两个方案最终的实现各有优缺点

方案一

优点:

  • 无障碍配合使用第三方下拉刷新库
  • ChildViewController 无需额外设置

缺点:

  • 实现较复杂
  • 滚动有细微的停顿感
  • 切换Tab不能保留进度
  • 点击状态栏不能返回到顶部

方案二

优点:

  • 实现简单
  • 滚动无停顿感
  • 切换Tab可保留进度
  • 点击状态栏可返回到顶部

缺点

  • ChildViewController 需要额外的设置(ChildScrollView 必须在顶部留出 HeaderView 和 MenuView 高度)
  • 下拉刷新只能在 ChildViewController 内实现

这里要提的是,由于方案二中 MainScrollView 并不会在纵向有滚动,所以下拉刷新必须放在 ChildViewController 内实现,但又因为 HeaderView 和 MenuView 需要根据 ChildScrollView 的偏移而移动,在配合MJRefresh时它们的偏移有明显的Bug(在本文发布前我并没深究解决方案),或许即刻也是因为这个原因而采用上面提到的解决办法。

以上两种解决方案的开源库:方案一:Aquaman方案二:Shazam,关于 MenuView 都设计成了交由开发者实现,这是因为即使 MenuView 集成各种样式的也难满足设计上的千奇百怪的要求,参考我的 Demo 就能很快实现一个自己想要的效果。

查看原文

赞 0 收藏 0 评论 0

bawn 发布了文章 · 2018-11-23

Swift 中的 Range

image

本文主要讲解 Range 家族类的一些实现细节和 Swift 中面向协议编程的一些具体表现。为了方便起见,无论是 class 或者 struct 都统称为『类』。

基本介绍

在 Swift 4.0 之前 Range 家族一共有 4 种类型:

let rang: Range = 0.0..<1.0 // 半开区间
let closedRange: ClosedRange = 0.0...1.0 // 闭区间
let countableRange: CountableRange = 0..<1 // Countable 半开区间
let countableClosedRange: CountableClosedRange = 0...1 // Countable 闭区间

之后 Swift 4.0 上新增了 4 种类型:

let partialRangeThrough: PartialRangeThrough = ...1.0 // 单侧区间
let partialRangeFrom: PartialRangeFrom = 0.0... // 单侧区间
let partialRangeUpTo: PartialRangeUpTo = ..<1.0 // 单侧区间
let countablePartialRangeFrom: CountablePartialRangeFrom  = 1... // Countable 单侧区间

但到了 Swift 4.2 又只剩下 5 种类型,分别是:RangeClosedRangePartialRangeThroughPartialRangeFromPartialRangeUpTo,所有的 Countable 类型都是对应的 typealias

public typealias CountableRange<Bound> = Range<Bound>
public typealias CountableClosedRange<Bound> = ClosedRange<Bound>
public typealias CountablePartialRangeFrom<Bound> = PartialRangeFrom<Bound>

基本构成

Range 的所有类型都是一个拥有 Bound 泛型的 struct,并且这个 Bound 必须继承 Comparable 协议。

public struct Range<Bound> where Bound : Comparable
public struct ClosedRange<Bound> where Bound : Comparable
public struct PartialRangeThrough<Bound> where Bound : Comparable
public struct PartialRangeFrom<Bound> where Bound : Comparable
public struct PartialRangeUpTo<Bound> where Bound : Comparable

在 swift 标准库中绝大多数基础类型都实现了此协议,所以包括 StringDateIndexPath 等。

let stringRange = "a"..<"z"
let dateRange = Date()...Date()
let indexRange = IndexPath(item: 0, section: 0)...IndexPath(row: 1, section: 0)

当需要用一个自定义的类创建 Range 也只是需要继承 Comparable 协议,并实现相应方法即可,例如

struct Foo: Comparable {
    var value: Int
    static func < (lhs: Foo, rhs: Foo) -> Bool {
        return lhs.value < rhs.value
    }
    
    init(_ v: Int) {
        value = v
    }
}

let range = Foo(1)...Foo(20)
foo.contains(Foo(2)) // true

而且 contains(:) 也被自动的实现了,这其实归功于 RangeExpression 协议:

public func contains(_ element: Self.Bound) -> Bool

究其原因是每个 Range 类型都有一个 extension:当泛型 Bound 遵守 Comparable 时扩展相应的类以现实 RangeExpression 协议。

extension Range : RangeExpression where Bound : Comparable 
extension ClosedRange : RangeExpression where Bound : Comparable 
extension PartialRangeThrough : RangeExpression where Bound : Comparable
extension PartialRangeFrom : RangeExpression where Bound : Comparable
extension PartialRangeUpTo : RangeExpression where Bound : Comparable

试想一下如果用面向对象的语言一般是如何实现 contains(:) 方法的?

Countable 的实现细节

前面讲到在 Swift 4.2 上所有的 Countable 类型都是 typealias,是否具有 Countable 能力被抽象到泛型 Bound 上,以 ClosedRange 为例

extension ClosedRange : Sequence where Bound : Strideable, Bound.Stride : SignedInteger {

    /// A type representing the sequence's elements.
    public typealias Element = Bound

    /// A type that provides the sequence's iteration interface and
    /// encapsulates its iteration state.
    public typealias Iterator = IndexingIterator<ClosedRange<Bound>>
}

可以看到为了继承 Sequence 协议,泛型 Bound 需要先继承 StrideableStrideable 协议定义如下:

public protocol Strideable : Comparable {
    /// A type that represents the distance between two values.
    associatedtype Stride : Comparable, SignedNumeric

    public func distance(to other: Self) -> Self.Stride

    public func advanced(by n: Self.Stride) -> Self
}

它有一个绑定类型 Stride 和两个需要实现的方法,那么Bound.Stride : SignedInteger 表示的就是Strideable 的绑定类型 Stride 需要继承 SignedInteger

总结下来 swift 通过泛型约束、协议绑定类型约束再结合 extension 能力,把 Countable 能力被抽象到泛型 Bound 上,最终由泛型 Bound 来决定 Range 是否具有 Sequence 能力。

为什么 Int 可以创建 Countable 的 Range

或许你只知道通过 Int 创建的 Range,它就是一个CountableRange,然而为什么是?首先 Int 继承于 FixedWidthInteger, SignedInteger

public struct Int : FixedWidthInteger, SignedInteger

SignedInteger 又继承于 BinaryInteger, SignedNumeric

public protocol SignedInteger : BinaryInteger, SignedNumeric {
}

BinaryInteger 在一定条件下又继承于 Strideable

public protocol BinaryInteger : CustomStringConvertible, Hashable, Numeric, Strideable where Self.Magnitude : BinaryInteger, Self.Magnitude == Self.Magnitude.Magnitude

继续查看BinaryIntegerStrideable 实现:

extension BinaryInteger {   
    public func distance(to other: Self) -> Int
    public func advanced(by n: Int) -> Self
}

会发现 Stride 类型就是 Int, 而 Int 本身就是继承于 SignedInteger,这样子就符合前面提到的 Bound.Stride : SignedInteger 条件。最后别忘了另外一个限定条件

where Self.Magnitude : BinaryInteger, Self.Magnitude == Self.Magnitude.Magnitude

Magnitude 是 Numeric 协议的绑定类型,Numeric定义如下:

public protocol Numeric : Equatable, ExpressibleByIntegerLiteral {
    associatedtype Magnitude : Comparable, Numeric
    public var magnitude: Self.Magnitude { get }
}

但未发现 BinaryInteger 有任何的 extension 给定 Magnitude 的类型。这只能说明 Magnitude 会在具体的类上被指定,回到 Int 上果然找到 Magnitude

public struct Int : FixedWidthInteger, SignedInteger {
    public typealias Magnitude = UInt
}

继续查看 UInt

public struct UInt : FixedWidthInteger, UnsignedInteger {
    public typealias Magnitude = UInt
}

UnsignedInteger 又继承于 BinaryInteger

public protocol UnsignedInteger : BinaryInteger {
}

所以Self.Magnitude : BinaryInteger, Self.Magnitude == Self.Magnitude.Magnitude 就相当于 Int.UInt : BinaryInteger, Int.UInt == Int.UInt.UInt。至此 Int 类型满足了一切条件,事实上不仅是 Int 整个 Int 家族和 UInt 家族类型都是符合这些条件,下面是关于IntUInt 粗略协议继承关系。

                                +---------------+   
                                |  Comparable   |    
                                +-------+-------+   
                                        ^
                                        |
                +-------------+   +-----+-------+
        +------>+   Numeric   |   | Strideable  |
        |       +------------++   +-----+-------+
        |                    ^          ^
        |                    |          |
+-------+-------+        +---+----------+----+ 
| SignedNumeric |        |   BinaryInteger   | 
+------+--------+        +---+-----+-----+---+
       ^         +-----------^     ^     ^----------+        
       |         |                 |                |  
+------+---------++    +-----------+--------+  +----+-------------+
|  SignedInteger  |    |  FixedWidthInteger |  |  UnsignedInteger |  
+---------------+-+    +-+----------------+-+  +--+---------------+
                ^        ^                ^       ^
                |        |                |       |
                |        |                |       |
               ++--------+-+             ++-------+--+     
               |Int family |             |UInt family|    
               +-----------+             +-----------+

手动实现 Strideable

struct Foo {
    var value: Int
    init(_ v: Int) {
        value = v
    }
}

extension Foo: Strideable {
    func distance(to other: Foo) -> Int {
        return other.value - self.value
    }
    
    func advanced(by n: Int) -> Foo {
        var result = self
        result.value += n
        return result
    }
}

Foo 继承 Strideable 的同时其绑定也被指定为 Int,这样子就可以创建自定义类型的 Range 了,并且继承于 Sequence 。

let fooRange = Foo(1)...Foo(20)
fooRange.contains(Foo(2))
Array((Foo(1)..<Foo(20)))
for item in fooRange {
    print(item)
}

总结

Swift 作为一门面向协议编程的语言,在 Range 的实现上可见一斑,随着 SE-0142SE-0143提案分别在 Swift 4.0 和 Swift 4.2 中被加入之后更是加强了在这方面的能力。

查看原文

赞 1 收藏 0 评论 0

bawn 发布了文章 · 2018-11-23

Texture 布局篇

image

Texture 拥有自己的一套成熟布局方案,虽然学习成本略高,但至少比原生的 AutoLayout 写起来舒服,重点是性能远好于 AutoLayoutTexture 文档上也指出了这套布局方案的的优点:

  • Fast: As fast as manual layout code and significantly faster than Auto Layout
  • Asynchronous & Concurrent: Layouts can be computed on background threads so user interactions are not interrupted.
  • Declarative: Layouts are declared with immutable data structures. This makes layout code easier to develop, document, code review, test, debug, profile, and maintain.
  • Cacheable: Layout results are immutable data structures so they can be precomputed in the background and cached to increase user perceived performance.
  • Extensible: Easy to share code between classes.

首先这套布局都是基于 Texture 组件的,所以当遇到要使用原生控件时,通过用 block 的方式包装一个原生组件再合适不过了,例如:

ASDisplayNode *animationImageNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{
    FLAnimatedImageView *animationImageView = [[FLAnimatedImageView alloc] init];
    animationImageView.layer.cornerRadius = 2.0f;
    animationImageView.clipsToBounds = YES;
    return animationImageView;
}];
[self addSubnode:animationImageNode];
self.animationImageNode = animationImageNode;

ASDisplayNode 在初始化之后会检查是否有子视图,如果有就会调用

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize

方法进行布局,所以对视图进行布局需要重写这个方法。看一个例子:

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_childNode];
    return insetLayout;
}

_childNode 相对于父视图边距都为 0,也就是AutoLayouttopbottomleftright 都为 0。

-----------------------------父视图----------------------------
|  -------------------------_childNode---------------------  |
|  |                                                      |  |
|  |                                                      |  |
|  ---------------------------  ---------------------------  |
--------------------------------------------------------------

可以看到layoutSpecThatFits:方法返回的必须是 ASLayoutSpec, ASInsetLayoutSpec 是它的子类之一,下面是所有的子类及其关系:

  • ASLayoutSpec

    • ASAbsoluteLayoutSpec // 绝对布局
    • ASBackgroundLayoutSpec // 背景布局
    • ASInsetLayoutSpec // 边距布局
    • ASOverlayLayoutSpec // 覆盖布局
    • ASRatioLayoutSpec // 比例布局
    • ASRelativeLayoutSpec // 顶点布局

      • ASCenterLayoutSpec // 居中布局
    • ASStackLayoutSpec // 盒子布局
    • ASWrapperLayoutSpec // 填充布局
    • ASCornerLayoutSpec // 角标布局

_

ASAbsoluteLayoutSpec

使用方法和原生的绝对布局类似

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
  self.childNode.style.layoutPosition = CGPointMake(100, 100);
  self.childNode.style.preferredLayoutSize = ASLayoutSizeMake(ASDimensionMake(100), ASDimensionMake(100));

  ASAbsoluteLayoutSpec *absoluteLayout = [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[self.childNode]];
  return absoluteLayout;
}

值得提的是:ASAbsoluteLayoutSpec 一般情况都会通过 ASOverlayLayoutSpecASOverlayLayoutSpec 着陆,因为只有上述两种布局才能保留 ASAbsoluteLayoutSpec 绝对布局的事实。举个例子当视图中只有一个控件需要用的是 ASAbsoluteLayoutSpec 布局,而其他控件布局用的是 ASStackLayoutSpec(后面会介绍),那么一旦 absoluteLayout 被加入到 ASStackLayoutSpec 也就失去它原本的布局的意义。

ASOverlayLayoutSpec *contentLayout = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:stackLayout overlay:absoluteLayout];

不过官方文档明确指出应该尽量少用这种布局方式:

Absolute layouts are less flexible and harder to maintain than other types of layouts.

_

ASBackgroundLayoutSpec

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
  ASBackgroundLayoutSpec *backgroundLayout = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:self.childNodeB background:self.childNodeA];
  return backgroundLayout;
}

childNodeA 做为 childNodeB 的背景,也就是 childNodeB 在上层,要注意的是 ASBackgroundLayoutSpec 事实上根本不会改变视图的层级关系,比如:

ASDisplayNode *childNodeB = [[ASDisplayNode alloc] init];
childNodeB.backgroundColor = [UIColor blueColor];
[self addSubnode:childNodeB];
self.childNodeB = childNodeB;

ASDisplayNode *childNodeA = [[ASDisplayNode alloc] init];
childNodeA.backgroundColor = [UIColor redColor];
[self addSubnode:childNodeA];
self.childNodeA = childNodeA;

那么即使使用上面的布局方式,childNodeB 依然在下层。

_

ASInsetLayoutSpec

比较常用的一个类,看图应该能一目了然(图片来自于官方文档
image

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    ASInsetLayoutSpec *inset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:_childNode];
    return insetLayout;
}

_childNode 相对于父视图边距都为 0,相当于填充整个父视图。它和之后会说到的
ASOverlayLayoutSpec 实际上更多的用来组合两个 Element 而已。

_

ASOverlayLayoutSpec

参考 ASBackgroundLayoutSpec

_

ASRatioLayoutSpec

image(图片来自于官方文档

也是比较常用的一个类,作用是设置自身的高宽比,例如设置正方形的视图

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    ASRatioLayoutSpec *ratioLayout = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.0f child:self.childNodeA];
    return ratioLayout;
}

_

ASRelativeLayoutSpec

把它称为顶点布局可能有点不恰当,实际上它可以把视图布局在:左上左下右上右下四个顶点以外,还可以设置成居中布局。

image

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    self.childNodeA.style.preferredSize = CGSizeMake(100, 100);
    ASRelativeLayoutSpec *relativeLayout = [ASRelativeLayoutSpec relativePositionLayoutSpecWithHorizontalPosition:ASRelativeLayoutSpecPositionEnd verticalPosition:ASRelativeLayoutSpecPositionStart sizingOption:ASRelativeLayoutSpecSizingOptionDefault child:self.childNodeA];
    return relativeLayout;
}

上面的例子就是把 childNodeA 显示在右上角。

_

ASCenterLayoutSpec

绝大多数情况下用来居中显示视图

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    self.childNodeA.style.preferredSize = CGSizeMake(100, 100);
    ASCenterLayoutSpec *relativeLayout = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:self.childNodeA];
    return relativeLayout;
}

_

ASStackLayoutSpec

可以说这是最常用的类,而且相对于其他类来说在功能上是最接近于 AutoLayout 的。
之所以称之为盒子布局是因为它和 CSS 中 Flexbox 很相似,关于 Flexbox 的可以看下阮一峰的这篇文章

先看一个例子:

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    self.childNodeA.style.preferredSize = CGSizeMake(100, 100);
    self.childNodeB.style.preferredSize = CGSizeMake(200, 200);
    ASStackLayoutSpec *stackLayout = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
                                                                             spacing:12
                                                                      justifyContent:ASStackLayoutJustifyContentStart
                                                                          alignItems:ASStackLayoutAlignItemsStart
                                                                            children:@[self.childNodeA, self.childNodeB]];
    return stackLayout;
}

简单的说明下各个参数的作用:

  1. direction:主轴的方向,有两个可选值:
  • 纵向:ASStackLayoutDirectionVertical
  • 横向:ASStackLayoutDirectionHorizontal
  1. spacing: 主轴上视图排列的间距,比如有四个视图,那么它们之间的存在三个间距值都应该是spacing
  2. justifyContent: 主轴上的排列方式,有五个可选值:
  • ASStackLayoutJustifyContentStart 从前往后排列
  • ASStackLayoutJustifyContentCenter 居中排列

    • ASStackLayoutJustifyContentEnd 从后往前排列
    • ASStackLayoutJustifyContentSpaceBetween 间隔排列,两端无间隔
    • ASStackLayoutJustifyContentSpaceAround 间隔排列,两端有间隔
  1. alignItems: 交叉轴上的排列方式,有五个可选值:
  • ASStackLayoutAlignItemsStart 从前往后排列
  • ASStackLayoutAlignItemsEnd 从后往前排列

    • ASStackLayoutAlignItemsCenter 居中排列
    • ASStackLayoutAlignItemsStretch 拉伸排列
    • ASStackLayoutAlignItemsBaselineFirst 以第一个文字元素基线排列(主轴是横向才可用)
    • ASStackLayoutAlignItemsBaselineLast 以最后一个文字元素基线排列(主轴是横向才可用)
  1. children: 包含的视图。数组内元素顺序同样代表着布局时排列的顺序,所以需要注意

主轴的方向设置尤为重要,如果主轴设置的是 ASStackLayoutDirectionVertical, 那么 justifyContent 各个参数的意义就是:

  • ASStackLayoutJustifyContentStart 从上往下排列
  • ASStackLayoutJustifyContentCenter 居中排列
  • ASStackLayoutJustifyContentEnd 从下往上排列
  • ASStackLayoutJustifyContentSpaceBetween 间隔排列,两端无间隔
  • ASStackLayoutJustifyContentSpaceAround 间隔排列,两端有间隔

alignItems 就是:

  • ASStackLayoutAlignItemsStart 从左往右排列
  • ASStackLayoutAlignItemsEnd 从右往左排列
  • ASStackLayoutAlignItemsCenter 居中排列
  • ASStackLayoutAlignItemsStretch 拉伸排列
  • ASStackLayoutAlignItemsBaselineFirst 无效
  • ASStackLayoutAlignItemsBaselineLast 无效

对于子视图间距不一样的布局方法,后面实战中会讲到。

_

ASWrapperLayoutSpec

填充整个视图

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{    
    ASWrapperLayoutSpec *wrapperLayout = [ASWrapperLayoutSpec wrapperWithLayoutElement:self.childNodeA];
    return wrapperLayout;
}

ASCornerLayoutSpec

顾名思义 ASCornerLayoutSpec 适用于类似于角标的布局

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
{
  let cornerSpec = ASCornerLayoutSpec(child: avatarNode, corner: badgeNode, location: .topRight)
  cornerSpec.offset = CGPoint(x: -3, y: 3)
}

最需要注意的是offset是控件的Center的偏移

布局实战

案例一

image

简单的文件覆盖在图片上,文字居中。

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    ASWrapperLayoutSpec *wrapperLayout = [ASWrapperLayoutSpec wrapperWithLayoutElement:self.coverImageNode];

    ASCenterLayoutSpec *centerSpec = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:self.textNode];
    ASOverlayLayoutSpec *overSpec = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:wrapperLayout overlay:centerSpec];
    return overSpec;
}
  1. ASWrapperLayoutSpec 把图片铺满整个视图
  2. ASCenterLayoutSpec 把文字居中显示
  3. ASOverlayLayoutSpec 把文字覆盖到图片上

注意第三步就是之前提到的 ASOverlayLayoutSpec/ASBackgroundLayoutSpec 的作用:用于组合两个 Element

案例二

image

这个是轻芒阅读(豌豆荚一览) APP 内 AppSo 频道 Cell 的布局,应该也是比较典型的布局之一。为了方便理解先给各个元素定一下名称,从上至下,从左往右分别是:

  • coverImageNode // 大图
  • titleNode // 标题
  • subTitleNode // 副标题
  • dateTextNode // 发布时间
  • shareImageNode // 分享图标
  • shareNumberNode // 分享数量
  • likeImageNode // 喜欢图标
  • likeNumberNode // 喜欢数量
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize{
    
    
    self.shareImageNode.style.preferredSize = CGSizeMake(15, 15);
    self.likeImageNode.style.preferredSize = CGSizeMake(15, 15);
    
    ASStackLayoutSpec *likeLayout = [ASStackLayoutSpec horizontalStackLayoutSpec];
    likeLayout.spacing = 4.0;
    likeLayout.justifyContent = ASStackLayoutJustifyContentStart;
    likeLayout.alignItems = ASStackLayoutAlignItemsCenter;
    likeLayout.children = @[self.likeImageNode, self.likeNumberNode];
    
    ASStackLayoutSpec *shareLayout = [ASStackLayoutSpec horizontalStackLayoutSpec];
    shareLayout.spacing = 4.0;
    shareLayout.justifyContent = ASStackLayoutJustifyContentStart;
    shareLayout.alignItems = ASStackLayoutAlignItemsCenter;
    shareLayout.children = @[self.shareImageNode, self.shareNumberNode];
    
    ASStackLayoutSpec *otherLayout = [ASStackLayoutSpec horizontalStackLayoutSpec];
    otherLayout.spacing = 12.0;
    otherLayout.justifyContent = ASStackLayoutJustifyContentStart;
    otherLayout.alignItems = ASStackLayoutAlignItemsCenter;
    otherLayout.children = @[likeLayout, shareLayout];
    
    ASStackLayoutSpec *bottomLayout = [ASStackLayoutSpec horizontalStackLayoutSpec];
    bottomLayout.justifyContent = ASStackLayoutJustifyContentSpaceBetween;
    bottomLayout.alignItems = ASStackLayoutAlignItemsCenter;
    bottomLayout.children = @[self.dateTextNode, otherLayout];
    
    self.titleNode.style.spacingBefore = 12.0f;
    
    self.subTitleNode.style.spacingBefore = 16.0f;
    self.subTitleNode.style.spacingAfter = 20.0f;
    
    ASRatioLayoutSpec *rationLayout = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:0.5 child:self.coverImageNode];
    
    ASStackLayoutSpec *contentLayout = [ASStackLayoutSpec horizontalStackLayoutSpec];
    contentLayout.justifyContent = ASStackLayoutJustifyContentStart;
    contentLayout.alignItems = ASStackLayoutAlignItemsStretch;
    contentLayout.children = @[
                               rationLayout,
                               self.titleNode,
                               self.subTitleNode,
                               bottomLayout
                               ];
    
    ASInsetLayoutSpec *insetLayout = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(16, 16, 16, 16) child:contentLayout];
    return insetLayout;
}

下面详细解释下布局,不过首先要明确的是,Texture 的这套布局方式遵守从里到外的布局原则,使用起来才会得心应手。

  1. 根据布局的原则,首先利用 ASStackLayoutSpec 布局 分享图标分享数量喜欢图标喜欢数量
  2. 还是通过 ASStackLayoutSpec 包装第一步的两个的布局得到 otherLayout 布局对象。
  3. 依然是 ASStackLayoutSpec 包装otherLayout发布时间。注意这里设置横向的排列方式 ASStackLayoutJustifyContentSpaceBetween已到达两端布局的目的,最终返回 bottomLayout
  4. 由于 大图 是网络图片,对于 Cell 来说,子视图的布局必能能决定其高度(Cell 宽度是默认等于 TableNode 的宽度),所以这里必须设置 大图 的高度,ASRatioLayoutSpec 设置了图片的高宽比。
  5. 接下来布局应该就是 大图标题副标题bottomLayout 的一个纵向布局,可以发现这里的视图间距并不相同,这时候 spacingBeforespacingAfter 就会很有用,它们用来分别设置元素在主轴上的前后间距。self.titleNode.style.spacingBefore = 12.0f; 意思就是 标题 相对于 大图 间距为 12。
  6. 最后通过一个 ASInsetLayoutSpec 设置一个边距。

可以看到不仅是 NodeASLayoutSpec 本身也可以作为布局元素,这是因为只要是遵守了 <ASLayoutElement> 协议的对象都可以作为布局元素。

案例三

image

    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        
        self.node1.style.preferredSize = CGSize(width: constrainedSize.max.width, height: 136)
        
        
        self.node2.style.preferredSize = CGSize(width: 58, height: 25)
        self.node2.style.layoutPosition = CGPoint(x: 14.0, y: 95.0)
        
        self.node3.style.height = ASDimensionMake(37.0)
        self.node4.style.preferredSize = CGSize(width: 80, height: 20)
        self.node5.style.preferredSize = CGSize(width: 80, height: 20)
        
        self.node4.style.spacingBefore = 14.0
        self.node5.style.spacingAfter = 14.0
        
        let absoluteLayout = ASAbsoluteLayoutSpec(children: [self.node2])
        
        let overlyLayout = ASOverlayLayoutSpec(child: self.node1, overlay: absoluteLayout)
        
        let insetLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(0, 14, 0, 14), child: self.node3)
        insetLayout.style.spacingBefore = 13.0
        insetLayout.style.spacingAfter = 25.0
        
        
        let bottomLayout = ASStackLayoutSpec.horizontal()
        bottomLayout.justifyContent = .spaceBetween
        bottomLayout.alignItems = .start
        bottomLayout.children = [self.node4, self.node5]
        bottomLayout.style.spacingAfter = 10.0
//        bottomLayout.style.width = ASDimensionMake(constrainedSize.max.width)
        
        
        let stackLayout = ASStackLayoutSpec.vertical()
        stackLayout.justifyContent = .start
        stackLayout.alignItems = .stretch
        stackLayout.children = [overlyLayout, insetLayout, bottomLayout]
        
        return stackLayout
    }

为了演示 ASAbsoluteLayoutSpec 的使用,这里 node3 我们用 ASAbsoluteLayoutSpec 布局。

接下来说下要点:

  1. node 和 layoutSpec 都可以设置 style 属性,因为它们都准守 ASLayoutElement 协议
  2. 当 spaceBetween 没有达到两端对齐的效果,尝试设置当前 layoutSpec 的 width(如注释)或它的上一级布局对象的 alignItems,在例子中就是 stackLayout.alignItems = .stretch
  3. ASAbsoluteLayoutSpec 必须有落点(除非是只有绝对布局),例子中 ASAbsoluteLayoutSpec 着落点就在 ASOverlayLayoutSpec

案例四

image

此案例主要为了演示 flexGrow 的用法,先介绍下 flexGrow 的作用(来自于简书九彩拼盘

该属性来设置,当父元素的宽度大于所有子元素的宽度的和时(即父元素会有剩余空间),子元素如何分配父元素的剩余空间。

flex-grow的默认值为0,意思是该元素不索取父元素的剩余空间,如果值大于0,表示索取。值越大,索取的越厉害。举个例子:

父元素宽400px,有两子元素:A和B。A宽为100px,B宽为200px,则空余空间为 400-(100+200)= 100px。

如果A,B都不索取剩余空间,则有100px的空余空间。

如果A索取剩余空间:设置flex-grow为1,B不索取。则最终A的大小为 自身宽度(100px)+ 剩余空间的宽度(100px)= 200px

如果A,B都设索取剩余空间,A设置flex-grow为1,B设置flex-grow为2。则最终A的大小为 自身宽度(100px)+ A获得的剩余空间的宽度(100px (1/(1+2))),最终B的大小为 自身宽度(200px)+ B获得的剩余空间的宽度(100px (2/(1+2)))

     override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        
        self.node1.style.height = ASDimensionMake(20.0)
        var imageLayoutArray = [ASLayoutElement]()
        
        [self.node2, self.node3, self.node4].forEach { (node) in
            let layout = ASRatioLayoutSpec(ratio: 2.0/3.0, child: node)
            layout.style.flexGrow = 1 // 相当于宽度相等
            imageLayoutArray.append(layout)
        }
        
        let imageLayout = ASStackLayoutSpec.horizontal()
        imageLayout.justifyContent = .start
        imageLayout.alignItems = .start
        imageLayout.spacing = 14.0
        imageLayout.children = imageLayoutArray
        
        let contentLayout = ASStackLayoutSpec.vertical()
        contentLayout.justifyContent = .start
        contentLayout.alignItems = .stretch
        contentLayout.spacing = 22.0
        contentLayout.children = [self.node1, imageLayout]
        
        return ASInsetLayoutSpec(insets: UIEdgeInsetsMake(22.0, 16.0, 22.0, 16.0), child: contentLayout)
    }

在这个案例中 node2、node3、node4 的宽度的总和小于父元素的宽度,所以为了达到宽度相同只需要设置三者的 flexGrow 相同就行(都为1),再通过 ASRatioLayoutSpec 固定各自的宽高比,那么对于这个三个控件来说最终的宽度是确定的。

案例四

image

此案例主要为了演示 flexShrink 的用法,同样还来自于简书九彩拼盘关于 flexShrink 的介绍

该属性来设置,当父元素的宽度小于所有子元素的宽度的和时(即子元素会超出父元素),子元素如何缩小自己的宽度的。

flex-shrink的默认值为1,当父元素的宽度小于所有子元素的宽度的和时,子元素的宽度会减小。值越大,减小的越厉害。如果值为0,表示不减小。

举个例子:父元素宽400px,有两子元素:A和B。A宽为200px,B宽为300px。则A,B总共超出父元素的宽度为(200+300)- 400 = 100px。

如果A,B都不减小宽度,即都设置flex-shrink为0,则会有100px的宽度超出父元素。如果A不减小宽度:设置flex-shrink为0,B减小。则最终B的大小为 自身宽度(300px)- 总共超出父元素的宽度(100px)= 200px如果A,B都减小宽度,A设置flex-shirk为3,B设置flex-shirk为2。则最终A的大小为 自身宽度(200px)- A减小的宽度(100px (200px 3/(200 3 + 300 2))) = 150px,最终B的大小为 自身宽度(300px)- B减小的宽度(100px (300px 2/(200 3 + 300 2))) = 250px

目前关于该属性最常见还是用于对文本的宽度限制,在上图中 textNode 和 displayNode 是两端对齐,而且需要限制文本的最大宽度,这时候设置 flexShrink 是最方便的。

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        self.displayNode.style.preferredSize = CGSize(width: 42.0, height: 18.0)
        self.textNode.style.flexShrink = 1
        
        let contentLayout = ASStackLayoutSpec.horizontal()
        contentLayout.justifyContent = .spaceBetween
        contentLayout.alignItems = .start
        contentLayout.children = [self.textNode, self.displayNode]
        
        let insetLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(16.0, 16.0, 16.0, 16.0), child: contentLayout)
        
        return insetLayout
        
    }

随便提一下的是如果 ASTextNode 出现莫名的文本截断问题,可以用 ASTextNode2 代替。

案例五

还算比较典型的例子

image

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {

        let otherLayout = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(10.0, 10.0, CGFloat(Float.infinity), CGFloat(Float.infinity)), child: topLeftNode)
        
        let contentLayout = ASOverlayLayoutSpec(child: coverImageNode, overlay: otherLayout)
        return contentLayout
    }

利用 ASInsetLayoutSpec 是最好的解决方案,值得注意的是对于红色控件只需要设置向上和向左的间距,那么其他方向的可以用 CGFloat(Float.infinity) 代替,并不需要给出具体数值。

查看原文

赞 5 收藏 4 评论 0

bawn 赞了回答 · 2017-03-24

React比Vue.js好在哪?

只有愚蠢的人类才会进行比较,有种两种都会!

关注 27 回答 17

bawn 关注了标签 · 2017-02-27

关注 0

bawn 关注了标签 · 2017-02-27

ios

iOS 是苹果公司为其移动产品开发的操作系统。它主要给 iPhone、iPod touch、iPad 以及 Apple TV 使用。原本这个系统名为 iPhone OS,直到2010年6月7日 WWDC 大会上宣布改名为 iOS。

系统结构

  iOS的系统结构分为以下四个层次:核心操作系统(the Core OS layer),核心服务层(the Core Services layer),媒体层(the Media layer),Cocoa 触摸框架层(the Cocoa Touch layer)。

发展历史

iOS最早于2007年1月9日的苹果Macworld展览会上公布,随后于同年的6月发布的第一版iOS操作系统,当初的名称为“iPhone 运行 OS X”。最初,由于没有人了解“iPhone 运行 OS X”的潜在价值和发展前景,导致没有一家软件公司、没有一个软件开发者给“iPhone 运行 OS X”开发软件或者提供软件支持。于是,苹果公司时任CEO斯蒂夫.乔布斯说服各大软件公司以及开发者可以先搭建低成本的网络应用程序(WEB APP)来使得它们能像iPhone的本地化程序一样来测试“iPhone runs OS X”平台。 

  1. 2007年10月17日,苹果公司发布了第一个本地化IPhone应用程序开发包(SDK),并且计划在2月发送到每个开发者以及开发商手中。

  2. 2008年3月6日,苹果发布了第一个测试版开发包,并且将“iPhone runs OS X”改名为”iPhone OS“。 

  3. 2010年2月27日,苹果公司发布iPad,iPad同样搭载了”iPhone OS”。这年,苹果公司重新设计了“iPhone OS”的系统结构和自带程序。 

  4. 2010年6月,苹果公司将“iPhone OS”改名为“iOS”,同时还获得了思科iOS的名称授权。 

  5. 2010年第四季度,苹果公司的iOS占据了全球智能手机操作系统26%的市场份额。

  6. 2011年10月4日,苹果公司宣布iOS平台的应用程序已经突破50万个。

  7. 2012年2月,应用总量达到552,247个,其中游戏应用最多,达到95,324个,比重为17.26%;书籍类以60,604个排在第二,比重为10.97%;娱乐应用排在第三,总量为56,998个,比重为10.32%。

  8. 2012年6月,苹果公司在WWDC 2012 上宣布了iOS 6,提供了超过 200 项新功能。

  9. 2013年9月11日凌晨苹果在秋季发布会上宣布iOS 7于9月18日正式推出,2013年9月19日凌晨1点开放免费下载更新。

  10. iOS 8于2014年9月17号向用户推送正式版。

  11. iOS 9于2015年9月16日正式推出。iOS 9系统比iOS8更稳定,功能更全面,而且还更加开放。iOS 9加入了更多的新功能,包括更加智能的Siri,新加入的省电模式。iOS 9为开发者提供5000个全新的API。

  12. 2015年12月9日,苹果正式推送了iOS 9.2,更新内容相当之多,修复BUG改善稳定性自然不必多说,还增加了很多新功能,比如邮件增加了Mail Drop功能可以发送大附件、iBooks开始支持3D Touch、Apple News新闻中的“热门报道”等等

关注 50508

bawn 发布了文章 · 2017-01-03

新大陆:AsyncDisplayKit

image

APP性能的优化,一直都是任重而道远,对于如今需要承载更多信息的APP来说更是突出,值得庆幸的苹果在这方面做得至少比安卓让开发者省心。UIKit 控件虽然在大多数情况下都能满足用户对于流畅性的需求,但有时候还是难以达到理想效果。

AsyncDisplayKit(以下简称ASDK) 的出现至少又给了开发者一个不错的选择。毕竟Paper(虽然 Facebook 已经关闭了这个应用)当年有着炫酷的效果的同时依然保持较好的流畅性也得益于 ASDK 的加入。在Paper发布的几个月后 Facebook 就干脆从中剥离出来成为一个独立的库,就在前两天 ASDK 刚好发布了 2.0 版本。

目前据我所知国内比较知名有 轻芒阅读(豌豆荚一览) 、 即刻Yep 在用ASDK。
即刻 来说包括 消息盒子主题的详情页动态通知我的喜欢评论页最近热门即刻小报他关注的人关注他的人以及搜索页 都用到了 ADSK。

目前 AsyncDisplayKit 已经从 facebook 迁移至 TextureGroup 新的项目地址是 Texture

控件

Texture 几乎涵盖了常用的控件,下面是 TextureUIKit 的对应关系,有些封装可以说非常良心。

Nodes:

TextureUIKit
ASDisplayNodeUIView
ASCellNodeUITableViewCell/UICollectionViewCell
ASTextNodeUILabel
ASImageNodeUIImageView
ASNetworkImageNodeUIImageView
ASVideoNodeAVPlayerLayer
ASControlNodeUIControl
ASScrollNodeUIScrollView
ASControlNodeUIControl
ASEditableTextNodeUITextView
ASMultiplexImageNodeUIImageView

Node Containers

TextureUIKit
ASViewControllerUIViewController
ASTableNodeUITableView
ASCollectionNodeUICollectionView
ASPagerNodeUICollectionView

子父类关系:

  • ASDisplayNode

    • ASCellNode

      • ASTextCellNode
    • ASCollectionNode

      • ASPagerNode
    • ASControlNode

      • ASButtonNode
      • ASImageNode

        • ASMapNode
        • ASMultiplexImageNode
        • ASNetworkImageNode

          • ASVideoNode
      • ASTextNode
      • ASTextNode2
    • ASEditableTextNode
    • ASScrollNode
    • ASTableNode
    • ASVideoPlayerNode

ASDisplayNode:

作用同等于UIView,是所有 Node 的父类,需要注意的是 ASDisplayNode 其实拥有一个view属性,所以ASDisplayNode及其子类都可以通过这个view来添加UIKit控件,这样一来 TextureUIKit混用是完全没问题的。

ASDisplayNode 中添加 UIKit

UIView *otherView = [[UIView alloc] init];
otherView.frame = ...;
[self.view addSubview:otherView];

ASDisplayNode *gradientNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{
    UIView *view = [[UIView alloc] init];
    return view;
}];

第二种的初始化最终生成的就是 block 返回的 UIKit 对象,但外部表现出来的是 ASDisplayNode。这样子的好处在于布局,关于布局,后面会讲到。

UIKit 中添加 ASDisplayNode

ASImageNode *imageNode = [[ASImageNode alloc] init];
imageNode.image = [UIImage imageNamed:@"iconShowMore"];
imageNode.frame = ...;
[self addSubnode:imageNode];
self.imageNode = imageNode;

ASCellNode:

作用同等于 UITableViewCellUICollectionViewCell,自带 indexPath 属性,有些时候很有用。

ASTextNode

作用同等于UILabel,和 UILabel 不同的是 ASTextNode 必须通过 attributedText 添加文字。

ASTextNode2

在 ASTextNode 基础修复了一些 Bug

ASImageNode

作用同等于 UIImageView,但是只能设置静态图片,如果需要使用网络图片,请使用 ASNetworkImageNode

ASNetworkImageNode

作用同等于 UIImageView,如果使用网络图片请使用此类,Texture 用的是第三方的图片加载库PINRemoteImageASNetworkImageNode 其实并不支持 gif,如果需要显示 gif 推荐使用FLAnimatedImage

ASButtonNode

作用同等于 UIButton,需要注意的是下面这个两个属性

@property (nonatomic, assign) CGFloat contentSpacing;// 设置图片和文字的间距
@property (nonatomic, assign) ASButtonNodeImageAlignment imageAlignment;// 图片和文字的排列方式,

简直要抱头痛哭一下😭,imageAlignment 可以设置两个值:

ASButtonNodeImageAlignmentBeginning, // 图片在前,文字在后
ASButtonNodeImageAlignmentEnd// 文字在前,图片在后

ASTableNode

作用同等于 UITableView,但是实现上并没有采用 UITableView 的重用机制,而是通过用户滚动对需要显示的视图进行add 和 不需要的进行remove 的操作(我猜的)。另外重要的一点:ASTableNode 并没有像 UITableView 一样提供一个-tableView:heightForRowAtIndexPath:协议方法来决定每个 Cell 的高度,而是由 ASCellNode 本身决定。这样带来的另外一个好处是,动态高度的实现可谓是易如反掌,具体可以看官方 Demo 中的 Kittens

如何正确的使用

对于现有的项目中出现的并不严重的性能问题,我的建议是用对应的 Texture 控件代替即可。

比如把 UIImageView -> ASImageNode/ASNetworkImageNodeUILabel -> ASTextNode之类的,而不是把原有的 UITableView -> ASTableNodeUICollectionView -> ASCollectionNode

在 Cell 中替换 UIImageView

ASImageNode *imageNode = [[ASImageNode alloc] init];
imageNode.image = [UIImage imageNamed:@"iconShowMore"];
imageNode.frame = ...;
[self.contentView addSubnode:imageNode];
self.imageNode = imageNode;

原因有以下几点:

  1. ASCellNode内部的布局会用到 Texture 本身有一套布局方案,然而这套布局学习成本略高。
  2. 包括 ASTableNodeASCollectionNode 和原生的 UITableViewUICollectionView有较大的 API 改变,侵略性较大,不太利于后期维护。
  3. 第三方的支持问题,例如 DZNEmptyDataSet 对于 ASTableNodeASCollectionNode 的支持还是有点问题。

所以当你还没有做好应付上面三个问题的准备,简单的 UIKit -> Texture 替换才是正确选择。

布局

阅读 Texture 布局篇

其他

刷新列表

无论是 ASTableNode 还是 ASCollectionNode 当列表中已经有数据显示了,调用 reloadData 你会发现列表会闪一下。最常见的案例是上拉加载更多获取到新数据后调用 reloadData 刷新列表用户体验会比较差,事实上官方文档在 [Batch Fetching API] 给出了解决办法:

- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context 
{
  // Fetch data most of the time asynchronoulsy from an API or local database
  NSArray *newPhotos = [SomeSource getNewPhotos];

  // Insert data into table or collection node
  [self insertNewRowsInTableNode:newPhotos];

  // Decide if it's still necessary to trigger more batch fetches in the future
  _stillDataToFetch = ...;

  // Properly finish the batch fetch
  [context completeBatchFetching:YES];
}

获取新数据后直接插入到列表中,而不是刷新整个列表,比如:

- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;

- (void)insertRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;

加载数据

细心的同学可能发现了前面提到内容中的就有相关的方法:

- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context;

现在绝大多数APP加载更多数据的方法都是通过下拉到列表底部再去请求数据然后添加到列表中,但是 Texture 提供了另外一种更“合理”的方式,原文是这样描述的:

By default, as a user is scrolling, when they approach the point in the table or collection where they are 2 “screens” away from the end of the current content, the table will try to fetch more data.

当列表滚到到距离底部还有两个屏幕高度请求新的数据,这个阈值是可以调整的。一旦距离底部达到两个屏幕的高度的时候,就会调用前面提到的方法。所以用起来大概是这样的:

- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context{
    [context beginBatchFetching];
    [listApi startWithBlockSuccess:^(HQHomeListApi *request) {
        @strongify(self);
        NSArray *array = [request responseJSONObject];
        [self.dataSourceArray addObjectsFromArray:array];
        [self.tableNode insertSections:[NSIndexSet indexSetWithIndexesInRange:rang] withRowAnimation:UITableViewRowAnimationNone];
        [self updateHavMore:array];
        [context completeBatchFetching:YES];
    } failure:NULL];
}

- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode{
    return self.haveMore;
}

shouldBatchFetchForTableNode 用来控制是否需要获取更多数据。这种方式优点在于在网络状况好的情况下用户都不会感受到已经加载了其他数据并显示,缺点在于网络状况不好的情况下用于即使列表已经下拉到底部也没有任何提示。

查看原文

赞 4 收藏 8 评论 0

bawn 关注了问题 · 2016-12-02

解决微信小程序点击切换内容问题

有两盒子,想点击底部粉红色区域,切换盒子,粉红色区域文字也要改变,菜鸟刚入门小程序,好多不懂啊

clipboard.png

clipboard.png

这是一个demo,结构跟项目是一样的,项目页面代码太多了,求大神解救

<view>

<view class="box1">图片版</view>
<view class="box2  none">文字版</view>

</view>

<view>

<view class="button none">点击切换到图片版</view>
<view class="button ">点击切换到文字版</view>

</view>

关注 4 回答 3

认证与成就

  • 获得 32 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-03-27
个人主页被 767 人浏览