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