mirror of
https://github.com/HackPlan/AsyncDisplayKit.git
synced 2026-04-07 08:47:45 +08:00
307 lines
22 KiB
HTML
307 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width initial-scale=1" />
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
|
||
<meta property="og:title" content="Custom nodes — AsyncDisplayKit">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:url" content="http://asyncdisplaykit.org/guide/2/">
|
||
<meta property="og:image" content="http://asyncdisplaykit.org/assets/logo-square.png">
|
||
<meta property="og:description" content="Smooth asynchronous user interfaces for iOS apps">
|
||
|
||
<title>Custom nodes — AsyncDisplayKit</title>
|
||
<meta name="description" content="Smooth asynchronous user interfaces for iOS apps.">
|
||
|
||
<link rel="stylesheet" href="/css/main.css">
|
||
<link rel="canonical" href="http://asyncdisplaykit.org/guide/2/">
|
||
|
||
<script>
|
||
if (location.host == "facebook.github.io") {
|
||
// get outta here
|
||
location = 'http://asyncdisplaykit.org';
|
||
}
|
||
</script>
|
||
</head>
|
||
|
||
|
||
<body>
|
||
|
||
<header class="site-header">
|
||
|
||
<div class="wrapper">
|
||
|
||
<a class="site-title" href="/">AsyncDisplayKit</a>
|
||
|
||
<nav class="site-nav">
|
||
<a href="#" class="menu-icon">
|
||
<svg viewBox="0 0 18 15">
|
||
<path fill="#424242" d="M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.031C17.335,0,18,0.665,18,1.484L18,1.484z"/>
|
||
<path fill="#424242" d="M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0c0-0.82,0.665-1.484,1.484-1.484 h15.031C17.335,6.031,18,6.696,18,7.516L18,7.516z"/>
|
||
<path fill="#424242" d="M18,13.516C18,14.335,17.335,15,16.516,15H1.484C0.665,15,0,14.335,0,13.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.031C17.335,12.031,18,12.696,18,13.516L18,13.516z"/>
|
||
</svg>
|
||
</a>
|
||
|
||
<div class="trigger">
|
||
<a class="page-link page-link-active" href="/guide">guide</a>
|
||
<a class="page-link" href="/appledoc">api</a>
|
||
<a class="page-link" href="https://github.com/facebook/AsyncDisplayKit">github</a>
|
||
</div>
|
||
</nav>
|
||
|
||
</div>
|
||
|
||
</header>
|
||
|
||
|
||
<div class="page-content">
|
||
<div class="wrapper">
|
||
<div class="post">
|
||
|
||
<header class="post-header">
|
||
<h1 class="post-title">
|
||
Custom nodes
|
||
<a class="edit-page-link" href="https://github.com/facebook/AsyncDisplayKit/tree/master/docs/guide/2-custom-nodes.md" target="_blank">[edit]</a>
|
||
</h1>
|
||
</header>
|
||
|
||
<article class="post-content">
|
||
<h2>View hierarchies</h2>
|
||
|
||
<p>Sizing and layout of custom view hierarchies are typically done all at once on
|
||
the main thread. For example, a custom UIView that minimally encloses a text
|
||
view and an image view might look like this:</p>
|
||
<div class="highlight"><pre><code class="language-objective-c" data-lang="objective-c"><span class="p">-</span> <span class="p">(</span><span class="bp">CGSize</span><span class="p">)</span><span class="nf">sizeThatFits:</span><span class="p">(</span><span class="bp">CGSize</span><span class="p">)</span><span class="nv">size</span>
|
||
<span class="p">{</span>
|
||
<span class="c1">// size the image</span>
|
||
<span class="bp">CGSize</span> <span class="n">imageSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_imageView</span> <span class="nl">sizeThatFits</span><span class="p">:</span><span class="n">size</span><span class="p">];</span>
|
||
|
||
<span class="c1">// size the text view</span>
|
||
<span class="bp">CGSize</span> <span class="n">maxTextSize</span> <span class="o">=</span> <span class="n">CGSizeMake</span><span class="p">(</span><span class="n">size</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">size</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
|
||
<span class="bp">CGSize</span> <span class="n">textSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_textView</span> <span class="nl">sizeThatFits</span><span class="p">:</span><span class="n">maxTextSize</span><span class="p">];</span>
|
||
|
||
<span class="c1">// make sure everything fits</span>
|
||
<span class="n">CGFloat</span> <span class="n">minHeight</span> <span class="o">=</span> <span class="n">MAX</span><span class="p">(</span><span class="n">imageSize</span><span class="p">.</span><span class="n">height</span><span class="p">,</span> <span class="n">textSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
|
||
<span class="k">return</span> <span class="n">CGSizeMake</span><span class="p">(</span><span class="n">size</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">minHeight</span><span class="p">);</span>
|
||
<span class="p">}</span>
|
||
|
||
<span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">layoutSubviews</span>
|
||
<span class="p">{</span>
|
||
<span class="bp">CGSize</span> <span class="n">size</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="n">bounds</span><span class="p">.</span><span class="n">size</span><span class="p">;</span> <span class="c1">// convenience</span>
|
||
|
||
<span class="c1">// size and layout the image</span>
|
||
<span class="bp">CGSize</span> <span class="n">imageSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_imageView</span> <span class="nl">sizeThatFits</span><span class="p">:</span><span class="n">size</span><span class="p">];</span>
|
||
<span class="n">_imageView</span><span class="p">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">CGRectMake</span><span class="p">(</span><span class="n">size</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="mf">0.0f</span><span class="p">,</span>
|
||
<span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
|
||
|
||
<span class="c1">// size and layout the text view</span>
|
||
<span class="bp">CGSize</span> <span class="n">maxTextSize</span> <span class="o">=</span> <span class="n">CGSizeMake</span><span class="p">(</span><span class="n">size</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">size</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
|
||
<span class="bp">CGSize</span> <span class="n">textSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_textView</span> <span class="nl">sizeThatFits</span><span class="p">:</span><span class="n">maxTextSize</span><span class="p">];</span>
|
||
<span class="n">_textView</span><span class="p">.</span><span class="n">frame</span> <span class="o">=</span> <span class="p">(</span><span class="bp">CGRect</span><span class="p">){</span> <span class="n">CGPointZero</span><span class="p">,</span> <span class="n">textSize</span> <span class="p">};</span>
|
||
<span class="p">}</span>
|
||
</code></pre></div>
|
||
<p>This isn't ideal. We're sizing our subviews twice — once to figure out
|
||
how big our view needs to be and once when laying it out — and while our
|
||
layout arithmetic is cheap and quick, we're also blocking the main thread on
|
||
expensive text sizing.</p>
|
||
|
||
<p>We could improve the situation by manually cacheing our subviews' sizes, but
|
||
that solution comes with its own set of problems. Just adding <code>_imageSize</code> and
|
||
<code>_textSize</code> ivars wouldn't be enough: for example, if the text were to change,
|
||
we'd need to recompute its size. The boilerplate would quickly become
|
||
untenable.</p>
|
||
|
||
<p>Further, even with a cache, we'll still be blocking the main thread on sizing
|
||
<em>sometimes</em>. We could try to shift sizing to a background thread with
|
||
<code>dispatch_async()</code>, but even if our own code is thread-safe, UIView methods are
|
||
documented to <a href="https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/index.html">only work on the main
|
||
thread</a>:</p>
|
||
|
||
<blockquote>
|
||
<p>Manipulations to your application’s user interface must occur on the main
|
||
thread. Thus, you should always call the methods of the UIView class from
|
||
code running in the main thread of your application. The only time this may
|
||
not be strictly necessary is when creating the view object itself but all
|
||
other manipulations should occur on the main thread.</p>
|
||
</blockquote>
|
||
|
||
<p>This is a pretty deep rabbit hole. We could attempt to work around the fact
|
||
that UILabels and UITextViews cannot safely be sized on background threads by
|
||
manually creating a TextKit stack and sizing the text ourselves... but that's a
|
||
laborious duplication of work. Further, if UITextView's layout behaviour
|
||
changes in an iOS update, our sizing code will break. (And did we mention that
|
||
TextKit isn't thread-safe either?)</p>
|
||
|
||
<h2>Node hierarchies</h2>
|
||
|
||
<p>Enter AsyncDisplayKit. Our custom node looks like this:</p>
|
||
<div class="highlight"><pre><code class="language-objective-c" data-lang="objective-c"><span class="cp">#import <AsyncDisplayKit/AsyncDisplayKit+Subclasses.h></span>
|
||
|
||
<span class="p">...</span>
|
||
|
||
<span class="c1">// perform expensive sizing operations on a background thread</span>
|
||
<span class="o">-</span> <span class="p">(</span><span class="bp">CGSize</span><span class="p">)</span><span class="nl">calculateSizeThatFits</span><span class="p">:(</span><span class="bp">CGSize</span><span class="p">)</span><span class="n">constrainedSize</span>
|
||
<span class="p">{</span>
|
||
<span class="c1">// size the image</span>
|
||
<span class="bp">CGSize</span> <span class="n">imageSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_imageNode</span> <span class="nl">measure</span><span class="p">:</span><span class="n">constrainedSize</span><span class="p">];</span>
|
||
|
||
<span class="c1">// size the text node</span>
|
||
<span class="bp">CGSize</span> <span class="n">maxTextSize</span> <span class="o">=</span> <span class="n">CGSizeMake</span><span class="p">(</span><span class="n">constrainedSize</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span>
|
||
<span class="n">constrainedSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
|
||
<span class="bp">CGSize</span> <span class="n">textSize</span> <span class="o">=</span> <span class="p">[</span><span class="n">_textNode</span> <span class="nl">measure</span><span class="p">:</span><span class="n">maxTextSize</span><span class="p">];</span>
|
||
|
||
<span class="c1">// make sure everything fits</span>
|
||
<span class="n">CGFloat</span> <span class="n">minHeight</span> <span class="o">=</span> <span class="n">MAX</span><span class="p">(</span><span class="n">imageSize</span><span class="p">.</span><span class="n">height</span><span class="p">,</span> <span class="n">textSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
|
||
<span class="k">return</span> <span class="nf">CGSizeMake</span><span class="p">(</span><span class="n">constrainedSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">minHeight</span><span class="p">);</span>
|
||
<span class="p">}</span>
|
||
|
||
<span class="c1">// do as little work as possible in main-thread layout</span>
|
||
<span class="o">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">layout</span>
|
||
<span class="p">{</span>
|
||
<span class="c1">// layout the image using its cached size</span>
|
||
<span class="bp">CGSize</span> <span class="n">imageSize</span> <span class="o">=</span> <span class="n">_imageNode</span><span class="p">.</span><span class="n">calculatedSize</span><span class="p">;</span>
|
||
<span class="n">_imageNode</span><span class="p">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">CGRectMake</span><span class="p">(</span><span class="nb">self</span><span class="p">.</span><span class="n">bounds</span><span class="p">.</span><span class="n">size</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="mf">0.0f</span><span class="p">,</span>
|
||
<span class="n">imageSize</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">imageSize</span><span class="p">.</span><span class="n">height</span><span class="p">);</span>
|
||
|
||
<span class="c1">// layout the text view using its cached size</span>
|
||
<span class="bp">CGSize</span> <span class="n">textSize</span> <span class="o">=</span> <span class="n">_textNode</span><span class="p">.</span><span class="n">calculatedSize</span><span class="p">;</span>
|
||
<span class="n">_textNode</span><span class="p">.</span><span class="n">frame</span> <span class="o">=</span> <span class="p">(</span><span class="bp">CGRect</span><span class="p">){</span> <span class="n">CGPointZero</span><span class="p">,</span> <span class="n">textSize</span> <span class="p">};</span>
|
||
<span class="p">}</span>
|
||
</code></pre></div>
|
||
<p>ASImageNode and ASTextNode, like the rest of AsyncDisplayKit, are thread-safe,
|
||
so we can size them on background threads. The <code>-measure:</code> method is like
|
||
<code>-sizeThatFits:</code>, but with side effects: it caches both the argument
|
||
(<code>constrainedSizeForCalculatedSize</code>) and the result (<code>calculatedSize</code>) for
|
||
quick access later on — like in our now-snappy <code>-layout</code> implementation.</p>
|
||
|
||
<p>As you can see, node hierarchies are sized and laid out in much the same way as
|
||
their view counterparts. Custom nodes do need to be written with a few things
|
||
in mind:</p>
|
||
|
||
<ul>
|
||
<li><p>Nodes must recursively measure all of their subnodes in their
|
||
<code>-calculateSizeThatFits:</code> implementations. Note that the <code>-measure:</code>
|
||
machinery will only call <code>-calculateSizeThatFits:</code> if a new measurement pass
|
||
is needed (e.g., if the constrained size has changed).</p></li>
|
||
<li><p>Nodes should perform any other expensive pre-layout calculations in
|
||
<code>-calculateSizeThatFits:</code>, cacheing useful intermediate results in ivars as
|
||
appropriate.</p></li>
|
||
<li><p>Nodes should call <code>[self invalidateCalculatedSize]</code> when necessary. For
|
||
example, ASTextNode invalidates its calculated size when its
|
||
<code>attributedString</code> property is changed.</p></li>
|
||
</ul>
|
||
|
||
<p>For more examples of custom sizing and layout, along with a demo of
|
||
ASTextNode's features, check out <code>BlurbNode</code> and <code>KittenNode</code> in the
|
||
<a href="https://github.com/facebook/AsyncDisplayKit/tree/master/examples/Kittens">Kittens</a>
|
||
sample project.</p>
|
||
|
||
<h2>Custom drawing</h2>
|
||
|
||
<p>To guarantee thread safety in its highly-concurrent drawing system, the node
|
||
drawing API diverges substantially from UIView's. Instead of implementing
|
||
<code>-drawRect:</code>, you must:</p>
|
||
|
||
<ol>
|
||
<li><p>Define an internal "draw parameters" class for your custom node. This
|
||
class should be able to store any state your node needs to draw itself
|
||
— it can be a plain old NSObject or even a dictionary.</p></li>
|
||
<li><p>Return a configured instance of your draw parameters class in
|
||
<code>-drawParametersForAsyncLayer:</code>. This method will always be called on the
|
||
main thread.</p></li>
|
||
<li><p>Implement either <code>+drawRect:withParameters:isCancelled:isRasterizing:</code> or
|
||
<code>+displayWithParameters:isCancelled:</code>. Note that these are <em>class</em> methods
|
||
that will not have access to your node's state — only the draw
|
||
parameters object. They can be called on any thread and must be
|
||
thread-safe.</p></li>
|
||
</ol>
|
||
|
||
<p>For example, this node will draw a rainbow:</p>
|
||
<div class="highlight"><pre><code class="language-objective-c" data-lang="objective-c"><span class="k">@interface</span> <span class="nc">RainbowNode</span> : <span class="nc">ASDisplayNode</span>
|
||
<span class="k">@end</span>
|
||
|
||
<span class="k">@implementation</span> <span class="nc">RainbowNode</span>
|
||
|
||
<span class="p">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">drawRect:</span><span class="p">(</span><span class="bp">CGRect</span><span class="p">)</span><span class="nv">bounds</span>
|
||
<span class="nf">withParameters:</span><span class="p">(</span><span class="kt">id</span><span class="o"><</span><span class="bp">NSObject</span><span class="o">></span><span class="p">)</span><span class="nv">parameters</span>
|
||
<span class="nf">isCancelled:</span><span class="p">(</span><span class="kt">asdisplaynode_iscancelled_block_t</span><span class="p">)</span><span class="nv">isCancelledBlock</span>
|
||
<span class="nf">isRasterizing:</span><span class="p">(</span><span class="kt">BOOL</span><span class="p">)</span><span class="nv">isRasterizing</span>
|
||
<span class="p">{</span>
|
||
<span class="c1">// clear the backing store, but only if we're not rasterising into another layer</span>
|
||
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isRasterizing</span><span class="p">)</span> <span class="p">{</span>
|
||
<span class="p">[[</span><span class="bp">UIColor</span> <span class="n">whiteColor</span><span class="p">]</span> <span class="n">set</span><span class="p">];</span>
|
||
<span class="n">UIRectFill</span><span class="p">(</span><span class="n">bounds</span><span class="p">);</span>
|
||
<span class="p">}</span>
|
||
|
||
<span class="c1">// UIColor sadly lacks +indigoColor and +violetColor methods</span>
|
||
<span class="bp">NSArray</span> <span class="o">*</span><span class="n">colors</span> <span class="o">=</span> <span class="l">@[</span> <span class="p">[</span><span class="bp">UIColor</span> <span class="n">redColor</span><span class="p">],</span>
|
||
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">orangeColor</span><span class="p">],</span>
|
||
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">yellowColor</span><span class="p">],</span>
|
||
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">greenColor</span><span class="p">],</span>
|
||
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">blueColor</span><span class="p">],</span>
|
||
<span class="p">[</span><span class="bp">UIColor</span> <span class="n">purpleColor</span><span class="p">]</span> <span class="l">]</span><span class="p">;</span>
|
||
<span class="n">CGFloat</span> <span class="n">stripeHeight</span> <span class="o">=</span> <span class="n">roundf</span><span class="p">(</span><span class="n">bounds</span><span class="p">.</span><span class="n">size</span><span class="p">.</span><span class="n">height</span> <span class="o">/</span> <span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">colors</span><span class="p">.</span><span class="n">count</span><span class="p">);</span>
|
||
|
||
<span class="c1">// draw the stripes</span>
|
||
<span class="k">for</span> <span class="p">(</span><span class="bp">UIColor</span> <span class="o">*</span><span class="n">color</span> <span class="k">in</span> <span class="n">colors</span><span class="p">)</span> <span class="p">{</span>
|
||
<span class="bp">CGRect</span> <span class="n">stripe</span> <span class="o">=</span> <span class="n">CGRectZero</span><span class="p">;</span>
|
||
<span class="n">CGRectDivide</span><span class="p">(</span><span class="n">bounds</span><span class="p">,</span> <span class="o">&</span><span class="n">stripe</span><span class="p">,</span> <span class="o">&</span><span class="n">bounds</span><span class="p">,</span> <span class="n">stripeHeight</span><span class="p">,</span> <span class="n">CGRectMinYEdge</span><span class="p">);</span>
|
||
<span class="p">[</span><span class="n">color</span> <span class="n">set</span><span class="p">];</span>
|
||
<span class="n">UIRectFill</span><span class="p">(</span><span class="n">stripe</span><span class="p">);</span>
|
||
<span class="p">}</span>
|
||
<span class="p">}</span>
|
||
|
||
<span class="k">@end</span>
|
||
</code></pre></div>
|
||
<p>This could easily be extended to support vertical rainbows too, by adding a
|
||
<code>vertical</code> property to the node, exporting it in
|
||
<code>-drawParametersForAsyncLayer:</code>, and referencing it in
|
||
<code>+drawRect:withParameters:isCancelled:isRasterizing:</code>. More-complex nodes can
|
||
be supported in much the same way.</p>
|
||
|
||
<p>For more on custom nodes, check out the <a href="https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit/ASDisplayNode%2BSubclasses.h">subclassing
|
||
header</a>
|
||
or read on!</p>
|
||
|
||
</article>
|
||
|
||
<div class="docs-prevnext">
|
||
|
||
<a class="docs-prev" href="/guide/">← prev</a>
|
||
|
||
|
||
<a class="docs-next" href="/guide/3/">next →</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<footer class="site-footer">
|
||
|
||
<div class="wrapper">
|
||
<div class="footer-col-wrapper">
|
||
<div class="footer-col footer-col-left">
|
||
<p class="text">a Facebook & Instagram collaboration ♥</p>
|
||
</div>
|
||
|
||
<div class="footer-col footer-col-right">
|
||
<p class="text">
|
||
© 2014 Facebook Inc (<a href="/license/">CC-BY-4.0</a>)
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</footer>
|
||
|
||
|
||
</body>
|
||
|
||
</html>
|