1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.globalactions; 18 19 import android.content.Context; 20 import android.util.AttributeSet; 21 import android.view.View; 22 import android.view.ViewGroup; 23 import android.widget.LinearLayout; 24 25 import com.android.internal.annotations.VisibleForTesting; 26 27 /** 28 * Layout which uses nested LinearLayouts to create a grid with the following behavior: 29 * 30 * * Try to maintain a 'square' grid (equal number of columns and rows) based on the expected item 31 * count. 32 * * Determine the position and parent of any item by its index and the total item count. 33 * * Display and hide sub-lists as needed, depending on the expected item count. 34 * 35 * While we could implement this behavior with a GridLayout, it would take significantly more 36 * time and effort, and would require more substantial refactoring of the existing code in 37 * GlobalActionsDialog, since it would require manipulation of layout properties on the child items 38 * themselves. 39 * 40 */ 41 42 public class ListGridLayout extends LinearLayout { 43 private static final String TAG = "ListGridLayout"; 44 private int mExpectedCount; 45 private int mCurrentCount = 0; 46 private boolean mSwapRowsAndColumns; 47 private boolean mReverseSublists; 48 private boolean mReverseItems; 49 50 // number of rows and columns to use for different numbers of items 51 private final int[][] mConfigs = { 52 // {rows, columns} 53 {0, 0}, // 0 items 54 {1, 1}, // 1 item 55 {1, 2}, // 2 items 56 {1, 3}, // 3 items 57 {2, 2}, // 4 items 58 {2, 3}, // 5 items 59 {2, 3}, // 6 items 60 {3, 3}, // 7 items 61 {3, 3}, // 8 items 62 {3, 3} // 9 items 63 }; 64 ListGridLayout(Context context, AttributeSet attrs)65 public ListGridLayout(Context context, AttributeSet attrs) { 66 super(context, attrs); 67 } 68 69 /** 70 * Sets whether this grid should prioritize filling rows or columns first. 71 */ setSwapRowsAndColumns(boolean swap)72 public void setSwapRowsAndColumns(boolean swap) { 73 mSwapRowsAndColumns = swap; 74 } 75 76 /** 77 * Sets whether this grid should fill sublists in reverse order. 78 */ setReverseSublists(boolean reverse)79 public void setReverseSublists(boolean reverse) { 80 mReverseSublists = reverse; 81 } 82 83 /** 84 * Sets whether this grid should add items to sublists in reverse order. 85 * @param reverse 86 */ setReverseItems(boolean reverse)87 public void setReverseItems(boolean reverse) { 88 mReverseItems = reverse; 89 } 90 91 /** 92 * Remove all items from this grid. 93 */ removeAllItems()94 public void removeAllItems() { 95 for (int i = 0; i < getChildCount(); i++) { 96 ViewGroup subList = getSublist(i); 97 if (subList != null) { 98 subList.removeAllViews(); 99 subList.setVisibility(View.GONE); 100 } 101 } 102 mCurrentCount = 0; 103 } 104 105 /** 106 * Adds a view item to this grid, placing it in the correct sublist and ensuring that the 107 * sublist is visible. 108 * 109 * This function is stateful, since it tracks how many items have been added thus far, to 110 * determine which sublist they should be added to. To ensure that this works correctly, call 111 * removeAllItems() instead of removing views individually with removeView() to ensure that the 112 * counter gets reset correctly. 113 * @param item 114 */ addItem(View item)115 public void addItem(View item) { 116 ViewGroup parent = getParentView(mCurrentCount, mReverseSublists, mSwapRowsAndColumns); 117 if (mReverseItems) { 118 parent.addView(item, 0); 119 } else { 120 parent.addView(item); 121 } 122 parent.setVisibility(View.VISIBLE); 123 mCurrentCount++; 124 } 125 126 /** 127 * Get the parent view associated with the item which should be placed at the given position. 128 * @param index The index of the item. 129 * @param reverseSublists Reverse the order of sublists. Ordinarily, sublists fill from first to 130 * last, whereas setting this to true will fill them last to first. 131 * @param swapRowsAndColumns Swap the order in which rows and columns are filled. By default, 132 * columns fill first, adding one item to each row. Setting this to 133 * true will cause rows to fill first, adding one item to each column. 134 * @return 135 */ 136 @VisibleForTesting getParentView(int index, boolean reverseSublists, boolean swapRowsAndColumns)137 protected ViewGroup getParentView(int index, boolean reverseSublists, 138 boolean swapRowsAndColumns) { 139 if (getRowCount() == 0 || index < 0) { 140 return null; 141 } 142 int targetIndex = Math.min(index, getMaxElementCount() - 1); 143 int row = getParentViewIndex(targetIndex, reverseSublists, swapRowsAndColumns); 144 return getSublist(row); 145 } 146 147 @VisibleForTesting getSublist(int index)148 protected ViewGroup getSublist(int index) { 149 return (ViewGroup) getChildAt(index); 150 } 151 reverseSublistIndex(int index)152 private int reverseSublistIndex(int index) { 153 return getChildCount() - (index + 1); 154 } 155 getParentViewIndex(int index, boolean reverseSublists, boolean swapRowsAndColumns)156 private int getParentViewIndex(int index, boolean reverseSublists, boolean swapRowsAndColumns) { 157 int sublistIndex; 158 int rows = getRowCount(); 159 if (swapRowsAndColumns) { 160 sublistIndex = (int) Math.floor(index / rows); 161 } else { 162 sublistIndex = index % rows; 163 } 164 if (reverseSublists) { 165 sublistIndex = reverseSublistIndex(sublistIndex); 166 } 167 return sublistIndex; 168 } 169 170 /** 171 * Sets the expected number of items that this grid will be responsible for rendering. 172 */ setExpectedCount(int count)173 public void setExpectedCount(int count) { 174 mExpectedCount = count; 175 } 176 getMaxElementCount()177 private int getMaxElementCount() { 178 return mConfigs.length - 1; 179 } 180 getConfig()181 private int[] getConfig() { 182 if (mExpectedCount < 0) { 183 return mConfigs[0]; 184 } 185 int targetElements = Math.min(getMaxElementCount(), mExpectedCount); 186 return mConfigs[targetElements]; 187 } 188 189 /** 190 * Get the number of rows which will be used to render children. 191 */ getRowCount()192 public int getRowCount() { 193 return getConfig()[0]; 194 } 195 196 /** 197 * Get the number of columns which will be used to render children. 198 */ getColumnCount()199 public int getColumnCount() { 200 return getConfig()[1]; 201 } 202 } 203