commit 1299747cf6fc28ff840df10bd8d72bdcab4fbd6c Author: hshristov Date: Thu Jul 16 00:11:50 2015 +0300 NativeScript layouts implemented in Java diff --git a/.classpath b/.classpath new file mode 100644 index 000000000..1457e3c35 --- /dev/null +++ b/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e7aaf9b35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/.metadata/ +node_modules/ +dist/ + +local.properties + +gen/ +armeabi-v7a/ +x86/ +obj/ +bin/ +.svn/ +.settings/ \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 000000000..bd27c6d4c --- /dev/null +++ b/.project @@ -0,0 +1,33 @@ + + + widgets + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 000000000..7dc37d112 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/lint.xml b/lint.xml new file mode 100644 index 000000000..ee0eead5b --- /dev/null +++ b/lint.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/proguard-project.txt b/proguard-project.txt new file mode 100644 index 000000000..f2fe1559a --- /dev/null +++ b/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/project.properties b/project.properties new file mode 100644 index 000000000..484dab075 --- /dev/null +++ b/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library=true diff --git a/src/org/nativescript/widgets/AbsoluteLayout.java b/src/org/nativescript/widgets/AbsoluteLayout.java new file mode 100644 index 000000000..bb5ca5206 --- /dev/null +++ b/src/org/nativescript/widgets/AbsoluteLayout.java @@ -0,0 +1,81 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class AbsoluteLayout extends LayoutBase { + + public AbsoluteLayout(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int measureWidth = 0; + int measureHeight = 0; + int childMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + int count = this.getChildCount(); + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.measureChild(child, childMeasureSpec, childMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + measureWidth = Math.max(measureWidth, childLayoutParams.left + childMeasuredWidth); + measureHeight = Math.max(measureHeight, childLayoutParams.top + childMeasuredHeight); + } + + // Add in our padding + measureWidth += this.getPaddingLeft() + this.getPaddingRight(); + measureHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + + int leftPadding = this.getPaddingLeft(); + int topPadding = this.getPaddingTop(); + int count = this.getChildCount(); + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + + int childLeft = leftPadding + childLayoutParams.left; + int childTop = topPadding + childLayoutParams.top; + int childRight = childLeft + childWidth + childLayoutParams.leftMargin + childLayoutParams.rightMargin; + int childBottom = childTop + childHeight + childLayoutParams.topMargin + childLayoutParams.bottomMargin; + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childRight, childBottom); + } + } +} diff --git a/src/org/nativescript/widgets/CommonLayoutParams.java b/src/org/nativescript/widgets/CommonLayoutParams.java new file mode 100644 index 000000000..16f9f53b9 --- /dev/null +++ b/src/org/nativescript/widgets/CommonLayoutParams.java @@ -0,0 +1,251 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup.LayoutParams; +import android.widget.FrameLayout; + +/** + * @author hhristov + * + */ +public class CommonLayoutParams extends FrameLayout.LayoutParams { + + static final String tag = "NSLayout"; + static int debuggable = -1; + private static final StringBuilder sb = new StringBuilder(); + + public CommonLayoutParams() { + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + public int left = 0; + public int top = 0; + public int row = 0; + public int column = 0; + public int rowSpan = 1; + public int columnSpan = 1; + public Dock dock = Dock.left; + + public static int getDesiredWidth(View view) { + CommonLayoutParams lp = (CommonLayoutParams)view.getLayoutParams(); + return view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } + + public static int getDesiredHeight(View view) { + CommonLayoutParams lp = (CommonLayoutParams)view.getLayoutParams(); + return view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + } + + // We use our own layout method because the one in FrameLayout is broken when margins are set and gravity is CENTER_VERTICAL or CENTER_HORIZONTAL. + @SuppressLint("RtlHardcoded") + public static void layoutChild(View child, int left, int top, int right, int bottom) { + if (child.getVisibility() == View.GONE) { + return; + } + + int childTop = 0; + int childLeft = 0; + + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + + CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); + int gravity = lp.gravity; + if (gravity == -1) { + gravity = Gravity.FILL; + } + + int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + // If we have explicit height and gravity is FILL we need to be centered otherwise our explicit height won't be taken into account. + if (lp.height >= 0 && verticalGravity == Gravity.FILL_VERTICAL) { + verticalGravity = Gravity.CENTER_VERTICAL; + } + + switch (verticalGravity) { + case Gravity.TOP: + childTop = top + lp.topMargin; + break; + + case Gravity.CENTER_VERTICAL: + childTop = top + (bottom - top - childHeight + lp.topMargin - lp.bottomMargin) / 2; + break; + + case Gravity.BOTTOM: + childTop = bottom - childHeight - lp.bottomMargin; + break; + + case Gravity.FILL_VERTICAL: + default: + childTop = top + lp.topMargin; + childHeight = bottom - top - (lp.topMargin + lp.bottomMargin); + break; + } + + int horizontalGravity = Gravity.getAbsoluteGravity(gravity, child.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + + // If we have explicit width and gravity is FILL we need to be centered otherwise our explicit width won't be taken into account. + if (lp.width >= 0 && horizontalGravity == Gravity.FILL_HORIZONTAL) { + horizontalGravity = Gravity.CENTER_HORIZONTAL; + } + + switch (horizontalGravity) { + case Gravity.LEFT: + childLeft = left + lp.leftMargin; + break; + + case Gravity.CENTER_HORIZONTAL: + childLeft = left + (right - left - childWidth + lp.leftMargin - lp.rightMargin) / 2; + break; + + case Gravity.RIGHT: + childLeft = right - childWidth - lp.rightMargin; + break; + + case Gravity.FILL_HORIZONTAL: + default: + childLeft = left + lp.leftMargin; + childWidth = right - left - (lp.leftMargin + lp.rightMargin); + break; + } + + int childRight = Math.round(childLeft + childWidth); + int childBottom = Math.round(childTop + childHeight); + childLeft = Math.round(childLeft); + childTop = Math.round(childTop); + + if (debuggable > 0) { + sb.setLength(0); + sb.append(child.getParent().toString()); + sb.append(" :layoutChild: "); + sb.append(child.toString()); + sb.append(" "); + sb.append(childLeft); + sb.append(", "); + sb.append(childTop); + sb.append(", "); + sb.append(childRight); + sb.append(", "); + sb.append(childBottom); + log(tag, sb.toString()); + } + + child.layout(childLeft, childTop, childRight, childBottom); + } + + public static void measureChild(View child, int widthMeasureSpec, int heightMeasureSpec) { + if (child.getVisibility() == View.GONE) { + return; + } + + // Negative means we are not initialized. + if(debuggable < 0) { + try { + Context context = child.getContext(); + int flags = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).applicationInfo.flags; + debuggable = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 ? 1 : 0; + } + catch (NameNotFoundException e) { + debuggable = 0; + } + } + + if (debuggable > 0) { + sb.setLength(0); + sb.append(child.getParent().toString()); + sb.append(" :measureChild: "); + sb.append(child.toString()); + sb.append(" "); + sb.append(MeasureSpec.toString(widthMeasureSpec)); + sb.append(", "); + sb.append(MeasureSpec.toString(heightMeasureSpec)); + log(tag, sb.toString()); + } + + int childWidthMeasureSpec = getMeasureSpec(child, widthMeasureSpec, true); + int childHeightMeasureSpec = getMeasureSpec(child, heightMeasureSpec, false); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + static void log(String tag, String message) { + Log.d(tag, message); + } + + static StringBuilder getStringBuilder() { + sb.setLength(0); + return sb; + } + + private static int getMeasureSpec(View view, int parentMeasureSpec, boolean horizontal) { + + int parentLength = MeasureSpec.getSize(parentMeasureSpec); + int parentSpecMode = MeasureSpec.getMode(parentMeasureSpec); + + CommonLayoutParams lp = (CommonLayoutParams)view.getLayoutParams(); + final int margins = horizontal ? lp.leftMargin + lp.rightMargin : lp.topMargin + lp.bottomMargin; + + int resultSize = 0; + int resultMode = 0; + + int measureLength = Math.max(0, parentLength - margins); + int childLength = horizontal ? lp.width : lp.height; + + // We want a specific size... let be it. + if (childLength >= 0) { + if (parentSpecMode != MeasureSpec.UNSPECIFIED) { + resultSize = Math.min(parentLength, childLength); + } + else { + resultSize = childLength; + } + + resultMode = MeasureSpec.EXACTLY; + } + else { + switch (parentSpecMode) { + // Parent has imposed an exact size on us + case MeasureSpec.EXACTLY: + resultSize = measureLength; + int gravity = LayoutBase.getGravity(view); + boolean stretched; + if (horizontal) { + final int horizontalGravity = Gravity.getAbsoluteGravity(gravity, view.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + stretched = horizontalGravity == Gravity.FILL_HORIZONTAL; + } + else { + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + stretched = verticalGravity == Gravity.FILL_VERTICAL; + } + + // if stretched - view wants to be our size. So be it. + // else - view wants to determine its own size. It can't be bigger than us. + resultMode = stretched ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; + break; + + // Parent has imposed a maximum size on us + case MeasureSpec.AT_MOST: + resultSize = measureLength; + resultMode = MeasureSpec.AT_MOST; + break; + + case MeasureSpec.UNSPECIFIED: + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + break; + } + } + + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } +} diff --git a/src/org/nativescript/widgets/ContentLayout.java b/src/org/nativescript/widgets/ContentLayout.java new file mode 100644 index 000000000..aadb72f40 --- /dev/null +++ b/src/org/nativescript/widgets/ContentLayout.java @@ -0,0 +1,78 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class ContentLayout extends LayoutBase { + + public ContentLayout(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int measureWidth = 0; + int measureHeight = 0; + + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.measureChild(child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + measureWidth = Math.max(measureWidth, childMeasuredWidth); + measureHeight = Math.max(measureHeight, childMeasuredHeight); + } + + // Add in our padding + measureWidth += this.getPaddingLeft() + this.getPaddingRight(); + measureHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childLeft = paddingLeft; + int childTop = paddingTop; + + int childRight = right - left - (paddingLeft + paddingRight); + int childBottom = bottom - top - (paddingRight + paddingBottom); + + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childRight, childBottom); + } + } +} diff --git a/src/org/nativescript/widgets/Dock.java b/src/org/nativescript/widgets/Dock.java new file mode 100644 index 000000000..e35a4e8ff --- /dev/null +++ b/src/org/nativescript/widgets/Dock.java @@ -0,0 +1,15 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public enum Dock { + left, + top, + right, + bottom +} diff --git a/src/org/nativescript/widgets/DockLayout.java b/src/org/nativescript/widgets/DockLayout.java new file mode 100644 index 000000000..a36355e7a --- /dev/null +++ b/src/org/nativescript/widgets/DockLayout.java @@ -0,0 +1,176 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class DockLayout extends LayoutBase { + + private boolean _stretchLastChild = true; + + public DockLayout(Context context) { + super(context); + } + + public boolean getStretchLastChild() { + return this._stretchLastChild; + } + public void setStretchLastChild(boolean value) { + this._stretchLastChild = value; + this.requestLayout(); + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int measureWidth = 0; + int measureHeight = 0; + + int width = View.MeasureSpec.getSize(widthMeasureSpec); + int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); + + int height = View.MeasureSpec.getSize(heightMeasureSpec); + int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); + + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + int remainingWidth = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : width - horizontalPadding; + int remainingHeight = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : height - verticalPadding; + + int tempHeight = 0; + int tempWidth = 0; + int childWidthMeasureSpec = 0; + int childHeightMeasureSpec = 0; + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + if (this._stretchLastChild && (i == (count - 1))) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(remainingWidth, widthMode); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(remainingHeight, heightMode); + } + else { + // Measure children with AT_MOST even if our mode is EXACT + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(remainingWidth, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(remainingHeight, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode); + } + + CommonLayoutParams.measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + Dock dock = childLayoutParams.dock; + switch (dock) { + case top: + case bottom: + remainingHeight = Math.max(0, remainingHeight - childMeasuredHeight); + tempHeight += childMeasuredHeight; + measureWidth = Math.max(measureWidth, tempWidth + childMeasuredWidth); + measureHeight = Math.max(measureHeight, tempHeight); + break; + + case left: + case right: + default: + remainingWidth = Math.max(0, remainingWidth - childMeasuredWidth); + tempWidth += childMeasuredWidth; + measureWidth = Math.max(measureWidth, tempWidth); + measureHeight = Math.max(measureHeight, tempHeight + childMeasuredHeight); + break; + } + } + + // Add in our padding + measureWidth += horizontalPadding; + measureHeight += verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + + int childLeft = this.getPaddingLeft(); + int childTop = this.getPaddingTop(); + + int x = childLeft; + int y = childTop; + + int remainingWidth = Math.max(0, right - left - (this.getPaddingLeft() + this.getPaddingRight())); + int remainingHeight = Math.max(0, bottom - top - (this.getPaddingTop() + this.getPaddingBottom())); + + int count = this.getChildCount(); + View childToStretch = null; + if (count > 0 && this._stretchLastChild) { + count--; + childToStretch = this.getChildAt(count); + } + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + int childWidth = CommonLayoutParams.getDesiredWidth(child); + int childHeight = CommonLayoutParams.getDesiredHeight(child); + + switch (childLayoutParams.dock) { + case top: + childLeft = x; + childTop = y; + childWidth = remainingWidth; + y += childHeight; + remainingHeight = Math.max(0, remainingHeight - childHeight); + break; + + case bottom: + childLeft = x; + childTop = y + remainingHeight - childHeight; + childWidth = remainingWidth; + remainingHeight = Math.max(0, remainingHeight - childHeight); + break; + + case right: + childLeft = x + remainingWidth - childWidth; + childTop = y; + childHeight = remainingHeight; + remainingWidth = Math.max(0, remainingWidth - childWidth); + break; + + case left: + default: + childLeft = x; + childTop = y; + childHeight = remainingHeight; + x += childWidth; + remainingWidth = Math.max(0, remainingWidth - childWidth); + break; + } + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childLeft + childWidth, childTop + childHeight); + } + + if (childToStretch != null) { + CommonLayoutParams.layoutChild(childToStretch, x, y, x + remainingWidth, y + remainingHeight); + } + } +} diff --git a/src/org/nativescript/widgets/GridLayout.java b/src/org/nativescript/widgets/GridLayout.java new file mode 100644 index 000000000..abc3a9701 --- /dev/null +++ b/src/org/nativescript/widgets/GridLayout.java @@ -0,0 +1,1158 @@ +/** + * + */ +package org.nativescript.widgets; + +import java.util.ArrayList; +import java.util.HashMap; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; + +/** + * @author hhristov + * + */ +public class GridLayout extends LayoutBase { + + private MeasureHelper helper = new MeasureHelper(this); + + private ArrayList _rows = new ArrayList(); + private ArrayList _cols = new ArrayList(); + private ArrayList columnOffsets = new ArrayList(); + private ArrayList rowOffsets = new ArrayList(); + private HashMap map = new HashMap(); + + public GridLayout(Context context) { + super(context); + } + + private static void validateItemSpec(ItemSpec itemSpec) { + if (itemSpec == null) { + throw new Error("itemSpec is null."); + } + + if (itemSpec.owner != null) { + throw new Error("itemSpec is already added to GridLayout."); + } + } + + public void addRow(ItemSpec itemSpec) { + validateItemSpec(itemSpec); + itemSpec.owner = this; + this._rows.add(itemSpec); + + ItemGroup rowGroup = new ItemGroup(itemSpec); + this.helper.rows.add(rowGroup); + + this.requestLayout(); + } + + public void addColumn(ItemSpec itemSpec) { + validateItemSpec(itemSpec); + itemSpec.owner = this; + this._cols.add(itemSpec); + + ItemGroup columnGroup = new ItemGroup(itemSpec); + this.helper.columns.add(columnGroup); + + this.requestLayout(); + } + + public void removeColumn(ItemSpec itemSpec) { + if (itemSpec == null) { + throw new Error("itemSpec is null."); + } + + int index = this._cols.indexOf(itemSpec); + if (itemSpec.owner != this || index < 0) { + throw new Error("itemSpec is not child of this GridLayout"); + } + + this.removeColumnAt(index); + } + + public void removeColumnAt(int index) { + this._cols.remove(index); + this.helper.columns.get(index).children.clear(); + this.helper.columns.remove(index); + this.requestLayout(); + } + + public void removeRow(ItemSpec itemSpec) { + if (itemSpec == null) { + throw new Error("itemSpec is null."); + } + + int index = this._rows.indexOf(itemSpec); + if (itemSpec.owner != this || index < 0) { + throw new Error("itemSpec is not child of this GridLayout"); + } + + this.removeRowAt(index); + } + + public void removeRowAt(int index) { + this._rows.remove(index); + this.helper.rows.get(index).children.clear(); + this.helper.rows.remove(index); + this.requestLayout(); + } + + public ItemSpec[] getColumns() { + ItemSpec copy[] = new ItemSpec[this._cols.size()]; + copy = this._cols.toArray(copy); + return copy; + } + + public ItemSpec[] getRows() { + ItemSpec copy[] = new ItemSpec[this._rows.size()]; + copy = this._rows.toArray(copy); + return copy; + } + + @Override + public void addView(View child) { + super.addView(child); + this.addToMap(child); + } + + @Override + public void addView(View child, int index) { + super.addView(child, index); + this.addToMap(child); + } + + @Override + public void addView(View child, LayoutParams params) { + super.addView(child, params); + this.addToMap(child); + } + + @Override + public void removeView(View view) { + this.removeFromMap(view); + super.removeView(view); + } + + @Override + public void removeViewAt(int index) { + View view = this.getChildAt(index); + this.removeFromMap(view); + super.removeViewAt(index); + } + + @Override + public void removeViews(int start, int count) { + int end = start + count; + for (int i = start; i < end; i++) { + View view = this.getChildAt(i); + this.removeFromMap(view); + } + + super.removeViews(start, count); + } + + private int getColumnIndex(CommonLayoutParams lp) { + return Math.max(0, Math.min(lp.column, this._cols.size() - 1)); + } + + private int getRowIndex(CommonLayoutParams lp) { + return Math.max(0, Math.min(lp.row, this._rows.size() - 1)); + } + + private ItemSpec getColumnSpec(CommonLayoutParams lp) { + if (this._cols.size() == 0) { + return this.helper.singleColumn; + } + + int columnIndex = Math.min(lp.column, this._cols.size() - 1); + return this._cols.get(columnIndex); + } + + private ItemSpec getRowSpec(CommonLayoutParams lp) { + if (this._rows.size() == 0) { + return this.helper.singleRow; + } + + int rowIndex = Math.min(lp.row, this._rows.size() - 1); + return this._rows.get(rowIndex); + } + + private int getColumnSpan(CommonLayoutParams lp, int columnIndex) { + if (this._cols.size() == 0) { + return 1; + } + + return Math.min(lp.columnSpan, this._cols.size() - columnIndex); + } + + private int getRowSpan(CommonLayoutParams lp, int rowIndex) { + if (this._rows.size() == 0) { + return 1; + } + + return Math.min(lp.rowSpan, this._rows.size() - rowIndex); + } + + private void updateMeasureSpecs(View child, MeasureSpecs measureSpec) { + CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); + int columnIndex = this.getColumnIndex(lp); + ItemSpec column = this.getColumnSpec(lp); + int rowIndex = this.getRowIndex(lp); + ItemSpec row = this.getRowSpec(lp); + int columnSpan = this.getColumnSpan(lp, columnIndex); + int rowSpan = this.getRowSpan(lp, rowIndex); + + measureSpec.setColumn(column); + measureSpec.setRow(row); + measureSpec.setColumnIndex(columnIndex); + measureSpec.setRowIndex(rowIndex); + measureSpec.setColumnSpan(columnSpan); + measureSpec.setRowSpan(rowSpan); + measureSpec.autoColumnsCount = 0; + measureSpec.autoRowsCount = 0; + measureSpec.measured = false; + measureSpec.pixelHeight = 0; + measureSpec.pixelWidth = 0; + measureSpec.starColumnsCount = 0; + measureSpec.starRowsCount = 0; + } + + private void addToMap(View child) { + MeasureSpecs measureSpec = new MeasureSpecs(child); + this.map.put(child, measureSpec); + } + + private void removeFromMap(View child) { + this.map.remove(child); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int measureWidth = 0; + int measureHeight = 0; + + int width = View.MeasureSpec.getSize(widthMeasureSpec); + int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); + + int height = View.MeasureSpec.getSize(heightMeasureSpec); + int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); + + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + boolean infinityWidth = widthMode == MeasureSpec.UNSPECIFIED; + boolean infinityHeight = heightMode == MeasureSpec.UNSPECIFIED; + + this.helper.width = width - horizontalPadding; + this.helper.height = height - verticalPadding; + + int gravity = getGravity(this); + int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + final int layoutDirection = this.getLayoutDirection(); + final int horizontalGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection) & Gravity.HORIZONTAL_GRAVITY_MASK; + + this.helper.stretchedHorizontally = horizontalGravity == Gravity.FILL_HORIZONTAL && !infinityWidth; + this.helper.stretchedVertically = verticalGravity == Gravity.FILL_VERTICAL && !infinityHeight; + + this.helper.setInfinityWidth(infinityWidth); + this.helper.setInfinityHeight(infinityHeight); + + this.helper.clearMeasureSpecs(); + this.helper.init(); + + int childrenCount = this.getChildCount(); + for (int i = 0; i < childrenCount; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + MeasureSpecs measureSpecs = this.map.get(child); + this.updateMeasureSpecs(child, measureSpecs); + this.helper.addMeasureSpec(measureSpecs); + } + + this.helper.measure(); + + // Add in our padding + measureWidth = this.helper.measuredWidth + horizontalPadding; + measureHeight = this.helper.measuredHeight + verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + + int paddingLeft = this.getPaddingLeft(); + int paddingTop = this.getPaddingTop(); + + this.columnOffsets.clear(); + this.rowOffsets.clear(); + + this.columnOffsets.add(paddingLeft); + this.rowOffsets.add(paddingTop); + + int offset = this.columnOffsets.get(0); + int roundedOffset = this.getPaddingLeft(); + int roundedLength = 0; + int actualLength = 0; + int size = this.helper.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.helper.columns.get(i); + offset += columnGroup.length; + this.columnOffsets.add(offset); + + actualLength = offset - roundedOffset; + roundedLength = Math.round(actualLength); + columnGroup.rowOrColumn._actualLength = roundedLength; + roundedOffset += roundedLength; + } + + offset = this.rowOffsets.get(0); + roundedOffset = this.getPaddingTop(); + roundedLength = 0; + actualLength = 0; + size = this.helper.rows.size(); + for (int i = 0; i < size; i++) { + ItemGroup rowGroup = this.helper.rows.get(i); + offset += rowGroup.length; + this.rowOffsets.add(offset); + + actualLength = offset - roundedOffset; + roundedLength = Math.round(actualLength); + rowGroup.rowOrColumn._actualLength = roundedLength; + roundedOffset += roundedLength; + } + + int columns = this.helper.columns.size(); + for (int i = 0; i < columns; i++) { + ItemGroup columnGroup = this.helper.columns.get(i); + int children = columnGroup.children.size(); + for (int j = 0; j < children; j++) { + + MeasureSpecs measureSpec = columnGroup.children.get(j); + int childLeft = this.columnOffsets.get(measureSpec.getColumnIndex()); + int childRight = this.columnOffsets.get(measureSpec.getColumnIndex() + measureSpec.getColumnSpan()); + int childTop = this.rowOffsets.get(measureSpec.getRowIndex()); + int childBottom = this.rowOffsets.get(measureSpec.getRowIndex() + measureSpec.getRowSpan()); + + // No need to include margins in the width, height + CommonLayoutParams.layoutChild(measureSpec.child, childLeft, childTop, childRight, childBottom); + } + } + } +} + +class MeasureSpecs { + + private int _columnSpan = 1; + private int _rowSpan = 1; + + public int pixelWidth = 0; + public int pixelHeight = 0; + + public int starColumnsCount = 0; + public int starRowsCount = 0; + + public int autoColumnsCount = 0; + public int autoRowsCount = 0; + + public boolean measured = false; + + public final View child; + private ItemSpec column; + private ItemSpec row; + private int columnIndex; + private int rowIndex; + + MeasureSpecs(View child) { + this.child = child; + } + + public boolean getSpanned() { + return this._columnSpan > 1 || this._rowSpan > 1; + } + + public boolean getIsStar() { + return this.starRowsCount > 0 || this.starColumnsCount > 0; + } + + public int getColumnSpan() { + return this._columnSpan; + } + + public int getRowSpan() { + return this._rowSpan; + } + + public void setRowSpan(int value) { + // cannot have zero rowSpan. + this._rowSpan = Math.max(1, value); + } + + public void setColumnSpan(int value) { + // cannot have zero colSpan. + this._columnSpan = Math.max(1, value); + } + + public int getRowIndex() { + return this.rowIndex; + } + + public int getColumnIndex() { + return this.columnIndex; + } + + public void setRowIndex(int value) { + this.rowIndex = value; + } + + public void setColumnIndex(int value) { + this.columnIndex = value; + } + + public ItemSpec getRow() { + return this.row; + } + + public ItemSpec getColumn() { + return this.column; + } + + public void setRow(ItemSpec value) { + this.row = value; + } + + public void setColumn(ItemSpec value) { + this.column = value; + } +} + +class ItemGroup { + int length = 0; + int measuredCount = 0; + ItemSpec rowOrColumn; + ArrayList children = new ArrayList(); + + public int measureToFix = 0; + public int currentMeasureToFixCount = 0; + private boolean infinityLength = false; + + ItemGroup(ItemSpec spec) { + this.rowOrColumn = spec; + } + + public void setIsLengthInfinity(boolean infinityLength) { + this.infinityLength = infinityLength; + } + + public void init() { + this.measuredCount = 0; + this.currentMeasureToFixCount = 0; + this.length = this.rowOrColumn.getIsAbsolute() ? this.rowOrColumn.getValue() : 0; + } + + public boolean getAllMeasured() { + return this.measuredCount == this.children.size(); + } + + public boolean getCanBeFixed() { + return this.currentMeasureToFixCount == this.measureToFix; + } + + public boolean getIsAuto() { + return this.rowOrColumn.getIsAuto() || (this.rowOrColumn.getIsStar() && this.infinityLength); + } + + public boolean getIsStar() { + return this.rowOrColumn.getIsStar() && !this.infinityLength; + } + + public boolean getIsAbsolute() { + return this.rowOrColumn.getIsAbsolute(); + } +} + +class MeasureHelper { + public final ItemSpec singleRow = new ItemSpec(); + public final ItemSpec singleColumn = new ItemSpec(); + public final GridLayout grid; + + static int infinity = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + ArrayList rows = new ArrayList(); + ArrayList columns = new ArrayList(); + + public int width; + public int height; + public boolean stretchedHorizontally = false; + public boolean stretchedVertically = false; + + private boolean infinityWidth = false; + private boolean infinityHeight = false; + + int measuredWidth; + int measuredHeight; + + private int columnStarValue; + private int rowStarValue; + + private boolean fakeRowAdded = false; + private boolean fakeColumnAdded = false; + + MeasureHelper(GridLayout grid) { + this.grid = grid; + } + + public boolean getColumnsFixed() { + return this.columnStarValue >= 0; + } + + public void setInfinityWidth(boolean value) { + if (this.infinityWidth != value) { + this.infinityWidth = value; + + int size = this.columns.size(); + for(int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + columnGroup.setIsLengthInfinity(value); + } + } + } + + public void setInfinityHeight(boolean value) { + if (this.infinityHeight != value) { + this.infinityHeight = value; + + int size = this.rows.size(); + for(int i = 0; i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + rowGroup.setIsLengthInfinity(value); + } + } + } + + public void addMeasureSpec(MeasureSpecs measureSpec) { + // Get column stats + int size = measureSpec.getColumnIndex() + measureSpec.getColumnSpan(); + for (int i = measureSpec.getColumnIndex(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + measureSpec.autoColumnsCount++; + } + else if (columnGroup.getIsStar()) { + measureSpec.starColumnsCount += columnGroup.rowOrColumn.getValue(); + } + else if (columnGroup.getIsAbsolute()) { + measureSpec.pixelWidth += columnGroup.rowOrColumn.getValue(); + } + } + + if (measureSpec.autoColumnsCount > 0 && measureSpec.starColumnsCount == 0) { + // Determine which auto columns are affected by this element + for (int i = measureSpec.getColumnIndex(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.measureToFix++; + } + } + } + + // Get row stats + size = measureSpec.getRowIndex() + measureSpec.getRowSpan(); + for (int i = measureSpec.getRowIndex(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + measureSpec.autoRowsCount++; + } + else if (rowGroup.getIsStar()) { + measureSpec.starRowsCount += rowGroup.rowOrColumn.getValue(); + } + else if (rowGroup.getIsAbsolute()) { + measureSpec.pixelHeight += rowGroup.rowOrColumn.getValue(); + } + } + + if (measureSpec.autoRowsCount > 0 && measureSpec.starRowsCount == 0) { + // Determine which auto rows are affected by this element + for (int i = measureSpec.getRowIndex(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.measureToFix++; + } + } + } + + this.columns.get(measureSpec.getColumnIndex()).children.add(measureSpec); + this.rows.get(measureSpec.getRowIndex()).children.add(measureSpec); + } + + public void clearMeasureSpecs() { + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + this.columns.get(i).children.clear(); + } + + size = this.rows.size(); + for (int i = 0; i < size; i++) { + this.rows.get(i).children.clear(); + } + } + + private static void initList(ArrayList list) { + int size = list.size(); + for(int i = 0; i < size; i++) { + ItemGroup item = list.get(i); + item.init(); + } + } + + private ItemGroup singleRowGroup = new ItemGroup(singleRow); + private ItemGroup singleColumnGroup = new ItemGroup(singleColumn); + + void init() { + + int rows = this.rows.size(); + if (rows == 0) { + singleRowGroup.setIsLengthInfinity(this.infinityHeight); + this.rows.add(singleRowGroup); + this.fakeRowAdded = true; + } else if (rows > 1 && this.fakeRowAdded) { + this.rows.remove(0); + this.fakeRowAdded = false; + } + + int cols = this.columns.size(); + if (cols == 0) { + this.fakeColumnAdded = true; + singleColumnGroup.setIsLengthInfinity(this.infinityWidth); + this.columns.add(singleColumnGroup); + } + else if (cols > 1 && this.fakeColumnAdded) { + this.columns.remove(0); + this.fakeColumnAdded = false; + } + + initList(this.rows); + initList(this.columns); + + this.columnStarValue = -1; + this.rowStarValue = -1; + } + + private void itemMeasured(MeasureSpecs measureSpec, boolean isFakeMeasure) { + if (!isFakeMeasure) { + this.columns.get(measureSpec.getColumnIndex()).measuredCount++; + this.rows.get(measureSpec.getRowIndex()).measuredCount++; + measureSpec.measured = true; + } + + if (measureSpec.autoColumnsCount > 0 && measureSpec.starColumnsCount == 0) { + int size = measureSpec.getColumnIndex() + measureSpec.getColumnSpan(); + for (int i = measureSpec.getColumnIndex(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.currentMeasureToFixCount++; + } + } + } + + if (measureSpec.autoRowsCount > 0 && measureSpec.starRowsCount == 0) { + int size = measureSpec.getRowIndex() + measureSpec.getRowSpan(); + for (int i = measureSpec.getRowIndex(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.currentMeasureToFixCount++; + } + } + } + } + + private void fixColumns() { + int currentColumnWidth = 0; + int columnStarCount = 0; + + int columnCount = this.columns.size(); + for(int i = 0; i < columnCount; i++) { + ItemGroup item = this.columns.get(i); + + // Star columns are still zeros (not calculated). + currentColumnWidth += item.length; + if (item.rowOrColumn.getIsStar()) { + columnStarCount += item.rowOrColumn.getValue(); + } + } + + this.columnStarValue = columnStarCount > 0 ? (this.width - currentColumnWidth) / columnStarCount : 0; + + for(int i = 0; i < columnCount; i++) { + ItemGroup item = this.columns.get(i); + if (item.getIsStar()) { + item.length = item.rowOrColumn.getValue() * this.columnStarValue; + } + } + } + + private void fixRows() { + int currentRowHeight = 0; + int rowStarCount = 0; + + int rowCount = this.rows.size(); + for(int i = 0; i < rowCount; i++) { + ItemGroup item = this.rows.get(i); + + // Star rows are still zeros (not calculated). + currentRowHeight += item.length; + if (item.rowOrColumn.getIsStar()) { + rowStarCount += item.rowOrColumn.getValue(); + } + } + + this.rowStarValue = rowStarCount > 0 ? (this.height - currentRowHeight) / rowStarCount : 0; + + for(int i = 0; i < rowCount; i++) { + ItemGroup item = this.rows.get(i); + if (item.getIsStar()) { + item.length = item.rowOrColumn.getValue() * this.rowStarValue; + } + } + } + + private void fakeMeasure() { + // Fake measure - measure all elements that have star rows and auto columns - with infinity Width and infinity Height + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getAllMeasured()) { + continue; + } + + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.starRowsCount > 0 && measureSpec.autoColumnsCount > 0 && measureSpec.starColumnsCount == 0) { + this.measureChild(measureSpec, true); + } + } + } + } + + private void measureFixedColumnsNoStarRows() { + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.starColumnsCount > 0 && measureSpec.starRowsCount == 0) { + this.measureChildFixedColumns(measureSpec); + } + } + } + } + + private void measureNoStarColumnsFixedRows() { + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount ; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.starRowsCount > 0 && measureSpec.starColumnsCount == 0) { + this.measureChildFixedRows(measureSpec); + } + } + } + } + + private static boolean canFix(ArrayList list) { + int size = list.size(); + for (int i = 0; i < size; i++) { + ItemGroup item = list.get(i); + if(!item.getCanBeFixed()) { + return false; + } + } + + return true; + } + + private static int getMeasureLength(ArrayList list) { + int result = 0; + int size = list.size(); + for (int i = 0; i < size; i++) { + ItemGroup item = list.get(i); + result += item.length; + } + + return result; + } + + public void measure() { + + // Measure auto & pixel columns and rows (no spans). + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.getIsStar() || measureSpec.getSpanned()) { + continue; + } + + this.measureChild(measureSpec, false); + } + } + + // Measure auto & pixel columns and rows (with spans). + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.getIsStar() || !measureSpec.getSpanned()) { + continue; + } + + this.measureChild(measureSpec, false); + } + } + + // try fix stars! + boolean fixColumns = canFix(this.columns); + boolean fixRows = canFix(this.rows); + + if (fixColumns) { + this.fixColumns(); + } + + if (fixRows) { + this.fixRows(); + } + + if (!fixColumns && !fixRows) { + // Fake measure - measure all elements that have star rows and auto columns - with infinity Width and infinity Height + // should be able to fix rows after that + this.fakeMeasure(); + + this.fixColumns(); + + // Measure all elements that have star columns(already fixed), but no stars at the rows + this.measureFixedColumnsNoStarRows(); + + this.fixRows(); + } + else if (fixColumns && !fixRows) { + // Measure all elements that have star columns(already fixed) but no stars at the rows + this.measureFixedColumnsNoStarRows(); + + // Then + this.fixRows(); + } + else if (!fixColumns && fixRows) { + // Measure all elements that have star rows(already fixed) but no star at the columns + this.measureNoStarColumnsFixedRows(); + + // Then + this.fixColumns(); + } + + // Rows and columns are fixed here - measure remaining elements + size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childCount = columnGroup.children.size(); + for (int j = 0; j < childCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (!measureSpec.measured) { + this.measureChildFixedColumnsAndRows(measureSpec); + } + } + } + + this.measuredWidth = getMeasureLength(this.columns); + this.measuredHeight = getMeasureLength(this.rows); + } + + private void measureChild(MeasureSpecs measureSpec, boolean isFakeMeasure) { + int widthMeasureSpec = (measureSpec.autoColumnsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelWidth, MeasureSpec.EXACTLY); + int heightMeasureSpec = (isFakeMeasure || measureSpec.autoRowsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelHeight, MeasureSpec.EXACTLY); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + int columnSpanEnd = measureSpec.getColumnIndex() + measureSpec.getColumnSpan(); + int rowSpanEnd = measureSpec.getRowIndex() + measureSpec.getRowSpan(); + + if (measureSpec.autoColumnsCount > 0) { + int remainingSpace = childMeasuredWidth; + + for (int i = measureSpec.getColumnIndex(); i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + int growSize = remainingSpace / measureSpec.autoColumnsCount; + for (int i = measureSpec.getColumnIndex(); i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.length += growSize; + } + } + } + } + + if (!isFakeMeasure && measureSpec.autoRowsCount > 0) { + int remainingSpace = childMeasuredHeight; + + for (int i = measureSpec.getRowIndex(); i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + int growSize = remainingSpace / measureSpec.autoRowsCount; + for (int i = measureSpec.getRowIndex(); i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, isFakeMeasure); + } + + private void measureChildFixedColumns(MeasureSpecs measureSpec) { + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + + int columnsWidth = 0; + int growSize = 0; + + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (!columnGroup.getIsStar()) { + columnsWidth += columnGroup.length; + } + } + + int measureWidth = columnsWidth + measureSpec.starColumnsCount * this.columnStarValue; + + int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measureWidth, this.stretchedHorizontally ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST); + int heightMeasureSpec = (measureSpec.autoRowsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelHeight, MeasureSpec.EXACTLY); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + // Distribute width over star columns + if (!this.stretchedHorizontally) { + int remainingSpace = childMeasuredWidth; + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.starColumnsCount; + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsStar()) { + columnGroup.length += growSize; + } + } + } + } + + // Distribute height over auto rows + if (measureSpec.autoRowsCount > 0) { + int remainingSpace = childMeasuredHeight; + + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.autoRowsCount; + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, false); + } + + private void measureChildFixedRows(MeasureSpecs measureSpec) { + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + int rowsHeight = 0; + + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (!rowGroup.getIsStar()) { + rowsHeight += rowGroup.length; + } + } + + int measureHeight = rowsHeight + measureSpec.starRowsCount * this.rowStarValue; + + int widthMeasureSpec = (measureSpec.autoColumnsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelWidth, MeasureSpec.EXACTLY); + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, this.stretchedVertically ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + int remainingSpace = 0; + int growSize = 0; + + // Distribute width over auto columns + if (measureSpec.autoColumnsCount > 0) { + remainingSpace = childMeasuredWidth; + + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.autoColumnsCount; + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + + if (columnGroup.getIsAuto()) { + columnGroup.length += growSize; + } + } + } + } + + // Distribute height over star rows + if (!this.stretchedVertically) { + remainingSpace = childMeasuredHeight; + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.starRowsCount; + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsStar()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, false); + } + + private void measureChildFixedColumnsAndRows(MeasureSpecs measureSpec) { + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + + ItemGroup columnGroup; + ItemGroup rowGroup; + + int columnsWidth = 0; + for (int i = columnIndex; i < columnSpanEnd; i++) { + columnGroup = this.columns.get(i); + if (!columnGroup.getIsStar()) { + columnsWidth += columnGroup.length; + } + } + + int rowsHeight = 0; + for (int i = rowIndex; i < rowSpanEnd; i++) { + rowGroup = this.rows.get(i); + if (!rowGroup.getIsStar()) { + rowsHeight += rowGroup.length; + } + } + + int measureWidth = columnsWidth + measureSpec.starColumnsCount * this.columnStarValue; + int measureHeight = rowsHeight + measureSpec.starRowsCount * this.rowStarValue; + + // if (have stars) & (not stretch) - at most + int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measureWidth, + (measureSpec.starColumnsCount > 0 && !this.stretchedHorizontally) ? MeasureSpec.AT_MOST : MeasureSpec.EXACTLY); + + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, + (measureSpec.starRowsCount > 0 && !this.stretchedVertically) ? MeasureSpec.AT_MOST : MeasureSpec.EXACTLY); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + int remainingSpace = childMeasuredWidth; + int growSize = 0; + + if (!this.stretchedHorizontally) { + for (int i = columnIndex; i < columnSpanEnd; i++) { + columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.starColumnsCount; + for (int i = columnIndex; i < columnSpanEnd; i++) { + columnGroup = this.columns.get(i); + if (columnGroup.getIsStar()) { + columnGroup.length += growSize; + } + } + } + } + + remainingSpace = childMeasuredHeight; + + if (!this.stretchedVertically) { + for (int i = rowIndex; i < rowSpanEnd; i++) { + rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.starRowsCount; + for (int i = rowIndex; i < rowSpanEnd; i++) { + rowGroup = this.rows.get(i); + if (rowGroup.getIsStar()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, false); + } +} diff --git a/src/org/nativescript/widgets/GridUnitType.java b/src/org/nativescript/widgets/GridUnitType.java new file mode 100644 index 000000000..d52a3b047 --- /dev/null +++ b/src/org/nativescript/widgets/GridUnitType.java @@ -0,0 +1,14 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public enum GridUnitType { + auto, + pixel, + star +} diff --git a/src/org/nativescript/widgets/HorizontalScrollView.java b/src/org/nativescript/widgets/HorizontalScrollView.java new file mode 100644 index 000000000..734101d59 --- /dev/null +++ b/src/org/nativescript/widgets/HorizontalScrollView.java @@ -0,0 +1,247 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +/** + * @author hhristov + * + */ +public class HorizontalScrollView extends android.widget.HorizontalScrollView { + + private final Rect mTempRect = new Rect(); + + private int contentMeasuredWidth = 0; + private int contentMeasuredHeight = 0; + private int scrollableLength = 0; + private SavedState mSavedState; + private boolean isFirstLayout = true; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + public HorizontalScrollView(Context context) { + super(context); + } + + public int getScrollableLength() { + return this.scrollableLength; + } + + @Override + public void requestLayout() { + this.mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mIsLayoutDirty) { + this.scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Don't call measure because it will measure content twice. + // ScrollView is expected to have single child so we measure only the first child. + View child = this.getChildCount() > 0 ? this.getChildAt(0) : null; + if (child == null) { + this.scrollableLength = 0; + this.contentMeasuredWidth = 0; + this.contentMeasuredHeight = 0; + } + else { + CommonLayoutParams.measureChild(child, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightMeasureSpec); + this.contentMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + this.contentMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + // Android ScrollView does not account to child margins so we set them as paddings. Otherwise you can never scroll to bottom. + CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); + this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + } + + // Don't add in our paddings because they are already added as child margins. (we will include them twice if we add them). +// this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight(); +// this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth()); + this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childWidth = 0; + if (this.getChildCount() > 0) { + View child = this.getChildAt(0); + childWidth = child.getMeasuredWidth(); + + int width = right - left; + int height = bottom - top; + + this.scrollableLength = this.contentMeasuredWidth - width; + CommonLayoutParams.layoutChild(child, 0, 0, Math.max(this.contentMeasuredWidth, width), height); + this.scrollableLength = Math.max(0, this.scrollableLength); + } + + this.mIsLayoutDirty = false; + // Give a child focus if it needs it + if (this.mChildToScrollTo != null && isViewDescendantOf(this.mChildToScrollTo, this)) { + this.scrollToChild(this.mChildToScrollTo); + } + + this.mChildToScrollTo = null; + + int scrollX = this.getScrollX(); + int scrollY = this.getScrollY(); + + if (this.isFirstLayout) { + this.isFirstLayout = false; + + final int scrollRange = Math.max(0, childWidth - (right - left - this.getPaddingLeft() - this.getPaddingRight())); + if (this.mSavedState != null) { + scrollX = (this.isLayoutRtl() == mSavedState.isLayoutRtl) ? mSavedState.scrollPosition : (scrollRange - this.mSavedState.scrollPosition); + mSavedState = null; + } else { + if (this.isLayoutRtl()) { + scrollX = scrollRange - scrollX; + } // mScrollX default value is "0" for LTR + } + // Don't forget to clamp + if (scrollX > scrollRange) { + scrollX = scrollRange; + } else if (scrollX < 0) { + scrollX = 0; + } + } + + // Calling this with the present values causes it to re-claim them + this.scrollTo(scrollX, scrollY); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + this.mSavedState = ss; + this.requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = this.getScrollX(); + ss.isLayoutRtl = this.isLayoutRtl(); + return ss; + } + + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + this.scrollBy(scrollDelta, 0); + } + } + + private boolean isLayoutRtl() { + return (this.getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + /** + * Return true if child is a descendant of parent, (or equal to the parent). + */ + static boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + static class SavedState extends BaseSavedState { + public int scrollPosition; + public boolean isLayoutRtl; + + SavedState(Parcelable superState) { + super(superState); + } + + public SavedState(Parcel source) { + super(source); + scrollPosition = source.readInt(); + isLayoutRtl = (source.readInt() == 0) ? true : false; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(scrollPosition); + dest.writeInt(isLayoutRtl ? 1 : 0); + } + + @Override + public String toString() { + return "HorizontalScrollView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " scrollPosition=" + scrollPosition + + " isLayoutRtl=" + isLayoutRtl + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/src/org/nativescript/widgets/ImageView.java b/src/org/nativescript/widgets/ImageView.java new file mode 100644 index 000000000..e4dd40e06 --- /dev/null +++ b/src/org/nativescript/widgets/ImageView.java @@ -0,0 +1,125 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +/** + * @author hhristov + * + */ +public class ImageView extends android.widget.ImageView { + + private double scaleW = 1; + private double scaleH = 1; + + public ImageView(Context context) { + super(context); + this.setScaleType(ScaleType.FIT_CENTER); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + Drawable drawable = this.getDrawable(); + int measureWidth; + int measureHeight; + if (drawable != null) { + measureWidth = drawable.getIntrinsicWidth(); + measureHeight = drawable.getIntrinsicHeight(); + } + else { + measureWidth = 0; + measureHeight = 0; + } + + boolean finiteWidth = widthMode != MeasureSpec.UNSPECIFIED; + boolean finiteHeight = heightMode != MeasureSpec.UNSPECIFIED; + + if (measureWidth != 0 && measureHeight != 0 && (finiteWidth || finiteHeight)) { + this.computeScaleFactor(width, height, finiteWidth, finiteHeight, measureWidth, measureHeight); + int resultW = (int) Math.floor(measureWidth * this.scaleW); + int resultH = (int) Math.floor(measureHeight * this.scaleH); + + measureWidth = finiteWidth ? Math.min(resultW, width) : resultW; + measureHeight = finiteHeight ? Math.min(resultH, height) : resultH; + } + + measureWidth += this.getPaddingLeft() + this.getPaddingRight(); + measureHeight += this.getPaddingTop() + this.getPaddingBottom(); + + measureWidth = Math.max(measureWidth, getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, getSuggestedMinimumHeight()); + + if (CommonLayoutParams.debuggable > 0) { + StringBuilder sb = CommonLayoutParams.getStringBuilder(); + sb.append("ImageView onMeasure: "); + sb.append(MeasureSpec.toString(widthMeasureSpec)); + sb.append(", "); + sb.append(MeasureSpec.toString(heightMeasureSpec)); + sb.append(", stretch: "); + sb.append(this.getScaleType()); + sb.append(", measureWidth: "); + sb.append(measureWidth); + sb.append(", measureHeight: "); + sb.append(measureHeight); + + CommonLayoutParams.log(CommonLayoutParams.tag, sb.toString()); + } + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + private void computeScaleFactor( + int measureWidth, + int measureHeight, + boolean widthIsFinite, + boolean heightIsFinite, + double nativeWidth, + double nativeHeight) { + + this.scaleW = 1; + this.scaleH = 1; + + ScaleType scale = this.getScaleType(); + if ((scale == ScaleType.CENTER_CROP || scale == ScaleType.FIT_CENTER || scale == ScaleType.FIT_XY) && + (widthIsFinite || heightIsFinite)) { + + this.scaleW = (nativeWidth > 0) ? measureWidth / nativeWidth : 0d; + this.scaleH = (nativeHeight > 0) ? measureHeight / nativeHeight : 0d; + + if (!widthIsFinite) { + this.scaleW = scaleH; + } + else if (!heightIsFinite) { + this.scaleH = scaleW; + } + else { + // No infinite dimensions. + switch (scale) { + case FIT_CENTER: + this.scaleH = this.scaleW < this.scaleH ? this.scaleW : this.scaleH; + this.scaleW = this.scaleH; + break; + case CENTER_CROP: + this.scaleH = this.scaleW > this.scaleH ? this.scaleW : this.scaleH; + this.scaleW = this.scaleH; + break; + default: + break; + } + } + } + } +} diff --git a/src/org/nativescript/widgets/ItemSpec.java b/src/org/nativescript/widgets/ItemSpec.java new file mode 100644 index 000000000..64fc3a884 --- /dev/null +++ b/src/org/nativescript/widgets/ItemSpec.java @@ -0,0 +1,60 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public class ItemSpec { + + private int _value; + private GridUnitType _unitType; + + public ItemSpec() { + this(1, GridUnitType.star); + } + + public ItemSpec(int value, GridUnitType unitType) { + this._value = value; + this._unitType = unitType; + } + + GridLayout owner; + int _actualLength = 0; + + @Override + public boolean equals(Object o) { + if (!(o instanceof ItemSpec)) { + return false; + } + + ItemSpec other = (ItemSpec)o; + return (this._unitType == other._unitType) && (this._value == other._value) && (this.owner == other.owner); + } + + public GridUnitType getGridUnitType() { + return this._unitType; + } + + public boolean getIsAbsolute() { + return this._unitType == GridUnitType.pixel; + } + + public boolean getIsAuto() { + return this._unitType == GridUnitType.auto; + } + + public boolean getIsStar() { + return this._unitType == GridUnitType.star; + } + + public int getValue() { + return this._value; + } + + public int getActualLength() { + return this._actualLength; + } +} diff --git a/src/org/nativescript/widgets/LayoutBase.java b/src/org/nativescript/widgets/LayoutBase.java new file mode 100644 index 000000000..3f504485b --- /dev/null +++ b/src/org/nativescript/widgets/LayoutBase.java @@ -0,0 +1,68 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * @author hhristov + * + */ +public abstract class LayoutBase extends ViewGroup { + + public LayoutBase(Context context) { + super(context); + } + + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof CommonLayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new CommonLayoutParams(); + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + protected static int getGravity(View view) { + int gravity = -1; + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof FrameLayout.LayoutParams) { + gravity = ((FrameLayout.LayoutParams)params).gravity; + } + + if (gravity == -1) { + gravity = Gravity.FILL; + } + + return gravity; + } +} diff --git a/src/org/nativescript/widgets/Orientation.java b/src/org/nativescript/widgets/Orientation.java new file mode 100644 index 000000000..bd91e6cfc --- /dev/null +++ b/src/org/nativescript/widgets/Orientation.java @@ -0,0 +1,13 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public enum Orientation { + horzontal, + vertical +} diff --git a/src/org/nativescript/widgets/StackLayout.java b/src/org/nativescript/widgets/StackLayout.java new file mode 100644 index 000000000..43e943434 --- /dev/null +++ b/src/org/nativescript/widgets/StackLayout.java @@ -0,0 +1,217 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.Gravity; +import android.view.View; + +/** + * @author hhristov + * + */ +public class StackLayout extends LayoutBase { + + private int _totalLength = 0; + private Orientation _orientation = Orientation.vertical; + + public StackLayout(Context context) { + super(context); + } + + public Orientation getOrientation() { + return this._orientation; + } + public void setOrientation(Orientation value) { + this._orientation = value; + this.requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int childState = 0; + int measureWidth = 0; + int measureHeight = 0; + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + boolean isVertical = this._orientation == Orientation.vertical; + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + int count = this.getChildCount(); + int measureSpecMode; + int remainingLength; + + int mode = isVertical ? heightMode : widthMode; + if (mode == MeasureSpec.UNSPECIFIED) { + measureSpecMode = MeasureSpec.UNSPECIFIED; + remainingLength = 0; + } + else { + measureSpecMode = MeasureSpec.AT_MOST; + remainingLength = isVertical ? height - verticalPadding : width - horizontalPadding; + } + + int childMeasureSpec; + if (isVertical) { + int childWidth = (widthMode == MeasureSpec.UNSPECIFIED) ? 0 : width - horizontalPadding; + childWidth = Math.max(0, childWidth); + childMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, widthMode); + } + else { + int childHeight = (heightMode == MeasureSpec.UNSPECIFIED) ? 0 : height - verticalPadding; + childHeight = Math.max(0, childHeight); + childMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, heightMode); + } + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + if (isVertical) { + CommonLayoutParams.measureChild(child, childMeasureSpec, MeasureSpec.makeMeasureSpec(remainingLength, measureSpecMode)); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + measureWidth = Math.max(measureWidth, childMeasuredWidth); + measureHeight += childMeasuredHeight; + remainingLength = Math.max(0, remainingLength - childMeasuredHeight); + } + else { + CommonLayoutParams.measureChild(child, MeasureSpec.makeMeasureSpec(remainingLength, measureSpecMode), heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + measureHeight = Math.max(measureHeight, childMeasuredHeight); + measureWidth += childMeasuredWidth; + remainingLength = Math.max(0, remainingLength - childMeasuredWidth); + } + + childState = combineMeasuredStates(childState, child.getMeasuredState()); + } + + // Add in our padding + measureWidth += horizontalPadding; + measureHeight += verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + this._totalLength = isVertical ? measureHeight : measureWidth; + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, isVertical ? childState : 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, isVertical ? 0 : childState); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + + if (this._orientation == Orientation.vertical) { + this.layoutVertical(l, t, r, b); + } + else { + this.layoutHorizontal(l, t, r, b); + } + } + + private void layoutVertical(int left, int top, int right, int bottom) { + + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childTop = 0; + int childLeft = paddingLeft; + int childRight = right - left - paddingRight; + + int gravity = getGravity(this); + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (verticalGravity) { + case Gravity.CENTER_VERTICAL: + childTop = (bottom - top - this._totalLength) / 2 + paddingTop - paddingBottom; + break; + + case Gravity.BOTTOM: + childTop = bottom - top - this._totalLength + paddingTop - paddingBottom; + break; + + case Gravity.TOP: + case Gravity.FILL_VERTICAL: + default: + childTop = paddingTop; + break; + } + + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + int childHeight = child.getMeasuredHeight() + childLayoutParams.topMargin + childLayoutParams.bottomMargin; + CommonLayoutParams.layoutChild(child, childLeft, childTop, childRight, childTop + childHeight); + childTop += childHeight; + } + } + + @SuppressLint("RtlHardcoded") + private void layoutHorizontal(int left, int top, int right, int bottom) { + + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childTop = paddingTop; + int childLeft = 0; + int childBottom = bottom - top - paddingBottom; + + int gravity = getGravity(this); + final int horizontalGravity = Gravity.getAbsoluteGravity(gravity, this.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + + switch (horizontalGravity) { + case Gravity.CENTER_HORIZONTAL: + childLeft = (right - left - this._totalLength) / 2 + paddingLeft - paddingRight; + break; + + case Gravity.RIGHT: + childLeft = right - left - this._totalLength + paddingLeft - paddingRight; + break; + + case Gravity.LEFT: + case Gravity.FILL_HORIZONTAL: + default: + childLeft = paddingLeft; + break; + } + + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + int childWidth = child.getMeasuredWidth() + childLayoutParams.leftMargin + childLayoutParams.rightMargin; + CommonLayoutParams.layoutChild(child, childLeft, childTop, childLeft + childWidth, childBottom); + childLeft += childWidth; + } + } +} diff --git a/src/org/nativescript/widgets/VerticalScrollView.java b/src/org/nativescript/widgets/VerticalScrollView.java new file mode 100644 index 000000000..d758b1174 --- /dev/null +++ b/src/org/nativescript/widgets/VerticalScrollView.java @@ -0,0 +1,186 @@ +/** + * + */ +package org.nativescript.widgets; + +import org.nativescript.widgets.HorizontalScrollView.SavedState; +import android.content.Context; +import android.graphics.Rect; +import android.os.Parcelable; +import android.view.View; +import android.widget.ScrollView; + +/** + * @author hhristov + * + */ +public class VerticalScrollView extends ScrollView { + + private final Rect mTempRect = new Rect(); + + private int contentMeasuredWidth = 0; + private int contentMeasuredHeight = 0; + private int scrollableLength = 0; + private SavedState mSavedState; + private boolean isFirstLayout = true; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + public VerticalScrollView(Context context) { + super(context); + } + + public int getScrollableLength() { + return this.scrollableLength; + } + + @Override + public void requestLayout() { + this.mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!this.mIsLayoutDirty) { + this.scrollToChild(focused); + } + else { + // The child may not be laid out yet, we can't compute the scroll yet + this.mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Don't call measure because it will measure content twice. + // ScrollView is expected to have single child so we measure only the first child. + View child = this.getChildCount() > 0 ? this.getChildAt(0) : null; + if (child == null) { + this.scrollableLength = 0; + this.contentMeasuredWidth = 0; + this.contentMeasuredHeight = 0; + this.setPadding(0, 0, 0, 0); + } + else { + CommonLayoutParams.measureChild(child, widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + this.contentMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + this.contentMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + // Android ScrollView does not account to child margins so we set them as paddings. Otherwise you can never scroll to bottom. + CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); + this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + } + + // Don't add in our paddings because they are already added as child margins. (we will include them twice if we add them). +// this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight(); +// this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth()); + this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childHeight = 0; + if (this.getChildCount() > 0) { + View child = this.getChildAt(0); + childHeight = child.getMeasuredHeight(); + + int width = right - left; + int height = bottom - top; + + this.scrollableLength = this.contentMeasuredHeight - height; + CommonLayoutParams.layoutChild(child, 0, 0, width, Math.max(this.contentMeasuredHeight, height)); + this.scrollableLength = Math.max(0, this.scrollableLength); + } + + this.mIsLayoutDirty = false; + // Give a child focus if it needs it + if (this.mChildToScrollTo != null && HorizontalScrollView.isViewDescendantOf(this.mChildToScrollTo, this)) { + this.scrollToChild(this.mChildToScrollTo); + } + + this.mChildToScrollTo = null; + + int scrollX = this.getScrollX(); + int scrollY = this.getScrollY(); + if (this.isFirstLayout) { + this.isFirstLayout = false; + + final int scrollRange = Math.max(0, childHeight - (bottom - top - this.getPaddingTop() - this.getPaddingBottom())); + if (this.mSavedState != null) { + scrollY = mSavedState.scrollPosition; + mSavedState = null; + } + + // Don't forget to clamp + if (scrollY > scrollRange) { + scrollY = scrollRange; + } else if (scrollY < 0) { + scrollY = 0; + } + } + + // Calling this with the present values causes it to re-claim them + this.scrollTo(scrollX, scrollY); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + this.mSavedState = ss; + this.requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = this.getScrollY(); + return ss; + } + + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + this.scrollBy(scrollDelta, 0); + } + } +} diff --git a/src/org/nativescript/widgets/WrapLayout.java b/src/org/nativescript/widgets/WrapLayout.java new file mode 100644 index 000000000..3559f419a --- /dev/null +++ b/src/org/nativescript/widgets/WrapLayout.java @@ -0,0 +1,236 @@ +/** + * + */ +package org.nativescript.widgets; + +import java.util.ArrayList; +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class WrapLayout extends LayoutBase { + + private int _itemWidth = -1; + private int _itemHeight = -1; + private Orientation _orientation = Orientation.horzontal; + private ArrayList _lenghts = new ArrayList(); + + public WrapLayout(Context context) { + super(context); + } + + public Orientation getOrientation() { + return this._orientation; + } + public void setOrientation(Orientation value) { + this._orientation = value; + this.requestLayout(); + } + + public int getItemWidth() { + return this._itemWidth; + } + public void setItemWidth(int value) { + this._itemWidth = value; + this.requestLayout(); + } + + public int getItemHeight() { + return this._itemHeight; + } + public void setItemHeight(int value) { + this._itemHeight = value; + this.requestLayout(); + } + + private static int getViewMeasureSpec(int parentMode, int parentLength, int itemLength) { + if (itemLength > 0) { + return MeasureSpec.makeMeasureSpec(itemLength, MeasureSpec.EXACTLY); + } + else if (parentMode == MeasureSpec.UNSPECIFIED) { + return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + else { + return MeasureSpec.makeMeasureSpec(parentLength, MeasureSpec.AT_MOST); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int measureWidth = 0; + int measureHeight = 0; + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + boolean isVertical = this._orientation == Orientation.vertical; + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + int childWidthMeasureSpec = getViewMeasureSpec(widthMode, width, this._itemWidth); + int childHeightMeasureSpec = getViewMeasureSpec(heightMode, height, this._itemHeight); + + int remainingWidth = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : width - horizontalPadding; + int remainingHeight = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : height - verticalPadding; + + int count = this.getChildCount(); + + this._lenghts.clear(); + int rowOrColumn = 0; + int maxLenght = 0; + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + if (isVertical) { + if (childMeasuredHeight > remainingHeight) { + rowOrColumn++; + maxLenght = Math.max(maxLenght, measureHeight); + measureHeight = childMeasuredHeight; + remainingWidth = height - childMeasuredHeight; + this._lenghts.add(rowOrColumn, childMeasuredWidth); + } + else { + remainingHeight -= childMeasuredHeight; + measureHeight += childMeasuredHeight; + } + } + else { + if (childMeasuredWidth > remainingWidth) { + rowOrColumn++; + maxLenght = Math.max(maxLenght, measureWidth); + measureWidth = childMeasuredWidth; + remainingWidth = width - childMeasuredWidth; + this._lenghts.add(rowOrColumn, childMeasuredHeight); + } + else { + remainingWidth -= childMeasuredWidth; + measureWidth += childMeasuredWidth; + } + } + + if(this._lenghts.size() <= rowOrColumn) { + this._lenghts.add(rowOrColumn, isVertical ? childMeasuredWidth : childMeasuredHeight); + } + else { + this._lenghts.set(rowOrColumn, Math.max(this._lenghts.get(rowOrColumn), isVertical ? childMeasuredWidth : childMeasuredHeight)); + } + } + + count = this._lenghts.size(); + if (isVertical) { + measureHeight = Math.max(maxLenght, measureHeight); + for (int i = 0; i < count; i++) { + measureWidth += this._lenghts.get(i); + } + } + else { + measureWidth = Math.max(maxLenght, measureWidth); + for (int i = 0; i < count; i++) { + measureHeight += this._lenghts.get(i); + } + } + + // Add in our padding + measureWidth += horizontalPadding; + measureHeight += verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + + boolean isVertical = this._orientation == Orientation.vertical; + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childLeft = paddingLeft; + int childTop = paddingTop; + int childrenLength = isVertical ? bottom - top - (paddingRight + paddingBottom) : right - left - (paddingLeft + paddingRight); + + int rowOrColumn = 0; + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + // Add margins because layoutChild will subtract them. + int childWidth = CommonLayoutParams.getDesiredWidth(child); + int childHeight = CommonLayoutParams.getDesiredHeight(child); + + int length = this._lenghts.get(rowOrColumn); + if (isVertical) { + + childWidth = length; + childHeight = this._itemHeight > 0 ? this._itemHeight : childHeight; + if (childTop + childHeight > childrenLength) { + // Move to top. + childTop = paddingTop; + + // Move to right with current column width. + childLeft += length; + + // Move to next column. + rowOrColumn++; + + // Take current column width. + childWidth = length = this._lenghts.get(rowOrColumn); + } + } + else { + childWidth = this._itemWidth > 0 ? this._itemWidth : childWidth; + childHeight = length; + if (childLeft + childWidth > childrenLength) { + // Move to left. + childLeft = paddingLeft; + + // Move to bottom with current row height. + childTop += length; + + // Move to next column. + rowOrColumn++; + + // Take current row height. + childHeight = length = this._lenghts.get(rowOrColumn); + } + } + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childLeft + childWidth, childTop + childHeight); + + if (isVertical) { + // Move next child Top position to bottom. + childTop += childHeight; + } + else { + // Move next child Left position to right. + childLeft += childWidth; + } + } + } +}