【问题标题】:Codename One custom Layout with percentage width and adaptive height代号一种具有百分比宽度和自适应高度的自定义布局
【发布时间】:2019-02-10 16:51:50
【问题描述】:

这个问题只涉及代号一。

用例

一些应用程序,如 Instagram,都有 x 轴可滚动框,就像在这个视频中一样:

https://www.informatica-libera.net/videoLavoro/Video-2019-02-07-11-16-59_0569.mp4

实现起来似乎很容易(将BoxLayout.x() 设置为在x 轴上可滚动),但并不那么容易。有一个隐藏的复杂性:每个框的宽度都是屏幕宽度的百分比,因为用户应该看到第一个框和第二个框的一小块才能理解滚动是可能的。也许这在 Instagram 应用中不够清晰,但在其他应用中更明显。

我做了什么

我不知道如何嵌套 Codename One 布局以满足 x-scrollable BoxLayout.x 的要求,其中每个 Component 应占据屏幕宽度的 60%。然而,我设法通过自定义Layout 获得了非常相似的东西,但有一个大问题:我没有找到一种方法来根据它们的内容自动计算盒子的高度。目前我有一个百分比宽度和一个固定高度。请观看在模拟器中拍摄的视频:

https://www.informatica-libera.net/videoLavoro/Video-2019-02-07-11-38-10_0570.mp4

我的方法的另一个问题是我的代码不适用于SpanLabel(我将文本拆分为标记,并为每个标记创建了一个Label)。

我的代码

以下代码是一个测试用例,可以轻松复制和运行。注意,实际问题是根据用户数据生成的,所以我事先不知道问题的长度。而且平板的屏幕宽度和智能手机的屏幕宽度是不一样的(所以这两种情况下的高度应该是不一样的)。目前我设置了20mm的固定高度。

TestBoxes.java

import static com.codename1.ui.CN.*;
import com.codename1.ui.Form;
import com.codename1.ui.Dialog;
import com.codename1.ui.Label;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;
import com.codename1.io.Log;
import com.codename1.ui.Button;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.Display;
import com.codename1.ui.Toolbar;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.layouts.FlowLayout;
import com.codename1.ui.layouts.Layout;
import com.codename1.util.StringUtil;
import java.util.LinkedHashMap;
import java.util.List;

/**
 * This file was generated by <a href="https://www.codenameone.com/">Codename
 * One</a> for the purpose of building native mobile applications using Java.
 */
public class TestBoxes {

    private Form current;
    private Resources theme;

    public void init(Object context) {
        // use two network threads instead of one
        updateNetworkThreadCount(2);

        theme = UIManager.initFirstTheme("/theme");

        // Enable Toolbar on all Forms by default
        Toolbar.setGlobalToolbar(true);

        // Pro only feature
        Log.bindCrashProtection(true);

        addNetworkErrorListener(err -> {
            // prevent the event from propagating
            err.consume();
            if (err.getError() != null) {
                Log.e(err.getError());
            }
            Log.sendLogAsync();
            Dialog.show("Connection Error", "There was a networking error in the connection to " + err.getConnectionRequest().getUrl(), "OK", null);
        });
    }

    public void start() {
        if (current != null) {
            current.show();
            return;
        }
        Form hi = new Form("Test Boxes", BoxLayout.y());
        hi.add(getCompleteProfileCnt());
        hi.show();
    }

    public void stop() {
        current = getCurrentForm();
        if (current instanceof Dialog) {
            ((Dialog) current).dispose();
            current = getCurrentForm();
        }
    }

    public void destroy() {
    }

    public Container getCompleteProfileCnt() {
        LinkedHashMap<String, Form> questions = new LinkedHashMap<>(6);
        int count = getRemainingQuestions(questions);
        Label completeProfileLabel = new Label("Complete your profile");

        Container boxQuestions = new Container(new BoxLayout(BoxLayout.X_AXIS_NO_GROW));
        boxQuestions.setScrollableX(true);

        for (String question : questions.keySet()) {
            Button button = new Button("Complete");
            if (questions.get(question) != null) {
                button.addActionListener(l -> {
                    questions.get(question).show();
                });
            } else {
                button.addActionListener(l -> {
                    Log.p("To be implemented...");
                });
            }
            Container boxSingleQuestion = new Container(new FixedBoxLayout(60, 20), "ProfileUtilities-BoxSingleQuestion");
            Container questionCnt = FlowLayout.encloseCenter(getArrayLabels(question, "Label"));
            Container singleQuestionCnt = BoxLayout.encloseYBottomLast(questionCnt, FlowLayout.encloseCenter(button));
            questionCnt.setUIID("ProfileUtilities-QuestionCnt");
            singleQuestionCnt.setUIID("ProfileUtilities-SingleQuestionCnt");
            boxSingleQuestion.add(singleQuestionCnt);
            boxQuestions.add(boxSingleQuestion);
        }

        Container resultCnt = new Container(BoxLayout.y());
        resultCnt.add(completeProfileLabel);
        resultCnt.add(boxQuestions);
        return resultCnt;

    }

