/** * */ 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; 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 int NOT_SET = Integer.MIN_VALUE; private static final StringBuilder sb = new StringBuilder(); public CommonLayoutParams() { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.FILL); } public CommonLayoutParams(ViewGroup.LayoutParams source) { super(source); } public CommonLayoutParams(ViewGroup.MarginLayoutParams source) { super(source); } public CommonLayoutParams(FrameLayout.LayoutParams source) { super((ViewGroup.MarginLayoutParams) source); this.gravity = source.gravity; } public CommonLayoutParams(CommonLayoutParams source) { this((FrameLayout.LayoutParams) source); this.widthPercent = source.widthPercent; this.heightPercent = source.heightPercent; this.topMargin = source.topMargin; this.leftMargin = source.leftMargin; this.bottomMargin = source.bottomMargin; this.rightMargin = source.rightMargin; this.left = source.left; this.top = source.top; this.row = source.row; this.column = source.column; this.rowSpan = source.rowSpan; this.columnSpan = source.columnSpan; this.dock = source.dock; } public float widthPercent = 0; public float heightPercent = 0; public float topMarginPercent = 0; public float leftMarginPercent = 0; public float bottomMarginPercent = 0; public float rightMarginPercent = 0; public int widthOriginal = NOT_SET; public int heightOriginal = NOT_SET; public int topMarginOriginal = NOT_SET; public int leftMarginOriginal = NOT_SET; public int bottomMarginOriginal = NOT_SET; public int rightMarginOriginal = NOT_SET; 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; protected static int getDesiredWidth(View view) { CommonLayoutParams lp = (CommonLayoutParams) view.getLayoutParams(); return view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; } protected 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") protected 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 || lp.heightPercent > 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 || lp.widthPercent > 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); // Re-measure TextView because it is not centered if layout width is larger than measure width. if (child instanceof android.widget.TextView) { boolean canChangeWidth = lp.width < 0; boolean canChangeHeight = lp.height < 0; int measuredWidth = child.getMeasuredWidth(); int measuredHeight = child.getMeasuredHeight(); int width = childRight - childLeft; int height = childBottom - childTop; if ((Math.abs(measuredWidth - width) > 1 && canChangeWidth) || (Math.abs(measuredHeight - height) > 1 && canChangeHeight)) { int widthMeasureSpec = MeasureSpec.makeMeasureSpec(canChangeWidth ? width : lp.width, MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(canChangeHeight ? height : lp.height, MeasureSpec.EXACTLY); if (debuggable > 0) { sb.setLength(0); sb.append("remeasure "); sb.append(child); sb.append(" with "); sb.append(MeasureSpec.toString(widthMeasureSpec)); sb.append(", "); sb.append(MeasureSpec.toString(heightMeasureSpec)); log(TAG, sb.toString()); } child.measure(widthMeasureSpec, heightMeasureSpec); } } 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); } protected static void measureChild(View child, int widthMeasureSpec, int heightMeasureSpec) { if (child.getVisibility() == View.GONE) { return; } // Negative means not initialized. if (debuggable < 0) { try { Context context = child.getContext(); ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), android.content.pm.PackageManager.GET_META_DATA); android.os.Bundle bundle = ai.metaData; Boolean debugLayouts = bundle != null ? bundle.getBoolean("debugLayouts", false) : false; debuggable = debugLayouts ? 1 : 0; } catch (NameNotFoundException e) { debuggable = 0; Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage()); } catch (NullPointerException e) { debuggable = 0; Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage()); } } int childWidthMeasureSpec = getMeasureSpec(child, widthMeasureSpec, true); int childHeightMeasureSpec = getMeasureSpec(child, heightMeasureSpec, false); if (debuggable > 0) { sb.setLength(0); sb.append(child.getParent().toString()); sb.append(" :measureChild: "); sb.append(child.toString()); sb.append(" "); sb.append(MeasureSpec.toString(childWidthMeasureSpec)); sb.append(", "); sb.append(MeasureSpec.toString(childHeightMeasureSpec)); log(TAG, sb.toString()); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } /** * Iterates over children and changes their width and height to one calculated from percentage * values. * * @param viewGroup The parent ViewGroup. * @param widthMeasureSpec Width MeasureSpec of the parent ViewGroup. * @param heightMeasureSpec Height MeasureSpec of the parent ViewGroup. */ protected static void adjustChildrenLayoutParams(ViewGroup viewGroup, int widthMeasureSpec, int heightMeasureSpec) { int availableWidth = MeasureSpec.getSize(widthMeasureSpec); int widthSpec = MeasureSpec.getMode(widthMeasureSpec); int availableHeight = MeasureSpec.getSize(heightMeasureSpec); int heightSpec = MeasureSpec.getMode(heightMeasureSpec); for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { View child = viewGroup.getChildAt(i); LayoutParams params = child.getLayoutParams(); if (params instanceof CommonLayoutParams) { CommonLayoutParams lp = (CommonLayoutParams) child.getLayoutParams(); if (widthSpec != MeasureSpec.UNSPECIFIED) { if (lp.widthPercent > 0) { // If we get measured twice we will override the original value with the one calculated from percentValue from the first measure. // So we set originalValue only the first time. if (lp.widthOriginal == NOT_SET) { lp.widthOriginal = lp.width; } lp.width = (int) (availableWidth * lp.widthPercent); } else { lp.widthOriginal = NOT_SET; } if (lp.leftMarginPercent > 0) { if (lp.leftMarginOriginal == NOT_SET) { lp.leftMarginOriginal = lp.leftMargin; } lp.leftMargin = (int) (availableWidth * lp.leftMarginPercent); } else { lp.leftMarginOriginal = NOT_SET; } if (lp.rightMarginPercent > 0) { if (lp.rightMarginOriginal == NOT_SET) { lp.rightMarginOriginal = lp.rightMargin; } lp.rightMargin = (int) (availableWidth * lp.rightMarginPercent); } else { lp.rightMarginOriginal = NOT_SET; } } if (heightSpec != MeasureSpec.UNSPECIFIED) { if (lp.heightPercent > 0) { if (lp.heightOriginal == NOT_SET) { lp.heightOriginal = lp.height; } lp.height = (int) (availableHeight * lp.heightPercent); } else { lp.heightOriginal = NOT_SET; } if (lp.topMarginPercent > 0) { if (lp.topMarginOriginal == NOT_SET) { lp.topMarginOriginal = lp.topMargin; } lp.topMargin = (int) (availableHeight * lp.topMarginPercent); } else { lp.topMarginOriginal = NOT_SET; } if (lp.bottomMarginPercent > 0) { if (lp.bottomMarginOriginal == NOT_SET) { lp.bottomMarginOriginal = lp.bottomMargin; } lp.bottomMargin = (int) (availableHeight * lp.bottomMarginPercent); } else { lp.bottomMarginOriginal = NOT_SET; } } } } } /** * Iterates over children and restores their original dimensions that were changed for * percentage values. */ protected static void restoreOriginalParams(ViewGroup viewGroup) { for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { View view = viewGroup.getChildAt(i); LayoutParams params = view.getLayoutParams(); if (params instanceof CommonLayoutParams) { CommonLayoutParams lp = (CommonLayoutParams) params; if (lp.widthPercent > 0) { lp.width = lp.widthOriginal; } if (lp.heightPercent > 0) { lp.height = lp.heightOriginal; } if (lp.leftMarginPercent > 0) { lp.leftMargin = lp.leftMarginOriginal; } if (lp.topMarginPercent > 0) { lp.topMargin = lp.topMarginOriginal; } if (lp.rightMarginPercent > 0) { lp.rightMargin = lp.rightMarginOriginal; } if (lp.bottomMarginPercent > 0) { lp.bottomMargin = lp.bottomMarginOriginal; } lp.widthOriginal = NOT_SET; lp.heightOriginal = NOT_SET; lp.leftMarginOriginal = NOT_SET; lp.topMarginOriginal = NOT_SET; lp.rightMarginOriginal = NOT_SET; lp.bottomMarginOriginal = NOT_SET; } } } static void log(String tag, String message) { Log.v(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 = MeasureSpec.UNSPECIFIED; 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); } }