<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Platform-View on Tarragon</title><link>https://tarrragon.github.io/blog/tags/platform-view/</link><description>Recent content in Platform-View on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 06 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/platform-view/index.xml" rel="self" type="application/rss+xml"/><item><title>Flutter 畫面落後邏輯狀態（log 正確、畫面不符）— 重繪訊號排查與心跳做法</title><link>https://tarrragon.github.io/blog/work-log/flutter_repaint_heartbeat/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/flutter_repaint_heartbeat/</guid><description>&lt;h2 id="現象">現象&lt;/h2>
&lt;p>畫面上的內容明明該更新了，實際卻&lt;strong>閃一下&lt;/strong>、&lt;strong>跳過&lt;/strong>某個狀態、或&lt;strong>凍在舊畫面&lt;/strong>。但程式的 log 完全正確：狀態、計時、順序都對。&lt;/p>
&lt;p>也就是**「log 正確、畫面不符」**——邏輯狀態是對的，實體畫面卻沒跟上。&lt;/p>
&lt;h2 id="原因">原因&lt;/h2>
&lt;p>Flutter 是&lt;strong>按需重繪&lt;/strong>：有內容要求時才排下一個 frame 去 build → paint → 合成 → 呈現（排 frame 的底層原語是 &lt;code>scheduleFrame()&lt;/code>，設計意義見 &lt;a href="../flutter_schedule_frame/">scheduleFrame()：按需 render 的最底層原語&lt;/a>）。會要求排 frame 的來源有好幾種——widget 被標記 dirty（&lt;code>setState&lt;/code>、動畫、依賴變更）、animation ticker 的每個 vsync、以及&lt;strong>外部 &lt;code>Texture&lt;/code> 產生新影格時引擎自動排 frame&lt;/strong>（Android 端 &lt;code>SurfaceTexture&lt;/code> 的 frame-available 會通知引擎 &lt;code>scheduleFrame&lt;/code>）。所以正常情況下，影片 / texture 這類平台端內容更新，是會驅動重繪的。&lt;/p>
&lt;p>畫面落後邏輯狀態，發生在「該更新」這個訊號&lt;strong>沒有傳到 Flutter 的 frame 排程&lt;/strong>。實際根因通常是這兩類，而不是「平台內容一律不驅動重繪」：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>plugin / platform view 沒正確回報&lt;/strong>：某個 plugin 更新了自己的畫面，卻沒呼叫 &lt;code>markTextureFrameAvailable&lt;/code>；或 hybrid-composition platform view（原生 view 直接進 Flutter 合成樹的那種嵌入模式）的邊界情況，讓引擎不知道要排 frame。&lt;/li>
&lt;li>&lt;strong>邏輯改了值、卻沒觸發 rebuild&lt;/strong>：你更新了 controller 或某個狀態，但顯示它的 widget 沒 &lt;code>setState&lt;/code>、也沒監聽那個 controller，畫面就停在舊值。&lt;/li>
&lt;/ul>
&lt;p>這時實體畫面會停在最後一次重繪那一格，落後於邏輯狀態，表現成閃、跳、凍住。&lt;/p>
&lt;blockquote>
&lt;p>一路在業務邏輯層修（計時、狀態切換時機）往往只是把同一個症狀換個樣子。&lt;strong>當 log 正確、畫面卻不符，就該懷疑重繪／合成這一層&lt;/strong>——先查「更新訊號有沒有到 Flutter」（plugin 有沒有回報影格、你有沒有監聽 controller），再決定是接回訊號、還是定時強制刷新。&lt;/p>&lt;/blockquote>
&lt;h2 id="解法幾種做法依訊號斷點與可改動範圍選">解法：幾種做法，依訊號斷點與可改動範圍選&lt;/h2>
&lt;p>畫面落後有幾種解法，選哪一種看「更新訊號斷在哪裡」以及「能不能改到出問題的那一層」。訊號接得回來就直接接回、改不動來源就定時強制刷新——以下每一種都可以試，心跳（定時強制排 frame）也是其中一種：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>controller-backed widget（video_player 等）&lt;/strong>：這類 plugin 的 controller 是 &lt;code>ValueNotifier&lt;/code>，用 &lt;code>addListener&lt;/code> 或 &lt;code>ValueListenableBuilder&lt;/code> 監聽，狀態變就 rebuild。這是內容落後最正規的解。（這裡落後的是 controller 暴露的狀態值——播放位置、播放旗標等，跟 texture 影格是兩條路。）&lt;/li>
&lt;li>&lt;strong>真的要持續每幀刷新&lt;/strong>：用 &lt;code>Ticker&lt;/code> / &lt;code>AnimationController(vsync:)&lt;/code>，每個 vsync 排一個 frame，是「每幀刷新」的慣用機制；偶爾推一個 frame 用 &lt;code>WidgetsBinding.instance.scheduleFrame()&lt;/code>。&lt;/li>
&lt;li>&lt;strong>&lt;code>RepaintBoundary&lt;/code>&lt;/strong> 隔離重繪範圍，避免無關區域一起重畫。&lt;/li>
&lt;/ul>
&lt;h3 id="定時強制排-frame重繪心跳">定時強制排 frame（重繪心跳）&lt;/h3>
&lt;p>&lt;strong>定時強制排 frame&lt;/strong> 是不改動更新來源、直接讓合成層週期性刷新的做法：每隔一段時間重新 present 一次——composite 會把 texture 圖層的最新內容一起呈現。適合 platform 端不配合、又改不動那個 plugin 的情況。真正驅動的是「定時 &lt;code>setState&lt;/code>／&lt;code>scheduleFrame&lt;/code>」，跟畫面上顯示什麼無關（實測中一個可見的文字計數器、跟一個隱形的換色元件，效果一樣）。最小形式連 widget 都不用：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">Timer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">periodic&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">const&lt;/span> &lt;span class="n">Duration&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">milliseconds:&lt;/span> &lt;span class="m">200&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">WidgetsBinding&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">instance&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">scheduleFrame&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果偏好包成一個可以疊進畫面的隱形 widget，下面是一個版本：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 重繪心跳：讓 Flutter 持續排 frame、畫面即時刷新。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 每 200ms 重繪一次角落 1px、每次換一個肉眼看不出的顏色（內容必須有變才會觸發呈現）。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 1px、近乎全透明，不留可見痕跡。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">FramePump&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">StatefulWidget&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kd">const&lt;/span> &lt;span class="n">FramePump&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="k">super&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="n">State&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">FramePump&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">createState&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">_FramePumpState&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">_FramePumpState&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">State&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">FramePump&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kt">int&lt;/span> &lt;span class="n">_tick&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="n">Timer&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">_timer&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="n">initState&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">super&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">initState&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="n">_timer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Timer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">periodic&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">const&lt;/span> &lt;span class="n">Duration&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">milliseconds:&lt;/span> &lt;span class="m">200&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">mounted&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">setState&lt;/span>&lt;span class="p">(()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">_tick&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="n">dispose&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="n">_timer&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">cancel&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="k">super&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dispose&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl"> &lt;span class="n">Widget&lt;/span> &lt;span class="n">build&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BuildContext&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">IgnorePointer&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl"> &lt;span class="nl">child:&lt;/span> &lt;span class="n">Align&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl"> &lt;span class="nl">alignment:&lt;/span> &lt;span class="n">Alignment&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">topLeft&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl"> &lt;span class="nl">child:&lt;/span> &lt;span class="n">SizedBox&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl"> &lt;span class="nl">width:&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl"> &lt;span class="nl">height:&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl"> &lt;span class="nl">child:&lt;/span> &lt;span class="n">CustomPaint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">painter:&lt;/span> &lt;span class="n">_FramePumpPainter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_tick&lt;/span>&lt;span class="p">)),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">39&lt;/span>&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">43&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">44&lt;/span>&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">_FramePumpPainter&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">CustomPainter&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">45&lt;/span>&lt;span class="cl"> &lt;span class="kd">const&lt;/span> &lt;span class="n">_FramePumpPainter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tick&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">46&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">47&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">tick&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">48&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">49&lt;/span>&lt;span class="cl"> &lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">50&lt;/span>&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="n">paint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Canvas&lt;/span> &lt;span class="n">canvas&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Size&lt;/span> &lt;span class="n">size&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">51&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 在兩個近乎全透明、肉眼看不出的顏色間交替；內容有變才會觸發重新呈現
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">52&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">paint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Paint&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">53&lt;/span>&lt;span class="cl"> &lt;span class="p">..&lt;/span>&lt;span class="n">color&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">tick&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">isEven&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="kd">const&lt;/span> &lt;span class="n">Color&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mh">0x01000000&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="kd">const&lt;/span> &lt;span class="n">Color&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mh">0x01010000&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">54&lt;/span>&lt;span class="cl"> &lt;span class="n">canvas&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">drawRect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">const&lt;/span> &lt;span class="n">Rect&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">fromLTWH&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">paint&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">55&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">56&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">57&lt;/span>&lt;span class="cl"> &lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">58&lt;/span>&lt;span class="cl"> &lt;span class="kt">bool&lt;/span> &lt;span class="n">shouldRepaint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_FramePumpPainter&lt;/span> &lt;span class="n">oldDelegate&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">oldDelegate&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tick&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">tick&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">59&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>疊進畫面（放在 Stack 最上層即可）：&lt;/p></description><content:encoded><![CDATA[<h2 id="現象">現象</h2>
<p>畫面上的內容明明該更新了，實際卻<strong>閃一下</strong>、<strong>跳過</strong>某個狀態、或<strong>凍在舊畫面</strong>。但程式的 log 完全正確：狀態、計時、順序都對。</p>
<p>也就是**「log 正確、畫面不符」**——邏輯狀態是對的，實體畫面卻沒跟上。</p>
<h2 id="原因">原因</h2>
<p>Flutter 是<strong>按需重繪</strong>：有內容要求時才排下一個 frame 去 build → paint → 合成 → 呈現（排 frame 的底層原語是 <code>scheduleFrame()</code>，設計意義見 <a href="../flutter_schedule_frame/">scheduleFrame()：按需 render 的最底層原語</a>）。會要求排 frame 的來源有好幾種——widget 被標記 dirty（<code>setState</code>、動畫、依賴變更）、animation ticker 的每個 vsync、以及<strong>外部 <code>Texture</code> 產生新影格時引擎自動排 frame</strong>（Android 端 <code>SurfaceTexture</code> 的 frame-available 會通知引擎 <code>scheduleFrame</code>）。所以正常情況下，影片 / texture 這類平台端內容更新，是會驅動重繪的。</p>
<p>畫面落後邏輯狀態，發生在「該更新」這個訊號<strong>沒有傳到 Flutter 的 frame 排程</strong>。實際根因通常是這兩類，而不是「平台內容一律不驅動重繪」：</p>
<ul>
<li><strong>plugin / platform view 沒正確回報</strong>：某個 plugin 更新了自己的畫面，卻沒呼叫 <code>markTextureFrameAvailable</code>；或 hybrid-composition platform view（原生 view 直接進 Flutter 合成樹的那種嵌入模式）的邊界情況，讓引擎不知道要排 frame。</li>
<li><strong>邏輯改了值、卻沒觸發 rebuild</strong>：你更新了 controller 或某個狀態，但顯示它的 widget 沒 <code>setState</code>、也沒監聽那個 controller，畫面就停在舊值。</li>
</ul>
<p>這時實體畫面會停在最後一次重繪那一格，落後於邏輯狀態，表現成閃、跳、凍住。</p>
<blockquote>
<p>一路在業務邏輯層修（計時、狀態切換時機）往往只是把同一個症狀換個樣子。<strong>當 log 正確、畫面卻不符，就該懷疑重繪／合成這一層</strong>——先查「更新訊號有沒有到 Flutter」（plugin 有沒有回報影格、你有沒有監聽 controller），再決定是接回訊號、還是定時強制刷新。</p></blockquote>
<h2 id="解法幾種做法依訊號斷點與可改動範圍選">解法：幾種做法，依訊號斷點與可改動範圍選</h2>
<p>畫面落後有幾種解法，選哪一種看「更新訊號斷在哪裡」以及「能不能改到出問題的那一層」。訊號接得回來就直接接回、改不動來源就定時強制刷新——以下每一種都可以試，心跳（定時強制排 frame）也是其中一種：</p>
<ul>
<li><strong>controller-backed widget（video_player 等）</strong>：這類 plugin 的 controller 是 <code>ValueNotifier</code>，用 <code>addListener</code> 或 <code>ValueListenableBuilder</code> 監聽，狀態變就 rebuild。這是內容落後最正規的解。（這裡落後的是 controller 暴露的狀態值——播放位置、播放旗標等，跟 texture 影格是兩條路。）</li>
<li><strong>真的要持續每幀刷新</strong>：用 <code>Ticker</code> / <code>AnimationController(vsync:)</code>，每個 vsync 排一個 frame，是「每幀刷新」的慣用機制；偶爾推一個 frame 用 <code>WidgetsBinding.instance.scheduleFrame()</code>。</li>
<li><strong><code>RepaintBoundary</code></strong> 隔離重繪範圍，避免無關區域一起重畫。</li>
</ul>
<h3 id="定時強制排-frame重繪心跳">定時強制排 frame（重繪心跳）</h3>
<p><strong>定時強制排 frame</strong> 是不改動更新來源、直接讓合成層週期性刷新的做法：每隔一段時間重新 present 一次——composite 會把 texture 圖層的最新內容一起呈現。適合 platform 端不配合、又改不動那個 plugin 的情況。真正驅動的是「定時 <code>setState</code>／<code>scheduleFrame</code>」，跟畫面上顯示什麼無關（實測中一個可見的文字計數器、跟一個隱形的換色元件，效果一樣）。最小形式連 widget 都不用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">Timer</span><span class="p">.</span><span class="n">periodic</span><span class="p">(</span><span class="kd">const</span> <span class="n">Duration</span><span class="p">(</span><span class="nl">milliseconds:</span> <span class="m">200</span><span class="p">),</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">WidgetsBinding</span><span class="p">.</span><span class="n">instance</span><span class="p">.</span><span class="n">scheduleFrame</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>如果偏好包成一個可以疊進畫面的隱形 widget，下面是一個版本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">/// 重繪心跳：讓 Flutter 持續排 frame、畫面即時刷新。
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 每 200ms 重繪一次角落 1px、每次換一個肉眼看不出的顏色（內容必須有變才會觸發呈現）。
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 1px、近乎全透明，不留可見痕跡。
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">FramePump</span> <span class="kd">extends</span> <span class="n">StatefulWidget</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">const</span> <span class="n">FramePump</span><span class="p">({</span><span class="k">super</span><span class="p">.</span><span class="n">key</span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="n">State</span><span class="o">&lt;</span><span class="n">FramePump</span><span class="o">&gt;</span> <span class="n">createState</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">_FramePumpState</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kd">class</span> <span class="nc">_FramePumpState</span> <span class="kd">extends</span> <span class="n">State</span><span class="o">&lt;</span><span class="n">FramePump</span><span class="o">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kt">int</span> <span class="n">_tick</span> <span class="o">=</span> <span class="m">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="n">Timer</span><span class="o">?</span> <span class="n">_timer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="kt">void</span> <span class="n">initState</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">super</span><span class="p">.</span><span class="n">initState</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">_timer</span> <span class="o">=</span> <span class="n">Timer</span><span class="p">.</span><span class="n">periodic</span><span class="p">(</span><span class="kd">const</span> <span class="n">Duration</span><span class="p">(</span><span class="nl">milliseconds:</span> <span class="m">200</span><span class="p">),</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="n">mounted</span><span class="p">)</span> <span class="n">setState</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="n">_tick</span><span class="o">++</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="kt">void</span> <span class="n">dispose</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="n">_timer</span><span class="o">?</span><span class="p">.</span><span class="n">cancel</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="k">super</span><span class="p">.</span><span class="n">dispose</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</span></span><span class="line"><span class="ln">29</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="k">return</span> <span class="n">IgnorePointer</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">      <span class="nl">child:</span> <span class="n">Align</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="nl">alignment:</span> <span class="n">Alignment</span><span class="p">.</span><span class="n">topLeft</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="nl">child:</span> <span class="n">SizedBox</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">          <span class="nl">width:</span> <span class="m">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">          <span class="nl">height:</span> <span class="m">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">          <span class="nl">child:</span> <span class="n">CustomPaint</span><span class="p">(</span><span class="nl">painter:</span> <span class="n">_FramePumpPainter</span><span class="p">(</span><span class="n">_tick</span><span class="p">)),</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">
</span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="kd">class</span> <span class="nc">_FramePumpPainter</span> <span class="kd">extends</span> <span class="n">CustomPainter</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">  <span class="kd">const</span> <span class="n">_FramePumpPainter</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">tick</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">
</span></span><span class="line"><span class="ln">47</span><span class="cl">  <span class="kd">final</span> <span class="kt">int</span> <span class="n">tick</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">
</span></span><span class="line"><span class="ln">49</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">  <span class="kt">void</span> <span class="n">paint</span><span class="p">(</span><span class="n">Canvas</span> <span class="n">canvas</span><span class="p">,</span> <span class="n">Size</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">    <span class="c1">// 在兩個近乎全透明、肉眼看不出的顏色間交替；內容有變才會觸發重新呈現
</span></span></span><span class="line"><span class="ln">52</span><span class="cl"><span class="c1"></span>    <span class="kd">final</span> <span class="n">paint</span> <span class="o">=</span> <span class="n">Paint</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">      <span class="p">..</span><span class="n">color</span> <span class="o">=</span> <span class="n">tick</span><span class="p">.</span><span class="n">isEven</span> <span class="o">?</span> <span class="kd">const</span> <span class="n">Color</span><span class="p">(</span><span class="mh">0x01000000</span><span class="p">)</span> <span class="o">:</span> <span class="kd">const</span> <span class="n">Color</span><span class="p">(</span><span class="mh">0x01010000</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">    <span class="n">canvas</span><span class="p">.</span><span class="n">drawRect</span><span class="p">(</span><span class="kd">const</span> <span class="n">Rect</span><span class="p">.</span><span class="n">fromLTWH</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="m">1</span><span class="p">),</span> <span class="n">paint</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">
</span></span><span class="line"><span class="ln">57</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">  <span class="kt">bool</span> <span class="n">shouldRepaint</span><span class="p">(</span><span class="n">_FramePumpPainter</span> <span class="n">oldDelegate</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">oldDelegate</span><span class="p">.</span><span class="n">tick</span> <span class="o">!=</span> <span class="n">tick</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>疊進畫面（放在 Stack 最上層即可）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">Stack</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nl">fit:</span> <span class="n">StackFit</span><span class="p">.</span><span class="n">expand</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nl">children:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">yourContentLayer</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="kd">const</span> <span class="n">FramePump</span><span class="p">(),</span> <span class="c1">// 重繪心跳
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="p">],</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><h3 id="更精簡scheduleframe-版包成-widget">更精簡：scheduleFrame 版包成 widget</h3>
<p>不需要 <code>CustomPaint</code> 換色那套——用 timer 定時 <code>scheduleFrame</code>、widget 本身什麼都不畫，同時把 timer 的生命週期收在 <code>initState</code>／<code>dispose</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">class</span> <span class="nc">FramePump</span> <span class="kd">extends</span> <span class="n">StatefulWidget</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">const</span> <span class="n">FramePump</span><span class="p">({</span><span class="k">super</span><span class="p">.</span><span class="n">key</span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="n">State</span><span class="o">&lt;</span><span class="n">FramePump</span><span class="o">&gt;</span> <span class="n">createState</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">_FramePumpState</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kd">class</span> <span class="nc">_FramePumpState</span> <span class="kd">extends</span> <span class="n">State</span><span class="o">&lt;</span><span class="n">FramePump</span><span class="o">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="n">Timer</span><span class="o">?</span> <span class="n">_timer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kt">void</span> <span class="n">initState</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">super</span><span class="p">.</span><span class="n">initState</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">_timer</span> <span class="o">=</span> <span class="n">Timer</span><span class="p">.</span><span class="n">periodic</span><span class="p">(</span><span class="kd">const</span> <span class="n">Duration</span><span class="p">(</span><span class="nl">milliseconds:</span> <span class="m">200</span><span class="p">),</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="n">WidgetsBinding</span><span class="p">.</span><span class="n">instance</span><span class="p">.</span><span class="n">scheduleFrame</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="kt">void</span> <span class="n">dispose</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="n">_timer</span><span class="o">?</span><span class="p">.</span><span class="n">cancel</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">super</span><span class="p">.</span><span class="n">dispose</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kd">const</span> <span class="n">SizedBox</span><span class="p">.</span><span class="n">shrink</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不 <code>setState</code>、不重繪任何 widget，只是每 200ms 請引擎排一個 frame；比 <code>setState + CustomPaint</code> 版少了 rebuild 與 paint 的開銷，是實務上比較推薦的隱形 widget 形式。</p>
<h2 id="幾個關鍵細節">幾個關鍵細節</h2>
<ul>
<li><strong>驅動的是 tick、不是顏色</strong>：<code>setState(() =&gt; _tick++)</code> 讓 widget dirty、排一個 frame，<code>shouldRepaint</code> 比對 tick（每次必變）觸發 paint。上面兩個 <code>const</code> 顏色交替其實可省——<code>shouldRepaint</code> 根本沒看顏色，而且 present 出去的是整張重新合成的 frame，引擎不會因為某層像素相同就跳過呈現。要更精簡就用前面的 <code>scheduleFrame</code> 版、連 CustomPaint 都不需要。</li>
<li><strong>要隱形</strong>：1px、alpha 只有 <code>0x01</code>（近乎全透明），肉眼看不出。</li>
<li><strong>頻率</strong>：200ms（約 5fps）通常就夠讓內容變化即時呈現，不必 60fps 狂刷。</li>
<li><strong><code>IgnorePointer</code></strong>：避免這層攔截觸控。</li>
<li><strong>背景不排 frame</strong>：<code>scheduleFrame</code> 受 <code>framesEnabled</code> 約束，app 進背景時不會排——前景播放的情境沒問題，若真要不論可見性都推 frame 才需 <code>scheduleForcedFrame()</code>。</li>
</ul>
<h2 id="判斷是否是這個問題">判斷是否是這個問題</h2>
<ul>
<li><strong>log 顯示邏輯正確</strong>（狀態、計時、順序都對），但實體畫面落後、閃、跳、凍住。</li>
<li>內容以<strong>靜態為主</strong>，或含 <strong>platform view / 外部 texture</strong>（影片播放器等），而畫面更新的訊號沒反映到 Flutter 的 frame 排程。</li>
</ul>
<p>符合以上，先查「更新訊號有沒有到 Flutter」——plugin 有沒有正確回報 texture 影格、你有沒有監聽 controller。訊號接得回來就接回；platform 端不配合、又改不動時，就用定時強制排 frame 定時刷新，不必繼續在業務邏輯層打轉。</p>
]]></content:encoded></item></channel></rss>