    /**
     * Inserts in the given "questions" Map the remaining questions to complete
     * the profile of the current logged user, and returns the number of all
     * questions (that can be >= to the remaining questions);
     *
     * @param questions, note that the Map will be cleared before adding the
     * remaining questions
     *
     * @return
     */
    private int getRemainingQuestions(LinkedHashMap<String, Form> questions) {
        // THIS METHOD IS AN EXAMPLE, the questions are generated according to the user data
        int countTotalQuestions = 3;
        if (questions == null) {
            throw new IllegalArgumentException("ProfileUtilities.getRemainingQuestions invalid \"questions\" param, because it's null");
        }
        questions.clear();

        questions.put("Question 1 - Suppose that this a long text, try to make it longer", null);
        questions.put("Question 2 - Suppose a short text", null);
        questions.put("Question 3 - Suppose a short text plus an icon", null);

        return countTotalQuestions;
    }

    public static List<String> tokenize(String text, String separator) {
        if (separator == null) {
            separator = "\n";
        }
        return StringUtil.tokenize(text, separator);
    }

    /**
     * Converts a string to an array of Labels, that can be placed in a
     * FlowLayout: conceptually similar to RichTextView, it serves for special
     * use cases (like custom layouts) where a SpanLabel doesn't work well.
     *
     * @param text
     * @param UIID for font style, note that margin and padding will be ignored
     * @return
     */
    public static Label[] getArrayLabels(String text, String UIID) {
        List words = tokenize(UIManager.getInstance().localize(text, text), " ");
        Label[] labels = new Label[words.size()];
        for (int i = 0; i < words.size(); i++) {
            labels[i] = new Label(words.get(i) + " ", UIID);
            labels[i].getAllStyles().setMargin(0, 0, 0, 0);
            labels[i].getAllStyles().setPadding(0, 0, 0, 0);
        }
        return labels;
    }

    class FixedBoxLayout extends Layout {

        private int preferredWidth;
        private final int preferredHeight;
        private boolean isListenerAdded = false;
        private int percentageWidth;

        public FixedBoxLayout(int percentageWidth, float heightMM) {
            preferredWidth = Display.getInstance().getDisplayWidth() * percentageWidth / 100;
            preferredHeight = Display.getInstance().convertToPixels(heightMM);
            this.percentageWidth = percentageWidth;
        }

        @Override
        public void layoutContainer(Container parent) {
            Component cmp = parent.getComponentAt(0);
            cmp.setWidth(preferredWidth);
            cmp.setPreferredW(preferredWidth);
            cmp.setHeight(preferredHeight);
            cmp.setPreferredH(preferredHeight);
            if (cmp instanceof Container) {
                for (Component inner : ((Container) cmp).getChildrenAsList(true)) {
                    inner.setWidth(preferredWidth);
                    inner.setPreferredW(preferredWidth);
                }
            }
            if (!isListenerAdded) {
                isListenerAdded = true;
                parent.getComponentForm().addSizeChangedListener(l -> {
                    preferredWidth = Display.getInstance().getDisplayWidth() * percentageWidth / 100;
                    parent.revalidate();
                });
            }
        }

        @Override
        public Dimension getPreferredSize(Container parent) {
            return new Dimension(preferredWidth, preferredHeight);
        }

    }
}

theme.css

#Constants {
    includeNativeBool: true; 
}

/* Default text and color */
Default, Label, TextArea, TextField {
    font-family: "native:MainRegular";
    font-size: 3mm;
    color: black;
}
Button {
    font-family: "native:MainRegular";
    font-size: 3mm;
    color: white;
    background-color: black;
    border: 0.2mm black cn1-pill-border;
    padding: 0.5mm 1mm 0.5mm 1mm; /* top, right, bottom, left */
    margin: 1mm;
}

Button.pressed, Button.selected {
    color: black;
    background-color: white;
}

Button.disabled {
    color: white;
    background-color: gray;
}

ProfileUtilities-completeProfileLabel {
    font-family: "native:MainBold";
    font-size: 3.5mm;
    color: black;
    margin: 1mm;
    padding: 0;
    margin-bottom: 0;
}

