Saltar al contenido
CrossFly es 100% open sourceSíguenos en GitHub
CrossFlyCrossFly
Empezar
Todos los ejemplos
RecetasComponentes propios161 líneas · 1 archivos

Writing your own component

Search dev/crossfly/ui first — most of what you are about to write is already there. When it genuinely is not, this is the shape.

Qué muestra esto

Writing a component of your own — and the check to run before you do.

Search the inventory first. CrossFly is larger than it looks and its components are not discoverable by guessing; the reference app on this framework rebuilt a single-choice selector, a character limit and a grid column that all already existed, with the framework source compiled into its own build. A missing knob on an existing component is a smaller change than a new component, and it is the change the framework wants.

When nothing fits, a component is four decisions, and the one below makes all four.

  1. What it holds — plain fields, set through a fluent setter that returns this, so it reads like every other widget.
  2. How big it wants to be — getAutoWidth/getAutoHeight, in font units, so it scales with density instead of being right on one screen.
  3. What it looks like — onPaint, taking every colour from a token so the theme and the skin reach it without a branch.
  4. What a tap means — onTouchUp, then tell the app through a callback. A component that acts on its own tap is a component that can only be used once.

Ejecutarlo en tu máquina

Cada ejemplo es una Window normal, así que el lanzador solo necesita su nombre.

gradlew lwjgl3:run --args="classname dev.cf.recipe.R28Component"
Editarlo

Java se compila antes de ejecutarse, así que el navegador no puede reconstruir una edición por su cuenta: el código que ves aquí se convirtió en JavaScript con TeaVM antes de publicarse. Copia el archivo a un proyecto y funciona sin cambios en las cuatro plataformas; el comando de arriba es toda la preparación.

Ejecutándose aquíCrossFly 0.1.32
Iniciando la aplicación…

Compilado a JavaScript por TeaVM: es el mismo código, no un vídeo de él. · Todos los ejemplos

Qué usa

Tipos del framework que toca este ejemplo. Los enlazados tienen página de referencia.

ComponentLabelMaterialIconPaintPalColorScreenStylesWidgets

El código

Completo, sin recortes. Es el archivo, no un fragmento.

Crudo
dev/cf/recipe/R28Component.java
package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.color.PalColor;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

import static dev.crossfly.ui.Widgets.*;

/**
 * Writing a component of your own — and the check to run before you do.
 *
 * <p><b>Search the inventory first.</b> CrossFly is larger than it looks and its components are not
 * discoverable by guessing; the reference app on this framework rebuilt a single-choice selector, a
 * character limit and a grid column that all already existed, with the framework source compiled
 * into its own build. A missing knob on an existing component is a smaller change than a new
 * component, and it is the change the framework wants.</p>
 *
 * <p>When nothing fits, a component is four decisions, and the one below makes all four.</p>
 *
 * <ol>
 *   <li><b>What it holds</b> — plain fields, set through a fluent setter that returns {@code this},
 *       so it reads like every other widget.</li>
 *   <li><b>How big it wants to be</b> — {@code getAutoWidth}/{@code getAutoHeight}, in font units,
 *       so it scales with density instead of being right on one screen.</li>
 *   <li><b>What it looks like</b> — {@code onPaint}, taking every colour from a token so the theme
 *       and the skin reach it without a branch.</li>
 *   <li><b>What a tap means</b> — {@code onTouchUp}, then tell the app through a callback. A
 *       component that acts on its own tap is a component that can only be used once.</li>
 * </ol>
 */
public class R28Component extends Recipe
{
   private Label chosen;

   public String title()
   {
      return "Writing your own component";
   }

   public String note()
   {
      return "Search dev/crossfly/ui first — most of what you are about to write is already there."
         + " When it genuinely is not, this is the shape.";
   }

   public void build(Component body) throws Exception
   {
      chosen = label("Nothing rated yet.", 1.5f, true, Tones.accent(), Position.START);
      body.add(LAY_AUTO, outgap(chosen, 0, 0, 0, .8f));

      Rating r = new Rating().stars(5)
         .value(3)
         .onChange(v ->
         {
            chosen.setText(v + " of 5");
            chosen.setValidLayout(false);
            Screen.repaint();
         });
      r.testTag("recipe.rating");
      body.add(LAY_AUTO, r);

      body.add(LAY_AUTO, outgap(label("Tap a star. The component reports the number and does"
         + " nothing else with it — which is what lets the same class drive a filter here and a"
         + " form field somewhere else.", 1f, false, Styles.captionColor, Position.START), 0, 1f, 0,
         0));
   }

   /** A row of stars: pick a value from one to N. */
   private static class Rating extends Component
   {
      /** Material star and star_border. */
      private static final String FILLED = "", EMPTY = "";

      private int stars = 5, value;
      private RunnableWithInt onChange;

      /** Glyphs and the shape are FIELDS: onPaint runs every frame and must not allocate. */
      private final MaterialIcon on = new MaterialIcon(FILLED, 1.6f),
         off = new MaterialIcon(EMPTY, 1.6f);

      Rating stars(int n)
      {
         stars = n;
         return this;
      }

      Rating value(int v)
      {
         value = v;
         return this;
      }

      Rating onChange(RunnableWithInt r)
      {
         onChange = r;
         return this;
      }

      /** One star per slot, square, sized from the font like everything else. */
      private int slot()
      {
         return Math.round(Styles.fontHeight * 2f);
      }

      @Override
      public int getAutoWidth()
      {
         return slot() * stars;
      }

      @Override
      public int getAutoHeight()
      {
         return slot();
      }

      @Override
      public void onPaint(Paint paint) throws Exception
      {
         int s = slot();
         for (int i = 0; i < stars; i++)
         {
            MaterialIcon g = i < value ? on : off;
            // Colour from a token, never a literal: this is what makes the component correct in
            // dark mode and under every skin without knowing which one is in force.
            PalColor tone = i < value ? Styles.primaryColor : Styles.captionColor;
            // drawCentered takes the CENTRE, so the glyph's own metrics stay the icon's business.
            g.drawCentered(paint, i * s + s / 2, height / 2, tone.color());
         }
      }

      /**
       * A tap picks a star and REPORTS it. It does not decide what that means — the screen does,
       * and that separation is the whole reason this class is reusable.
       */
      @Override
      public void onTouchUp(float x, float y, int button) throws Exception
      {
         int picked = Math.min(stars, Math.max(1, (int) (x / slot()) + 1));
         if (picked != value)
         {
            value = picked;
            if (onChange != null)
               onChange.run(picked);
            Screen.repaint();
         }
      }
   }

   /** A one-method callback carrying an int — the framework's RunnableWith, narrowed. */
   private interface RunnableWithInt
   {
      void run(int value);
   }
}
Para una IA

Versiones de esta página legibles por máquina, para que un modelo lea el estante en vez de adivinar la API.

Este ejemplo, en JSONEste ejemplo, en Markdown
AnteriorKeyboard and focusSiguienteBrand and dark mode