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