net.grobas.widget.AutoLinearLayout.java Source code

Java tutorial

Introduction

Here is the source code for net.grobas.widget.AutoLinearLayout.java

Source

/*
 * 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;
        }
    }

}