O que isto mostra
Fieldwork — the field-capture shape: a checklist of device captures, every one of which can fail.
gradlew lwjgl3:run --args="classname dev.cf.app.capture.CaptureApp"
What is actually hard about this shape
Nothing here is hard to draw. Four rows and a button is an afternoon. What is hard is that each of the four is a request to hardware that can be refused, absent, or simply broken — and the interface has to stay honest about it while somebody stands in a yard holding a phone. The whole example is organised around that.
1 · Every capture has three states, not two
Empty, captured, and unavailable on this device. The third is the one that gets
forgotten, and forgetting it produces the worst version of this screen: a button that does
nothing when tapped, on a device with no such hardware, with no explanation. Here
hasCamera() and hasLocation() decide up front — by asking the capability, never
by naming platforms — and an unavailable capture says why instead of pretending.
2 · The pickers are windows, and they own the camera
BarcodePicker, PhotoPicker and LocationPicker are Windows layered over
a native view. They must be opened and closed, never embedded in a scrolling body: a native
camera surface sits on top of the GL canvas rather than inside it, so it does not scroll, does
not clip, and does not take part in a transition. That is also why a picker that hosts one turns
transitions off — an animated fade over a native view shows a black rectangle for its whole
duration.
3 · Finishing states what is missing, and lets you finish anyway
Field work is not tidy: sometimes there is genuinely no signature because nobody was there. The screen does not block on it — it records what is missing, so the debt is visible later instead of being invented later. Blocking would only teach people to capture a scribble.
Rodar na sua máquina
Todo exemplo é uma Window comum, então o launcher só precisa do nome dela.
gradlew lwjgl3:run --args="classname dev.cf.app.capture.CaptureApp"Java é compilado antes de rodar, então o navegador não consegue reconstruir uma edição sozinho — o código que você vê aqui virou JavaScript pelo TeaVM antes de ser publicado. Copie o arquivo para um projeto e ele roda sem mudanças nas quatro plataformas; o comando acima é toda a preparação.
O que ele usa
Tipos do framework que este exemplo toca. Os que têm link têm página de referência.
O código
Completo, sem cortes. É o arquivo, não um trecho dele.
package dev.cf.app.capture;
import dev.cf.Described;
import dev.cf.Tones;
import dev.cf.app.AppBar;
import dev.cf.app.AppTheme;
import dev.crossfly.Capability;
import dev.crossfly.PlatformUtils;
import dev.crossfly.map.LatLon;
import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.picker.*;
import dev.crossfly.ui.style.*;
import static dev.crossfly.ui.Widgets.*;
/**
* <b>Fieldwork</b> — the field-capture shape: a checklist of device captures, every one of which
* can fail.
*
* <pre>
* gradlew lwjgl3:run --args="classname dev.cf.app.capture.CaptureApp"
* </pre>
*
* <h2>What is actually hard about this shape</h2>
*
* <p>Nothing here is hard to <i>draw</i>. Four rows and a button is an afternoon. What is hard is
* that each of the four is a request to hardware that can be refused, absent, or simply broken —
* and the interface has to stay honest about it while somebody stands in a yard holding a phone.
* The whole example is organised around that.</p>
*
* <h3>1 · Every capture has three states, not two</h3>
*
* <p>Empty, captured, and <b>unavailable on this device</b>. The third is the one that gets
* forgotten, and forgetting it produces the worst version of this screen: a button that does
* nothing when tapped, on a device with no such hardware, with no explanation. Here
* {@link #hasCamera()} and {@link #hasLocation()} decide up front — by asking the capability, never
* by naming platforms — and an unavailable capture says why instead of pretending.</p>
*
* <h3>2 · The pickers are windows, and they own the camera</h3>
*
* <p>{@link BarcodePicker}, {@link PhotoPicker} and {@link LocationPicker} are Windows layered over
* a native view. They must be opened and closed, never embedded in a scrolling body: a native
* camera surface sits on top of the GL canvas rather than inside it, so it does not scroll, does
* not clip, and does not take part in a transition. That is also why a picker that hosts one turns
* transitions off — an animated fade over a native view shows a black rectangle for its whole
* duration.</p>
*
* <h3>3 · Finishing states what is missing, and lets you finish anyway</h3>
*
* <p>Field work is not tidy: sometimes there is genuinely no signature because nobody was there.
* The screen does not block on it — it records what is missing, so the debt is visible later
* instead of being invented later. Blocking would only teach people to capture a scribble.</p>
*/
public class CaptureApp extends Window implements Described
{
/** The four captures, in the order the visit does them. */
private static final int ROW_LOCATION = 0, ROW_BARCODE = 1, ROW_PHOTO = 2, ROW_SIGNATURE = 3;
private static final String[] ROW_TITLE = { "Location", "Asset code", "Photo", "Signature" };
private static final String[] ROW_HINT = { "Where the visit happened", "Scan the asset label",
"One photo of the installation", "Signed off by whoever received it" };
private static final int[] ROW_ICON = { 0xE55F, 0xE905, 0xE412, 0xE745 };
/** What each row captured, or null while it is still empty. */
private final String[] captured = new String[4];
private final Component[] rows = new Component[4];
private final Label[] status = new Label[4];
private Label summary;
private LatLon where;
@Override
public String title()
{
return "Fieldwork — capturing from the device";
}
@Override
public String note()
{
return "Every capture has three states, not two: empty, captured, and unavailable on this"
+ " device — and the third is the one a phone-only developer never sees.";
}
/** Hands the palette back to whatever was in force before this app opened. */
@Override
public void onRemove()
{
super.onRemove();
AppTheme.leave();
}
@Override
public void onAdd(boolean firstTime) throws Exception
{
if (!firstTime)
return;
// The brand FIRST: fullscreenColor below reads a token, and reading it before the palette
// is declared bakes in the skin's neutral grey for the life of the window.
AppTheme.enter();
// The PAGE, not the surface. Cards, buttons and fields are all drawn on the surface, so a
// page painted the same colour makes every one of their edges disappear. Read through
// onRestyle so a light/dark flip re-derives it instead of keeping the old mode's value.
fullscreenColor = AppTheme.pageColor();
onRestyle(() -> fullscreenColor = AppTheme.pageColor());
setLayout(new VerticalLayout(Position.START, Position.START));
add(0f, new AppBar("Visit 4812", isRoot() ? null : this::hide));
Component body = new Component();
body.setLayout(new VerticalLayout(Position.START, Position.START));
body.setScrollable(ScrollDirection.VERTICAL);
body.getStyle()
.ingap(Gap.ofFont(1f, 1f, 1f, 1f));
body.testTag("capture.body");
add(LAY_FILL, body);
body.add(LAY_AUTO, outgap(label("Northgate Industrial · Bay 3", 1.5f, true, Styles.textColor,
Position.START), 0, 0, 0, .2f));
body.add(LAY_AUTO, outgap(label("Capture what you can. Anything missing is recorded as"
+ " missing.", 1f, false, Styles.captionColor, Position.START), 0, 0, 0, 1.2f));
for (int i = 0; i < rows.length; i++)
{
rows[i] = captureRow(i);
body.add(LAY_AUTO, outgap(rows[i], 0, 0, 0, .6f));
}
summary = label("", 1f, false, Styles.captionColor, Position.START);
summary.testTag("capture.summary");
body.add(LAY_AUTO, outgap(summary, 0, .8f, 0, .6f));
Button finish = Tones.action("Finish visit");
finish.testTag("capture.finish");
finish.onAction(c -> finish());
body.add(LAY_AUTO, finish);
updateSummary();
}
/**
* One capture row: glyph, title, current state, and the button that starts the capture.
*
* <p>Built once and updated in place rather than rebuilt — a capture is the one interaction here
* where the user is watching the exact row they just acted on, and rebuilding the list under
* them makes the screen flinch.</p>
*/
private Component captureRow(final int i) throws Exception
{
Card card = new Card();
card.setLayout(new GridLayout(Position.START, Position.CENTER));
card.testTag("capture.row." + i);
Component text = new Component();
text.setLayout(new VerticalLayout(Position.START, Position.START));
text.add(LAY_AUTO, label(ROW_TITLE[i], 1.2f, true, Styles.textColor, Position.START));
status[i] = label(ROW_HINT[i], 1f, false, Styles.captionColor, Position.START);
status[i].testTag("capture.status." + i);
text.add(LAY_AUTO, status[i]);
String blocked = unavailable(i);
// The whole point of the screen is that "unavailable" is a THIRD state, so it gets a tone of
// its own. Written in the same grey as the hint it replaces, it was a fourth kind of caption.
PalColor tom = blocked == null ? Tones.accent() : Tones.danger();
Button go = new Button(MaterialIcon.of(blocked == null ? ROW_ICON[i] : 0xE002, 1.4f));
go.getStyle()
.iconColor(tom);
go.testTag("capture.go." + i);
go.setEnabled(blocked == null);
go.onAction(c -> capture(i));
if (blocked != null)
{
// Not a silent disable: a dead button with no explanation reads as a bug, and the person
// holding the phone has no way to tell the difference.
status[i].setText(blocked);
status[i].getStyle()
.textColor(Tones.danger())
.font(Font.get(1f, BOLD));
status[i].setValidLayout(false);
}
card.add(0f, 1f, 0f, Tones.tile(ROW_ICON[i], tom), outgap(text, .8f, 0, .8f, 0), go);
return card;
}
/**
* Why this capture cannot run here, or null when it can.
*
* @param i the row
* @return the message to show, or null
*/
private String unavailable(int i)
{
if ((i == ROW_BARCODE || i == ROW_PHOTO) && !hasCamera())
return "No camera on this device.";
if (i == ROW_LOCATION && !hasLocation())
return "No location service on this device.";
return null;
}
/**
* Whether this device can drive a camera.
* <p>
* This read {@code Device.platform == ANDROID || == IPHONE} until the capability contract
* existed, and a list of platform names is wrong in every direction at once. It was wrong about
* what it excluded — the desktop has had a webcam backend all along, and the browser gets the
* camera through {@code getUserMedia}, so this screen dimmed two rows that worked. It was wrong
* about what it included — an iPad reports {@code IPAD}, not {@code IPHONE}, so the one device
* most likely to be held in a yard got the "no camera" message. And it goes stale on its own:
* adding a platform silently subtracts from every list like it, which is the failure mode that
* needs no commit to appear.
* <p>
* An example that models honest degradation cannot be the thing teaching the dishonest check.
* {@code R21Device} carries the same correction; this screen is where the pattern gets used in
* anger.
*/
private static boolean hasCamera()
{
return PlatformUtils.isSupported(Capability.CAMERA);
}
/** Same question for GPS. Kept separate: a device may have one and not the other. */
private static boolean hasLocation()
{
return PlatformUtils.isSupported(Capability.LOCATION);
}
/** Starts one capture. Each branch opens a Window and comes back through a callback. */
private void capture(int i)
{
switch (i)
{
case ROW_LOCATION:
// minPdop 50, draggable pin, no high precision, no street view — the ordinary case.
new LocationPicker(latlon ->
{
where = latlon;
set(ROW_LOCATION, latlon == null ? null : latlon.toString());
}, 50.0, true, false, false).show();
break;
case ROW_BARCODE:
new BarcodePicker("Point at the asset label")
{
protected void onBarcode(com.google.zxing.BarcodeFormat format, String code)
{
set(ROW_BARCODE, code);
hide();
}
}.show();
break;
case ROW_PHOTO:
new PhotoPicker(PhotoPicker.Preview.PHOTO)
.show(answer -> set(ROW_PHOTO, answer == null ? null : "photo captured"));
break;
case ROW_SIGNATURE:
new SignatureSheet(() -> set(ROW_SIGNATURE, "signed")).show();
break;
default:
break;
}
}
/** Records one capture and updates just that row. */
private void set(int i, String value)
{
captured[i] = value;
status[i].setText(captured[i] == null ? ROW_HINT[i] : captured[i]);
status[i].setValidLayout(false);
updateSummary();
Screen.repaint();
}
private void updateSummary()
{
int done = 0;
for (String s : captured)
if (s != null)
done++;
summary.setText(done + " of " + captured.length + " captured");
summary.setValidLayout(false);
}
/**
* Ends the visit, naming what is missing.
*
* <p>The message lists the gaps rather than counting them. "2 items missing" sends the user back
* to look; "Missing: photo, signature" tells them what to decide about.</p>
*/
private void finish()
{
StringBuilder missing = new StringBuilder();
for (int i = 0; i < captured.length; i++)
if (captured[i] == null)
missing.append(missing.length() == 0 ? "" : ", ")
.append(ROW_TITLE[i].toLowerCase());
if (missing.length() == 0)
{
Hint.show("Visit closed — everything captured", 2500);
return;
}
Alert a = new Alert("Finish with gaps?", "Missing: " + missing + ".\nThey will be recorded"
+ " as missing, not as done.", "Go back", "Finish anyway");
a.buttonAction(1, c -> Hint.show("Visit closed with " + missing + " missing", 3000));
a.show();
}
/** Where the visit happened, once captured — kept so a later screen could plot it. */
public LatLon location()
{
return where;
}
}package dev.cf.app.capture;
import dev.crossfly.ui.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;
import static dev.crossfly.ui.Widgets.*;
/**
* Capturing a signature: a {@link DrawingBoard} in a sheet, with a clear and a confirm.
*
* <p><b>Why a sheet and not a screen.</b> Signing is a step inside the visit, not a departure from
* it. A sheet keeps the checklist visible behind it, so the person signing can see what they are
* signing for, and dismissing it is a gesture rather than a navigation.</p>
*
* <p>Three details are what separate a signature pad that works in the field from one that does
* not, and none of them is about drawing:</p>
*
* <ul>
* <li><b>The board gets a white background of its own</b>, not the theme's surface. A signature
* is captured to be printed and attached to a document; capturing white-on-navy because the
* user happened to have dark mode on produces an unusable image.</li>
* <li><b>Confirm is disabled until something is drawn.</b> An empty signature is the most common
* thing to capture by accident, and it is indistinguishable from a real one downstream.</li>
* <li><b>Clear asks nothing.</b> It is instantly and obviously reversible — you sign again — so a
* confirmation dialog here is friction with no payoff.</li>
* </ul>
*/
public class SignatureSheet extends BottomSheet
{
private final Runnable onSigned;
private DrawingBoard board;
private Button confirm;
private boolean drawn;
/**
* @param onSigned runs when the user confirms a non-empty signature
*/
public SignatureSheet(Runnable onSigned)
{
super("SIGNATURE", "Sign below");
this.onSigned = onSigned;
heightRatio(.7f);
}
@Override
public void onAdd(boolean firstTime) throws Exception
{
super.onAdd(firstTime);
if (!firstTime)
return;
Component b = body();
b.setLayout(new VerticalLayout(Position.START, Position.START));
// The pad: white, framed, and tall enough that a signature is not cramped into a strip.
Component pad = new Component();
pad.setLayout(new VerticalLayout(Position.START, Position.START));
pad.getStyle()
.backColor(0xFFFFFFFF)
.borderColor(Styles.borderColor)
.borderThick(Styles.borderThick)
.borderRadius(Styles.borderRadius)
.autoHeight(Relative.ofFont(11f));
pad.testTag("sign.pad");
// Anonymous subclass rather than a listener: the DrawingBoard reports strokes by letting you
// override the touch handlers, which is how most CrossFly components are extended. There is
// no callback to look for, and adding one to the framework for this would be the wrong fix.
board = new DrawingBoard()
{
@Override
public void onTouchUp(float x, float y, int button) throws Exception
{
super.onTouchUp(x, y, button);
setDrawn(true);
}
};
board.setPenColor(0xFF111111);
pad.add(LAY_FILL, board);
b.add(LAY_AUTO, outgap(pad, 0, 0, 0, .6f));
b.add(LAY_AUTO, outgap(label("Sign with your finger or a stylus.", 1f, false,
Styles.captionColor, Position.START), 0, 0, 0, .8f));
Component row = new Component();
row.setLayout(new GridLayout(Position.START, Position.CENTER));
Button clear = new Button("Clear");
clear.testTag("sign.clear");
clear.onAction(c ->
{
board.clear();
setDrawn(false);
Screen.repaint();
});
confirm = new Button("Confirm");
confirm.testTag("sign.confirm");
confirm.setEnabled(false);
confirm.onAction(c ->
{
onSigned.run();
hide();
});
row.add(.5f, .5f, clear, confirm);
b.add(LAY_AUTO, row);
}
private void setDrawn(boolean b)
{
if (drawn == b)
return;
drawn = b;
confirm.setEnabled(b);
}
}package dev.cf.app;
import dev.crossfly.ui.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;
import static dev.crossfly.ui.Widgets.*;
/**
* The top bar the four example apps share: back, title, and up to a couple of actions.
*
* <p>There is no top bar in CrossFly, and that is not an omission. A title bar is the single most
* app-specific piece of chrome there is — height, colour, whether it collapses, whether the title
* centres, what sits on the right — and a framework that decided those would be fought by every
* app that used it. What the framework supplies is what a bar is <i>made of</i>: a container, a
* layout, buttons, glyphs, and a style that follows the skin.</p>
*
* <p>So this is an <b>app-level</b> class, shared by the four examples here because they are four
* examples in one repository — in a real project it would live in your own {@code ui} package. It
* is about forty lines, which is roughly what a top bar costs, and it is worth reading as the
* answer to "how do I compose chrome out of primitives".</p>
*
* <pre>
* add(0f, new AppBar("Part", this::hide).action(0xE3C9, this::edit));
* </pre>
*/
public class AppBar extends Component
{
private final String title;
private final Runnable onBack;
private final int[] icons = new int[3];
private final Runnable[] actions = new Runnable[3];
private final String[] tags = new String[3];
private int count;
/**
* @param title the heading
* @param onBack what the back arrow does, or null for a bar with no back arrow
*/
public AppBar(String title, Runnable onBack)
{
this.title = title;
this.onBack = onBack;
}
/**
* Adds a right-hand action. Up to three; past that the answer is a menu, not a fourth glyph.
*
* @param codepoint the Material Icons codepoint
* @param tag the test tag for the button
* @param action what it does
* @return this bar, for chaining
*/
public AppBar action(int codepoint, String tag, Runnable action)
{
if (count < icons.length)
{
icons[count] = codepoint;
tags[count] = tag;
actions[count] = action;
count++;
}
return this;
}
@Override
public void onAdd(boolean firstTime) throws Exception
{
if (!firstTime)
return;
// A strip of surface with a hairline of elevation under it. Both colours are theme tokens, so
// the bar is right in dark mode and under every skin without a branch anywhere — and the
// shadow is expressed as a ratio of the font height, so it survives a high-DPI screen, where
// a shadow measured in pixels all but disappears.
getStyle().backColor(Styles.backColor)
.shadow(0, .06f, .25f, Styles.borderColor)
.ingap(Gap.ofFont(.5f, .4f, .5f, .4f));
setLayout(new GridLayout(Position.START, Position.CENTER));
// A bar with no back arrow gets no COLUMN for one. The obvious shortcut — always build the
// column and fill it with a bare `new Component()` — silently kills the entire row: an empty
// container has no layout, an auto-width column cannot measure it, and the bar comes out
// zero-high with the title inside it. It looks like the title was never added.
boolean hasBack = onBack != null;
int columns = (hasBack ? 1 : 0) + 1 + count;
Object[] cells = new Object[columns * 2];
int k = 0;
if (hasBack)
cells[k++] = 0f; // back: its own width
cells[k++] = 1f; // title: everything left over
for (int i = 0; i < count; i++)
cells[k++] = 0f; // actions: their own width
if (hasBack)
{
Button b = new Button(MaterialIcon.of(0xE5C4, 1.4f)); // arrow_back
b.testTag("bar.back");
b.onAction(c -> onBack.run());
b.getStyle()
.outgap(Gap.ofFont(0, 0, .5f, 0));
cells[k++] = b;
}
Label t = label(title, 1.5f, true, Styles.textColor, Position.START);
t.testTag("bar.title");
cells[k++] = t;
for (int i = 0; i < count; i++)
{
Button b = new Button(MaterialIcon.of(icons[i], 1.4f));
final Runnable a = actions[i];
b.testTag(tags[i]);
b.onAction(c -> a.run());
b.getStyle()
.outgap(Gap.ofFont(.4f, 0, 0, 0));
cells[k++] = b;
}
add(cells);
}
}package dev.cf.app;
import dev.crossfly.ui.color.ColorScheme;
import dev.crossfly.ui.style.Styles;
/**
* The one line that gives an app its colour, and the reason the four examples here have one.
*
* <p><b>An app declares its brand; it does not pick colours.</b> One seed goes in, and CrossFly
* derives the whole scheme from it — primary, surface, on-surface, containers, the tonal fills
* behind an active tab, the ring of a gauge, both themes. Nothing downstream names a colour, which
* is what makes dark mode and the skin picker work without a single branch.</p>
*
* <p>Skip this and the app is not "unstyled", it is <b>grey</b>: the skin's neutral palette is what
* you get when nobody declared anything, and it looks exactly like a bug. That is what happened to
* these four — they were correct from the samples menu, which declares a brand of its own, and
* monochrome when launched straight from {@code classname}, which is how anybody copying them would
* run them.</p>
*
* <h3>Why enter/leave and not just a call at startup</h3>
*
* <p>The brand is global state. A real app declares it once in {@code Main} and never thinks about
* it again — and if that is your case, {@code Styles.setBrand(BRAND)} at startup is the whole story
* and this class is more machinery than you need.</p>
*
* <p>These four are different: they are guests. They open from a menu that has its own brand, and
* from a test suite whose later screenshots are compared against committed images. A guest that
* changes global state and walks away leaves its host repainted in someone else's colour — the
* samples menu comes back blue, the goldens fail three tests later, and neither failure points at
* the app that caused it. So each app calls {@link #enter()} when it opens and {@link #leave()}
* when it closes, and the palette that was in force is handed back.</p>
*/
public final class AppTheme
{
/**
* The seed every colour in the four example apps is derived from.
*
* <p>Deep blue, chosen against orange for one reason: orange is the brand of the production app
* these examples were extracted from, and an example that wears it invites the reader to think
* the two are related. For orange instead, this is the only line to change —
* {@code 0xFFE2622C} is a good one. Nothing else in the four apps names a colour.</p>
*/
public static final int BRAND = 0xFF1565C0;
/**
* The page behind the cards: the surface, pulled a little way towards the brand.
*
* <p><b>Why a page needs its own colour at all.</b> Cards, buttons and fields are all drawn on
* the skin's surface, which in a light theme is white or near it. Put them on a white page and
* every edge disappears — the card stops reading as a card and a white button on white looks
* like a mistake rather than a control. What separates them is not a border, it is the page
* being a shade the foreground is not.</p>
*
* <p><b>Why it is derived and not a hex.</b> {@code 0xFFF2F5F9} would be a perfectly nice light
* blue and would be wrong twice: it stops being light blue the moment somebody changes
* {@link #BRAND} to orange, and it stays light in dark mode, where the page must go darker than
* the cards rather than lighter. Mixing the primary into the surface at 8% follows both — the
* tint always matches the brand, and in a dark theme the same call yields a deep, slightly
* tinted ground.</p>
*
* <p>Read it through {@code onRestyle} rather than once, so a theme flip re-derives it.</p>
*
* @return the ARGB page colour for the palette currently in force
*/
public static int pageColor()
{
return dev.cf.Tones.page();
}
/** What was in force before the outermost {@link #enter()}. */
private static Integer previousBrand;
private static ColorScheme previousScheme;
/** How many example apps are open. Restores only when the last one closes. */
private static int depth;
private AppTheme()
{
}
/**
* Declares {@link #BRAND}, remembering whatever was in force.
*
* <p>Call it first thing in {@code onAdd}. It triggers a restyle, so every window already open
* re-resolves its colours — including this one, which is why it can run before the screen is
* built.</p>
*/
public static void enter()
{
if (depth++ == 0)
{
previousBrand = Styles.hasBrand() ? Integer.valueOf(Styles.brand()) : null;
ColorScheme live = Styles.colorScheme();
if (live != null)
{
// A SNAPSHOT, never the reference. Styles keeps ONE ColorScheme instance for the life
// of the app and mutates its roles in place on every seed change — deliberately, so
// that a PalColor captured into a style at build time keeps resolving and follows the
// theme forever. The consequence for anybody saving and restoring: the object you kept
// IS the object about to be overwritten, so handing it back restores nothing at all.
// This cost four broken golden images in a suite that had been green all morning.
previousScheme = ColorScheme.fromSeed(BRAND, live.isDark);
previousScheme.copyFrom(live);
}
}
Styles.setBrand(BRAND);
}
/**
* Hands the palette back to the host.
*
* <p>Call it from {@code onRemove}, which CrossFly runs on {@code hide()}. Two cases: the host
* had declared a brand, so re-declaring it regenerates everything; or nothing was declared, so
* the declaration is dropped <b>and</b> the scheme that was actually in force is restored —
* clearing the brand alone would leave this app's blue behind, still painted, just no longer
* owned by anybody.</p>
*/
public static void leave()
{
if (depth > 0 && --depth == 0)
{
if (previousBrand != null)
Styles.setBrand(previousBrand.intValue());
else
{
Styles.clearBrand();
if (previousScheme != null)
Styles.setColorScheme(previousScheme);
}
previousBrand = null;
previousScheme = null;
}
}
}Versões desta página legíveis por máquina, para um modelo ler a prateleira em vez de adivinhar a API.