Home | History | Annotate | Download | only in widget
      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 
     17 package androidx.slice.widget;
     18 
     19 import static android.app.slice.Slice.HINT_LARGE;
     20 import static android.app.slice.Slice.HINT_NO_TINT;
     21 import static android.app.slice.Slice.HINT_TITLE;
     22 import static android.app.slice.SliceItem.FORMAT_ACTION;
     23 import static android.app.slice.SliceItem.FORMAT_IMAGE;
     24 import static android.app.slice.SliceItem.FORMAT_LONG;
     25 import static android.app.slice.SliceItem.FORMAT_SLICE;
     26 import static android.app.slice.SliceItem.FORMAT_TEXT;
     27 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
     28 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
     29 
     30 import static androidx.slice.widget.SliceView.MODE_SMALL;
     31 
     32 import android.app.PendingIntent;
     33 import android.content.Context;
     34 import android.content.res.Resources;
     35 import android.util.AttributeSet;
     36 import android.util.Log;
     37 import android.util.Pair;
     38 import android.util.TypedValue;
     39 import android.view.Gravity;
     40 import android.view.LayoutInflater;
     41 import android.view.View;
     42 import android.view.ViewGroup;
     43 import android.widget.FrameLayout;
     44 import android.widget.ImageView;
     45 import android.widget.ImageView.ScaleType;
     46 import android.widget.LinearLayout;
     47 import android.widget.TextView;
     48 
     49 import androidx.annotation.ColorInt;
     50 import androidx.annotation.RestrictTo;
     51 import androidx.slice.SliceItem;
     52 import androidx.slice.core.SliceQuery;
     53 import androidx.slice.view.R;
     54 
     55 import java.util.ArrayList;
     56 import java.util.Iterator;
     57 import java.util.List;
     58 
     59 /**
     60  * @hide
     61  */
     62 @RestrictTo(RestrictTo.Scope.LIBRARY)
     63 public 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