मुख्य सामग्री पर जाएँ
CrossFly 100% ओपन सोर्सGitHub पर फ़ॉलो करें
CrossFlyCrossFly
शुरू करें
सभी उदाहरण
ऐपडिवाइस445 पंक्तियाँ · 2 फ़ाइलें

Fieldwork — capturing from the device

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.

यह क्या दिखाता है

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.

अपनी मशीन पर चलाएँ

हर उदाहरण एक साधारण Window है, इसलिए लॉन्चर को सिर्फ़ उसका नाम चाहिए।

gradlew lwjgl3:run --args="classname dev.cf.app.capture.CaptureApp"
इसे बदलना

Java पहले से संकलित होता है, इसलिए ब्राउज़र आपके बदलाव को खुद दोबारा नहीं बना सकता — यहाँ दिखा कोड प्रकाशित होने से पहले TeaVM द्वारा JavaScript में बदला गया था। फ़ाइल को किसी प्रोजेक्ट में कॉपी कीजिए और वह चारों प्लेटफ़ॉर्म पर बिना बदलाव चलती है; ऊपर दी गई कमांड ही पूरी तैयारी है।

यहीं चल रहा हैCrossFly 0.1.32
ऐप शुरू हो रहा है…

TeaVM ने इसे JavaScript में संकलित किया — यह वही कोड है, उसका वीडियो नहीं। · सभी उदाहरण

यह क्या इस्तेमाल करता है

इस उदाहरण में छुए गए फ़्रेमवर्क टाइप। जिनमें लिंक है उनका संदर्भ पृष्ठ मौजूद है।

AlertBarcodePickerBottomSheetButtonCapabilityCardComponentDrawingBoardFontGapGridLayoutHintLabelLatLonLocationPickerMaterialIconPalColorPhotoPickerPlatformUtilsRelativeScreenStylesVerticalLayoutWidgetsWindow

सोर्स कोड

पूरा, बिना कटौती के। यह वही फ़ाइल है, उसका अंश नहीं।

कच्चा
dev/cf/app/capture/CaptureApp.java
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;
   }
}
एआई सहायक के लिए

इस पृष्ठ के मशीन-पठनीय रूप, ताकि मॉडल API का अनुमान लगाने के बजाय उदाहरण पढ़ सके।

यह उदाहरण, JSON मेंयह उदाहरण, Markdown में
पिछलाPulse — a dashboardअगलाOrder entry — a wizard