SliceStyle.java

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.slice.widget;

import static androidx.slice.core.SliceHints.ICON_IMAGE;
import static androidx.slice.core.SliceHints.RAW_IMAGE_LARGE;
import static androidx.slice.core.SliceHints.UNKNOWN_IMAGE;
import static androidx.slice.widget.SliceView.MODE_LARGE;
import static androidx.slice.widget.SliceView.MODE_SMALL;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.SparseArray;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.slice.SliceItem;
import androidx.slice.view.R;

import java.util.ArrayList;
import java.util.List;

/**
 * Holds style information shared between child views of a slice
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(19)
public class SliceStyle {
    private int mTintColor = -1;
    private final int mTitleColor;
    private final int mSubtitleColor;
    private final int mHeaderTitleSize;
    private final int mHeaderSubtitleSize;
    private final int mVerticalHeaderTextPadding;
    private final int mTitleSize;
    private final int mSubtitleSize;
    private final int mVerticalTextPadding;
    private final int mGridTitleSize;
    private final int mGridSubtitleSize;
    private final int mVerticalGridTextPadding;
    private final int mGridTopPadding;
    private final int mGridBottomPadding;

    private final int mRowMaxHeight;
    private final int mRowTextWithRangeHeight;
    private final int mRowSingleTextWithRangeHeight;
    private final int mRowMinHeight;
    private final int mRowRangeHeight;
    private final int mRowSelectionHeight;
    private final int mRowTextWithSelectionHeight;
    private final int mRowSingleTextWithSelectionHeight;
    private final int mRowInlineRangeHeight;

    private final int mGridBigPicMinHeight;
    private final int mGridBigPicMaxHeight;
    private final int mGridAllImagesHeight;
    private final int mGridImageTextHeight;
    private final int mGridRawImageTextHeight;
    private final int mGridMaxHeight;
    private final int mGridMinHeight;

    private final int mListMinScrollHeight;
    private final int mListLargeHeight;

    private final boolean mExpandToAvailableHeight;
    private final boolean mHideHeaderRow;

    private final int mDefaultRowStyleRes;
    private final SparseArray<RowStyle> mResourceToRowStyle = new SparseArray<>();
    private RowStyleFactory mRowStyleFactory;

    private final Context mContext;

    private final float mImageCornerRadius;

    public SliceStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
                defStyleAttr, defStyleRes);
        try {
            int themeColor = a.getColor(R.styleable.SliceView_tintColor, -1);
            mTintColor = themeColor != -1 ? themeColor : mTintColor;
            mTitleColor = a.getColor(R.styleable.SliceView_titleColor, 0);
            mSubtitleColor = a.getColor(R.styleable.SliceView_subtitleColor, 0);

            mHeaderTitleSize = (int) a.getDimension(
                    R.styleable.SliceView_headerTitleSize, 0);
            mHeaderSubtitleSize = (int) a.getDimension(
                    R.styleable.SliceView_headerSubtitleSize, 0);
            mVerticalHeaderTextPadding = (int) a.getDimension(
                    R.styleable.SliceView_headerTextVerticalPadding, 0);

            mTitleSize = (int) a.getDimension(R.styleable.SliceView_titleSize, 0);
            mSubtitleSize = (int) a.getDimension(
                    R.styleable.SliceView_subtitleSize, 0);
            mVerticalTextPadding = (int) a.getDimension(
                    R.styleable.SliceView_textVerticalPadding, 0);

            mGridTitleSize = (int) a.getDimension(R.styleable.SliceView_gridTitleSize, 0);
            mGridSubtitleSize = (int) a.getDimension(
                    R.styleable.SliceView_gridSubtitleSize, 0);
            int defaultVerticalGridPadding = context.getResources().getDimensionPixelSize(
                    R.dimen.abc_slice_grid_text_inner_padding);
            mVerticalGridTextPadding = (int) a.getDimension(
                    R.styleable.SliceView_gridTextVerticalPadding, defaultVerticalGridPadding);
            mGridTopPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
            mGridBottomPadding = (int) a.getDimension(R.styleable.SliceView_gridBottomPadding, 0);

            mDefaultRowStyleRes = a.getResourceId(R.styleable.SliceView_rowStyle, 0);

            int defaultRowMinHeight = context.getResources().getDimensionPixelSize(
                    R.dimen.abc_slice_row_min_height);
            mRowMinHeight = (int) a.getDimension(
                    R.styleable.SliceView_rowMinHeight, defaultRowMinHeight);

            int defaultRowMaxHeight = context.getResources().getDimensionPixelSize(
                    R.dimen.abc_slice_row_max_height);
            mRowMaxHeight = (int) a.getDimension(
                    R.styleable.SliceView_rowMaxHeight, defaultRowMaxHeight);

            int defaultRowRangeHeight = context.getResources().getDimensionPixelSize(
                    R.dimen.abc_slice_row_range_height);
            mRowRangeHeight = (int) a.getDimension(
                    R.styleable.SliceView_rowRangeHeight, defaultRowRangeHeight);

            int defaultRowSingleTextWithRangeHeight = context.getResources().getDimensionPixelSize(
                    R.dimen.abc_slice_row_range_single_text_height);
            mRowSingleTextWithRangeHeight = (int) a.getDimension(
                    R.styleable.SliceView_rowRangeSingleTextHeight,
                    defaultRowSingleTextWithRangeHeight);

            int defaultRowInlineRangeHeight = context.getResources().getDimensionPixelSize(
                    R.dimen.abc_slice_row_range_inline_height);
            mRowInlineRangeHeight = (int) a.getDimension(
                    R.styleable.SliceView_rowInlineRangeHeight, defaultRowInlineRangeHeight);

            mExpandToAvailableHeight = a.getBoolean(
                    R.styleable.SliceView_expandToAvailableHeight, false);

            mHideHeaderRow = a.getBoolean(R.styleable.SliceView_hideHeaderRow, false);

            mContext = context;

            mImageCornerRadius = a.getDimension(R.styleable.SliceView_imageCornerRadius, 0);
        } finally {
            a.recycle();
        }

        // Note: The above colors and dimensions are styleable, but the below ones are not.

        final Resources r = context.getResources();

        mRowTextWithRangeHeight = r.getDimensionPixelSize(
                R.dimen.abc_slice_row_range_multi_text_height);
        mRowSelectionHeight = r.getDimensionPixelSize(R.dimen.abc_slice_row_selection_height);
        mRowTextWithSelectionHeight = r.getDimensionPixelSize(
                R.dimen.abc_slice_row_selection_multi_text_height);
        mRowSingleTextWithSelectionHeight = r.getDimensionPixelSize(
                R.dimen.abc_slice_row_selection_single_text_height);

        mGridBigPicMinHeight = r.getDimensionPixelSize(R.dimen.abc_slice_big_pic_min_height);
        mGridBigPicMaxHeight = r.getDimensionPixelSize(R.dimen.abc_slice_big_pic_max_height);
        mGridAllImagesHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_image_only_height);
        mGridImageTextHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_image_text_height);
        mGridRawImageTextHeight = r.getDimensionPixelSize(
                R.dimen.abc_slice_grid_raw_image_text_offset);
        mGridMinHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_min_height);
        mGridMaxHeight = r.getDimensionPixelSize(R.dimen.abc_slice_grid_max_height);

        mListMinScrollHeight = r.getDimensionPixelSize(R.dimen.abc_slice_row_min_height);
        mListLargeHeight = r.getDimensionPixelSize(R.dimen.abc_slice_large_height);
    }

    public int getRowMinHeight() {
        return mRowMinHeight;
    }

    public int getRowMaxHeight() {
        return mRowMaxHeight;
    }

    public int getRowInlineRangeHeight() {
        return mRowInlineRangeHeight;
    }

    public void setTintColor(int tint) {
        mTintColor = tint;
    }

    public int getTintColor() {
        return mTintColor;
    }

    public int getTitleColor() {
        return mTitleColor;
    }

    public int getSubtitleColor() {
        return mSubtitleColor;
    }

    public int getHeaderTitleSize() {
        return mHeaderTitleSize;
    }

    public int getHeaderSubtitleSize() {
        return mHeaderSubtitleSize;
    }

    public int getVerticalHeaderTextPadding() {
        return mVerticalHeaderTextPadding;
    }

    public int getTitleSize() {
        return mTitleSize;
    }

    public int getSubtitleSize() {
        return mSubtitleSize;
    }

    public int getVerticalTextPadding() {
        return mVerticalTextPadding;
    }

    public int getGridTitleSize() {
        return mGridTitleSize;
    }

    public int getGridSubtitleSize() {
        return mGridSubtitleSize;
    }

    public int getVerticalGridTextPadding() {
        return mVerticalGridTextPadding;
    }

    public int getGridTopPadding() {
        return mGridTopPadding;
    }

    public int getGridBottomPadding() {
        return mGridBottomPadding;
    }

    /**
     * Returns the {@link RowStyle} to use for the given {@link SliceItem}.
     */
    @NonNull
    public RowStyle getRowStyle(@Nullable SliceItem sliceItem) {
        int rowStyleRes = mDefaultRowStyleRes;

        if (sliceItem != null && mRowStyleFactory != null) {
            int maybeStyleRes = mRowStyleFactory.getRowStyleRes(sliceItem);
            if (maybeStyleRes != 0) {
                rowStyleRes = maybeStyleRes;
            }
        }

        if (rowStyleRes == 0) {
            // Return default values.
            return new RowStyle(mContext, this);
        }

        RowStyle rowStyle = mResourceToRowStyle.get(rowStyleRes);
        if (rowStyle == null) {
            rowStyle = new RowStyle(mContext, rowStyleRes, this);
            mResourceToRowStyle.put(rowStyleRes, rowStyle);
        }
        return rowStyle;
    }

    /**
     * Sets the {@link RowStyleFactory} which allows multiple children to have different styles.
     */
    public void setRowStyleFactory(@Nullable RowStyleFactory rowStyleFactory) {
        mRowStyleFactory = rowStyleFactory;
    }

    public int getRowRangeHeight() {
        return mRowRangeHeight;
    }

    public int getRowSelectionHeight() {
        return mRowSelectionHeight;
    }

    public boolean getExpandToAvailableHeight() {
        return mExpandToAvailableHeight;
    }

    public boolean getHideHeaderRow() {
        return mHideHeaderRow;
    }

    public boolean getApplyCornerRadiusToLargeImages() {
        return mImageCornerRadius > 0;
    }

    public float getImageCornerRadius() {
        return mImageCornerRadius;
    }

    public int getRowHeight(RowContent row, SliceViewPolicy policy) {
        int maxHeight = policy.getMaxSmallHeight() > 0 ? policy.getMaxSmallHeight() : mRowMaxHeight;

        if (row.getRange() == null && row.getSelection() == null
                && policy.getMode() != MODE_LARGE) {
            return maxHeight;
        }

        if (row.getRange() != null) {
            // If no StartItem, keep to use original layout.
            if (row.getStartItem() == null) {
                // Range element always has set height and then the height of the text
                // area on the row will vary depending on 0,1 or 2 lines of text.
                int textAreaHeight =
                        row.getLineCount() == 0
                                ? 0
                                : (row.getLineCount() > 1
                                        ? mRowTextWithRangeHeight
                                        : mRowSingleTextWithRangeHeight);
                return textAreaHeight + mRowRangeHeight;
            } else {
                // If has StartItem then Range element is inline, the row height should be more to
                // fit thumb ripple.
                return mRowInlineRangeHeight;
            }
        }

        if (row.getSelection() != null) {
            // Selection element always has set height and then the height of the text
            // area on the row will vary depending on if 1 or 2 lines of text.
            int textAreaHeight = row.getLineCount() > 1 ? mRowTextWithSelectionHeight
                    : mRowSingleTextWithSelectionHeight;
            return textAreaHeight + mRowSelectionHeight;
        }

        return (row.getLineCount() > 1 || row.getIsHeader()) ? maxHeight : mRowMinHeight;
    }

    public int getGridHeight(GridContent grid, SliceViewPolicy policy) {
        boolean isSmall = policy.getMode() == MODE_SMALL;
        if (!grid.isValid()) {
            return 0;
        }
        int largestImageMode = grid.getLargestImageMode();
        int height;
        if (grid.isAllImages()) {
            height = (grid.getGridContent().size() == 1)
                    ? (isSmall
                    ? mGridBigPicMinHeight
                    : mGridBigPicMaxHeight)
                    : (largestImageMode == ICON_IMAGE
                            ? mGridMinHeight
                            : (largestImageMode == RAW_IMAGE_LARGE
                                    ? grid.getFirstImageSize(mContext).y
                                    : mGridAllImagesHeight));
        } else {
            boolean twoLines = grid.getMaxCellLineCount() > 1;
            boolean hasImage = grid.hasImage();
            boolean iconImagesOrNone = largestImageMode == ICON_IMAGE
                    || largestImageMode == UNKNOWN_IMAGE;
            height = largestImageMode == RAW_IMAGE_LARGE
                    ? (grid.getFirstImageSize(mContext).y
                        + (twoLines ? 2 : 1) * mGridRawImageTextHeight)
                    : (twoLines && !isSmall)
                            ? (hasImage
                            ? mGridMaxHeight
                            : mGridMinHeight)
                            : (iconImagesOrNone
                                    ? mGridMinHeight
                                    : mGridImageTextHeight);
        }
        int topPadding = grid.isAllImages() && grid.getRowIndex() == 0
                ? mGridTopPadding : 0;
        int bottomPadding = grid.isAllImages() && grid.getIsLastIndex()
                ? mGridBottomPadding : 0;
        return height + topPadding + bottomPadding;
    }

    public int getListHeight(ListContent list, SliceViewPolicy policy) {
        if (policy.getMode() == MODE_SMALL) {
            return list.getHeader().getHeight(this, policy);
        }
        int maxHeight = policy.getMaxHeight();
        boolean scrollable = policy.isScrollable();

        int desiredHeight = getListItemsHeight(list.getRowItems(), policy);
        if (maxHeight > 0) {
            // Always ensure we're at least the height of our small version.
            int smallHeight = list.getHeader().getHeight(this, policy);
            maxHeight = Math.max(smallHeight, maxHeight);
        }
        int maxLargeHeight = maxHeight > 0
                ? maxHeight
                : mListLargeHeight;
        // Do we have enough content to reasonably scroll in our max?
        boolean bigEnoughToScroll = desiredHeight - maxLargeHeight >= mListMinScrollHeight;

        // Adjust for scrolling
        int height = bigEnoughToScroll && !getExpandToAvailableHeight() ? maxLargeHeight
                : maxHeight <= 0 ? desiredHeight
                : Math.min(maxLargeHeight, desiredHeight);
        if (!scrollable) {
            height = getListItemsHeight(
                getListItemsForNonScrollingList(list, height, policy).getDisplayedItems(),
                policy);
        }
        return height;
    }

    public int getListItemsHeight(List<SliceContent> listItems, SliceViewPolicy policy) {
        if (listItems == null) {
            return 0;
        }

        int height = 0;
        for (int i = 0; i < listItems.size(); i++) {
            SliceContent listItem = listItems.get(i);
            if (i == 0 && shouldSkipFirstListItem(listItems)) {
                continue;
            }
            height += listItem.getHeight(this, policy);
        }
        return height;
    }

    /**
     * Returns a list of items that can fit in the provided height. If this list
     * has a see more item this will be displayed in the list if appropriate.
     *
     * @param list the list from which to source the items.
     * @param availableHeight to use to determine the row items to return.
     * @param policy the policy info (scrolling, mode) to use when determining row items to return.
     *
     * @return the list of items that can be displayed in the provided height.
     */
    @NonNull
    public DisplayedListItems getListItemsForNonScrollingList(ListContent list,
                                                             int availableHeight,
                                                             SliceViewPolicy policy) {
        ArrayList<SliceContent> visibleItems = new ArrayList<>();
        int hiddenItemCount = 0;
        if (list.getRowItems() == null || list.getRowItems().size() == 0) {
            return new DisplayedListItems(visibleItems, hiddenItemCount);
        }

        final boolean skipFirstItem = shouldSkipFirstListItem(list.getRowItems());

        int visibleHeight = 0;
        final int rowCount = list.getRowItems().size();
        for (int i = 0; i < rowCount; i++) {
            SliceContent listItem = list.getRowItems().get(i);
            if (i == 0 && skipFirstItem) {
                continue;
            }
            int itemHeight = listItem.getHeight(this, policy);
            if (availableHeight > 0 && visibleHeight + itemHeight > availableHeight) {
                hiddenItemCount = rowCount - i;
                break;
            } else {
                visibleHeight += itemHeight;
                visibleItems.add(listItem);
            }
        }


        // Only add see more if we're at least showing one item and it's not the header.
        final int minItemCountForSeeMore = skipFirstItem ? 1 : 2;
        if (list.getSeeMoreItem() != null && visibleItems.size() >= minItemCountForSeeMore
                && hiddenItemCount > 0) {
            // Need to show see more
            int seeMoreHeight = list.getSeeMoreItem().getHeight(this, policy);
            visibleHeight += seeMoreHeight;

            // Free enough vertical space to fit the see more item.
            while (visibleHeight > availableHeight
                    && visibleItems.size() >= minItemCountForSeeMore) {
                int lastIndex = visibleItems.size() - 1;
                SliceContent lastItem = visibleItems.get(lastIndex);
                visibleHeight -= lastItem.getHeight(this, policy);
                visibleItems.remove(lastIndex);
                hiddenItemCount++;
            }

            if (visibleItems.size() >= minItemCountForSeeMore) {
                visibleItems.add(list.getSeeMoreItem());
            } else {
                // Not possible to free enough vertical space. We'll show only the header.
                visibleHeight -= seeMoreHeight;
            }
        }
        if (visibleItems.size() == 0) {
            // Didn't have enough space to show anything; should still show something
            visibleItems.add(list.getRowItems().get(0));
        }
        return new DisplayedListItems(visibleItems, hiddenItemCount);
    }

    /**
     * Returns a list of items that should be displayed to the user.
     *
     * @param list the list from which to source the items.
     */
    @NonNull
    public List<SliceContent> getListItemsToDisplay(@NonNull ListContent list) {
        List<SliceContent> rowItems = list.getRowItems();
        if (rowItems.size() > 0 && shouldSkipFirstListItem(rowItems)) {
            return rowItems.subList(1, rowItems.size());
        }
        return rowItems;
    }

    /** Returns true if the first item of a list should be skipped. */
    private boolean shouldSkipFirstListItem(List<SliceContent> rowItems) {
        // Hide header row if requested, but only if there is at least one non-header row.
        return getHideHeaderRow() && rowItems.size() > 1 && rowItems.get(0) instanceof RowContent
                && ((RowContent) rowItems.get(0)).getIsHeader();
    }

}