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

Empty, loading and broken

Three of these are usually left to render as blank space. Blank space reads as a bug, never as an answer.

它展示了什么

The four states every screen has, and the three nobody designs.

A screen is drawn once, for the case where the data arrived and there is plenty of it. Then it ships, and users meet the other three: nothing yet, nothing matching, and something went wrong. All three render as an empty rectangle unless somebody decided otherwise, and an empty rectangle says only one thing to the person looking at it — this is broken.

The distinction that matters most is the one between the first two. "You have not added anything yet" invites an action and is a good first impression. "Nothing matches that search" is an answer to a question the user asked. Showing the first when the second is true tells a long-standing customer their account is empty.

在你的机器上运行

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

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

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

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

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

它用到了什么

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

ButtonCardChipComponentDebugGapGridLayoutHintHorizontalLayoutPalColorScreenStylesVerticalLayoutWidgets

源码

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

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

import dev.cf.Tones;

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

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

/**
 * The four states every screen has, and the three nobody designs.
 *
 * <p>A screen is drawn once, for the case where the data arrived and there is plenty of it. Then it
 * ships, and users meet the other three: nothing yet, nothing matching, and something went wrong.
 * All three render as an empty rectangle unless somebody decided otherwise, and an empty rectangle
 * says only one thing to the person looking at it — <b>this is broken</b>.</p>
 *
 * <p>The distinction that matters most is the one between the first two. "You have not added
 * anything yet" invites an action and is a good first impression. "Nothing matches that search" is
 * an answer to a question the user asked. Showing the first when the second is true tells a
 * long-standing customer their account is empty.</p>
 */
public class R30Empty extends Recipe
{
   private enum State
   {
      LOADED, FIRST_RUN, NO_MATCH, FAILED
   }

   private State state = State.FIRST_RUN;
   private Component stage;

   public String title()
   {
      return "Empty, loading and broken";
   }

   public String note()
   {
      return "Three of these are usually left to render as blank space. Blank space reads as a bug,"
         + " never as an answer.";
   }

   public void build(Component body) throws Exception
   {
      Component switcher = new Component();
      switcher.setLayout(new HorizontalLayout(Position.START, Position.CENTER));
      for (final State s : State.values())
      {
         Chip c = new Chip(s.name()
            .toLowerCase()
            .replace('_', ' '));
         c.setSelected(s == state);
         c.getStyle()
            .outgap(Gap.ofFont(0, 0, .4f, 0));
         c.onAction(e ->
         {
            state = s;
            rebuild();
            Screen.repaint();
         });
         switcher.add(LAY_AUTO, c);
      }
      body.add(LAY_AUTO, outgap(switcher, 0, 0, 0, 1f));

      stage = new Component();
      stage.setLayout(new VerticalLayout(Position.START, Position.START));
      stage.testTag("recipe.states");
      body.add(LAY_AUTO, stage);
      rebuild();
   }

   private void rebuild()
   {
      try
      {
         stage.removeAll();
         // removeAll() takes the layout with it.
         stage.setLayout(new VerticalLayout(Position.START, Position.START));

         switch (state)
         {
            case LOADED:
               for (int i = 1; i <= 4; i++)
               {
                  Card c = new Card();
                  c.setLayout(new GridLayout(Position.START, Position.CENTER));
                  c.getStyle()
                     .outgap(Gap.ofFont(0, 0, 0, .5f));
                  Component t = new Component();
                  t.setLayout(new VerticalLayout(Position.START, Position.START));
                  t.add(LAY_AUTO, label("Order " + (4180 + i), 1.2f, true, Styles.textColor,
                     Position.START));
                  t.add(LAY_AUTO, label("Placed this week", 1f, false, Styles.captionColor,
                     Position.START));
                  c.add(0f, 1f, Tones.tile(0xE8CC, Tones.cycle(i)), outgap(t, .6f, 0, 0, 0));
                  stage.add(LAY_AUTO, c);
               }
               break;

            case FIRST_RUN:
               // An invitation, with the action right there. This is a first impression, and the
               // one empty state worth spending a sentence on.
               // An invitation is not a problem: it gets the tone of the thing being offered.
               stage.add(LAY_AUTO, state(0xE8CC, Tones.accent(), "No orders yet",
                  "When you place one, it will show up here.", "Place your first order"));
               break;

            case NO_MATCH:
               // An ANSWER to what the user typed, not an invitation. Offering "add your first
               // order" to somebody who searched for one they know exists reads as being ignored.
               // Nothing is broken — the search simply has no answer. Not red.
               stage.add(LAY_AUTO, state(0xE8B6, Tones.support(), "Nothing matches “ironvale”",
                  "Check the spelling, or clear the search to see everything.",
                  "Clear the search"));
               break;

            case FAILED:
               // What happened, and what they can do. Not the exception text: it names something
               // the user has no way to act on, and it frightens them.
               // The only one of the three that actually failed, and the only one in the error
               // tone. Three identical grey glyphs said all three were the same event.
               stage.add(LAY_AUTO, state(0xE000, Tones.danger(), "We could not load your orders",
                  "Nothing was lost. Check your connection and try again.", "Try again"));
               break;

            default:
               break;
         }
         stage.setValidLayout(false);
      }
      catch (Exception e)
      {
         dev.crossfly.util.Debug.handle(e);
      }
   }

   /** One shape for all three: a glyph, a sentence, a reason, and exactly one way forward. */
   private Component state(int glyph, PalColor tom, String title, String detail, String action)
      throws Exception
   {
      Component c = new Component();
      c.setLayout(new VerticalLayout(Position.CENTER, Position.CENTER));
      c.getStyle()
         .ingap(Gap.ofFont(1f, 2f, 1f, 2f));
      c.add(LAY_AUTO, outgap(matglyph(glyph, tom, 3f), 0, 0, 0, .8f));
      c.add(LAY_AUTO, outgap(label(title, 1.5f, true, Styles.textColor, Position.CENTER), 0, 0, 0,
         .3f));
      c.add(LAY_AUTO, outgap(label(detail, 1f, false, Styles.captionColor, Position.CENTER), 0, 0,
         0, 1f));
      Button b = Tones.action(action, tom);
      b.onAction(e -> Hint.show(action, 1500));
      c.add(LAY_AUTO, b);
      return c;
   }
}
给 AI 助手

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

本示例(JSON)本示例(Markdown)
上一个Brand and dark mode下一个Files, sharing and printing