1/* 2 * Copyright 2017 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 17package androidx.slice.widget; 18 19import static android.app.slice.Slice.HINT_LARGE; 20import static android.app.slice.Slice.HINT_NO_TINT; 21import static android.app.slice.Slice.HINT_TITLE; 22import static android.app.slice.SliceItem.FORMAT_ACTION; 23import static android.app.slice.SliceItem.FORMAT_IMAGE; 24import static android.app.slice.SliceItem.FORMAT_LONG; 25import static android.app.slice.SliceItem.FORMAT_SLICE; 26import static android.app.slice.SliceItem.FORMAT_TEXT; 27import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 28import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 29 30import static androidx.slice.widget.SliceView.MODE_SMALL; 31 32import android.app.PendingIntent; 33import android.content.Context; 34import android.content.res.Resources; 35import android.util.AttributeSet; 36import android.util.Log; 37import android.util.Pair; 38import android.util.TypedValue; 39import android.view.Gravity; 40import android.view.LayoutInflater; 41import android.view.View; 42import android.view.ViewGroup; 43import android.widget.FrameLayout; 44import android.widget.ImageView; 45import android.widget.ImageView.ScaleType; 46import android.widget.LinearLayout; 47import android.widget.TextView; 48 49import androidx.annotation.ColorInt; 50import androidx.annotation.RestrictTo; 51import androidx.slice.SliceItem; 52import androidx.slice.core.SliceQuery; 53import androidx.slice.view.R; 54 55import java.util.ArrayList; 56import java.util.Iterator; 57import java.util.List; 58 59/** 60 * @hide 61 */ 62@RestrictTo(RestrictTo.Scope.LIBRARY) 63public class GridRowView extends SliceChildView implements View.OnClickListener { 64 65 private static final String TAG = "GridView"; 66 67 private static final int TITLE_TEXT_LAYOUT = R.layout.abc_slice_title; 68 private static final int TEXT_LAYOUT = R.layout.abc_slice_secondary_text; 69 70 // Max number of normal cell items that can be shown in a row 71 private static final int MAX_CELLS = 5; 72 73 // Max number of text items that can show in a cell 74 private static final int MAX_CELL_TEXT = 2; 75 // Max number of text items that can show in a cell if the mode is small 76 private static final int MAX_CELL_TEXT_SMALL = 1; 77 // Max number of images that can show in a cell 78 private static final int MAX_CELL_IMAGES = 1; 79 80 private int mRowIndex; 81 private int mRowCount; 82 83 private int mSmallImageSize; 84 private int mIconSize; 85 private int mGutter; 86 private int mTextPadding; 87 88 private GridContent mGridContent; 89 private LinearLayout mViewContainer; 90 91 public GridRowView(Context context) { 92 this(context, null); 93 } 94 95 public GridRowView(Context context, AttributeSet attrs) { 96 super(context, attrs); 97 final Resources res = getContext().getResources(); 98 mViewContainer = new LinearLayout(getContext()); 99 mViewContainer.setOrientation(LinearLayout.HORIZONTAL); 100 addView(mViewContainer, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 101 mViewContainer.setGravity(Gravity.CENTER_VERTICAL); 102 mIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_icon_size); 103 mSmallImageSize = res.getDimensionPixelSize(R.dimen.abc_slice_small_image_size); 104 mGutter = res.getDimensionPixelSize(R.dimen.abc_slice_grid_gutter); 105 mTextPadding = res.getDimensionPixelSize(R.dimen.abc_slice_grid_text_padding); 106 } 107 108 @Override 109 public int getSmallHeight() { 110 // GridRow is small if its the first element in a list without a header presented in small 111 if (mGridContent == null) { 112 return 0; 113 } 114 return mGridContent.getSmallHeight() + getExtraTopPadding() + getExtraBottomPadding(); 115 } 116 117 @Override 118 public int getActualHeight() { 119 if (mGridContent == null) { 120 return 0; 121 } 122 return mGridContent.getActualHeight() + getExtraTopPadding() + getExtraBottomPadding(); 123 } 124 125 private int getExtraTopPadding() { 126 if (mGridContent != null && mGridContent.isAllImages()) { 127 // Might need to add padding if in first or last position 128 if (mRowIndex == 0) { 129 return mGridTopPadding; 130 } 131 } 132 return 0; 133 } 134 135 private int getExtraBottomPadding() { 136 if (mGridContent != null && mGridContent.isAllImages()) { 137 if (mRowIndex == mRowCount - 1 || getMode() == MODE_SMALL) { 138 return mGridBottomPadding; 139 } 140 } 141 return 0; 142 } 143 144 @Override 145 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 146 int height = getMode() == MODE_SMALL ? getSmallHeight() : getActualHeight(); 147 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 148 mViewContainer.getLayoutParams().height = height; 149 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 150 } 151 152 @Override 153 public void setTint(@ColorInt int tintColor) { 154 super.setTint(tintColor); 155 if (mGridContent != null) { 156 GridContent gc = mGridContent; 157 // TODO -- could be smarter about this 158 resetView(); 159 populateViews(gc); 160 } 161 } 162 163 /** 164 * This is called when GridView is being used as a component in a larger template. 165 */ 166 @Override 167 public void setSliceItem(SliceItem slice, boolean isHeader, int rowIndex, 168 int rowCount, SliceView.OnSliceActionListener observer) { 169 resetView(); 170 setSliceActionListener(observer); 171 mRowIndex = rowIndex; 172 mRowCount = rowCount; 173 mGridContent = new GridContent(getContext(), slice); 174 populateViews(mGridContent); 175 mViewContainer.setPadding(0, getExtraTopPadding(), 0, getExtraBottomPadding()); 176 } 177 178 private void populateViews(GridContent gc) { 179 if (gc.getContentIntent() != null) { 180 EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT, 181 EventInfo.ROW_TYPE_GRID, mRowIndex); 182 Pair<SliceItem, EventInfo> tagItem = new Pair<>(gc.getContentIntent(), info); 183 mViewContainer.setTag(tagItem); 184 makeClickable(mViewContainer, true); 185 } 186 CharSequence contentDescr = gc.getContentDescription(); 187 if (contentDescr != null) { 188 mViewContainer.setContentDescription(contentDescr); 189 } 190 ArrayList<GridContent.CellContent> cells = gc.getGridContent(); 191 boolean hasSeeMore = gc.getSeeMoreItem() != null; 192 for (int i = 0; i < cells.size(); i++) { 193 if (mViewContainer.getChildCount() >= MAX_CELLS) { 194 if (hasSeeMore) { 195 addSeeMoreCount(cells.size() - MAX_CELLS); 196 } 197 break; 198 } 199 addCell(cells.get(i), i, Math.min(cells.size(), MAX_CELLS)); 200 } 201 } 202 203 private void addSeeMoreCount(int numExtra) { 204 // Remove last element 205 View last = mViewContainer.getChildAt(mViewContainer.getChildCount() - 1); 206 mViewContainer.removeView(last); 207 208 SliceItem seeMoreItem = mGridContent.getSeeMoreItem(); 209 int index = mViewContainer.getChildCount(); 210 int total = MAX_CELLS; 211 if ((FORMAT_SLICE.equals(seeMoreItem.getFormat()) 212 || FORMAT_ACTION.equals(seeMoreItem.getFormat())) 213 && seeMoreItem.getSlice().getItems().size() > 0) { 214 // It's a custom see more cell, add it 215 addCell(new GridContent.CellContent(seeMoreItem), index, total); 216 return; 217 } 218 219 // Default see more, create it 220 LayoutInflater inflater = LayoutInflater.from(getContext()); 221 TextView extraText; 222 ViewGroup seeMoreView; 223 if (mGridContent.isAllImages()) { 224 seeMoreView = (FrameLayout) inflater.inflate(R.layout.abc_slice_grid_see_more_overlay, 225 mViewContainer, false); 226 seeMoreView.addView(last, 0, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 227 extraText = seeMoreView.findViewById(R.id.text_see_more_count); 228 } else { 229 seeMoreView = (LinearLayout) inflater.inflate( 230 R.layout.abc_slice_grid_see_more, mViewContainer, false); 231 extraText = seeMoreView.findViewById(R.id.text_see_more_count); 232 233 // Update text appearance 234 TextView moreText = seeMoreView.findViewById(R.id.text_see_more); 235 moreText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mGridTitleSize); 236 moreText.setTextColor(mTitleColor); 237 } 238 mViewContainer.addView(seeMoreView, new LinearLayout.LayoutParams(0, MATCH_PARENT, 1)); 239 extraText.setText(getResources().getString(R.string.abc_slice_more_content, numExtra)); 240 241 // Make it clickable 242 EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_SEE_MORE, 243 EventInfo.ROW_TYPE_GRID, mRowIndex); 244 info.setPosition(EventInfo.POSITION_CELL, index, total); 245 Pair<SliceItem, EventInfo> tagItem = new Pair<>(seeMoreItem, info); 246 seeMoreView.setTag(tagItem); 247 makeClickable(seeMoreView, true); 248 } 249 250 /** 251 * Adds a cell to the grid view based on the provided {@link SliceItem}. 252 */ 253 private void addCell(GridContent.CellContent cell, int index, int total) { 254 final int maxCellText = getMode() == MODE_SMALL 255 ? MAX_CELL_TEXT_SMALL 256 : MAX_CELL_TEXT; 257 LinearLayout cellContainer = new LinearLayout(getContext()); 258 cellContainer.setOrientation(LinearLayout.VERTICAL); 259 cellContainer.setGravity(Gravity.CENTER_HORIZONTAL); 260 261 ArrayList<SliceItem> cellItems = cell.getCellItems(); 262 SliceItem contentIntentItem = cell.getContentIntent(); 263 264 int textCount = 0; 265 int imageCount = 0; 266 boolean added = false; 267 boolean singleItem = cellItems.size() == 1; 268 List<SliceItem> textItems = null; 269 // In small format we display one text item and prefer titles 270 if (!singleItem && getMode() == MODE_SMALL) { 271 // Get all our text items 272 textItems = new ArrayList<>(); 273 for (SliceItem cellItem : cellItems) { 274 if (FORMAT_TEXT.equals(cellItem.getFormat())) { 275 textItems.add(cellItem); 276 } 277 } 278 // If we have more than 1 remove non-titles 279 Iterator<SliceItem> iterator = textItems.iterator(); 280 while (textItems.size() > 1) { 281 SliceItem item = iterator.next(); 282 if (!item.hasAnyHints(HINT_TITLE, HINT_LARGE)) { 283 iterator.remove(); 284 } 285 } 286 } 287 SliceItem prevItem = null; 288 for (int i = 0; i < cellItems.size(); i++) { 289 SliceItem item = cellItems.get(i); 290 final String itemFormat = item.getFormat(); 291 int padding = determinePadding(prevItem); 292 if (textCount < maxCellText && (FORMAT_TEXT.equals(itemFormat) 293 || FORMAT_LONG.equals(itemFormat))) { 294 if (textItems != null && !textItems.contains(item)) { 295 continue; 296 } 297 if (addItem(item, mTintColor, cellContainer, padding)) { 298 prevItem = item; 299 textCount++; 300 added = true; 301 } 302 } else if (imageCount < MAX_CELL_IMAGES && FORMAT_IMAGE.equals(item.getFormat())) { 303 if (addItem(item, mTintColor, cellContainer, 0)) { 304 prevItem = item; 305 imageCount++; 306 added = true; 307 } 308 } 309 } 310 if (added) { 311 CharSequence contentDescr = cell.getContentDescription(); 312 if (contentDescr != null) { 313 cellContainer.setContentDescription(contentDescr); 314 } 315 mViewContainer.addView(cellContainer, 316 new LinearLayout.LayoutParams(0, WRAP_CONTENT, 1)); 317 if (index != total - 1) { 318 // If we're not the last or only element add space between items 319 MarginLayoutParams lp = 320 (LinearLayout.MarginLayoutParams) cellContainer.getLayoutParams(); 321 lp.setMarginEnd(mGutter); 322 cellContainer.setLayoutParams(lp); 323 } 324 if (contentIntentItem != null) { 325 EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_BUTTON, 326 EventInfo.ROW_TYPE_GRID, mRowIndex); 327 info.setPosition(EventInfo.POSITION_CELL, index, total); 328 Pair<SliceItem, EventInfo> tagItem = new Pair<>(contentIntentItem, info); 329 cellContainer.setTag(tagItem); 330 makeClickable(cellContainer, true); 331 } 332 } 333 } 334 335 /** 336 * Adds simple items to a container. Simple items include icons, text, and timestamps. 337 * 338 * @param item item to add to the container. 339 * @param container the container to add to. 340 * @param padding the padding to apply to the item. 341 * 342 * @return Whether an item was added. 343 */ 344 private boolean addItem(SliceItem item, int color, ViewGroup container, int padding) { 345 final String format = item.getFormat(); 346 View addedView = null; 347 if (FORMAT_TEXT.equals(format) || FORMAT_LONG.equals(format)) { 348 boolean title = SliceQuery.hasAnyHints(item, HINT_LARGE, HINT_TITLE); 349 TextView tv = (TextView) LayoutInflater.from(getContext()).inflate(title 350 ? TITLE_TEXT_LAYOUT : TEXT_LAYOUT, null); 351 tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, title ? mGridTitleSize : mGridSubtitleSize); 352 tv.setTextColor(title ? mTitleColor : mSubtitleColor); 353 CharSequence text = FORMAT_LONG.equals(format) 354 ? SliceViewUtil.getRelativeTimeString(item.getTimestamp()) 355 : item.getText(); 356 tv.setText(text); 357 container.addView(tv); 358 tv.setPadding(0, padding, 0, 0); 359 addedView = tv; 360 } else if (FORMAT_IMAGE.equals(format)) { 361 ImageView iv = new ImageView(getContext()); 362 iv.setImageDrawable(item.getIcon().loadDrawable(getContext())); 363 LinearLayout.LayoutParams lp; 364 if (item.hasHint(HINT_LARGE)) { 365 iv.setScaleType(ScaleType.CENTER_CROP); 366 lp = new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); 367 } else { 368 boolean isIcon = !item.hasHint(HINT_NO_TINT); 369 int size = isIcon ? mIconSize : mSmallImageSize; 370 iv.setScaleType(isIcon ? ScaleType.CENTER_INSIDE : ScaleType.CENTER_CROP); 371 lp = new LinearLayout.LayoutParams(size, size); 372 } 373 if (color != -1 && !item.hasHint(HINT_NO_TINT)) { 374 iv.setColorFilter(color); 375 } 376 container.addView(iv, lp); 377 addedView = iv; 378 } 379 return addedView != null; 380 } 381 382 private int determinePadding(SliceItem prevItem) { 383 if (prevItem == null) { 384 // No need for top padding 385 return 0; 386 } else if (FORMAT_IMAGE.equals(prevItem.getFormat())) { 387 return mTextPadding; 388 } else if (FORMAT_TEXT.equals(prevItem.getFormat()) 389 || FORMAT_LONG.equals(prevItem.getFormat())) { 390 return mVerticalGridTextPadding; 391 } 392 return 0; 393 } 394 395 private void makeClickable(View layout, boolean isClickable) { 396 layout.setOnClickListener(isClickable ? this : null); 397 layout.setBackground(isClickable 398 ? SliceViewUtil.getDrawable(getContext(), android.R.attr.selectableItemBackground) 399 : null); 400 layout.setClickable(isClickable); 401 } 402 403 @Override 404 public void onClick(View view) { 405 Pair<SliceItem, EventInfo> tagItem = (Pair<SliceItem, EventInfo>) view.getTag(); 406 final SliceItem actionItem = tagItem.first; 407 final EventInfo info = tagItem.second; 408 if (actionItem != null && FORMAT_ACTION.equals(actionItem.getFormat())) { 409 try { 410 actionItem.fireAction(null, null); 411 if (mObserver != null) { 412 mObserver.onSliceAction(info, actionItem); 413 } 414 } catch (PendingIntent.CanceledException e) { 415 Log.e(TAG, "PendingIntent for slice cannot be sent", e); 416 } 417 } 418 } 419 420 @Override 421 public void resetView() { 422 mViewContainer.removeAllViews(); 423 makeClickable(mViewContainer, false); 424 } 425} 426