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.systemui.R;
13 import com.android.systemui.qs.QSPanel.QSTileLayout;
14 import com.android.systemui.qs.QSPanel.TileRecord;
15 
16 import java.util.ArrayList;
17 
18 public class TileLayout extends ViewGroup implements QSTileLayout {
19 
20     public static final int NO_MAX_COLUMNS = 100;
21     private static final float TILE_ASPECT = 1.2f;
22 
23     private static final String TAG = "TileLayout";
24 
25     protected int mColumns;
26     protected int mCellWidth;
27     protected int mCellHeight;
28     protected int mCellMarginHorizontal;
29     protected int mCellMarginVertical;
30     protected int mSidePadding;
31     protected int mRows = 1;
32 
33     protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
34     private int mCellMarginTop;
35     protected boolean mListening;
36     protected int mMaxAllowedRows = 3;
37 
38     // Prototyping with less rows
39     private final boolean mLessRows;
40     private int mMinRows = 1;
41     private int mMaxColumns = NO_MAX_COLUMNS;
42     private int mResourceColumns;
43 
TileLayout(Context context)44     public TileLayout(Context context) {
45         this(context, null);
46     }
47 
TileLayout(Context context, AttributeSet attrs)48     public TileLayout(Context context, AttributeSet attrs) {
49         super(context, attrs);
50         setFocusableInTouchMode(true);
51         mLessRows = (Settings.System.getInt(context.getContentResolver(), "qs_less_rows", 0) != 0)
52                 || useQsMediaPlayer(context);
53         updateResources();
54 
55     }
56 
57     @Override
getOffsetTop(TileRecord tile)58     public int getOffsetTop(TileRecord tile) {
59         return getTop();
60     }
61 
62     @Override
setListening(boolean listening)63     public void setListening(boolean listening) {
64         if (mListening == listening) return;
65         mListening = listening;
66         for (TileRecord record : mRecords) {
67             record.tile.setListening(this, mListening);
68         }
69     }
70 
71     @Override
setMinRows(int minRows)72     public boolean setMinRows(int minRows) {
73         if (mMinRows != minRows) {
74             mMinRows = minRows;
75             updateResources();
76             return true;
77         }
78         return false;
79     }
80 
81     @Override
setMaxColumns(int maxColumns)82     public boolean setMaxColumns(int maxColumns) {
83         mMaxColumns = maxColumns;
84         return updateColumns();
85     }
86 
addTile(TileRecord tile)87     public void addTile(TileRecord tile) {
88         mRecords.add(tile);
89         tile.tile.setListening(this, mListening);
90         addTileView(tile);
91     }
92 
addTileView(TileRecord tile)93     protected void addTileView(TileRecord tile) {
94         addView(tile.tileView);
95     }
96 
97     @Override
removeTile(TileRecord tile)98     public void removeTile(TileRecord tile) {
99         mRecords.remove(tile);
100         tile.tile.setListening(this, false);
101         removeView(tile.tileView);
102     }
103 
removeAllViews()104     public void removeAllViews() {
105         for (TileRecord record : mRecords) {
106             record.tile.setListening(this, false);
107         }
108         mRecords.clear();
109         super.removeAllViews();
110     }
111 
updateResources()112     public boolean updateResources() {
113         final Resources res = mContext.getResources();
114         mResourceColumns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns));
115         mCellHeight = mContext.getResources().getDimensionPixelSize(R.dimen.qs_tile_height);
116         mCellMarginHorizontal = res.getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal);
117         mCellMarginVertical= res.getDimensionPixelSize(R.dimen.qs_tile_margin_vertical);
118         mCellMarginTop = res.getDimensionPixelSize(R.dimen.qs_tile_margin_top);
119         mMaxAllowedRows = Math.max(1, getResources().getInteger(R.integer.quick_settings_max_rows));
120         if (mLessRows) mMaxAllowedRows = Math.max(mMinRows, mMaxAllowedRows - 1);
121         if (updateColumns()) {
122             requestLayout();
123             return true;
124         }
125         return false;
126     }
127 
updateColumns()128     private boolean updateColumns() {
129         int oldColumns = mColumns;
130         mColumns = Math.min(mResourceColumns, mMaxColumns);
131         return oldColumns != mColumns;
132     }
133 
134     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)135     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
136         // If called with AT_MOST, it will limit the number of rows. If called with UNSPECIFIED
137         // it will show all its tiles. In this case, the tiles have to be entered before the
138         // container is measured. Any change in the tiles, should trigger a remeasure.
139         final int numTiles = mRecords.size();
140         final int width = MeasureSpec.getSize(widthMeasureSpec);
141         final int availableWidth = width - getPaddingStart() - getPaddingEnd();
142         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
143         if (heightMode == MeasureSpec.UNSPECIFIED) {
144             mRows = (numTiles + mColumns - 1) / mColumns;
145         }
146         mCellWidth =
147                 (availableWidth - (mCellMarginHorizontal * mColumns)) / mColumns;
148 
149         // Measure each QS tile.
150         View previousView = this;
151         for (TileRecord record : mRecords) {
152             if (record.tileView.getVisibility() == GONE) continue;
153             record.tileView.measure(exactly(mCellWidth), exactly(mCellHeight));
154             previousView = record.tileView.updateAccessibilityOrder(previousView);
155         }
156 
157         // Only include the top margin in our measurement if we have more than 1 row to show.
158         // Otherwise, don't add the extra margin buffer at top.
159         int height = (mCellHeight + mCellMarginVertical) * mRows +
160                 (mRows != 0 ? (mCellMarginTop - mCellMarginVertical) : 0);
161         if (height < 0) height = 0;
162 
163         setMeasuredDimension(width, height);
164     }
165 
166     /**
167      * Determines the maximum number of rows that can be shown based on height. Clips at a minimum
168      * of 1 and a maximum of mMaxAllowedRows.
169      *
170      * @param allowedHeight The height this view has visually available
171      * @param tilesCount Upper limit on the number of tiles to show. to prevent empty rows.
172      */
updateMaxRows(int allowedHeight, int tilesCount)173     public boolean updateMaxRows(int allowedHeight, int tilesCount) {
174         final int availableHeight =  allowedHeight - mCellMarginTop
175                 // Add the cell margin in order to divide easily by the height + the margin below
176                 + mCellMarginVertical;
177         final int previousRows = mRows;
178         mRows = availableHeight / (mCellHeight + mCellMarginVertical);
179         if (mRows < mMinRows) {
180             mRows = mMinRows;
181         } else if (mRows >= mMaxAllowedRows) {
182             mRows = mMaxAllowedRows;
183         }
184         if (mRows > (tilesCount + mColumns - 1) / mColumns) {
185             mRows = (tilesCount + mColumns - 1) / mColumns;
186         }
187         return previousRows != mRows;
188     }
189 
190     @Override
hasOverlappingRendering()191     public boolean hasOverlappingRendering() {
192         return false;
193     }
194 
exactly(int size)195     protected static int exactly(int size) {
196         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
197     }
198 
199 
layoutTileRecords(int numRecords)200     protected void layoutTileRecords(int numRecords) {
201         final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
202         int row = 0;
203         int column = 0;
204 
205         // Layout each QS tile.
206         final int tilesToLayout = Math.min(numRecords, mRows * mColumns);
207         for (int i = 0; i < tilesToLayout; i++, column++) {
208             // If we reached the last column available to layout a tile, wrap back to the next row.
209             if (column == mColumns) {
210                 column = 0;
211                 row++;
212             }
213 
214             final TileRecord record = mRecords.get(i);
215             final int top = getRowTop(row);
216             final int left = getColumnStart(isRtl ? mColumns - column - 1 : column);
217             final int right = left + mCellWidth;
218             record.tileView.layout(left, top, right, top + record.tileView.getMeasuredHeight());
219         }
220     }
221 
222     @Override
onLayout(boolean changed, int l, int t, int r, int b)223     protected void onLayout(boolean changed, int l, int t, int r, int b) {
224         layoutTileRecords(mRecords.size());
225     }
226 
getRowTop(int row)227     private int getRowTop(int row) {
228         return row * (mCellHeight + mCellMarginVertical) + mCellMarginTop;
229     }
230 
getColumnStart(int column)231     protected int getColumnStart(int column) {
232         return getPaddingStart() + mCellMarginHorizontal / 2 +
233                 column *  (mCellWidth + mCellMarginHorizontal);
234     }
235 
236     @Override
getNumVisibleTiles()237     public int getNumVisibleTiles() {
238         return mRecords.size();
239     }
240 }
241