1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.twopanelsettings.slices.compat.widget; 18 19 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.ICON_IMAGE; 20 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.RAW_IMAGE_LARGE; 21 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.UNKNOWN_IMAGE; 22 import static com.android.tv.twopanelsettings.slices.compat.widget.SliceViewPolicy.MODE_LARGE; 23 import static com.android.tv.twopanelsettings.slices.compat.widget.SliceViewPolicy.MODE_SMALL; 24 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.content.res.TypedArray; 28 import android.util.AttributeSet; 29 import android.util.SparseArray; 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import com.android.tv.twopanelsettings.R; 33 import com.android.tv.twopanelsettings.slices.compat.SliceItem; 34 import java.util.ArrayList; 35 import java.util.List; 36 37 /** Holds style information shared between child views of a slice */ 38 // @RestrictTo(RestrictTo.Scope.LIBRARY) 39 // @Deprecated // Supported for TV 40 public class SliceStyle { 41 private int mTintColor = -1; 42 private final int mTitleColor; 43 private final int mSubtitleColor; 44 private final int mHeaderTitleSize; 45 private final int mHeaderSubtitleSize; 46 private final int mVerticalHeaderTextPadding; 47 private final int mTitleSize; 48 private final int mSubtitleSize; 49 private final int mVerticalTextPadding; 50 private final int mGridTitleSize; 51 private final int mGridSubtitleSize; 52 private final int mVerticalGridTextPadding; 53 private final int mGridTopPadding; 54 private final int mGridBottomPadding; 55 56 private final int mRowMaxHeight; 57 private final int mRowTextWithRangeHeight; 58 private final int mRowSingleTextWithRangeHeight; 59 private final int mRowMinHeight; 60 private final int mRowRangeHeight; 61 private final int mRowSelectionHeight; 62 private final int mRowTextWithSelectionHeight; 63 private final int mRowSingleTextWithSelectionHeight; 64 private final int mRowInlineRangeHeight; 65 66 private final int mGridBigPicMinHeight; 67 private final int mGridBigPicMaxHeight; 68 private final int mGridAllImagesHeight; 69 private final int mGridImageTextHeight; 70 private final int mGridRawImageTextHeight; 71 private final int mGridMaxHeight; 72 private final int mGridMinHeight; 73 74 private final int mListMinScrollHeight; 75 private final int mListLargeHeight; 76 77 private final boolean mExpandToAvailableHeight; 78 private final boolean mHideHeaderRow; 79 80 private final int mDefaultRowStyleRes; 81 private final SparseArray<RowStyle> mResourceToRowStyle = new SparseArray<>(); 82 private RowStyleFactory mRowStyleFactory; 83 84 private final Context mContext; 85 86 private final float mImageCornerRadius; 87 SliceStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)88 public SliceStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 89 TypedArray a = 90 context 91 .getTheme() 92 .obtainStyledAttributes(attrs, R.styleable.SliceView, defStyleAttr, defStyleRes); 93 try { 94 int themeColor = a.getColor(R.styleable.SliceView_tintColor, -1); 95 mTintColor = themeColor != -1 ? themeColor : mTintColor; 96 mTitleColor = a.getColor(R.styleable.SliceView_titleColor, 0); 97 mSubtitleColor = a.getColor(R.styleable.SliceView_subtitleColor, 0); 98 99 mHeaderTitleSize = (int) a.getDimension(R.styleable.SliceView_headerTitleSize, 0); 100 mHeaderSubtitleSize = (int) a.getDimension(R.styleable.SliceView_headerSubtitleSize, 0); 101 mVerticalHeaderTextPadding = 102 (int) a.getDimension(R.styleable.SliceView_headerTextVerticalPadding, 0); 103 104 mTitleSize = (int) a.getDimension(R.styleable.SliceView_titleSize, 0); 105 mSubtitleSize = (int) a.getDimension(R.styleable.SliceView_subtitleSize, 0); 106 mVerticalTextPadding = (int) a.getDimension(R.styleable.SliceView_textVerticalPadding, 0); 107 108 mGridTitleSize = (int) a.getDimension(R.styleable.SliceView_gridTitleSize, 0); 109 mGridSubtitleSize = (int) a.getDimension(R.styleable.SliceView_gridSubtitleSize, 0); 110 int defaultVerticalGridPadding = 111 context.getResources().getDimensionPixelSize(R.dimen.abc_slice_grid_text_inner_padding); 112 mVerticalGridTextPadding = 113 (int) 114 a.getDimension( 115 R.styleable.SliceView_gridTextVerticalPadding, defaultVerticalGridPadding); 116 mGridTopPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0); 117 mGridBottomPadding = (int) a.getDimension(R.styleable.SliceView_gridBottomPadding, 0); 118 119 mDefaultRowStyleRes = a.getResourceId(R.styleable.SliceView_rowStyle, 0); 120 121 int defaultRowMinHeight = 122 context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_min_height); 123 mRowMinHeight = (int) a.getDimension(R.styleable.SliceView_rowMinHeight, defaultRowMinHeight); 124 125 int defaultRowMaxHeight = 126 context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_max_height); 127 mRowMaxHeight = (int) a.getDimension(R.styleable.SliceView_rowMaxHeight, defaultRowMaxHeight); 128 129 int defaultRowRangeHeight = 130 context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_range_height); 131 mRowRangeHeight = 132 (int) a.getDimension(R.styleable.SliceView_rowRangeHeight, defaultRowRangeHeight); 133 134 int defaultRowSingleTextWithRangeHeight = 135 context 136 .getResources() 137 .getDimensionPixelSize(R.dimen.abc_slice_row_range_single_text_height); 138 mRowSingleTextWithRangeHeight = 139 (int) 140 a.getDimension( 141 R.styleable.SliceView_rowRangeSingleTextHeight, 142 defaultRowSingleTextWithRangeHeight); 143 144 int defaultRowInlineRangeHeight = 145 context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_range_inline_height); 146 mRowInlineRangeHeight = 147 (int) 148 a.getDimension( 149 R.styleable.SliceView_rowInlineRangeHeight, defaultRowInlineRangeHeight); 150 151 mExpandToAvailableHeight = a.getBoolean(R.styleable.SliceView_expandToAvailableHeight, false); 152 153 mHideHeaderRow = a.getBoolean(R.styleable.SliceView_hideHeaderRow, false); 154 155 mContext = context; 156 157 mImageCornerRadius = a.getDimension(R.styleable.SliceView_imageCornerRadius, 0); 158 } finally { 159 a.recycle(); 160 } 161 162 // Note: The above colors and dimensions are styleable, but the below ones are not. 163 164 final Resources r = context.getResources(); 165 166 mRowTextWithRangeHeight = 167 r.getDimensionPixelSize(R.dimen.abc_slice_row_range_multi_text_height); 168 mRowSelectionHeight = r.getDimensionPixelSize(R.dimen.abc_slice_row_selection_height); 169 mRowTextWithSelectionHeight = 170 r.getDimensionPixelSize(R.dimen.abc_slice_row_selection_multi_text_height); 171 mRowSingleTextWithSelectionHeight = 172 r.getDimensionPixelSize(R.dimen.abc_slice_row_selection_single_text_height); 173 174 mGridBigPicMinHeight = r.getDimensionPixelSize(R.dimen.abc_slice_big_pic_min_height); 175 mGridBigPicMaxHeight = r.getDimensionPixelSize(R.dimen.abc_slice_big_pic_max_height); 176 mGridAllImagesHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_image_only_height); 177 mGridImageTextHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_image_text_height); 178 mGridRawImageTextHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_raw_image_text_offset); 179 mGridMinHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_min_height); 180 mGridMaxHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_max_height); 181 182 mListMinScrollHeight = r.getDimensionPixelSize(R.dimen.abc_slice_row_min_height); 183 mListLargeHeight = r.getDimensionPixelSize(R.dimen.abc_slice_large_height_internal); 184 } 185 getRowMinHeight()186 public int getRowMinHeight() { 187 return mRowMinHeight; 188 } 189 getRowMaxHeight()190 public int getRowMaxHeight() { 191 return mRowMaxHeight; 192 } 193 getRowInlineRangeHeight()194 public int getRowInlineRangeHeight() { 195 return mRowInlineRangeHeight; 196 } 197 setTintColor(int tint)198 public void setTintColor(int tint) { 199 mTintColor = tint; 200 } 201 getTintColor()202 public int getTintColor() { 203 return mTintColor; 204 } 205 getTitleColor()206 public int getTitleColor() { 207 return mTitleColor; 208 } 209 getSubtitleColor()210 public int getSubtitleColor() { 211 return mSubtitleColor; 212 } 213 getHeaderTitleSize()214 public int getHeaderTitleSize() { 215 return mHeaderTitleSize; 216 } 217 getHeaderSubtitleSize()218 public int getHeaderSubtitleSize() { 219 return mHeaderSubtitleSize; 220 } 221 getVerticalHeaderTextPadding()222 public int getVerticalHeaderTextPadding() { 223 return mVerticalHeaderTextPadding; 224 } 225 getTitleSize()226 public int getTitleSize() { 227 return mTitleSize; 228 } 229 getSubtitleSize()230 public int getSubtitleSize() { 231 return mSubtitleSize; 232 } 233 getVerticalTextPadding()234 public int getVerticalTextPadding() { 235 return mVerticalTextPadding; 236 } 237 getGridTitleSize()238 public int getGridTitleSize() { 239 return mGridTitleSize; 240 } 241 getGridSubtitleSize()242 public int getGridSubtitleSize() { 243 return mGridSubtitleSize; 244 } 245 getVerticalGridTextPadding()246 public int getVerticalGridTextPadding() { 247 return mVerticalGridTextPadding; 248 } 249 getGridTopPadding()250 public int getGridTopPadding() { 251 return mGridTopPadding; 252 } 253 getGridBottomPadding()254 public int getGridBottomPadding() { 255 return mGridBottomPadding; 256 } 257 258 /** Returns the {@link RowStyle} to use for the given {@link SliceItem}. */ 259 @NonNull getRowStyle(@ullable SliceItem sliceItem)260 public RowStyle getRowStyle(@Nullable SliceItem sliceItem) { 261 int rowStyleRes = mDefaultRowStyleRes; 262 263 if (sliceItem != null && mRowStyleFactory != null) { 264 int maybeStyleRes = mRowStyleFactory.getRowStyleRes(sliceItem); 265 if (maybeStyleRes != 0) { 266 rowStyleRes = maybeStyleRes; 267 } 268 } 269 270 if (rowStyleRes == 0) { 271 // Return default values. 272 return new RowStyle(mContext, this); 273 } 274 275 RowStyle rowStyle = mResourceToRowStyle.get(rowStyleRes); 276 if (rowStyle == null) { 277 rowStyle = new RowStyle(mContext, rowStyleRes, this); 278 mResourceToRowStyle.put(rowStyleRes, rowStyle); 279 } 280 return rowStyle; 281 } 282 283 /** Sets the {@link RowStyleFactory} which allows multiple children to have different styles. */ setRowStyleFactory(@ullable RowStyleFactory rowStyleFactory)284 public void setRowStyleFactory(@Nullable RowStyleFactory rowStyleFactory) { 285 mRowStyleFactory = rowStyleFactory; 286 } 287 getRowRangeHeight()288 public int getRowRangeHeight() { 289 return mRowRangeHeight; 290 } 291 getRowSelectionHeight()292 public int getRowSelectionHeight() { 293 return mRowSelectionHeight; 294 } 295 getExpandToAvailableHeight()296 public boolean getExpandToAvailableHeight() { 297 return mExpandToAvailableHeight; 298 } 299 getHideHeaderRow()300 public boolean getHideHeaderRow() { 301 return mHideHeaderRow; 302 } 303 getApplyCornerRadiusToLargeImages()304 public boolean getApplyCornerRadiusToLargeImages() { 305 return mImageCornerRadius > 0; 306 } 307 getImageCornerRadius()308 public float getImageCornerRadius() { 309 return mImageCornerRadius; 310 } 311 getRowHeight(RowContent row, SliceViewPolicy policy)312 public int getRowHeight(RowContent row, SliceViewPolicy policy) { 313 int maxHeight = policy.getMaxSmallHeight() > 0 ? policy.getMaxSmallHeight() : mRowMaxHeight; 314 315 if (row.getRange() == null && row.getSelection() == null && policy.getMode() != MODE_LARGE) { 316 return maxHeight; 317 } 318 319 if (row.getRange() != null) { 320 // If no StartItem, keep to use original layout. 321 if (row.getStartItem() == null) { 322 // Range element always has set height and then the height of the text 323 // area on the row will vary depending on 0,1 or 2 lines of text. 324 int textAreaHeight = 325 row.getLineCount() == 0 326 ? 0 327 : (row.getLineCount() > 1 328 ? mRowTextWithRangeHeight 329 : mRowSingleTextWithRangeHeight); 330 return textAreaHeight + mRowRangeHeight; 331 } else { 332 // If has StartItem then Range element is inline, the row height should be more to 333 // fit thumb ripple. 334 return mRowInlineRangeHeight; 335 } 336 } 337 338 if (row.getSelection() != null) { 339 // Selection element always has set height and then the height of the text 340 // area on the row will vary depending on if 1 or 2 lines of text. 341 int textAreaHeight = 342 row.getLineCount() > 1 ? mRowTextWithSelectionHeight : mRowSingleTextWithSelectionHeight; 343 return textAreaHeight + mRowSelectionHeight; 344 } 345 346 return (row.getLineCount() > 1 || row.getIsHeader()) ? maxHeight : mRowMinHeight; 347 } 348 getGridHeight(GridContent grid, SliceViewPolicy policy)349 public int getGridHeight(GridContent grid, SliceViewPolicy policy) { 350 boolean isSmall = policy.getMode() == MODE_SMALL; 351 if (!grid.isValid()) { 352 return 0; 353 } 354 int largestImageMode = grid.getLargestImageMode(); 355 int height; 356 if (grid.isAllImages()) { 357 height = 358 (grid.getGridContent().size() == 1) 359 ? (isSmall ? mGridBigPicMinHeight : mGridBigPicMaxHeight) 360 : (largestImageMode == ICON_IMAGE 361 ? mGridMinHeight 362 : (largestImageMode == RAW_IMAGE_LARGE 363 ? grid.getFirstImageSize(mContext).y 364 : mGridAllImagesHeight)); 365 } else { 366 boolean twoLines = grid.getMaxCellLineCount() > 1; 367 boolean hasImage = grid.hasImage(); 368 boolean iconImagesOrNone = 369 largestImageMode == ICON_IMAGE || largestImageMode == UNKNOWN_IMAGE; 370 height = 371 largestImageMode == RAW_IMAGE_LARGE 372 ? (grid.getFirstImageSize(mContext).y + (twoLines ? 2 : 1) * mGridRawImageTextHeight) 373 : (twoLines && !isSmall) 374 ? (hasImage ? mGridMaxHeight : mGridMinHeight) 375 : (iconImagesOrNone ? mGridMinHeight : mGridImageTextHeight); 376 } 377 int topPadding = grid.isAllImages() && grid.getRowIndex() == 0 ? mGridTopPadding : 0; 378 int bottomPadding = grid.isAllImages() && grid.getIsLastIndex() ? mGridBottomPadding : 0; 379 return height + topPadding + bottomPadding; 380 } 381 getListHeight(ListContent list, SliceViewPolicy policy)382 public int getListHeight(ListContent list, SliceViewPolicy policy) { 383 if (policy.getMode() == MODE_SMALL) { 384 return list.getHeader().getHeight(this, policy); 385 } 386 int maxHeight = policy.getMaxHeight(); 387 boolean scrollable = policy.isScrollable(); 388 389 int desiredHeight = getListItemsHeight(list.getRowItems(), policy); 390 if (maxHeight > 0) { 391 // Always ensure we're at least the height of our small version. 392 int smallHeight = list.getHeader().getHeight(this, policy); 393 maxHeight = Math.max(smallHeight, maxHeight); 394 } 395 int maxLargeHeight = maxHeight > 0 ? maxHeight : mListLargeHeight; 396 // Do we have enough content to reasonably scroll in our max? 397 boolean bigEnoughToScroll = desiredHeight - maxLargeHeight >= mListMinScrollHeight; 398 399 // Adjust for scrolling 400 int height = 401 bigEnoughToScroll && !getExpandToAvailableHeight() 402 ? maxLargeHeight 403 : maxHeight <= 0 ? desiredHeight : Math.min(maxLargeHeight, desiredHeight); 404 if (!scrollable) { 405 height = 406 getListItemsHeight( 407 getListItemsForNonScrollingList(list, height, policy).getDisplayedItems(), policy); 408 } 409 return height; 410 } 411 getListItemsHeight(List<SliceContent> listItems, SliceViewPolicy policy)412 public int getListItemsHeight(List<SliceContent> listItems, SliceViewPolicy policy) { 413 if (listItems == null) { 414 return 0; 415 } 416 417 int height = 0; 418 for (int i = 0; i < listItems.size(); i++) { 419 SliceContent listItem = listItems.get(i); 420 if (i == 0 && shouldSkipFirstListItem(listItems)) { 421 continue; 422 } 423 height += listItem.getHeight(this, policy); 424 } 425 return height; 426 } 427 428 /** 429 * Returns a list of items that can fit in the provided height. If this list has a see more item 430 * this will be displayed in the list if appropriate. 431 * 432 * @param list the list from which to source the items. 433 * @param availableHeight to use to determine the row items to return. 434 * @param policy the policy info (scrolling, mode) to use when determining row items to return. 435 * @return the list of items that can be displayed in the provided height. 436 */ 437 @NonNull getListItemsForNonScrollingList( ListContent list, int availableHeight, SliceViewPolicy policy)438 public DisplayedListItems getListItemsForNonScrollingList( 439 ListContent list, int availableHeight, SliceViewPolicy policy) { 440 ArrayList<SliceContent> visibleItems = new ArrayList<>(); 441 int hiddenItemCount = 0; 442 if (list.getRowItems() == null || list.getRowItems().isEmpty()) { 443 return new DisplayedListItems(visibleItems, hiddenItemCount); 444 } 445 446 final boolean skipFirstItem = shouldSkipFirstListItem(list.getRowItems()); 447 448 int visibleHeight = 0; 449 final int rowCount = list.getRowItems().size(); 450 for (int i = 0; i < rowCount; i++) { 451 SliceContent listItem = list.getRowItems().get(i); 452 if (i == 0 && skipFirstItem) { 453 continue; 454 } 455 int itemHeight = listItem.getHeight(this, policy); 456 if (availableHeight > 0 && visibleHeight + itemHeight > availableHeight) { 457 hiddenItemCount = rowCount - i; 458 break; 459 } else { 460 visibleHeight += itemHeight; 461 visibleItems.add(listItem); 462 } 463 } 464 465 // Only add see more if we're at least showing one item and it's not the header. 466 final int minItemCountForSeeMore = skipFirstItem ? 1 : 2; 467 if (list.getSeeMoreItem() != null 468 && visibleItems.size() >= minItemCountForSeeMore 469 && hiddenItemCount > 0) { 470 // Need to show see more 471 int seeMoreHeight = list.getSeeMoreItem().getHeight(this, policy); 472 visibleHeight += seeMoreHeight; 473 474 // Free enough vertical space to fit the see more item. 475 while (visibleHeight > availableHeight && visibleItems.size() >= minItemCountForSeeMore) { 476 int lastIndex = visibleItems.size() - 1; 477 SliceContent lastItem = visibleItems.get(lastIndex); 478 visibleHeight -= lastItem.getHeight(this, policy); 479 visibleItems.remove(lastIndex); 480 hiddenItemCount++; 481 } 482 483 if (visibleItems.size() >= minItemCountForSeeMore) { 484 visibleItems.add(list.getSeeMoreItem()); 485 } else { 486 // Not possible to free enough vertical space. We'll show only the header. 487 visibleHeight -= seeMoreHeight; 488 } 489 } 490 if (visibleItems.isEmpty()) { 491 // Didn't have enough space to show anything; should still show something 492 visibleItems.add(list.getRowItems().get(0)); 493 } 494 return new DisplayedListItems(visibleItems, hiddenItemCount); 495 } 496 497 /** 498 * Returns a list of items that should be displayed to the user. 499 * 500 * @param list the list from which to source the items. 501 */ 502 @NonNull getListItemsToDisplay(@onNull ListContent list)503 public List<SliceContent> getListItemsToDisplay(@NonNull ListContent list) { 504 List<SliceContent> rowItems = list.getRowItems(); 505 if (!rowItems.isEmpty() && shouldSkipFirstListItem(rowItems)) { 506 return rowItems.subList(1, rowItems.size()); 507 } 508 return rowItems; 509 } 510 511 /** Returns true if the first item of a list should be skipped. */ shouldSkipFirstListItem(List<SliceContent> rowItems)512 private boolean shouldSkipFirstListItem(List<SliceContent> rowItems) { 513 // Hide header row if requested, but only if there is at least one non-header row. 514 return getHideHeaderRow() 515 && rowItems.size() > 1 516 && rowItems.get(0) instanceof RowContent 517 && ((RowContent) rowItems.get(0)).getIsHeader(); 518 } 519 } 520