Skip to content

Install the app's vector style from MapViewBase - #7

Merged
wf9a5m75 merged 10 commits into
mainfrom
checkpoint/vector-style-20261010
Oct 24, 2026
Merged

wf9a5m75 merged 10 commits into
mainfrom
checkpoint/vector-style-20261010

Conversation

@wf9a5m75

Copy link
Copy Markdown
Contributor

概要

MapViewBase で style: MapViewStyle? / onStyleDiagnostics を受け取り、MapViewStyleEffect でスタイルを設置するようにしました。スタイルが各バックエンドで何をするかは、ここではなく、バックエンドが登録した能力で決まります。android-sdk-core の checkpoint/vector-style-20261010(MapConductor/android-sdk-core#15)の MapViewStyle / MapViewStyleEffect を使います。

  • build.gradle.kts を ktlint に合わせて整形しました。
  • API ベースラインを更新しました。

main をマージする前に、各ファイルの改行コードを main と同じに戻しました(checkpoint のコミットで LF と CRLF の間で丸ごと書き換わっていたためです)。内容は変わっていません。

The map is an AndroidView laid under a full-screen Compose overlay, and an
event nobody consumed goes to it. So tapping the padding of a bubble, or its
title, or anywhere its content did not itself handle, was read as a tap on the
map: on the street tree page that selected whichever tree happened to sit under
the bubble and swapped the contents for another one.

"Hit the bubble and nothing wanted it" is not the same as "missed the bubble".
The overlay Box now consumes what its content leaves behind, so the first case
stops there and the second still falls through to the map.

Consumption happens on the Main pass. Compose delivers Main from the child
outwards, so by the time it arrives here the bubble's own buttons and clickable
rows have already taken theirs; consuming on Initial would have killed them.
Verified on the tablet with the Multiple Bubbles sample, whose bubbles close
themselves when tapped: the tapped one still closes and its neighbour stays
open.

The Box is the bubble's bounding rectangle, so the corners beside the tail
count as the bubble. That is a few pixels either way and the alternative is
hit-testing the drawn shape, which is not worth it to make a corner pixel pan
the map.

Panning still works, and a tap on the map away from the bubble still dismisses
it. All three InfoBubble entry points share this overlay, so all three are
fixed.
The consuming modifier was on InfoBubbleOverlay, whose Box is the bounding
rectangle. That made the corners either side of the tail count as bubble: an
area half the bubble's width by the tail's height where the map is plainly
visible and a tap did nothing.

The shape is known to DrawInfoBubble, not to the overlay, so the consuming
moves there. isInsideBubble is the same geometry the Canvas draws — the body
down to height minus the tail, plus the triangle closing linearly to its tip.
Corner rounding is ignored; at the default it is 4dp against a tail-side area
of half the width by 8dp, which is what this is about.

InfoBubbleCustom draws its own shape and so keeps its own hit behaviour. It has
no tail this code knows about and no rectangle worth guessing at.

DrawInfoBubbleHitTest pins the boundary. Consuming too much and consuming too
little both look like "it sometimes ignores me" and neither logs anything, so
the edges are worth stating: body inside, beside the tail outside, the tail
itself inside and narrowing.
The tail is 8dp at the default. Hit-testing the triangle costs a line of
geometry and a test to hold it up, and buys the case where someone aims at the
point itself — where the consequence of missing is that the map below responds,
not that something they were aiming at fails to move.

The rule is now the body rectangle, which is what the consuming was for.
Dragging the street tree map was heavy on a Pixel 5a long after the tile
rendering stopped being the reason. Frame stats put 270 to 380 ms of every
janky frame between AnimationStart and PerformTraversalsStart, with the GPU
at a 16 ms 90th percentile; simpleperf put three quarters of the main thread
in androidx.collection.ScatterSet.contains, under advanceGlobalSnapshot, under
a snapshotFlow's apply observer.

That observer belonged to the collector. To find in-place changes it read every
state's fingerprint inside one snapshotFlow, and Compose asks such an observer,
on every snapshot commit anywhere in the app, whether any object it read is
among the changed ones — one contains per object read. 144,183 markers of a
dozen fields each is about 1.7 million probes, and a drag commits a snapshot
every frame.

The loop ran the wrong way round. Asking "of everything I read, did any of it
change?" costs the size of the collection whether or not anything changed;
being told "this one changed" costs one call per actual change, and during a
drag markers do not change at all. So the states push: every mutable field
goes through StateMutationSignal.notifying, which writes through to the same
mutableStateOf — the field stays readable from a composition exactly as before
— and reports the write.

On a Pixel 5a, same 144k markers, same drag:

  janky frames   17.7%  ->  1.4%
  90th            350ms ->   21ms
  99th            700ms ->   24ms
  slow UI thread     19 ->      2

which is where an empty map already was (0.2%, 16ms, 19ms). Quartering the
markers had quartered the cost, so this was never about what was drawn.

The collector keeps its own id-to-state map of who is reporting. Membership
reaches `flow` through a debounce, so a state added and removed inside one
window is not in that map when the removal arrives; clearing by id there found
nothing and left the state reporting for the rest of the collector's life,
which the removal test caught.

Two things go with it. The FingerPrint type parameter, which existed only for
the diff — fingerPrint() stays, several providers do their own diffing with it.
And asFlow() on all six state types plus CollectAndUpdateEach, the per-state
watcher this replaced twice over: nothing called either, and they are the shape
that must not come back.

The failure mode of the swap is silent — a setter that forgets to report leaves
the renderer drawing the old value, with nothing logged and nothing thrown — so
OverlayCollectorMutationTest states the contract: a write arrives, an equal
write does not, repeats inside a window collapse to the latest, a removed state
goes quiet, and two states changing together both arrive.
OverlayCollector is @InternalMapConductorApi in android-sdk-core now: it
is the overlay collection wiring, not something an app calls, and its
type parameters changed with the collector redesign. This module is that
wiring, so it opts in per module the way android-for-* does.

The baseline follows: the collectors lose their FingerPrint type
argument, and CollectAndUpdateEach is gone with the redesign — states
report their own writes instead of being read for them.
The checkpoint commit rewrote whole files between LF and CRLF, which buried
its real changes under line-ending noise. Content is unchanged.
main and this branch each added it, in different places; Kotlin rejects
the pair as a conflicting import.
@wf9a5m75
wf9a5m75 merged commit 6eeaefa into main Oct 24, 2026
2 of 3 checks passed
@wf9a5m75
wf9a5m75 deleted the checkpoint/vector-style-20261010 branch October 24, 2026 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant