blob: 8c93b13921e6a399d652e3c40a7f8e74253cfef8 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* 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 com.android.systemui.globalactions;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import com.android.internal.annotations.VisibleForTesting;
/**
* Layout which uses nested LinearLayouts to create a grid with the following behavior:
*
* * Try to maintain a 'square' grid (equal number of columns and rows) based on the expected item
* count.
* * Determine the position and parent of any item by its index and the total item count.
* * Display and hide sub-lists as needed, depending on the expected item count.
*
* While we could implement this behavior with a GridLayout, it would take significantly more
* time and effort, and would require more substantial refactoring of the existing code in
* GlobalActionsDialog, since it would require manipulation of layout properties on the child items
* themselves.
*
*/
public class ListGridLayout extends LinearLayout {
private static final String TAG = "ListGridLayout";
private int mExpectedCount;
private int mCurrentCount = 0;
private boolean mSwapRowsAndColumns;
private boolean mReverseSublists;
private boolean mReverseItems;
// number of rows and columns to use for different numbers of items
private final int[][] mConfigs = {
// {rows, columns}
{0, 0}, // 0 items
{1, 1}, // 1 item
{1, 2}, // 2 items
{1, 3}, // 3 items
{2, 2}, // 4 items
{2, 3}, // 5 items
{2, 3}, // 6 items
{3, 3}, // 7 items
{3, 3}, // 8 items
{3, 3} // 9 items
};
public ListGridLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Sets whether this grid should prioritize filling rows or columns first.
*/
public void setSwapRowsAndColumns(boolean swap) {
mSwapRowsAndColumns = swap;
}
/**
* Sets whether this grid should fill sublists in reverse order.
*/
public void setReverseSublists(boolean reverse) {
mReverseSublists = reverse;
}
/**
* Sets whether this grid should add items to sublists in reverse order.
* @param reverse
*/
public void setReverseItems(boolean reverse) {
mReverseItems = reverse;
}
/**
* Remove all items from this grid.
*/
public void removeAllItems() {
for (int i = 0; i < getChildCount(); i++) {
ViewGroup subList = getSublist(i);
if (subList != null) {
subList.removeAllViews();
subList.setVisibility(View.GONE);
}
}
mCurrentCount = 0;
}
/**
* Adds a view item to this grid, placing it in the correct sublist and ensuring that the
* sublist is visible.
*
* This function is stateful, since it tracks how many items have been added thus far, to
* determine which sublist they should be added to. To ensure that this works correctly, call
* removeAllItems() instead of removing views individually with removeView() to ensure that the
* counter gets reset correctly.
* @param item
*/
public void addItem(View item) {
ViewGroup parent = getParentView(mCurrentCount, mReverseSublists, mSwapRowsAndColumns);
if (mReverseItems) {
parent.addView(item, 0);
} else {
parent.addView(item);
}
parent.setVisibility(View.VISIBLE);
mCurrentCount++;
}
/**
* Get the parent view associated with the item which should be placed at the given position.
* @param index The index of the item.
* @param reverseSublists Reverse the order of sublists. Ordinarily, sublists fill from first to
* last, whereas setting this to true will fill them last to first.
* @param swapRowsAndColumns Swap the order in which rows and columns are filled. By default,
* columns fill first, adding one item to each row. Setting this to
* true will cause rows to fill first, adding one item to each column.
* @return
*/
@VisibleForTesting
protected ViewGroup getParentView(int index, boolean reverseSublists,
boolean swapRowsAndColumns) {
if (getRowCount() == 0 || index < 0) {
return null;
}
int targetIndex = Math.min(index, getMaxElementCount() - 1);
int row = getParentViewIndex(targetIndex, reverseSublists, swapRowsAndColumns);
return getSublist(row);
}
@VisibleForTesting
protected ViewGroup getSublist(int index) {
return (ViewGroup) getChildAt(index);
}
private int reverseSublistIndex(int index) {
return getChildCount() - (index + 1);
}
private int getParentViewIndex(int index, boolean reverseSublists, boolean swapRowsAndColumns) {
int sublistIndex;
int rows = getRowCount();
if (swapRowsAndColumns) {
sublistIndex = (int) Math.floor(index / rows);
} else {
sublistIndex = index % rows;
}
if (reverseSublists) {
sublistIndex = reverseSublistIndex(sublistIndex);
}
return sublistIndex;
}
/**
* Sets the expected number of items that this grid will be responsible for rendering.
*/
public void setExpectedCount(int count) {
mExpectedCount = count;
}
private int getMaxElementCount() {
return mConfigs.length - 1;
}
private int[] getConfig() {
if (mExpectedCount < 0) {
return mConfigs[0];
}
int targetElements = Math.min(getMaxElementCount(), mExpectedCount);
return mConfigs[targetElements];
}
/**
* Get the number of rows which will be used to render children.
*/
public int getRowCount() {
return getConfig()[0];
}
/**
* Get the number of columns which will be used to render children.
*/
public int getColumnCount() {
return getConfig()[1];
}
}