跳到内容
CrossFly 是 100% 开源在 GitHub 关注我们
CrossFlyCrossFly
开始使用
全部示例
配方设备244 行 · 1 个文件

Camera, barcode and GPS

Ask whether the device HAS it before offering it. PlatformUtils.isSupported answers per capability, at runtime — never a check on which platform this is.

它展示了什么

Camera, barcode and location — the three that can say no.

These differ from R20Pickers in one way that changes the whole screen: the hardware may be missing, the permission may be refused, and the user may cancel. Three outcomes, not two, and the one that gets forgotten is the first.

Forgetting it produces the worst version of this screen: a button that does nothing when tapped, on a desktop with no camera, with no explanation. A person holding the device has no way to tell that from a bug — so the availability check happens before the button exists, and an unavailable capture says why instead of pretending.

The second half of the screen is the device APIs that are not pickers — vibration, speech both ways, the battery, the network. They are here for the same reason: each is missing somewhere, and on the web "somewhere" is decided by the browser the person opened rather than by the build, so the same bundle shows a different set of enabled rows on a phone and on a laptop. One row is the exception that proves the rule: Lock to portrait stays disabled on the web although setScreenRotation really does attempt the lock there, because the browser keeps one only in fullscreen — a page state, not device hardware, and therefore not something isSupported could answer honestly at the moment an app asks.

Two more things worth knowing before you embed one of these. A camera or map picker is a native view layered on top of the GL canvas: it does not scroll, does not clip to a parent, and does not take part in a transition — which is why such a picker turns transitions off, and why you open it rather than putting it in a page. And BarcodePicker is abstract: you subclass it to say what a scan means.

在你的机器上运行

每个示例都是普通的 Window,启动器只需要它的名字。

gradlew lwjgl3:run --args="classname dev.cf.recipe.R21Device"
修改它

Java 是提前编译的,浏览器无法自行重建你的修改 —— 这里的代码在发布前已由 TeaVM 转成 JavaScript。把文件复制到项目里,它在四个平台上原样运行;上面那条命令就是全部准备工作。

正在此处运行CrossFly 0.1.32
正在启动应用…

由 TeaVM 编译为 JavaScript —— 是同一份代码,不是录像。 · 全部示例

它用到了什么

本示例触及的框架类型。带链接的有参考页。

BarcodePickerButtonCapabilityCardComponentDeviceGapGridLayoutLabelLocationPickerMaterialIconPalColorPhotoPickerPlatformUtilsScreenStylesUIConstantsVerticalLayoutWidgets

源码

完整无删减。这就是那个文件,不是片段。

原始
dev/cf/recipe/R21Device.java
package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.Capability;
import dev.crossfly.PlatformUtils;
import dev.crossfly.device.Device;
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.*;

/**
 * Camera, barcode and location — the three that can say no.
 *
 * <p>These differ from {@link R20Pickers} in one way that changes the whole screen: the hardware
 * may be missing, the permission may be refused, and the user may cancel. <b>Three outcomes, not
 * two</b>, and the one that gets forgotten is the first.</p>
 *
 * <p>Forgetting it produces the worst version of this screen: a button that does nothing when
 * tapped, on a desktop with no camera, with no explanation. A person holding the device has no way
 * to tell that from a bug — so the availability check happens before the button exists, and an
 * unavailable capture says why instead of pretending.</p>
 *
 * <p>The second half of the screen is the device APIs that are not pickers — vibration, speech
 * both ways, the battery, the network. They are here for the same reason: each is missing
 * somewhere, and on the web "somewhere" is decided by the browser the person opened rather than by
 * the build, so the same bundle shows a different set of enabled rows on a phone and on a laptop.
 * One row is the exception that proves the rule: <b>Lock to portrait</b> stays disabled on the web
 * although {@code setScreenRotation} really does attempt the lock there, because the browser keeps
 * one only in fullscreen — a page state, not device hardware, and therefore not something
 * {@code isSupported} could answer honestly at the moment an app asks.</p>
 *
 * <p>Two more things worth knowing before you embed one of these. A camera or map picker is a
 * <b>native view layered on top of the GL canvas</b>: it does not scroll, does not clip to a
 * parent, and does not take part in a transition — which is why such a picker turns transitions
 * off, and why you open it rather than putting it in a page. And {@link BarcodePicker} is
 * abstract: you subclass it to say what a scan means.</p>
 */
