/* * Catroid: An on-device visual programming system for Android devices * Copyright (C) 2010-2016 The Catrobat Team * (<http://developer.catrobat.org/credits>) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * An additional term exception under section 7 of the GNU Affero * General Public License, version 3, is available at * http://developer.catrobat.org/license_additional_term * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.catrobat.catroid.ui; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.widget.Spinner; import android.widget.TextView; import org.catrobat.catroid.R; import java.util.LinkedList; /** * Author: Romain Guy * <p/> * Using example: <?xml version="4.0" encoding="utf-8"?> <com.example.android.layout.FlowLayout * xmlns:f="http://schemas.android.com/apk/res/org.apmem.android" * xmlns:android="http://schemas.android.com/apk/res/android" f:horizontalSpacing="6dip" f:verticalSpacing="12dip" * android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="6dip" * android:paddingTop="6dip" android:paddingRight="12dip"> <Button android:layout_width="wrap_content" * android:layout_height="wrap_content" f:layout_horizontalSpacing="32dip" f:layout_breakLine="true" * android:text="Cancel" /> * <p/> * </com.example.android.layout.FlowLayout> */ public class BrickLayout extends ViewGroup { public static final int HORIZONTAL = 0; public static final int VERTICAL = 1; private final int minTextFieldWidthDp = 100; private final int linesToAllocate = 10; private final int elementsToAllocatePerLine = 10; private int horizontalSpacing = 0; private int verticalSpacing = 0; private int orientation = 0; protected boolean debugDraw = true; protected LinkedList<LineData> lines; public BrickLayout(Context context) { super(context); allocateLineData(); this.readStyleParameters(context, null); } public BrickLayout(Context context, AttributeSet attributeSet) { super(context, attributeSet); allocateLineData(); this.readStyleParameters(context, attributeSet); } public BrickLayout(Context context, AttributeSet attributeSet, int defStyle) { super(context, attributeSet, defStyle); allocateLineData(); this.readStyleParameters(context, attributeSet); } protected void allocateLineData() { lines = new LinkedList<LineData>(); for (int i = 0; i < linesToAllocate; i++) { allocateNewLine(); } } protected LineData allocateNewLine() { LineData lineData = new LineData(); for (int i = 0; i < elementsToAllocatePerLine; i++) { lineData.elements.add(new ElementData(null, 0, 0, 0, 0)); } lines.add(lineData); return lineData; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - this.getPaddingRight() - this.getPaddingLeft(); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec); int lineThicknessWithHorizontalSpacing = 0; int lineThickness = 0; int lineLengthWithHorizontalSpacing = 0; int lineLength = 0; int prevLinePosition = 0; int controlMaxLength = 0; int controlMaxThickness = 0; for (LineData lineData : lines) { lineData.allowableTextFieldWidth = 0; lineData.height = 0; lineData.minHeight = 0; lineData.numberOfTextFields = 0; lineData.totalTextFieldWidth = 0; for (ElementData elementData : lineData.elements) { elementData.height = 0; elementData.width = 0; elementData.posY = 0; elementData.posX = 0; elementData.view = null; } } LineData currentLine = lines.getFirst(); // ************************ BEGIN PRE-LAYOUT (decide on a maximum width for text fields) ************************ // 1. adding text to a text field never causes a line break // 2. text fields use as much space as possible // 3. on wider screens, line breaks are removed entirely and the layout is one line final int count = getChildCount(); int elementInLineIndex = 0; int totalLengthOfContent = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } totalLengthOfContent += horizontalSpacing + preLayoutMeasureWidth(child, sizeWidth, sizeHeight, modeWidth, modeHeight); } int combinedLengthOfPreviousLines = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); int childWidth = preLayoutMeasureWidth(child, sizeWidth, sizeHeight, modeWidth, modeHeight); lineLength = lineLengthWithHorizontalSpacing + childWidth; lineLengthWithHorizontalSpacing = lineLength + horizontalSpacing; boolean newLine = (layoutParams.newLine && totalLengthOfContent - combinedLengthOfPreviousLines > sizeWidth); boolean lastChildWasSpinner = false; if (i > 0) { lastChildWasSpinner = getChildAt(i - 1) instanceof Spinner; } newLine = newLine || child instanceof Spinner || lastChildWasSpinner; if (newLine) { int childWidthNotCountingField = (layoutParams.textField ? childWidth : 0); int endingWidthOfLineMinusFields = (lineLength - (childWidthNotCountingField + horizontalSpacing + currentLine.totalTextFieldWidth)); float allowalbeWidth = (float) (sizeWidth - (endingWidthOfLineMinusFields)) / currentLine.numberOfTextFields; currentLine.allowableTextFieldWidth = (int) Math.floor(allowalbeWidth); currentLine = getNextLine(currentLine); combinedLengthOfPreviousLines += (lineLength - (childWidth + horizontalSpacing)); lineLength = childWidth; lineLengthWithHorizontalSpacing = lineLength + horizontalSpacing; elementInLineIndex = 0; } getElement(currentLine, elementInLineIndex).view = child; elementInLineIndex++; if (layoutParams.textField) { currentLine.totalTextFieldWidth += childWidth; currentLine.numberOfTextFields++; } } int endingWidthOfLineMinusFields = (lineLength - currentLine.totalTextFieldWidth); float allowalbeWidth = (float) (sizeWidth - endingWidthOfLineMinusFields) / currentLine.numberOfTextFields; currentLine.allowableTextFieldWidth = (int) Math.floor(allowalbeWidth); int minAllowableTextFieldWidth = Integer.MAX_VALUE; for (LineData lineData : lines) { if (lineData.allowableTextFieldWidth > 0 && lineData.allowableTextFieldWidth < minAllowableTextFieldWidth) { minAllowableTextFieldWidth = lineData.allowableTextFieldWidth; } } for (LineData lineData : lines) { for (ElementData elementData : lineData.elements) { if (elementData.view != null) { LayoutParams layoutParams = (LayoutParams) elementData.view.getLayoutParams(); if (layoutParams.textField) { ((TextView) elementData.view).setMaxWidth(minAllowableTextFieldWidth); } } } } // ************************ BEGIN LAYOUT ************************ lineThicknessWithHorizontalSpacing = 0; lineThickness = 0; lineLengthWithHorizontalSpacing = 0; lineLength = 0; prevLinePosition = 0; controlMaxLength = 0; controlMaxThickness = 0; currentLine = lines.getFirst(); boolean firstLine = true; for (LineData line : lines) { boolean newLine = !firstLine; for (ElementData element : line.elements) { View child = element.view; if (child == null || child.getVisibility() == GONE) { continue; } if (child instanceof Spinner) { child.measure(MeasureSpec.makeMeasureSpec(sizeWidth, MeasureSpec.EXACTLY), MeasureSpec .makeMeasureSpec(sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight)); } else { child.measure(MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth), MeasureSpec .makeMeasureSpec(sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight)); } LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); int horizontalSpacing = this.getHorizontalSpacing(layoutParams); int verticalSpacing = this.getVerticalSpacing(layoutParams); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); boolean updateSmallestHeight = currentLine.minHeight == 0 || currentLine.minHeight > childHeight; currentLine.minHeight = (updateSmallestHeight ? childHeight : currentLine.minHeight); lineLength = lineLengthWithHorizontalSpacing + childWidth; lineLengthWithHorizontalSpacing = lineLength + horizontalSpacing; if (layoutParams.newLine && !newLine) { lineLength += horizontalSpacing; lineLengthWithHorizontalSpacing += horizontalSpacing; } if (newLine) { newLine = false; prevLinePosition = prevLinePosition + lineThicknessWithHorizontalSpacing; currentLine = getNextLine(currentLine); lineThickness = childHeight; lineLength = childWidth; lineThicknessWithHorizontalSpacing = childHeight + verticalSpacing; lineLengthWithHorizontalSpacing = lineLength + horizontalSpacing; } lineThicknessWithHorizontalSpacing = Math.max(lineThicknessWithHorizontalSpacing, childHeight + verticalSpacing); lineThickness = Math.max(lineThickness, childHeight); currentLine.height = lineThickness; int posX = getPaddingLeft() + lineLength - childWidth; int posY = getPaddingTop() + prevLinePosition; element.posX = posX; element.posY = posY; element.width = childWidth; element.height = childHeight; controlMaxLength = Math.max(controlMaxLength, lineLength); controlMaxThickness = prevLinePosition + lineThickness; } firstLine = false; } int x = controlMaxLength; int y = controlMaxThickness; y += getPaddingTop() + getPaddingBottom(); int centerVertically = 0; if (y < getSuggestedMinimumHeight()) { centerVertically = (getSuggestedMinimumHeight() - y) / 2; } y = Math.max(y, getSuggestedMinimumHeight()); for (LineData lineData : lines) { for (ElementData elementData : lineData.elements) { if (elementData.view != null) { int centerVerticallyWithinLine = 0; if (elementData.height < lineData.height) { centerVerticallyWithinLine = Math.round((lineData.height - elementData.height) * 0.5f); } elementData.posY += centerVertically + centerVerticallyWithinLine; LayoutParams layoutParams = (LayoutParams) elementData.view.getLayoutParams(); layoutParams.setPosition(elementData.posX, elementData.posY); } } } this.setMeasuredDimension(resolveSize(x, widthMeasureSpec), resolveSize(y, heightMeasureSpec)); } private int preLayoutMeasureWidth(View child, int sizeWidth, int sizeHeight, int modeWidth, int modeHeight) { if (child instanceof Spinner) { child.measure(MeasureSpec.makeMeasureSpec(sizeWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight)); } else { child.measure(MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth), MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight)); } Resources resources = getResources(); LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth(); if (layoutParams.textField) { childWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, minTextFieldWidthDp, resources.getDisplayMetrics()); } if (child instanceof Spinner) { childWidth = sizeWidth; } return childWidth; } protected LineData getNextLine(LineData currentLine) { int index = lines.indexOf(currentLine) + 1; if (index < lines.size()) { return lines.get(index); } else { return allocateNewLine(); } } protected ElementData getElement(LineData currentLine, int elementInLineIndex) { if (elementInLineIndex < currentLine.elements.size()) { return currentLine.elements.get(elementInLineIndex); } else { ElementData elementData = new ElementData(null, 0, 0, 0, 0); currentLine.elements.add(elementData); return elementData; } } protected int getHorizontalSpacing(LayoutParams layoutParams) { int verticalSpacing; if (layoutParams.verticalSpacingSpecified()) { verticalSpacing = layoutParams.verticalSpacing; } else { verticalSpacing = this.verticalSpacing; } return verticalSpacing; } protected int getVerticalSpacing(LayoutParams layoutParams) { int verticalSpacing; if (layoutParams.verticalSpacingSpecified()) { verticalSpacing = layoutParams.verticalSpacing; } else { verticalSpacing = this.verticalSpacing; } return verticalSpacing; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); child.layout(layoutParams.positionX, layoutParams.positionY, layoutParams.positionX + child.getMeasuredWidth(), layoutParams.positionY + child.getMeasuredHeight()); } } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean more = super.drawChild(canvas, child, drawingTime); this.drawDebugInfo(canvas, child); return more; } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams layoutParams) { return layoutParams instanceof LayoutParams; } @Override protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } @Override public LayoutParams generateLayoutParams(AttributeSet attributeSet) { return new LayoutParams(getContext(), attributeSet); } @Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams layoutParameters) { return new LayoutParams(layoutParameters); } private void readStyleParameters(Context context, AttributeSet attributeSet) { TypedArray styledAttributes = context.obtainStyledAttributes(attributeSet, R.styleable.BrickLayout); try { horizontalSpacing = styledAttributes.getDimensionPixelSize(R.styleable.BrickLayout_horizontalSpacing, 0); verticalSpacing = styledAttributes.getDimensionPixelSize(R.styleable.BrickLayout_verticalSpacing, 0); orientation = styledAttributes.getInteger(R.styleable.BrickLayout_orientation, HORIZONTAL); debugDraw = styledAttributes.getBoolean(R.styleable.BrickLayout_debugDraw, false); } finally { styledAttributes.recycle(); } } public void drawDebugInfo(Canvas canvas, View child) { if (!debugDraw) { return; } Paint childPaint = this.createPaint(0xffffff00); Paint layoutPaint = this.createPaint(0xff00ff00); Paint newLinePaint = this.createPaint(0xffff0000); LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); if (layoutParams.horizontalSpacing > 0) { float x = child.getRight(); float y = child.getTop() + child.getHeight() / 2.0f; canvas.drawLine(x, y, x + layoutParams.horizontalSpacing, y, childPaint); canvas.drawLine(x + layoutParams.horizontalSpacing - 4.0f, y - 4.0f, x + layoutParams.horizontalSpacing, y, childPaint); canvas.drawLine(x + layoutParams.horizontalSpacing - 4.0f, y + 4.0f, x + layoutParams.horizontalSpacing, y, childPaint); } else if (this.horizontalSpacing > 0) { float x = child.getRight(); float y = child.getTop() + child.getHeight() / 2.0f; canvas.drawLine(x, y, x + this.horizontalSpacing, y, layoutPaint); canvas.drawLine(x + this.horizontalSpacing - 4.0f, y - 4.0f, x + this.horizontalSpacing, y, layoutPaint); canvas.drawLine(x + this.horizontalSpacing - 4.0f, y + 4.0f, x + this.horizontalSpacing, y, layoutPaint); } if (layoutParams.verticalSpacing > 0) { float x = child.getLeft() + child.getWidth() / 2.0f; float y = child.getBottom(); canvas.drawLine(x, y, x, y + layoutParams.verticalSpacing, childPaint); canvas.drawLine(x - 4.0f, y + layoutParams.verticalSpacing - 4.0f, x, y + layoutParams.verticalSpacing, childPaint); canvas.drawLine(x + 4.0f, y + layoutParams.verticalSpacing - 4.0f, x, y + layoutParams.verticalSpacing, childPaint); } else if (this.verticalSpacing > 0) { float x = child.getLeft() + child.getWidth() / 2.0f; float y = child.getBottom(); canvas.drawLine(x, y, x, y + this.verticalSpacing, layoutPaint); canvas.drawLine(x - 4.0f, y + this.verticalSpacing - 4.0f, x, y + this.verticalSpacing, layoutPaint); canvas.drawLine(x + 4.0f, y + this.verticalSpacing - 4.0f, x, y + this.verticalSpacing, layoutPaint); } if (layoutParams.newLine) { if (orientation == HORIZONTAL) { float x = child.getLeft(); float y = child.getTop() + child.getHeight() / 2.0f; canvas.drawLine(x, y - 6.0f, x, y + 6.0f, newLinePaint); } else { float x = child.getLeft() + child.getWidth() / 2.0f; float y = child.getTop(); canvas.drawLine(x - 6.0f, y, x + 6.0f, y, newLinePaint); } } } protected Paint createPaint(int color) { Paint paint = new Paint(); paint.setAntiAlias(true); paint.setColor(color); paint.setStrokeWidth(2.0f); return paint; } protected class LineData { public int totalTextFieldWidth; public int allowableTextFieldWidth; public int numberOfTextFields; public int minHeight; public int height; public LinkedList<ElementData> elements; public LineData() { elements = new LinkedList<ElementData>(); } } protected class ElementData { public int posX; public int posY; public int height; public int width; public View view; public ElementData(View view, int posX, int posY, int childWidth, int childHeight) { this.posX = posX; this.posY = posY; this.height = childHeight; this.width = childWidth; this.view = view; } } public static class LayoutParams extends ViewGroup.LayoutParams { private static final int NO_SPACING = -1; private int positionX; private int positionY; private int horizontalSpacing = NO_SPACING; private int verticalSpacing = NO_SPACING; private boolean newLine = false; private boolean textField = false; private InputType inputType = InputType.NUMBER; public enum InputType { NUMBER, TEXT } public LayoutParams(Context context, AttributeSet attributeSet) { super(context, attributeSet); this.readStyleParameters(context, attributeSet); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(ViewGroup.LayoutParams layoutParams) { super(layoutParams); } public boolean horizontalSpacingSpecified() { return horizontalSpacing != NO_SPACING; } public boolean verticalSpacingSpecified() { return verticalSpacing != NO_SPACING; } public void setPosition(int x, int y) { this.positionX = x; this.positionY = y; } public void setWidth(int width) { this.width = width; } public void setNewLine(boolean newLine) { this.newLine = newLine; } public boolean getNewLine() { return newLine; } public InputType getInputType() { return inputType; } private void readStyleParameters(Context context, AttributeSet attributeSet) { TypedArray styledAttributes = context.obtainStyledAttributes(attributeSet, R.styleable.FlowLayout_LayoutParams); try { horizontalSpacing = styledAttributes.getDimensionPixelSize( R.styleable.FlowLayout_LayoutParams_layout_horizontalSpacing, NO_SPACING); verticalSpacing = styledAttributes.getDimensionPixelSize( R.styleable.FlowLayout_LayoutParams_layout_verticalSpacing, NO_SPACING); newLine = styledAttributes.getBoolean(R.styleable.FlowLayout_LayoutParams_layout_newLine, false); textField = styledAttributes.getBoolean(R.styleable.FlowLayout_LayoutParams_layout_textField, false); String inputTypeString = styledAttributes .getString(R.styleable.FlowLayout_LayoutParams_layout_inputType); inputType = (inputTypeString != null && inputTypeString.equals("text") ? InputType.TEXT : InputType.NUMBER); } finally { styledAttributes.recycle(); } } } }