Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TIMOB-9386: Android: Implement conditional horizontal layout wrapping #2368

Merged
merged 12 commits into from
Jun 18, 2012
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public TiUISpinner(TiViewProxy proxy, Activity activity)
{
this(proxy);
TiCompositeLayout layout = new TiCompositeLayout(activity, LayoutArrangement.HORIZONTAL, proxy);
layout.setDisableHorizontalWrap(true);
layout.setEnableHorizontalWrap(true);
setNativeView(layout);
}

Expand Down
5 changes: 5 additions & 0 deletions android/titanium/src/java/org/appcelerator/titanium/TiC.java
Original file line number Diff line number Diff line change
Expand Up @@ -1911,6 +1911,11 @@ public class TiC
*/
public static final String PROPERTY_WORK = "work";

/**
* @module.api
*/
public static final String PROPERTY_WRAP = "wrap";

/**
* @module.api
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ public enum LayoutArrangement {
private int horizontalLayoutTopBuffer = 0;
private int horizontalLayoutCurrentLeft = 0;
private int horizontalLayoutLineHeight = 0;
private boolean disableHorizontalWrap = false;
private boolean enableHorizontalWrap = true;
private int horizontalLayoutLastIndexBeforeWrap = 0;
private int horiztonalLayoutPreviousRight = 0;

private float alpha = 1.0f;
private Method setAlphaMethod;
Expand Down Expand Up @@ -431,7 +433,7 @@ private int calculateHeightFromPins(LayoutParams params, int parentTop, int pare
if (centerY != null) {
height = (centerY.getAsPixels(this) - parentTop - top.getAsPixels(this)) * 2;
} else if (bottom != null) {
height = parentBottom - top.getAsPixels(this) - bottom.getAsPixels(this);
height = parentHeight - top.getAsPixels(this) - bottom.getAsPixels(this);
}
} else if (centerY != null && bottom != null) {
height = (parentBottom - bottom.getAsPixels(this) - centerY.getAsPixels(this)) * 2;
Expand Down Expand Up @@ -487,27 +489,34 @@ protected void onLayout(boolean changed, int l, int t, int r, int b)
int[] vertical = new int[2];

int currentHeight = 0; // Used by vertical arrangement calcs

for (int i = 0; i < count; i++) {
View child = getChildAt(i);
TiCompositeLayout.LayoutParams params =
(TiCompositeLayout.LayoutParams) child.getLayoutParams();
if (child.getVisibility() != View.GONE) {
// Dimension is required from Measure. Positioning is determined here.

// Try to calculate width/height from pins, and default to measured width/height. We have to do this in
// onLayout since we can't get the correct top, bottom, left, and right values inside constrainChild().
int childMeasuredHeight = calculateHeightFromPins(params, top, bottom, getHeight(), child.getMeasuredHeight());
int childMeasuredWidth = calculateWidthFromPins(params, left, right, getWidth(), child.getMeasuredWidth());
int childMeasuredHeight = child.getMeasuredHeight();
int childMeasuredWidth = child.getMeasuredWidth();

if (isHorizontalArrangement()) {
if (i == 0) {
horizontalLayoutCurrentLeft = left;
horizontalLayoutLineHeight = 0;
horizontalLayoutTopBuffer = 0;
horizontalLayoutLastIndexBeforeWrap = 0;
horiztonalLayoutPreviousRight = 0;
updateRowForHorizontalWrap(right, i);
}
computeHorizontalLayoutPosition(params, childMeasuredWidth, childMeasuredHeight, right, top, bottom, horizontal, vertical);
computeHorizontalLayoutPosition(params, childMeasuredWidth, childMeasuredHeight, right, top, bottom, horizontal, vertical, i);

} else {
// Try to calculate width/height from pins, and default to measured width/height. We have to do this in
// onLayout since we can't get the correct top, bottom, left, and right values inside constrainChild().
childMeasuredHeight = calculateHeightFromPins(params, top, bottom, getHeight(), childMeasuredHeight);
childMeasuredWidth = calculateWidthFromPins(params, left, right, getWidth(), childMeasuredWidth);

computePosition(this, params.optionLeft, params.optionCenterX, params.optionRight, childMeasuredWidth, left, right, horizontal);
if (isVerticalArrangement()) {
computeVerticalLayoutPosition(currentHeight, params.optionTop, params.optionBottom, childMeasuredHeight, top, bottom, vertical, bottom);
Expand Down Expand Up @@ -667,32 +676,82 @@ private void computeVerticalLayoutPosition(int currentHeight,
pos[1] = bottom;
}

private void computeHorizontalLayoutPosition(TiCompositeLayout.LayoutParams params, int measuredWidth, int measuredHeight, int layoutRight, int layoutTop, int layoutBottom, int[] hpos, int[] vpos)
private void computeHorizontalLayoutPosition(TiCompositeLayout.LayoutParams params, int measuredWidth,
int measuredHeight, int layoutRight, int layoutTop, int layoutBottom, int[] hpos, int[] vpos, int currentIndex)
{

TiDimension optionLeft = params.optionLeft;
int left = horizontalLayoutCurrentLeft;
TiDimension optionRight = params.optionRight;
int left = horizontalLayoutCurrentLeft + horiztonalLayoutPreviousRight;
int optionLeftValue = 0;
if (optionLeft != null) {
left += optionLeft.getAsPixels(this);
optionLeftValue = optionLeft.getAsPixels(this);
left += optionLeftValue;
}
horiztonalLayoutPreviousRight = (optionRight == null) ? 0 : optionRight.getAsPixels(this);

int right = left + measuredWidth;
if (right > layoutRight && !disableHorizontalWrap) {
// Too long for the current "line" that it's on. Need to move it down.
left = 0;
right = measuredWidth;
if (enableHorizontalWrap && ((right + horiztonalLayoutPreviousRight) > layoutRight)) {
// Too long for the current "line" that it's on. Need to move it down.
left = optionLeftValue;
right = measuredWidth + left;
horizontalLayoutTopBuffer = horizontalLayoutTopBuffer + horizontalLayoutLineHeight;
horizontalLayoutLineHeight = 0;
}
hpos[0] = left;
hpos[1] = right;
horizontalLayoutCurrentLeft = right;

if (enableHorizontalWrap) {
// Don't update row on the first iteration since we already do it beforehand
if (currentIndex != 0 && currentIndex > horizontalLayoutLastIndexBeforeWrap) {
updateRowForHorizontalWrap(layoutRight, currentIndex);
}
measuredHeight = calculateHeightFromPins(params, horizontalLayoutTopBuffer, horizontalLayoutTopBuffer
+ horizontalLayoutLineHeight, horizontalLayoutLineHeight, measuredHeight);
layoutBottom = horizontalLayoutLineHeight;
}

// Get vertical position into vpos
computePosition(this, params.optionTop, params.optionCenterY, params.optionBottom, measuredHeight, layoutTop, layoutBottom, vpos);
horizontalLayoutLineHeight = Math.max(horizontalLayoutLineHeight, vpos[1] - vpos[0]);
computePosition(this, params.optionTop, params.optionCenterY, params.optionBottom, measuredHeight, layoutTop,
layoutBottom, vpos);
// account for moving the item "down" to later line(s) if there has been wrapping.
vpos[0] = vpos[0] + horizontalLayoutTopBuffer;
vpos[1] = vpos[1] + horizontalLayoutTopBuffer;
}

private void updateRowForHorizontalWrap(int maxRight, int currentIndex)
{
int rowWidth = 0;
int rowHeight = 0;
int i = 0;
int parentHeight = getHeight();
horizontalLayoutLineHeight = 0;

for (i = currentIndex; i < getChildCount(); i++) {
View child = getChildAt(i);
// Calculate row width/height with padding
rowWidth += child.getMeasuredWidth() + getViewWidthPadding(child, getWidth());
rowHeight = child.getMeasuredHeight() + getViewHeightPadding(child, parentHeight);
if (rowWidth > maxRight) {
horizontalLayoutLastIndexBeforeWrap = i - 1;
return;

} else if (rowWidth == maxRight) {
// If we couldn't determine a height from the children, just use the parent height as the max height
if (horizontalLayoutLineHeight == 0) {
horizontalLayoutLineHeight = (rowHeight == 0) ? parentHeight : rowHeight;
}
break;
}

if (horizontalLayoutLineHeight < rowHeight && rowHeight != parentHeight) {
horizontalLayoutLineHeight = rowHeight;
}
}
horizontalLayoutLastIndexBeforeWrap = i;
}

// Determine whether we have a conflict where a parent has size behavior, and child has fill behavior.
private boolean hasSizeFillConflict(View parent, int[] conflicts, boolean firstIteration)
{
Expand Down Expand Up @@ -824,9 +883,9 @@ public void setLayoutArrangement(String arrangementProperty)
}
}

public void setDisableHorizontalWrap(boolean disable)
public void setEnableHorizontalWrap(boolean enable)
{
disableHorizontalWrap = disable;
enableHorizontalWrap = enable;
}

public void setProxy(TiViewProxy proxy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,11 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP
layoutParams.optionHeight = null;
}
layoutNativeView();
} else if (key.equals(TiC.PROPERTY_WRAP)) {
if (nativeView instanceof TiCompositeLayout) {
((TiCompositeLayout) nativeView).setEnableHorizontalWrap(TiConvert.toBoolean(newValue));
}
layoutNativeView();
} else if (key.equals(TiC.PROPERTY_WIDTH)) {
if (newValue != null) {
if (!newValue.equals(TiC.SIZE_AUTO)) {
Expand Down Expand Up @@ -566,7 +571,12 @@ public void processProperties(KrollDict d)
}
if (TiConvert.fillLayout(d, layoutParams) && !nativeViewNull) {
nativeView.requestLayout();
}

if (d.containsKey(TiC.PROPERTY_WRAP)) {
if (nativeView instanceof TiCompositeLayout) {
((TiCompositeLayout) nativeView).setEnableHorizontalWrap(TiConvert.toBoolean(d, TiC.PROPERTY_WRAP));
}
}

Integer bgColor = null;
Expand Down
30 changes: 20 additions & 10 deletions apidoc/Titanium/UI/View.yml
Original file line number Diff line number Diff line change
Expand Up @@ -952,12 +952,15 @@ properties:

Each child is positioned horizontally as in the composite layout mode.

* `horizontal`. Somewhat like the vertical layout, except children are laid out horizontally
* `horizontal`. Horizontal layouts have different behavior depending on whether
the wrap property is enabled. Horizontal layouts with wrapping behavior are
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrap property is set to true.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add paragraph break after introducing the concept of "wrapping" and before describing the wrap = true case. Here's my suggestion:

*   `horizontal`. Horizontal layouts have different behavior depending on whether wrapping is enabled. Wrapping is enabled
      by default (the `wrap` property is `true`).

    With wrapping enabled, children are laid out horizontally from left to right, _in rows_. If a child requires more horizontal
    space than exists in the current row, it is wrapped to a new row. The height of each row is equal to the maximum height of
    the children in that row.

somewhat like vertical layouts, except the children are laid out horizontally
from left to right, _in rows_. If a child requires more horizontal space than
exists in the current row, it is wrapped to a new row. The height of each row
is equal to the maximum height of the children in that row.
is equal to the maximum height of the children in that row. The wrapping behavior
is enabled by default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

horizontalWrap property

On iOS and Mobile Web (Release 2.1.0 and later), the first row is placed
On iOS, Android and Mobile Web (Release 2.1.0 and later), the first row is placed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only when wrap property is set to true

at the top of the parent view, and successive rows are placed below the first
row. Each child is positioned vertically _within its row_ somewhat like
composite layout mode. In particular:
Expand All @@ -966,15 +969,15 @@ properties:
row.
* If either `top` or `bottom` is specified, the child is aligned to either
the top or bottom of the row, with the specified amount of padding.
* If *both* `top` and `bottom` is specified for a given child, the behavior
is undefined.
* If *both* `top` and `bottom` is specified for a given child, the properties
are both treated as padding.
* The `center` property is ignored in this mode.

On Android, the first row is centered vertically in the parent view, and
successive rows are placed below the first row as on iOS. However, the `top`
and `bottom` values are interpreted relative to the parent view.
When the wrap property is disabled, the behavior is identical to a vertical layout,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

property can not be disabled. Should read when wrap property is set to false.

except children are laid out horizontally from left to right, _in rows_.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be in rows when wrap is false. It's a single row. Something like this maybe?

If the `wrap` property is false, the behavior is more equivalent to a vertical layout. Children are laid or horizontally from left to right in a single row. The `left` and `right` properties are used as padding between the children, and the `top` and `bottom` properties are used to position the children vertically.


On Mobile Web _prior to_ Release 2.1.0, the horizontal layout does not wrap.
On Mobile Web _prior to_ Release 2.1.0, the horizontal layout does not wrap by default,
and does not support the wrap property.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

horizontalWrap property.

Also, I think we should add a paragraph:

On Android and iOS prior to Release 2.1.0, the horizontal layout always wraps and thehorizontalWrap property is not supported.

default: Composite layout

- name: opacity
Expand Down Expand Up @@ -1079,7 +1082,14 @@ properties:
the view's size once rendered, use the [rect](Titanium.UI.View.rect) or
[size](Titanium.UI.View.size) properties.
type: [Number,String]


- name: wrap
summary: Determines whether the layout has wrapping behavior.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a less ambiguous name like horizontalWrap

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Property still not renamed to horizontalWrap is docs

description: This is only available when the layout mode is set to 'horizontal'.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also include a cross-reference--this doesn't explain anything about what it does.

For more information, see the discussion of horizontal layout mode in the description of the [layout](Titanium.UI.View.layout) property.

type: Boolean
default: true
platforms: [android, iphone, ipad]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If mobileweb is going to support this, it should be included here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since: "2.1.0"


- name: zIndex
summary: Z-index stack order position, relative to other sibling views.
description: |
Expand Down
37 changes: 37 additions & 0 deletions drillbit/tests/ui.layout.horizontalVertical/tiapp.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<ti:app xmlns:ti="http://ti.appcelerator.org" xmlns:android="http://schemas.android.com/apk/res/android">
<id>org.appcelerator.titanium.testharness</id>
<name>test_harness</name>
<version>1.0</version>
<publisher>not specified</publisher>
<url>not specified</url>
<description>not specified</description>
<copyright>not specified</copyright>
<icon>appicon.png</icon>
<persistent-wifi>false</persistent-wifi>
<prerendered-icon>false</prerendered-icon>
<statusbar-style>default</statusbar-style>
<statusbar-hidden>false</statusbar-hidden>
<fullscreen>false</fullscreen>
<navbar-hidden>false</navbar-hidden>
<analytics>false</analytics>
<guid>CF0D2CB7-B4BD-488F-9F8E-669E6B53E0C4</guid>
<android>
<screens small="false" normal="true" large="true" anyDensity="false" />
<manifest>
<instrumentation
android:targetPackage="org.appcelerator.titanium.testharness"
android:name="org.appcelerator.titanium.drillbit.TestHarnessRunner">
<meta-data android:name="class" android:value="org.appcelerator.titanium.testharness.Test_harnessActivity"/>
</instrumentation>
</manifest>
</android>
<property name="ti.android.enablecoverage" type="bool">true</property>
<property name="ti.android.include_all_modules" type="bool">true</property>
<property name="ti.android.fastdev" type="bool">false</property>
<property name="ti.android.runtime"><%= androidRuntime %></property>
<property name="ti.android.threadstacksize" type="int">102400</property>

<property name="ti.ios.enablecoverage" type="bool">true</property>
<property name="ti.ios.enablemdfind" type="bool">false</property>
</ti:app>