public class R21Device extends Recipe
{
   private Label answer;

   public String title()
   {
      return "Camera, barcode and GPS";
   }

   public String note()
   {
      return "Ask whether the device HAS it before offering it. PlatformUtils.isSupported answers"
         + " per capability, at runtime — never a check on which platform this is.";
   }

   public void build(Component body) throws Exception
   {
      answer = label("Nothing captured yet.", 1f, false, Styles.captionColor, Position.START);
      answer.testTag("device.answer");
      // Pinned below the scrolling body, so the answer stays in view (see Recipe.pin).
      pin(answer);

      row(body, 0xE8B6, "Scan a barcode", PlatformUtils.isSupported(Capability.BARCODE_SCAN),
         () -> new BarcodePicker("Point at a barcode")
      {
         protected void onBarcode(com.google.zxing.BarcodeFormat format, String code)
         {
            said("Barcode", code);
            hide();
         }
      }.show());

      row(body, 0xE412, "Take a photo", hasCamera(),
         () -> new PhotoPicker(PhotoPicker.Preview.PHOTO).show(a -> said("Photo", a)));

      row(body, 0xE55F, "Pick a location", hasLocation(),
         () -> new LocationPicker(latlon -> said("Location", latlon), 50.0, true, false, false)
            .show());

      // The device APIs that are not a picker. They belong on this screen for the same reason the
      // three above do: every one of them is missing somewhere, and the web is the target where
      // "somewhere" is decided by the browser the person happened to open, not by the build.
      row(body, 0xE62E, "Vibrate", PlatformUtils.isSupported(Capability.VIBRATION),
         () ->
         {
            Device.vibrate(300);
            said("Vibrate", "asked for 300 ms");
         });

      row(body, 0xE050, "Speak a sentence", PlatformUtils.isSupported(Capability.TEXT_TO_SPEECH),
         () ->
         {
            // The voice is the device's (it follows Locale.getDefault()), so the sentence has to be in
            // the device's language too: a Portuguese voice reading English sounds like letters
            // read one by one, which is what a person heard on a pt-BR Windows (13/10/2026).
            String sentence = sentenceInDeviceLanguage();
            Device.textToVoice(sentence);
            said("Speak", sentence);
         });

      row(body, 0xE029, "Dictate", PlatformUtils.isSupported(Capability.SPEECH_TO_TEXT),
         () -> Device.voiceToText(new Device.VoiceToTextListener()
      {
         public void onStart()
         {
            said("Dictate", "listening…");
         }

         public void onResult(String text)
         {
            said("Dictate", text);
         }

         public void onError(int code, String message)
         {
            // The browser's own error name comes through here: "not-allowed" is a refused
            // microphone and "no-speech" is silence, and a person needs to tell those apart.
            said("Dictate", "failed: " + message);
         }

         public void onFinish()
         {
         }
      }));

      row(body, 0xE1A4, "Read the battery", PlatformUtils.isSupported(Capability.BATTERY),
         () ->
         {
            int pct = PlatformUtils.instance.getRemainingBattery();
            // -1 here does not mean "no battery API" — the capability already answered that. It
            // means the browser has not resolved its promise yet, which is a real state in the
            // first moments after startup and worth saying rather than printing "-1%".
            said("Battery", pct < 0 ? "not known yet" : pct + "%");
         });

      row(body, 0xE1BA, "Network state", PlatformUtils.isSupported(Capability.WIRELESS_STATE),
         () -> said("Network", PlatformUtils.instance
            .radiodeviceGetState(UIConstants.WirelessType.WIFI)));

      // The odd one out, and deliberately so: setScreenRotation is implemented on the web and
      // SCREEN_ROTATION is NOT declared there, because the browser keeps an orientation lock only
      // while the document is fullscreen. So this row offers what the platform declares, and on
      // the web it is the row that stays disabled while the method behind it still exists.
      // One row that is both the lock and the way back: its name says what the next tap does. Two
      // rows ("Lock" and "Unlock") left the screen saying both at once (iPhone, 19/10/2026).
      Label[] rotationName = new Label[1];
      rotationName[0] = row(body, 0xE1C1, "Lock to portrait",
         PlatformUtils.isSupported(Capability.SCREEN_ROTATION), () ->
         {
            portraitLocked = !portraitLocked;
            Screen.setRotation(portraitLocked ? UIConstants.ScreenRotation.PORTRAIT
               : UIConstants.ScreenRotation.ANY);
            rotationName[0].setText(portraitLocked ? "Unlock rotation" : "Lock to portrait");
            rotationName[0].invalidateMeasure();
            said("Rotation", portraitLocked ? "locked to portrait" : "any orientation");
         });
   }

