java.lang.Object
↳ViewGroup
↳androidx.slice.widget.SliceView
Gradle dependencies
compile group: 'androidx.slice', name: 'slice-view', version: '1.1.0-alpha02'
- groupId: androidx.slice
- artifactId: slice-view
- version: 1.1.0-alpha02
Artifact androidx.slice:slice-view:1.1.0-alpha02 it located at Google repository (https://maven.google.com/)
Androidx artifact mapping:
androidx.slice:slice-view com.android.support:slices-view
Overview
A view for displaying Slices.
A slice is a piece of app content and actions that can be surfaced outside of the app it
originates from. SliceView is able to interpret the structure and contents of a slice and display
it. This structure is defined by the app providing the slice when the slice is constructed with a
TemplateSliceBuilder.
SliceView is able to display slices in a couple of different modes via SliceView.setMode(int).
- Small: The small format has a restricted height and display top-level information
and actions from the slice.
- Large: The large format displays as much of the slice as it can based on the space
provided for SliceView, if the slice overflows the space SliceView will scroll the content if
scrolling has been enabled on SliceView, SliceView.setScrollable(boolean).
- Shortcut: A shortcut shows minimal information and is presented as a tappable icon
representing the main content or action associated with the slice.
Slices can contain dynamic content that may update due to user interaction or a change in the
data being displayed in the slice. SliceView can be configured to listen for these updates easily
using SliceLiveData. Example usage:
SliceView v = new SliceView(getContext());
v.setMode(desiredMode);
LiveData liveData = SliceLiveData.fromUri(sliceUri);
liveData.observe(lifecycleOwner, v);
SliceView supports various style options, see .
Summary
Fields |
---|
public static final int | MODE_LARGE Mode indicating this slice should be presented in large format, as much or all of the slice
contents are shown. |
public static final int | MODE_SHORTCUT Mode indicating this slice should be presented as a tappable icon. |
public static final int | MODE_SMALL Mode indicating this slice should be presented in small format, only top-level information
and actions from the slice are shown. |
public static final java.util.Comparator<SliceAction> | SLICE_ACTION_PRIORITY_COMPARATOR |
Constructors |
---|
public | SliceView(Context context)
|
public | SliceView(Context context, AttributeSet attrs)
|
public | SliceView(Context context, AttributeSet attrs, int defStyleAttr)
|
public | SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
|
Methods |
---|
protected void | configureViewPolicy(int maxHeight)
Sets the maximum height for a slice through the view policy. |
public int | getHiddenItemCount()
Returns the number of slice items not displayed in this view. |
public int | getMode()
|
public Slice | getSlice()
|
public java.util.List<SliceAction> | getSliceActions()
Returns the slice actions presented in this view. |
public boolean | isScrollable()
Whether this view allow scrollable content when presenting in SliceView.MODE_LARGE. |
public boolean | isShowingActionRow()
|
public boolean | isSliceViewClickable()
Indicates whether this view reacts to click events or not. |
public static java.lang.String | modeToString(int mode)
|
protected void | onAttachedToWindow()
|
public void | onChanged(Slice slice)
|
public void | onClick(View v)
|
protected void | onDetachedFromWindow()
|
public boolean | onInterceptTouchEvent(MotionEvent ev)
|
protected void | onLayout(boolean changed, int l, int t, int r, int b)
|
protected void | onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
public boolean | onTouchEvent(MotionEvent ev)
|
protected void | onVisibilityChanged(View changedView, int visibility)
|
protected void | onWindowVisibilityChanged(int visibility)
|
public void | setAccentColor(int accentColor)
Contents of a slice such as icons, text, and controls (e.g. |
public void | setClickInfo(int[] info[])
Sets the event info for logging a click. |
public void | setMode(int mode)
Set the mode this view should present in. |
public void | setMode(int mode, boolean animate)
|
public void | setOnClickListener(View.OnClickListener listener)
|
public void | setOnLongClickListener(View.OnLongClickListener listener)
|
public void | setOnSliceActionListener(SliceView.OnSliceActionListener observer)
Sets the listener to notify when an interaction event occurs on the view. |
public void | setRowStyleFactory(RowStyleFactory rowStyleFactory)
Sets the RowStyleFactory which allows multiple children to have different styles. |
public void | setScrollable(boolean isScrollable)
Set whether this view should allow scrollable content when presenting in SliceView.MODE_LARGE. |
public void | setShowActionDividers(boolean enabled)
Whether this view should show action dividers for rows. |
public void | setShowActionRow(boolean show)
|
public void | setShowHeaderDivider(boolean enabled)
Whether this view should show the header divider. |
public void | setShowTitleItems(boolean enabled)
Whether this view should show title items on the first row of the slice. |
public void | setSlice(Slice slice)
Populates this view to the provided Slice. |
public void | setSliceActions(java.util.List<SliceAction> newActions)
Sets the slice actions to display for the slice contained in this view. |
public void | showActionDividers(boolean enabled)
|
public void | showHeaderDivider(boolean enabled)
|
public void | showTitleItems(boolean enabled)
|
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final int
MODE_SMALLMode indicating this slice should be presented in small format, only top-level information
and actions from the slice are shown.
public static final int
MODE_LARGEMode indicating this slice should be presented in large format, as much or all of the slice
contents are shown.
public static final int
MODE_SHORTCUTMode indicating this slice should be presented as a tappable icon.
public static final java.util.Comparator<SliceAction>
SLICE_ACTION_PRIORITY_COMPARATORConstructors
public
SliceView(Context context)
public
SliceView(Context context, AttributeSet attrs)
public
SliceView(Context context, AttributeSet attrs, int defStyleAttr)
public
SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
Methods
public boolean
isSliceViewClickable()
Indicates whether this view reacts to click events or not.
public void
setClickInfo(int[] info[])
Sets the event info for logging a click.
public void
onClick(View v)
public void
setOnClickListener(View.OnClickListener listener)
public void
setOnLongClickListener(View.OnLongClickListener listener)
public boolean
onInterceptTouchEvent(MotionEvent ev)
public boolean
onTouchEvent(MotionEvent ev)
protected void
configureViewPolicy(int maxHeight)
Sets the maximum height for a slice through the view policy.
protected void
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected void
onLayout(boolean changed, int l, int t, int r, int b)
public void
onChanged(
Slice slice)
public void
setSlice(
Slice slice)
Populates this view to the provided Slice.
This will not update automatically if the slice content changes, for live
content see SliceLiveData.
Returns:
the slice being used to populate this view.
public java.util.List<SliceAction>
getSliceActions()
Returns the slice actions presented in this view.
Note that these may be different from SliceMetadata.getSliceActions() if the actions
set on the view have been adjusted using SliceView.setSliceActions(List).
public void
setSliceActions(java.util.List<SliceAction> newActions)
Sets the slice actions to display for the slice contained in this view. Normally SliceView
will automatically show actions, however, it is possible to reorder or omit actions on the
view using this method. This is generally discouraged.
It is required that the slice be set on this view before actions can be set, otherwise
this will throw java.lang.IllegalStateException
. If any of the actions supplied are not
available for the slice set on this view (i.e. the action is not returned by
SliceMetadata.getSliceActions() this will throw java.lang.IllegalArgumentException
.
public void
setMode(int mode)
Set the mode this view should present in.
public void
setScrollable(boolean isScrollable)
Set whether this view should allow scrollable content when presenting in SliceView.MODE_LARGE.
public boolean
isScrollable()
Whether this view allow scrollable content when presenting in SliceView.MODE_LARGE.
Sets the listener to notify when an interaction event occurs on the view.
See also: EventInfo
public void
setAccentColor(int accentColor)
Contents of a slice such as icons, text, and controls (e.g. toggle) can be tinted. Normally
a color for tinting will be provided by the slice. Using this method will override
the slice-provided color information and instead tint elements with the color set here.
Parameters:
accentColor: the color to use for tinting contents of this view.
Sets the RowStyleFactory which allows multiple children to have different styles.
public void
setMode(int mode, boolean animate)
Returns:
the mode this view is presenting in.
public void
setShowTitleItems(boolean enabled)
Whether this view should show title items on the first row of the slice.
Title items appear at the start of the row.
public void
showTitleItems(boolean enabled)
Deprecated: TO BE REMOVED
public void
setShowHeaderDivider(boolean enabled)
Whether this view should show the header divider.
public void
showHeaderDivider(boolean enabled)
Deprecated: TO BE REMOVED
public void
setShowActionDividers(boolean enabled)
Whether this view should show action dividers for rows.
public void
showActionDividers(boolean enabled)
Deprecated: TO BE REMOVED
public void
setShowActionRow(boolean show)
public boolean
isShowingActionRow()
Returns:
whether this view is showing a row of actions.
public int
getHiddenItemCount()
Returns the number of slice items not displayed in this view.
public static java.lang.String
modeToString(int mode)
Returns:
String representation of the provided mode.
protected void
onAttachedToWindow()
protected void
onDetachedFromWindow()
protected void
onVisibilityChanged(View changedView, int visibility)
protected void
onWindowVisibilityChanged(int visibility)
Source
/*
* Copyright 2017 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 android.app.slice.Slice.SUBTYPE_COLOR;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.UNSPECIFIED;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Observer;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.SliceMetadata;
import androidx.slice.core.SliceAction;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
/**
* A view for displaying {@link Slice}s.
* <p>
* A slice is a piece of app content and actions that can be surfaced outside of the app it
* originates from. SliceView is able to interpret the structure and contents of a slice and display
* it. This structure is defined by the app providing the slice when the slice is constructed with a
* {@link androidx.slice.builders.TemplateSliceBuilder}.
* </p>
* <p>
* SliceView is able to display slices in a couple of different modes via {@see #setMode}.
* <ul>
* <li><b>Small</b>: The small format has a restricted height and display top-level information
* and actions from the slice.</li>
* <li><b>Large</b>: The large format displays as much of the slice as it can based on the space
* provided for SliceView, if the slice overflows the space SliceView will scroll the content if
* scrolling has been enabled on SliceView, {@see #setScrollable}.</li>
* <li><b>Shortcut</b>: A shortcut shows minimal information and is presented as a tappable icon
* representing the main content or action associated with the slice.</li>
* </ul>
* </p>
* <p>
* Slices can contain dynamic content that may update due to user interaction or a change in the
* data being displayed in the slice. SliceView can be configured to listen for these updates easily
* using {@link SliceLiveData}. Example usage:
* <pre class="prettyprint">
* SliceView v = new SliceView(getContext());
* v.setMode(desiredMode);
* LiveData<Slice> liveData = SliceLiveData.fromUri(sliceUri);
* liveData.observe(lifecycleOwner, v);
* </pre>
* </p>
* <p>
* SliceView supports various style options, see {@link R.styleable#SliceView SliceView Attributes}.
*
* @see Slice
* @see SliceLiveData
*/
@RequiresApi(19)
public class SliceView extends ViewGroup implements Observer<Slice>, View.OnClickListener {
private static final String TAG = "SliceView";
/**
* Implement this interface to be notified of interactions with the slice displayed
* in this view.
* @see EventInfo
*/
public interface OnSliceActionListener {
/**
* Called when an interaction has occurred with an element in this view.
* @param info the type of event that occurred.
* @param item the specific item within the {@link Slice} that was interacted with.
*/
void onSliceAction(@NonNull EventInfo info, @NonNull SliceItem item);
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@IntDef({
MODE_SMALL, MODE_LARGE, MODE_SHORTCUT
})
@Retention(RetentionPolicy.SOURCE)
public @interface SliceMode {}
/**
* Mode indicating this slice should be presented in small format, only top-level information
* and actions from the slice are shown.
*/
public static final int MODE_SMALL = 1;
/**
* Mode indicating this slice should be presented in large format, as much or all of the slice
* contents are shown.
*/
public static final int MODE_LARGE = 2;
/**
* Mode indicating this slice should be presented as a tappable icon.
*/
public static final int MODE_SHORTCUT = 3;
/**
* Refresh last updated label every 60 seconds when the slice is visible.
*/
private static final int REFRESH_LAST_UPDATED_IN_MILLIS = 60000;
@SuppressWarnings("WeakerAccess") /* synthetic access */
ListContent mListContent;
@SuppressWarnings("WeakerAccess") /* synthetic access */
SliceChildView mCurrentView;
@SuppressWarnings("WeakerAccess") /* synthetic access */
View.OnLongClickListener mLongClickListener;
@SuppressWarnings("WeakerAccess") /* synthetic access */
Handler mHandler;
@SuppressWarnings("WeakerAccess") /* synthetic access */
SliceMetadata mSliceMetadata;
private Slice mCurrentSlice;
private SliceMetrics mCurrentSliceMetrics;
private List<SliceAction> mActions;
private ActionRow mActionRow;
private boolean mShowActions = false;
private boolean mShowLastUpdated = true;
private boolean mCurrentSliceLoggedVisible = false;
private boolean mShowTitleItems = false;
private boolean mShowHeaderDivider = false;
private boolean mShowActionDividers = false;
private int mShortcutSize;
private int mMinTemplateHeight;
private int mLargeHeight;
private int mActionRowHeight;
private SliceViewPolicy mViewPolicy;
private SliceStyle mSliceStyle;
private int mThemeTintColor = -1;
private OnSliceActionListener mSliceObserver;
private int mTouchSlopSquared;
private View.OnClickListener mOnClickListener;
private int mDownX;
private int mDownY;
boolean mPressing;
boolean mInLongpress;
int[] mClickInfo;
public SliceView(Context context) {
this(context, null);
}
public SliceView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.sliceViewStyle);
}
public SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, R.style.Widget_SliceView);
}
@RequiresApi(21)
public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
}
@SuppressWarnings("deprecation")
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mSliceStyle = new SliceStyle(context, attrs, defStyleAttr, defStyleRes);
mThemeTintColor = mSliceStyle.getTintColor();
mShortcutSize = getContext().getResources()
.getDimensionPixelSize(R.dimen.abc_slice_shortcut_size);
mMinTemplateHeight = getContext().getResources()
.getDimensionPixelSize(R.dimen.abc_slice_row_min_height);
mLargeHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_large_height);
mActionRowHeight = getResources().getDimensionPixelSize(
R.dimen.abc_slice_action_row_height);
mViewPolicy = new SliceViewPolicy();
mCurrentView = new TemplateView(getContext());
mCurrentView.setPolicy(mViewPolicy);
addView(mCurrentView, getChildLp(mCurrentView));
applyConfigurations();
// TODO: action row background should support light / dark / maybe presenter customization
mActionRow = new ActionRow(getContext(), true);
mActionRow.setBackground(new ColorDrawable(0xffeeeeee));
addView(mActionRow, getChildLp(mActionRow));
updateActions();
final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mTouchSlopSquared = slop * slop;
mHandler = new Handler();
setClipToPadding(false);
super.setOnClickListener(this);
}
@VisibleForTesting
void setSliceViewPolicy(SliceViewPolicy policy) {
mViewPolicy = policy;
}
/**
* Indicates whether this view reacts to click events or not.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public boolean isSliceViewClickable() {
return mOnClickListener != null
|| (mListContent != null && mListContent.getShortcut(getContext()) != null);
}
/**
* Sets the event info for logging a click.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void setClickInfo(int[] info) {
mClickInfo = info;
}
@Override
public void onClick(View v) {
if (mListContent != null && mListContent.getShortcut(getContext()) != null) {
try {
SliceActionImpl sa = (SliceActionImpl) mListContent.getShortcut(getContext());
SliceItem actionItem = sa.getActionItem();
boolean loading = actionItem != null
&& actionItem.fireActionInternal(getContext(), null);
if (loading) {
mCurrentView.setActionLoading(sa.getSliceItem());
}
if (actionItem != null && mSliceObserver != null && mClickInfo != null
&& mClickInfo.length > 1) {
EventInfo eventInfo = new EventInfo(getMode(),
EventInfo.ACTION_TYPE_CONTENT, mClickInfo[0], mClickInfo[1]);
mSliceObserver.onSliceAction(eventInfo, sa.getSliceItem());
logSliceMetricsOnTouch(sa.getSliceItem(), eventInfo);
}
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
} else if (mOnClickListener != null) {
mOnClickListener.onClick(this);
}
}
@Override
public void setOnClickListener(View.OnClickListener listener) {
mOnClickListener = listener;
}
@Override
public void setOnLongClickListener(View.OnLongClickListener listener) {
super.setOnLongClickListener(listener);
mLongClickListener = listener;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return (mLongClickListener != null && handleTouchForLongpress(ev))
|| super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return (mLongClickListener != null && handleTouchForLongpress(ev))
|| super.onTouchEvent(ev);
}
private boolean handleTouchForLongpress(MotionEvent ev) {
int action = ev.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
mHandler.removeCallbacks(mLongpressCheck);
mDownX = (int) ev.getRawX();
mDownY = (int) ev.getRawY();
mPressing = true;
mInLongpress = false;
mHandler.postDelayed(mLongpressCheck, ViewConfiguration.getLongPressTimeout());
return false;
case MotionEvent.ACTION_MOVE:
final int deltaX = (int) ev.getRawX() - mDownX;
final int deltaY = (int) ev.getRawY() - mDownY;
int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance > mTouchSlopSquared) {
mPressing = false;
mHandler.removeCallbacks(mLongpressCheck);
}
// If a long press has already happened, consume further movement.
return mInLongpress;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
boolean wasInLongpress = mInLongpress;
mPressing = false;
mInLongpress = false;
mHandler.removeCallbacks(mLongpressCheck);
// If a long press just happened, consume up event to avoid a duplicate short click.
return wasInLongpress;
default:
return false;
}
}
/**
* Sets the maximum height for a slice through the view policy.
*/
protected void configureViewPolicy(int maxHeight) {
if (mListContent != null && mListContent.isValid() && getMode() != MODE_SHORTCUT) {
if (maxHeight > 0 && maxHeight < mSliceStyle.getRowMaxHeight()) {
if (maxHeight <= mMinTemplateHeight) {
maxHeight = mMinTemplateHeight;
}
mViewPolicy.setMaxSmallHeight(maxHeight);
} else {
mViewPolicy.setMaxSmallHeight(0);
}
mViewPolicy.setMaxHeight(maxHeight);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
if (MODE_SHORTCUT == getMode()) {
// TODO: consider scaling the shortcut to fit if too small
width = mShortcutSize + getPaddingLeft() + getPaddingRight();
}
final int actionHeight = mActionRow.getVisibility() != View.GONE
? mActionRowHeight
: 0;
final int heightAvailable = MeasureSpec.getSize(heightMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
LayoutParams lp = getLayoutParams();
final int maxHeight = (lp != null && lp.height == LayoutParams.WRAP_CONTENT)
|| heightMode == UNSPECIFIED
? -1 // no max, be default sizes
: heightAvailable;
configureViewPolicy(maxHeight);
// Remove the padding from our available height
int childrenHeight = heightAvailable - getPaddingTop() - getPaddingBottom();
// never change the height if set to exactly
if (heightMode != EXACTLY) {
if (mListContent == null || !mListContent.isValid()) {
childrenHeight = actionHeight;
} else if (getMode() == MODE_SHORTCUT) {
// No compromise in case of shortcut
childrenHeight = mShortcutSize + actionHeight;
} else {
int requiredHeight =
mListContent.getHeight(mSliceStyle, mViewPolicy) + actionHeight;
if (childrenHeight > requiredHeight || heightMode == UNSPECIFIED) {
// Available space is larger than what the slice wants
childrenHeight = requiredHeight;
} else {
// Not enough space available for slice in current mode
if (mSliceStyle.getExpandToAvailableHeight()) {
// Don't request more space than we're allowed to have.
requiredHeight = childrenHeight;
} else if (getMode() == MODE_LARGE
&& childrenHeight >= mLargeHeight + actionHeight) {
childrenHeight = mLargeHeight + actionHeight;
} else if (childrenHeight <= mMinTemplateHeight) {
childrenHeight = mMinTemplateHeight;
}
}
}
}
// Measure directly instead of calling measureChild as the later substracts padding
// from the provided size
int childWidthSpec = makeMeasureSpec(width, EXACTLY);
int actionRowHeight = actionHeight > 0 ? (actionHeight + getPaddingBottom()) : 0;
mActionRow.measure(childWidthSpec, makeMeasureSpec(actionRowHeight, EXACTLY));
// Include the bottom padding for currentView only if action row is invisible
int currentViewHeight = childrenHeight + getPaddingTop()
+ (actionHeight > 0 ? 0 : getPaddingBottom());
mCurrentView.measure(childWidthSpec, makeMeasureSpec(currentViewHeight, EXACTLY));
setMeasuredDimension(width,
mCurrentView.getMeasuredHeight() + mActionRow.getMeasuredHeight());
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View v = mCurrentView;
v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
if (mActionRow.getVisibility() != View.GONE) {
int top = v.getMeasuredHeight();
mActionRow.layout(0, top, mActionRow.getMeasuredWidth(),
mActionRow.getMeasuredHeight() + top);
}
}
@Override
public void onChanged(@Nullable Slice slice) {
setSlice(slice);
}
/**
* Populates this view to the provided {@link Slice}.
*
* This will not update automatically if the slice content changes, for live
* content see {@link SliceLiveData}.
*/
public void setSlice(@Nullable Slice slice) {
LocationBasedViewTracker.trackInputFocused(this);
LocationBasedViewTracker.trackA11yFocus(this);
initSliceMetrics(slice);
boolean isUpdate = slice != null && mCurrentSlice != null
&& slice.getUri().equals(mCurrentSlice.getUri());
SliceMetadata oldSliceData = mSliceMetadata;
mCurrentSlice = slice;
mSliceMetadata = mCurrentSlice != null ? SliceMetadata.from(getContext(), mCurrentSlice)
: null;
if (isUpdate) {
// If its an update check the loading state
SliceMetadata newSliceData = mSliceMetadata;
if (oldSliceData.getLoadingState() == SliceMetadata.LOADED_ALL
&& newSliceData.getLoadingState() == SliceMetadata.LOADED_NONE) {
// If it's the same slice going from "loaded all" to "loaded none"... let's
// ignore the update.
return;
}
} else {
mCurrentView.resetView();
}
mListContent = mSliceMetadata != null ? mSliceMetadata.getListContent() : null;
if (mShowTitleItems) {
showTitleItems(true);
}
if (mShowHeaderDivider) {
showHeaderDivider(true);
}
if (mShowActionDividers) {
showActionDividers(true);
}
if (mListContent == null || !mListContent.isValid()) {
mActions = null;
mCurrentView.resetView();
updateActions();
return;
}
// New slice means we shouldn't have any actions loading
mCurrentView.setLoadingActions(null);
// Check if the slice content is expired and show when it was last updated
mActions = mSliceMetadata.getSliceActions();
mCurrentView.setLastUpdated(mSliceMetadata.getLastUpdatedTime());
mCurrentView.setShowLastUpdated(mShowLastUpdated && mSliceMetadata.isExpired());
mCurrentView.setAllowTwoLines(mSliceMetadata.isPermissionSlice());
// Tint color can come with the slice, so may need to update it
mCurrentView.setTint(getTintColor());
if (mListContent.getLayoutDir() != -1) {
mCurrentView.setLayoutDirection(mListContent.getLayoutDir());
} else {
mCurrentView.setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
}
// Set the slice
mCurrentView.setSliceContent(mListContent);
updateActions();
// Log slice metrics visible.
logSliceMetricsVisibilityChange(true /* visible */);
// Automatically refresh the last updated label when the slice TTL isn't infinity.
refreshLastUpdatedLabel(true /* visible */);
}
/**
* @return the slice being used to populate this view.
*/
@Nullable
public Slice getSlice() {
return mCurrentSlice;
}
/**
* Returns the slice actions presented in this view.
* <p>
* Note that these may be different from {@link SliceMetadata#getSliceActions()} if the actions
* set on the view have been adjusted using {@link #setSliceActions(List)}.
*/
@Nullable
public List<SliceAction> getSliceActions() {
if (mActions != null && mActions.isEmpty()) {
// They're empty because presenter set null slice actions, return null to be consistent.
return null;
}
return mActions;
}
/**
* Sets the slice actions to display for the slice contained in this view. Normally SliceView
* will automatically show actions, however, it is possible to reorder or omit actions on the
* view using this method. This is generally discouraged.
* <p>
* It is required that the slice be set on this view before actions can be set, otherwise
* this will throw {@link IllegalStateException}. If any of the actions supplied are not
* available for the slice set on this view (i.e. the action is not returned by
* {@link SliceMetadata#getSliceActions()} this will throw {@link IllegalArgumentException}.
*/
public void setSliceActions(@Nullable List<SliceAction> newActions) {
// Check that these actions are part of available set
if (mCurrentSlice == null || mSliceMetadata == null) {
throw new IllegalStateException("Trying to set actions on a view without a slice");
}
List<SliceAction> availableActions = mSliceMetadata.getSliceActions();
if (availableActions != null && newActions != null) {
for (int i = 0; i < newActions.size(); i++) {
if (!availableActions.contains(newActions.get(i))) {
throw new IllegalArgumentException(
"Trying to set an action that isn't available: " + newActions.get(i));
}
}
}
mActions = newActions == null ? new ArrayList<SliceAction>() : newActions;
updateActions();
}
/**
* Set the mode this view should present in.
*/
public void setMode(@SliceMode int mode) {
setMode(mode, false /* animate */);
}
/**
* Set whether this view should allow scrollable content when presenting in {@link #MODE_LARGE}.
*/
public void setScrollable(boolean isScrollable) {
if (isScrollable != mViewPolicy.isScrollable()) {
mViewPolicy.setScrollable(isScrollable);
}
}
/**
* Whether this view allow scrollable content when presenting in {@link #MODE_LARGE}.
*/
public boolean isScrollable() {
return mViewPolicy.isScrollable();
}
/**
* Sets the listener to notify when an interaction event occurs on the view.
* @see EventInfo
*/
public void setOnSliceActionListener(@Nullable OnSliceActionListener observer) {
mSliceObserver = observer;
mCurrentView.setSliceActionListener(mSliceObserver);
}
/**
* Contents of a slice such as icons, text, and controls (e.g. toggle) can be tinted. Normally
* a color for tinting will be provided by the slice. Using this method will override
* the slice-provided color information and instead tint elements with the color set here.
*
* @param accentColor the color to use for tinting contents of this view.
*/
public void setAccentColor(@ColorInt int accentColor) {
mThemeTintColor = accentColor;
mSliceStyle.setTintColor(mThemeTintColor);
mCurrentView.setTint(getTintColor());
}
/**
* Sets the {@link RowStyleFactory} which allows multiple children to have different styles.
*/
public void setRowStyleFactory(@Nullable RowStyleFactory rowStyleFactory) {
mSliceStyle.setRowStyleFactory(rowStyleFactory);
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void setMode(@SliceMode int mode, boolean animate) {
if (animate) {
Log.e(TAG, "Animation not supported yet");
}
if (mViewPolicy.getMode() == mode) {
return;
}
if (mode != MODE_SMALL && mode != MODE_LARGE && mode != MODE_SHORTCUT) {
Log.w(TAG, "Unknown mode: " + mode
+ " please use one of MODE_SHORTCUT, MODE_SMALL, MODE_LARGE");
mode = MODE_LARGE;
}
mViewPolicy.setMode(mode);
updateViewConfig();
}
/**
* @return the mode this view is presenting in.
*/
public @SliceMode int getMode() {
return mViewPolicy.getMode();
}
/**
* Whether this view should show title items on the first row of the slice.
* Title items appear at the start of the row.
*/
public void setShowTitleItems(boolean enabled) {
mShowTitleItems = enabled;
if (mListContent != null) {
mListContent.showTitleItems(enabled);
}
}
/**
* @deprecated TO BE REMOVED
* @removed
*/
@Deprecated
public void showTitleItems(boolean enabled) {
setShowTitleItems(enabled);
}
/**
* Whether this view should show the header divider.
*/
public void setShowHeaderDivider(boolean enabled) {
mShowHeaderDivider = enabled;
if (mListContent != null) {
mListContent.showHeaderDivider(enabled);
}
}
/**
* @deprecated TO BE REMOVED
* @removed
*/
@Deprecated
public void showHeaderDivider(boolean enabled) {
setShowHeaderDivider(enabled);
}
/**
* Whether this view should show action dividers for rows.
*/
public void setShowActionDividers(boolean enabled) {
mShowActionDividers = enabled;
if (mListContent != null) {
mListContent.showActionDividers(enabled);
}
}
/**
* @deprecated TO BE REMOVED
* @removed
*/
@Deprecated
public void showActionDividers(boolean enabled) {
setShowActionDividers(enabled);
}
/**
* @hide
*
* Whether this view should show a row of actions with it.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void setShowActionRow(boolean show) {
mShowActions = show;
updateActions();
}
/**
* @return whether this view is showing a row of actions.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public boolean isShowingActionRow() {
return mShowActions;
}
/**
* Returns the number of slice items not displayed in this view.
*
* <ul>
* <li> In {@link #MODE_LARGE}: If the slice is not scrollable, this is the number
* of slice items that don't fit into the available height. If it is scrollable, this method
* returns 0.</li>
* <li> In {@link #MODE_SMALL}: Returns the number of items not presented to the user.</li>
* <li> In {@link #MODE_SHORTCUT}: Returns 0.</li>
* </ul>
*/
public int getHiddenItemCount() {
return mCurrentView.getHiddenItemCount();
}
/**
* Updates the current view to represent the correct type of view for the current mode.
* If the view is changed for the mode any configurations are also applied to it.
*/
private void updateViewConfig() {
boolean newView = false;
// Check if our view is right for the current mode
int mode = getMode();
boolean isCurrentViewShortcut = mCurrentView instanceof ShortcutView;
Set<SliceItem> loadingActions = mCurrentView.getLoadingActions();
if (mode == MODE_SHORTCUT && !isCurrentViewShortcut) {
removeView(mCurrentView);
mCurrentView = new ShortcutView(getContext());
addView(mCurrentView, getChildLp(mCurrentView));
newView = true;
} else if (mode != MODE_SHORTCUT && isCurrentViewShortcut) {
removeView(mCurrentView);
mCurrentView = new TemplateView(getContext());
addView(mCurrentView, getChildLp(mCurrentView));
newView = true;
}
// If the view changes we should apply any configurations to it
if (newView) {
mCurrentView.setPolicy(mViewPolicy);
applyConfigurations();
if (mListContent != null && mListContent.isValid()) {
mCurrentView.setSliceContent(mListContent);
}
mCurrentView.setLoadingActions(loadingActions);
}
updateActions();
}
private void applyConfigurations() {
mCurrentView.setSliceActionListener(mSliceObserver);
mCurrentView.setStyle(mSliceStyle, mSliceStyle.getRowStyle(/* sliceItem= */ null));
mCurrentView.setTint(getTintColor());
if (mListContent != null && mListContent.getLayoutDir() != -1) {
mCurrentView.setLayoutDirection(mListContent.getLayoutDir());
} else {
mCurrentView.setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
}
}
private void updateActions() {
if (mActions == null) {
// No actions, hide the row, clear out the view
mActionRow.setVisibility(View.GONE);
mCurrentView.setSliceActions(null);
mCurrentView.setInsets(getPaddingStart(), getPaddingTop(), getPaddingEnd(),
getPaddingBottom());
return;
}
// Sort actions based on priority and set them in action rows.
List<SliceAction> sortedActions = new ArrayList<>(mActions);
Collections.sort(sortedActions, SLICE_ACTION_PRIORITY_COMPARATOR);
if (mShowActions && getMode() != MODE_SHORTCUT && mActions.size() >= 2) {
// Show in action row if available
mActionRow.setActions(sortedActions, getTintColor());
mActionRow.setVisibility(View.VISIBLE);
// Hide them on the template
mCurrentView.setSliceActions(null);
mCurrentView.setInsets(getPaddingStart(), getPaddingTop(), getPaddingEnd(), 0);
mActionRow.setPaddingRelative(getPaddingStart(), 0, getPaddingEnd(),
getPaddingBottom());
} else {
// Otherwise set them on the template
mCurrentView.setSliceActions(sortedActions);
mCurrentView.setInsets(getPaddingStart(), getPaddingTop(), getPaddingEnd(),
getPaddingBottom());
mActionRow.setVisibility(View.GONE);
}
}
private int getTintColor() {
if (mThemeTintColor != -1) {
// Theme has specified a color, use that
return mThemeTintColor;
} else {
final SliceItem colorItem = SliceQuery.findSubtype(
mCurrentSlice, FORMAT_INT, SUBTYPE_COLOR);
return colorItem != null
? colorItem.getInt()
: SliceViewUtil.getColorAccent(getContext());
}
}
private LayoutParams getChildLp(View child) {
if (child instanceof ShortcutView) {
return new LayoutParams(mShortcutSize, mShortcutSize);
} else {
return new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
}
/**
* @return String representation of the provided mode.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static String modeToString(@SliceMode int mode) {
switch(mode) {
case MODE_SHORTCUT:
return "MODE SHORTCUT";
case MODE_SMALL:
return "MODE SMALL";
case MODE_LARGE:
return "MODE LARGE";
default:
return "unknown mode: " + mode;
}
}
Runnable mLongpressCheck = new Runnable() {
@Override
public void run() {
if (mPressing && mLongClickListener != null) {
mInLongpress = true;
mLongClickListener.onLongClick(SliceView.this);
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
}
};
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (isShown()) {
logSliceMetricsVisibilityChange(true /* visible */);
refreshLastUpdatedLabel(true /* visible */);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
logSliceMetricsVisibilityChange(false /* not visible */);
refreshLastUpdatedLabel(false /* not visible */);
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (isAttachedToWindow()) {
logSliceMetricsVisibilityChange(visibility == VISIBLE);
refreshLastUpdatedLabel(visibility == VISIBLE);
}
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
logSliceMetricsVisibilityChange(visibility == VISIBLE);
refreshLastUpdatedLabel(visibility == VISIBLE);
}
private void initSliceMetrics(@Nullable Slice slice) {
if (slice == null || slice.getUri() == null) {
logSliceMetricsVisibilityChange(false /* not visible */);
mCurrentSliceMetrics = null;
} else if (mCurrentSlice == null || !mCurrentSlice.getUri().equals(slice.getUri())) {
logSliceMetricsVisibilityChange(false /* not visible */);
mCurrentSliceMetrics =
SliceMetrics.getInstance(getContext(), slice.getUri());
}
}
private void logSliceMetricsVisibilityChange(boolean visibility) {
if (mCurrentSliceMetrics != null) {
if (visibility && !mCurrentSliceLoggedVisible) {
mCurrentSliceMetrics.logVisible();
mCurrentSliceLoggedVisible = true;
}
if (!visibility && mCurrentSliceLoggedVisible) {
mCurrentSliceMetrics.logHidden();
mCurrentSliceLoggedVisible = false;
}
}
}
private void logSliceMetricsOnTouch(SliceItem item, EventInfo info) {
if (mCurrentSliceMetrics != null) {
if (item.getSlice() != null && item.getSlice().getUri() != null) {
mCurrentSliceMetrics.logTouch(
info.actionType,
item.getSlice().getUri());
}
}
}
private void refreshLastUpdatedLabel(boolean visibility) {
if (mShowLastUpdated && mSliceMetadata != null && !mSliceMetadata.neverExpires()) {
if (visibility) {
mHandler.postDelayed(mRefreshLastUpdated, mSliceMetadata.isExpired()
? REFRESH_LAST_UPDATED_IN_MILLIS
: mSliceMetadata.getTimeToExpiry() + REFRESH_LAST_UPDATED_IN_MILLIS);
} else {
mHandler.removeCallbacks(mRefreshLastUpdated);
}
}
}
Runnable mRefreshLastUpdated = new Runnable() {
@Override
public void run() {
if (mSliceMetadata != null && mSliceMetadata.isExpired()) {
mCurrentView.setShowLastUpdated(true);
mCurrentView.setSliceContent(mListContent);
}
mHandler.postDelayed(this, REFRESH_LAST_UPDATED_IN_MILLIS);
}
};
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static final Comparator<SliceAction> SLICE_ACTION_PRIORITY_COMPARATOR =
new Comparator<SliceAction>() {
@Override
public int compare(SliceAction action1, SliceAction action2) {
// Priority 0 is the highest and -1 meaning no priority.
int priority1 = action1.getPriority();
int priority2 = action2.getPriority();
if (priority1 < 0 && priority2 < 0) {
return 0;
} else if (priority1 < 0) {
return 1;
} else if (priority2 < 0) {
return -1;
} else if (priority2 < priority1) {
return 1;
} else if (priority2 > priority1) {
return -1;
} else {
return 0;
}
}
};
}