1 package com.android.systemui.qs;
2 
3 import static com.android.systemui.util.Utils.useQsMediaPlayer;
4 
5 import android.content.Context;
6 import android.content.res.Resources;
7 import android.provider.Settings;
8 import android.util.AttributeSet;
9 import android.view.View;
10 import android.view.ViewGroup;
11 
12 import com.android.internal.logging.UiEventLogger;
13 import com.android.systemui.R;
14 import com.android.systemui.qs.QSPanel.QSTileLayout;
15 import com.android.systemui.qs.QSPanelControllerBase.TileRecord;
16 import com.android.systemui.qs.tileimpl.HeightOverrideable;
17 
18 import java.util.ArrayList;
19 
20 public class TileLayout extends ViewGroup implements QSTileLayout {
21 
22     public static final int NO_MAX_COLUMNS = 100;
23 
24     private static final String TAG = "TileLayout";
25 
26     protected int mColumns;
27     protected int mCellWidth;
28     protected int mCellHeightResId = R.dimen.qs_tile_height;
29     protected int mCellHeight;
30     protected int mMaxCellHeight;
31     protected int mCellMarginHorizontal;
32     protected int mCellMarginVertical;
33     protected int mSidePadding;
34     protected int mRows = 1;
35 
36     protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
37     protected boolean mListening;
38     protected int mMaxAllowedRows = 3;
39 
40     // Prototyping with less rows
41     private final boolean mLessRows;
42     private int mMinRows = 1;
43     private int mMaxColumns = NO_MAX_COLUMNS;
44     protected int mResourceColumns;
45     private float mSquishinessFraction = 1f;
46     private int mLastTileBottom;
47 
TileLayout(Context context)48     public TileLayout(Context context) {
49         this(context, null);
50     }
51 
TileLayout(Context context, AttributeSet attrs)52     public TileLayout(Context context, AttributeSet attrs) {
53         super(context, attrs);
54         setFocusableInTouchMode(true);
55         mLessRows = ((Settings.System.getInt(context.getContentResolver(), "qs_less_rows", 0) != 0)
56                 || useQsMediaPlayer(context));
57         updateResources();
58     }
59 
60     @Override
getOffsetTop(TileRecord tile)61     public int getOffsetTop(TileRecord tile) {
62         return getTop();
63     }
64 
setListening(boolean listening)65     public void setListening(boolean listening) {
66         setListening(listening, null);
67     }
68 
69     @Override
setListening(boolean listening, UiEventLogger uiEventLogger)70     public void setListening(boolean listening, UiEventLogger uiEventLogger) {
71         if (mListening == listening) return;
72         mListening = listening;
73         for (TileRecord record : mRecords) {
74             record.tile.setListening(this, mListening);
75         }
76     }
77 
78     @Override
setMinRows(int minRows)79     public boolean setMinRows(int minRows) {
80         if (mMinRows != minRows) {
81             mMinRows = minRows;
82             updateResources();
83             return true;
84         }
85         return false;
86     }
87 
88     @Override
setMaxColumns(int maxColumns)89     public boolean setMaxColumns(int maxColumns) {
90         mMaxColumns = maxColumns;
91         return updateColumns();
92     }
93 
addTile(TileRecord tile)94     public void addTile(TileRecord tile) {
95         mRecords.add(tile);
96         tile.tile.setListening(this, mListening);
97         addTileView(tile);
98     }
99 
addTileView(TileRecord tile)100     protected void addTileView(TileRecord tile) {
101         addView(tile.tileView);
102     }
103 
104     @Override
removeTile(TileRecord tile)105     public void removeTile(TileRecord tile) {
106         mRecords.remove(tile);
107         tile.tile.setListening(this, false);
108         removeView(tile.tileView);
109     }
110 
removeAllViews()111     public void removeAllViews() {
112         for (TileRecord record : mRecords) {
113             record.tile.setListening(this, false);
114         }
115         mRecords.clear();
116         super.removeAllViews();
117     }
118 
updateResources()119     public boolean updateResources() {
120         final Resources res = mContext.getResources();
121         mResourceColumns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns));
122         updateColumns();
123         mMaxCellHeight = mContext.getResources().getDimensionPixelSize(mCellHeightResId);
124         mCellMarginHorizontal = res.getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal);
125         mSidePadding = useSidePadding() ? mCellMarginHorizontal / 2 : 0;
126         mCellMarginVertical= res.getDimensionPixelSize(R.dimen.qs_tile_margin_vertical);
127         mMaxAllowedRows = Math.max(1, getResources().getInteger(R.integer.quick_settings_max_rows));
128         if (mLessRows) mMaxAllowedRows = Math.max(mMinRows, mMaxAllowedRows - 1);
129         if (updateColumns()) {
130             requestLayout();
131             return true;
132         }
133         return false;
134     }
135 
useSidePadding()136     protected boolean useSidePadding() {
137         return true;
138     }
139 
updateColumns()140     private boolean updateColumns() {
141         int oldColumns = mColumns;
142         mColumns = Math.min(mResourceColumns, mMaxColumns);
143         return oldColumns != mColumns;
144     }
145 
146     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)147     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
148         // If called with AT_MOST, it will limit the number of rows. If called with UNSPECIFIED
149         // it will show all its tiles. In this case, the tiles have to be entered before the
150         // container is measured. Any change in the tiles, should trigger a remeasure.
151         final int numTiles = mRecords.size();
152         final int width = MeasureSpec.getSize(widthMeasureSpec);
153         final int availableWidth = width - getPaddingStart() - getPaddingEnd();
154         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
155         if (heightMode == MeasureSpec.UNSPECIFIED) {
156             mRows = (numTiles + mColumns - 1) / mColumns;
157         }
158         final int gaps = mColumns - 1;
159         mCellWidth =
160                 (availableWidth - (mCellMarginHorizontal * gaps) - mSidePadding * 2) / mColumns;
161 
162         // Measure each QS tile.
163         View previousView = this;
164         int verticalMeasure = exactly(getCellHeight());
165         for (TileRecord record : mRecords) {
166             if (record.tileView.getVisibility() == GONE) continue;
167             record.tileView.measure(exactly(mCellWidth), verticalMeasure);
168             previousView = record.tileView.updateAccessibilityOrder(previousView);
169             mCellHeight = record.tileView.getMeasuredHeight();
170         }
171 
172         int height = (mCellHeight + mCellMarginVertical) * mRows;
173         height -= mCellMarginVertical;
174 
175         if (height < 0) height = 0;
176 
177         setMeasuredDimension(width, height);
178     }
179 
180     /**
181      * Determines the maximum number of rows that can be shown based on height. Clips at a minimum
182      * of 1 and a maximum of mMaxAllowedRows.
183      *
184      * @param allowedHeight The height this view has visually available
185      * @param tilesCount Upper limit on the number of tiles to show. to prevent empty rows.
186      */
updateMaxRows(int allowedHeight, int tilesCount)187     public boolean updateMaxRows(int allowedHeight, int tilesCount) {
188         // Add the cell margin in order to divide easily by the height + the margin below
189         final int availableHeight =  allowedHeight + mCellMarginVertical;
190         final int previousRows = mRows;
191         mRows = availableHeight / (getCellHeight() + mCellMarginVertical);
192         if (mRows < mMinRows) {
193             mRows = mMinRows;
194         } else if (mRows >= mMaxAllowedRows) {
195             mRows = mMaxAllowedRows;
196         }
197         if (mRows > (tilesCount + mColumns - 1) / mColumns) {
198             mRows = (tilesCount + mColumns - 1) / mColumns;
199         }
200         return previousRows != mRows;
201     }
202 
203     @Override
hasOverlappingRendering()204     public boolean hasOverlappingRendering() {
205         return false;
206     }
207 
exactly(int size)208     protected static int exactly(int size) {
209         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
210     }
211 
getCellHeight()212     protected int getCellHeight() {
213         return mMaxCellHeight;
214     }
215 
layoutTileRecords(int numRecords, boolean forLayout)216     private void layoutTileRecords(int numRecords, boolean forLayout) {
217         final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
218         int row = 0;
219         int column = 0;
220         mLastTileBottom = 0;
221 
222         // Layout each QS tile.
223         final int tilesToLayout = Math.min(numRecords, mRows * mColumns);
224         for (int i = 0; i < tilesToLayout; i++, column++) {
225             // If we reached the last column available to layout a tile, wrap back to the next row.
226             if (column == mColumns) {
227                 column = 0;
228                 row++;
229             }
230 
231             final TileRecord record = mRecords.get(i);
232             final int top = getRowTop(row);
233             final int left = getColumnStart(isRtl ? mColumns - column - 1 : column);
234             final int right = left + mCellWidth;
235             final int bottom = top + record.tileView.getMeasuredHeight();
236             if (forLayout) {
237                 record.tileView.layout(left, top, right, bottom);
238             } else {
239                 record.tileView.setLeftTopRightBottom(left, top, right, bottom);
240             }
241             mLastTileBottom = bottom;
242         }
243     }
244 
245     @Override
onLayout(boolean changed, int l, int t, int r, int b)246     protected void onLayout(boolean changed, int l, int t, int r, int b) {
247         layoutTileRecords(mRecords.size(), true /* forLayout */);
248     }
249 
getRowTop(int row)250     protected int getRowTop(int row) {
251         return (int) (row * (mCellHeight * mSquishinessFraction + mCellMarginVertical));
252     }
253 
getColumnStart(int column)254     protected int getColumnStart(int column) {
255         return getPaddingStart() + mSidePadding
256                 + column *  (mCellWidth + mCellMarginHorizontal);
257     }
258 
259     @Override
getNumVisibleTiles()260     public int getNumVisibleTiles() {
261         return mRecords.size();
262     }
263 
isFull()264     public boolean isFull() {
265         return false;
266     }
267 
268     /**
269      * @return The maximum number of tiles this layout can hold
270      */
maxTiles()271     public int maxTiles() {
272         // Each layout should be able to hold at least one tile. If there's not enough room to
273         // show even 1 or there are no tiles, it probably means we are in the middle of setting
274         // up.
275         return Math.max(mColumns * mRows, 1);
276     }
277 
278     @Override
getTilesHeight()279     public int getTilesHeight() {
280         return mLastTileBottom + getPaddingBottom();
281     }
282 
283     @Override
setSquishinessFraction(float squishinessFraction)284     public void setSquishinessFraction(float squishinessFraction) {
285         if (Float.compare(mSquishinessFraction, squishinessFraction) == 0) {
286             return;
287         }
288         mSquishinessFraction = squishinessFraction;
289         layoutTileRecords(mRecords.size(), false /* forLayout */);
290 
291         for (TileRecord record : mRecords) {
292             if (record.tileView instanceof HeightOverrideable) {
293                 ((HeightOverrideable) record.tileView).setSquishinessFraction(mSquishinessFraction);
294             }
295         }
296     }
297 }
298