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