Java tutorial
/* * Copyright (C) 2014 Albert Grobas * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.grobas.widget; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.support.v4.view.GravityCompat; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.widget.FrameLayout; import java.util.ArrayList; /** * Arrange views in columns and rows using orientation (default horizontal). * Extends from FrameLayout for an easy implementation and reuse of FrameLayout.LayoutParams. */ public class AutoLinearLayout extends FrameLayout { private int mOrientation; private int mGravity = Gravity.TOP | GravityCompat.START; public final static int HORIZONTAL = 0; public final static int VERTICAL = 1; private ArrayList<ViewPosition> mListPositions = new ArrayList<>(); public AutoLinearLayout(Context context) { super(context); init(context, null, 0, 0); } public AutoLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0, 0); } public AutoLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, 0); } @TargetApi(21) public AutoLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs, defStyleAttr, defStyleRes); } private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AutoLinearLayout, defStyleAttr, defStyleRes); try { mOrientation = a.getInt(R.styleable.AutoLinearLayout_auto_orientation, HORIZONTAL); int gravity = a.getInt(R.styleable.AutoLinearLayout_auto_gravity, -1); if (gravity >= 0) { setGravity(gravity); } } finally { a.recycle(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } } private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) { int wSize = MeasureSpec.getSize(widthMeasureSpec) - (getPaddingLeft() + getPaddingRight()); //Scrollview case if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) wSize = Integer.MAX_VALUE; int count = getChildCount(); int rowWidth = 0; int totalHeight = 0; int rowMaxHeight = 0; int childWidth; int childHeight; int maxRowWidth = getPaddingLeft() + getPaddingRight(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //keep max height value stored rowMaxHeight = Math.max(rowMaxHeight, childHeight); //exceed max width start new row and update total height if (childWidth + rowWidth > wSize) { totalHeight += rowMaxHeight; maxRowWidth = Math.max(maxRowWidth, rowWidth); rowWidth = childWidth; rowMaxHeight = childHeight; } else { rowWidth += childWidth; } } } //plus last child height and width if (rowWidth != 0) { maxRowWidth = Math.max(maxRowWidth, rowWidth); totalHeight += rowMaxHeight; } //set width to max value if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) wSize = maxRowWidth + (getPaddingLeft() + getPaddingRight()); setMeasuredDimension(resolveSize(wSize, widthMeasureSpec), resolveSize(totalHeight + getPaddingTop() + getPaddingBottom(), heightMeasureSpec)); } private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { int hSize = MeasureSpec.getSize(heightMeasureSpec) - (getPaddingTop() + getPaddingBottom()); int count = getChildCount(); int columnHeight = 0; int totalWidth = 0, maxColumnHeight = 0; int columnMaxWidth = 0; int childWidth; int childHeight; //Scrollview case if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) hSize = Integer.MAX_VALUE; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //keep max width value stored columnMaxWidth = Math.max(columnMaxWidth, childWidth); //exceed max height start new column and update total width if (childHeight + columnHeight > hSize) { totalWidth += columnMaxWidth; maxColumnHeight = Math.max(maxColumnHeight, columnHeight); columnHeight = childHeight; columnMaxWidth = childWidth; } else { columnHeight += childHeight; } } } //plus last child width if (columnHeight != 0) { maxColumnHeight = Math.max(maxColumnHeight, columnHeight); totalWidth += columnMaxWidth; } //set height to max value if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) hSize = maxColumnHeight + (getPaddingTop() + getPaddingBottom()); setMeasuredDimension(resolveSize(totalWidth + getPaddingRight() + getPaddingLeft(), widthMeasureSpec), resolveSize(hSize, heightMeasureSpec)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { mListPositions.clear(); if (mOrientation == VERTICAL) layoutVertical(left, top, right, bottom); else layoutHorizontal(left, top, right, bottom); } /** * Arranges the children in columns. Takes care about child margin, padding, gravity and * child layout gravity. * * @param left parent left * @param top parent top * @param right parent right * @param bottom parent bottom */ void layoutVertical(int left, int top, int right, int bottom) { final int count = getChildCount(); if (count == 0) return; final int width = right - getPaddingLeft() - left - getPaddingRight(); final int height = bottom - getPaddingTop() - top - getPaddingBottom(); int childTop = getPaddingTop(); int childLeft = getPaddingLeft(); int totalHorizontal = getPaddingLeft() + getPaddingRight(); int totalVertical = 0; int column = 0; int maxChildWidth = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child != null && child.getVisibility() != View.GONE) { //if child is not updated yet call measure if (child.getMeasuredHeight() == 0 || child.getMeasuredWidth() == 0) child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); //if there is not enough space jump to another column if (childTop + childHeight + lp.topMargin + lp.bottomMargin > height + getPaddingTop()) { //before change column update positions if the gravity is present updateChildPositionVertical(height, totalVertical, column, maxChildWidth); childTop = getPaddingTop(); childLeft += maxChildWidth; maxChildWidth = 0; column++; totalVertical = 0; } childTop += lp.topMargin; mListPositions.add(new ViewPosition(childLeft, childTop, column)); //check max child width int currentWidth = childWidth + lp.leftMargin + lp.rightMargin; if (maxChildWidth < currentWidth) maxChildWidth = currentWidth; //get ready for next child childTop += childHeight + lp.bottomMargin; totalVertical += childHeight + lp.topMargin + lp.bottomMargin; } } //update positions for last column updateChildPositionVertical(height, totalVertical, column, maxChildWidth); totalHorizontal += childLeft + maxChildWidth; //final update for horizontal gravities and layout views updateChildPositionHorizontal(width, totalHorizontal, column, 0); //mListPositions.clear(); } /** * Arranges the children in rows. Takes care about child margin, padding, gravity and * child layout gravity. Analog to vertical. * * @param left parent left * @param top parent top * @param right parent right * @param bottom parent bottom */ void layoutHorizontal(int left, int top, int right, int bottom) { final int count = getChildCount(); if (count == 0) return; final int pWidth = right - getPaddingLeft() - left - getPaddingRight(); final int pHeight = bottom - getPaddingTop() - top - getPaddingBottom(); int childTop = getPaddingTop(); int childLeft = getPaddingLeft(); int totalHorizontal = 0; int totalVertical = getPaddingTop() + getPaddingBottom(); int row = 0; int maxChildHeight = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child != null && child.getVisibility() != View.GONE) { if (child.getMeasuredHeight() == 0 || child.getMeasuredWidth() == 0) child.measure(MeasureSpec.makeMeasureSpec(pWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(pHeight, MeasureSpec.AT_MOST)); LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); if (childLeft + childWidth + lp.leftMargin + lp.rightMargin > pWidth + getPaddingLeft()) { updateChildPositionHorizontal(pWidth, totalHorizontal, row, maxChildHeight); childLeft = getPaddingLeft(); childTop += maxChildHeight; maxChildHeight = 0; row++; totalHorizontal = 0; } childLeft += lp.leftMargin; mListPositions.add(new ViewPosition(childLeft, childTop, row)); int currentHeight = childHeight + lp.topMargin + lp.bottomMargin; if (maxChildHeight < currentHeight) maxChildHeight = currentHeight; childLeft += childWidth + lp.rightMargin; totalHorizontal += childWidth + lp.rightMargin + lp.leftMargin; } } updateChildPositionHorizontal(pWidth, totalHorizontal, row, maxChildHeight); totalVertical += childTop + maxChildHeight; updateChildPositionVertical(pHeight, totalVertical, row, 0); //mListPositions.clear(); } /** * Updates children positions. Takes cares about gravity and layout gravity. * Finally layout children to parent if needed. * * @param parentHeight parent parentHeight * @param totalSize total vertical size used by children in a column * @param column column number * @param maxChildWidth the biggest child width */ private void updateChildPositionVertical(int parentHeight, int totalSize, int column, int maxChildWidth) { for (int i = 0; i < mListPositions.size(); i++) { ViewPosition pos = mListPositions.get(i); final View child = getChildAt(i); //(android:gravity) //update children position inside parent layout if (mOrientation == HORIZONTAL || pos.position == column) { updateTopPositionByGravity(pos, parentHeight - totalSize, mGravity); } //(android:layout_gravity) //update children position inside their space if (mOrientation == VERTICAL && pos.position == column) { LayoutParams lp = (LayoutParams) child.getLayoutParams(); int size = maxChildWidth - child.getMeasuredWidth() - lp.leftMargin - lp.rightMargin; updateLeftPositionByGravity(pos, size, lp.gravity); } //update children into layout parent if (mOrientation == HORIZONTAL) layout(child, pos); } } /** * Updates children positions. Takes cares about gravity and layout gravity. * Finally layout children to parent if needed. Analog to vertical. * * @param parentWidth parent parentWidth * @param totalSize total horizontal size used by children in a row * @param row row number * @param maxChildHeight the biggest child height */ private void updateChildPositionHorizontal(int parentWidth, int totalSize, int row, int maxChildHeight) { for (int i = 0; i < mListPositions.size(); i++) { ViewPosition pos = mListPositions.get(i); final View child = getChildAt(i); if (mOrientation == VERTICAL || pos.position == row) { updateLeftPositionByGravity(pos, parentWidth - totalSize, mGravity); } if (mOrientation == HORIZONTAL && pos.position == row) { LayoutParams lp = (LayoutParams) child.getLayoutParams(); int size = maxChildHeight - child.getMeasuredHeight() - lp.topMargin - lp.bottomMargin; updateTopPositionByGravity(pos, size, lp.gravity); } if (mOrientation == VERTICAL) layout(child, pos); } } private void updateLeftPositionByGravity(ViewPosition pos, int size, int gravity) { switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case GravityCompat.END: pos.left += (size > 0) ? size : 0; break; case Gravity.CENTER_HORIZONTAL: pos.left += ((size > 0) ? size : 0) / 2; break; } } private void updateTopPositionByGravity(ViewPosition pos, int size, int gravity) { switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.BOTTOM: pos.top += (size > 0) ? size : 0; break; case Gravity.CENTER_VERTICAL: pos.top += ((size > 0) ? size : 0) / 2; break; } } private void layout(View child, ViewPosition pos) { LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (mOrientation == HORIZONTAL) child.layout(pos.left, pos.top + lp.topMargin, pos.left + child.getMeasuredWidth(), pos.top + child.getMeasuredHeight() + lp.topMargin); else child.layout(pos.left + lp.leftMargin, pos.top, pos.left + child.getMeasuredWidth() + lp.leftMargin, pos.top + child.getMeasuredHeight()); } /** * Describes how the child views are positioned. Defaults to GRAVITY_TOP. If * this layout has a VERTICAL orientation, this controls where all the child * views are placed if there is extra vertical space. If this layout has a * HORIZONTAL orientation, this controls the alignment of the children. * * @param gravity See {@link android.view.Gravity} */ public void setGravity(int gravity) { if (mGravity != gravity) { if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { gravity |= GravityCompat.START; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { gravity |= Gravity.TOP; } mGravity = gravity; requestLayout(); } } public void setHorizontalGravity(int horizontalGravity) { final int gravity = horizontalGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK; if ((mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != gravity) { mGravity = (mGravity & ~GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravity; requestLayout(); } } public void setVerticalGravity(int verticalGravity) { final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK; if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) { mGravity = (mGravity & ~Gravity.VERTICAL_GRAVITY_MASK) | gravity; requestLayout(); } } /** * Should the layout be a column or a row. * @param orientation Pass HORIZONTAL or VERTICAL. Default * value is HORIZONTAL. */ public void setOrientation(int orientation) { if (mOrientation != orientation) { mOrientation = orientation; requestLayout(); } } /** * Returns the current orientation. * * @return either {@link #HORIZONTAL} or {@link #VERTICAL} */ public int getOrientation() { return mOrientation; } /** * Helper inner class that stores child position */ static class ViewPosition { int left; int top; int position; //row or column ViewPosition(int l, int t, int p) { this.left = l; this.top = t; this.position = p; } @Override public String toString() { return "left-" + left + " top" + top + " pos" + position; } } }