Reputation: 6249
This question is referred to Codename One only.
Use case
Several apps, like Instagram, have x-axis scrollable boxes, like in this video:
https://www.informatica-libera.net/videoLavoro/Video-2019-02-07-11-16-59_0569.mp4
It seems quite easy to implement (with a BoxLayout.x()
set as scrollable on x-axis), but it's not so easy. There is an hidden complexity: the width of each box is in percentage of the screen width, because the user should see the first box and a small piece of the second box to understand that the scrolling is possible. Maybe this is not enough clear in the Instagram app, but it more evident in other apps.
What I've done
I wasn't able to figure how to nest the Codename One layouts to meet the requirement of a x-scrollable BoxLayout.x
in which each Component
should occupy the 60% of the screen width. However I managed to get something very similar with a custom Layout
, but with a big problem: I didn't find a way to automatically calculate the height of the boxes according to their content. At the moment I have a percentage width and a fixed height. Please see this video taken in the Simulator:
https://www.informatica-libera.net/videoLavoro/Video-2019-02-07-11-38-10_0570.mp4
Another issue of my approach is that my code doesn't work with SpanLabel
(I split the text in tokens and for each token I created a Label
).
My code
The following code is a test case that can be easily copied and run. Note that the actual questions are generated according to the user data, so I don't know the length of the questions in advance. Moreover the screen width of a tablet is different from the screen width of a smartphone (so the height in these two cases should be different). At the moment I set a fixed height of 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;
}
Screenshot of this test case:
My question
I need a code suitable for this use case, improving my existing code (or writing a new one if my code is too much wrong). Thank you
Upvotes: 3
Views: 850
Reputation: 52760
Edit, this still requires some tuning to your needs but this is the gist of my changes:
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);
I removed the special layout and usage of the YLast layout which was designed for full screen and might not work as you intend for this case. Generally what I did was reduce the preferred width then increased the preferred height by a fixed value. By using BorderLayout the button on the bottom will always be visible.
Original answer below:
You are changing the preferred height/width of the component from the layout. That's wrong, you should never do that. Layout should just set the width/height. If you set the width you can just use the preferred height to get the right height. To make sure all components have the same height just loop over the components:
int height = 0;
for(Component c : parent) {
height = Math.max(height, c.getPreferredH());
}
This should give you the height assuming you fix the code to not change preferred width or height (width will also impact height!).
A slightly simpler approach would be to use BoxLayout.X and just override calcPreferredSize() for your components to return a value equal to 60 percent of the screens size.
Upvotes: 2