ProfileUtilities-completeProfileBox {
    margin: 0;
    margin-top: 1mm;
    margin-bottom: 1mm;
    border: 1pt #3399ff solid;
    border-radius: 2mm;
}

ProfileUtilities-CompletedQuestions {
    font-family: "native:MainRegular";
    font-size: 3mm;
    color: darkgoldenrod;
    margin: 1mm;
    margin-top: 0;
    padding: 0;
}

ProfileUtilities-SingleQuestionLabel {
    font-family: "native:MainBold";
    font-size: 3mm;
    color: darkslateblue;
    text-align: center;
    padding: 2mm;
}

ProfileUtilities-BoxSingleQuestion {
    border: 1pt darkmagenta solid;
    border-radius: 2mm;
    /* This is the margin between boxes */
    margin: 1mm;
}

ProfileUtilities-SingleQuestionCnt {
    /* This is the padding inside each box */
    padding: 0;
    padding-top: 1mm;
    padding-bottom: 1mm;
}

ProfileUtilities-QuestionCnt {
    /* This is the padding of each question text */
    padding: 2mm;
}

此测试用例的屏幕截图:

我的问题

我需要一个适合此用例的代码,改进我现有的代码(或者如果我的代码错误太多,则编写一个新代码)。谢谢

【问题讨论】:

  • 也许我对高度不够清楚:所有的盒子都应该有相同的高度,应该等于根据盒子内容计算的每个盒子的最大首选高度。

标签: codenameone


【解决方案1】:

编辑,这仍然需要根据您的需要进行一些调整,但这是我更改的要点:

        Container boxSingleQuestion = new Container(BoxLayout.y(), "ProfileUtilities-BoxSingleQuestion");
        Container questionCnt = new Container(new FlowLayout(CENTER)) {
            @Override
            protected Dimension calcPreferredSize() {
                Dimension d = super.calcPreferredSize(); 
                d.setWidth(Math.min(getDisplayWidth(), getDisplayHeight()) / 10 * 6);
                d.setHeight(d.getHeight() / 10 * 18);
                return d;
            }                
        };
        questionCnt.addAll(getArrayLabels(question, "Label"));
        Container singleQuestionCnt = BorderLayout.center(questionCnt).
                add(SOUTH, FlowLayout.encloseCenter(button));
        questionCnt.setUIID("ProfileUtilities-QuestionCnt");
        singleQuestionCnt.setUIID("ProfileUtilities-SingleQuestionCnt");
        boxSingleQuestion.add(singleQuestionCnt);
        boxQuestions.add(boxSingleQuestion);

我删除了专为全屏设计的 YLast 布局的特殊布局和用法,在这种情况下可能无法正常工作。 一般来说,我所做的是减少首选宽度,然后将首选高度增加一个固定值。通过使用 BorderLayout,底部的按钮将始终可见。

原答案如下:

您正在从布局中更改组件的首选高度/宽度。这是错误的,你永远不应该那样做。布局应该只设置宽度/高度。如果您设置宽度,您可以使用首选高度来获得正确的高度。为确保所有组件具有相同的高度,只需遍历组件即可:

int height = 0;
for(Component c : parent) {
    height = Math.max(height, c.getPreferredH());
}

假设您修复代码以不更改首选宽度或高度(宽度也会影响高度!),这应该会给您高度。

一个稍微简单的方法是使用 BoxLayout.X 并为您的组件重写 calcPreferredSize() 以返回一个等于屏幕大小 60% 的值。

【讨论】:

  • 亲爱的 Shai,您的回答肯定是正确的,但对我没有帮助。我花了很多时间试图解决这个问题,但我无法让我的测试用例正常工作。您能否尝试修复我的代码并将新的正确代码粘贴到新答案或 Github gist 中?我不懒,我做了很多试验,我也试过你的建议覆盖calcPreferredSize(),没有成功。也许有些事情我不知道或者我不明白。感谢您的帮助。
  • 我周末去看看
  • 我看了一下这个并添加了另一个答案
  • 我刚刚完成了对您的修复的微调,我发现d.setHeight(d.getHeight() / 10 * 18); 不是必需的,实际上如果我使用Component.setSameSize 来确保所有BorderLayout 容器将具有完全相同的大小。这就是我想要的:)
猜你喜欢
  • 2015-07-06
  • 2011-01-29
  • 1970-01-01
  • 2015-09-11
  • 1970-01-01
  • 2015-12-10
  • 2012-08-22
  • 2011-09-06
  • 2012-06-02
相关资源
最近更新 更多