   private boolean portraitLocked;

   /**
    * Whether this platform can drive a camera.
    * <p>
    * This used to read {@code Device.platform == ANDROID || == IPHONE}, and that was wrong twice
    * over: the desktop has had a webcam backend all along, and the web build gets the browser's
    * camera through {@code getUserMedia} — so the recipe disabled two buttons that worked. Asking
    * the capability instead of the platform is the whole point of the screen, and now the screen
    * does it.
    */
   private static boolean hasCamera()
   {
      return PlatformUtils.isSupported(Capability.CAMERA);
   }

   /** Kept separate from the camera: a device may have one and not the other. */
   private static boolean hasLocation()
   {
      return PlatformUtils.isSupported(Capability.LOCATION);
   }

   /** Adds one capability card; returns its name label, for a row whose name changes. */
   private Label row(Component body, int icone, String name, boolean available, Runnable go)
      throws Exception
   {
      Card c = new Card();
      c.setLayout(new GridLayout(Position.START, Position.CENTER));
      c.getStyle()
         .outgap(Gap.ofFont(0, 0, 0, .5f));

      // The whole recipe is "ask before you offer", so whether the device HAS the thing is the
      // one fact on the card — and it was written in the same grey as everything else.
      PalColor tom = available ? Tones.accent() : Tones.danger();

      Component text = new Component();
      text.setLayout(new VerticalLayout(Position.START, Position.START));
      Label nameLabel = label(name, 1.2f, true, Styles.textColor, Position.START);
      text.add(LAY_AUTO, nameLabel);
      text.add(LAY_AUTO, label(available ? "Ready" : "Not available on this device", 1f, true,
         tom, Position.START));

      Button b = new Button(MaterialIcon.of(available ? 0xE5C8 : 0xE002, 1.4f));
      b.setEnabled(available);
      b.onAction(e -> go.run());
      // "device.take-a-photo": a test reaches each row by what it does, not by its place in the list.
      b.testTag("device." + name.toLowerCase(java.util.Locale.ROOT).replace(' ', '-'));

      c.add(0f, 1f, 0f, Tones.tile(icone, tom), outgap(text, .6f, 0, 0, 0), b);
      body.add(LAY_AUTO, c);
      return nameLabel;
   }

   /** The sentence "Speak" says, in the language the device's voice speaks. */
   private static String sentenceInDeviceLanguage()
   {
      switch (java.util.Locale.getDefault().getLanguage())
      {
         case "pt":
            return "O CrossFly roda em cinco plataformas.";
         case "es":
            return "CrossFly funciona en cinco plataformas.";
         case "de":
            return "CrossFly läuft auf fünf Plattformen.";
         case "fr":
            return "CrossFly fonctionne sur cinq plateformes.";
         default:
            return "CrossFly runs on five platforms.";
      }
   }

   /** One place for the answer, and it handles the cancel every one of them can return. */
   private void said(String what, Object a)
   {
      answer.setText(a == null ? what + ": cancelled" : what + ": " + a);
      // invalidateMeasure, not setValidLayout(false): the answer can grow from one line to two (an
      // error message does), and only the climb to the root gives the body room for the second line.
      // The bit alone re-laid the label inside the height it already had, and the second line was cut.
      answer.invalidateMeasure();
      Screen.repaint();
   }
}
给 AI 助手

本页的机器可读版本,让模型直接读取示例集,而不是猜测 API。

本示例(JSON)本示例(Markdown)
上一个Pickers下一个Animation