Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 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.app.slice.widget; |
| 18 | |
Mady Mellor | 5b2c0ce | 2017-12-08 12:16:37 -0800 | [diff] [blame] | 19 | import static android.app.slice.Slice.HINT_HORIZONTAL; |
Jason Monk | 98ae4f8 | 2017-12-18 11:29:07 -0500 | [diff] [blame] | 20 | import static android.app.slice.Slice.SUBTYPE_COLOR; |
| 21 | import static android.app.slice.SliceItem.FORMAT_INT; |
Jason Monk | 0c76d30 | 2017-11-21 14:02:27 -0500 | [diff] [blame] | 22 | |
Jason Monk | 698ad8c | 2017-11-10 10:31:19 -0500 | [diff] [blame] | 23 | import android.arch.lifecycle.Observer; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 24 | import android.content.Context; |
Mady Mellor | 0922d59 | 2018-01-23 16:30:59 -0800 | [diff] [blame] | 25 | import android.content.res.TypedArray; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 26 | import android.graphics.drawable.ColorDrawable; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 27 | import android.support.annotation.IntDef; |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 28 | import android.support.annotation.NonNull; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 29 | import android.support.annotation.Nullable; |
| 30 | import android.support.annotation.RestrictTo; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 31 | import android.util.AttributeSet; |
| 32 | import android.util.Log; |
| 33 | import android.view.View; |
| 34 | import android.view.ViewGroup; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 35 | |
| 36 | import java.util.List; |
| 37 | |
Jason Monk | dcb5e2f | 2017-11-15 20:19:43 -0500 | [diff] [blame] | 38 | import androidx.app.slice.Slice; |
| 39 | import androidx.app.slice.SliceItem; |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 40 | import androidx.app.slice.SliceUtils; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 41 | import androidx.app.slice.core.SliceQuery; |
| 42 | import androidx.app.slice.view.R; |
| 43 | |
| 44 | /** |
| 45 | * A view for displaying a {@link Slice} which is a piece of app content and actions. SliceView is |
| 46 | * able to present slice content in a templated format outside of the associated app. The way this |
| 47 | * content is displayed depends on the structure of the slice, the hints associated with the |
| 48 | * content, and the mode that SliceView is configured for. The modes that SliceView supports are: |
| 49 | * <ul> |
| 50 | * <li><b>Shortcut</b>: A shortcut is presented as an icon and a text label representing the main |
| 51 | * content or action associated with the slice.</li> |
| 52 | * <li><b>Small</b>: The small format has a restricted height and can present a single |
| 53 | * {@link SliceItem} or a limited collection of items.</li> |
| 54 | * <li><b>Large</b>: The large format displays multiple small templates in a list, if scrolling is |
| 55 | * not enabled (see {@link #setScrollable(boolean)}) the view will show as many items as it can |
| 56 | * comfortably fit.</li> |
| 57 | * </ul> |
| 58 | * <p> |
| 59 | * When constructing a slice, the contents of it can be annotated with hints, these provide the OS |
| 60 | * with some information on how the content should be displayed. For example, text annotated with |
Jason Monk | dcb5e2f | 2017-11-15 20:19:43 -0500 | [diff] [blame] | 61 | * {@link android.app.slice.Slice#HINT_TITLE} would be placed in the title position of a template. |
| 62 | * A slice annotated with {@link android.app.slice.Slice#HINT_LIST} would present the child items |
| 63 | * of that slice in a list. |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 64 | * <p> |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 65 | * Example usage: |
| 66 | * |
| 67 | * <pre class="prettyprint"> |
| 68 | * SliceView v = new SliceView(getContext()); |
| 69 | * v.setMode(desiredMode); |
Jason Monk | 698ad8c | 2017-11-10 10:31:19 -0500 | [diff] [blame] | 70 | * LiveData<Slice> liveData = SliceLiveData.fromUri(sliceUri); |
| 71 | * liveData.observe(lifecycleOwner, v); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 72 | * </pre> |
Jason Monk | 698ad8c | 2017-11-10 10:31:19 -0500 | [diff] [blame] | 73 | * @see SliceLiveData |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 74 | */ |
Jason Monk | 698ad8c | 2017-11-10 10:31:19 -0500 | [diff] [blame] | 75 | public class SliceView extends ViewGroup implements Observer<Slice> { |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 76 | |
| 77 | private static final String TAG = "SliceView"; |
| 78 | |
| 79 | /** |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 80 | * Implement this interface to be notified of interactions with the slice displayed |
| 81 | * in this view. |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 82 | * @see EventInfo |
| 83 | */ |
Mady Mellor | abd7ffd | 2018-01-10 14:03:45 -0800 | [diff] [blame] | 84 | public interface OnSliceActionListener { |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 85 | /** |
| 86 | * Called when an interaction has occurred with an element in this view. |
| 87 | * @param info the type of event that occurred. |
| 88 | * @param item the specific item within the {@link Slice} that was interacted with. |
| 89 | */ |
| 90 | void onSliceAction(@NonNull EventInfo info, @NonNull SliceItem item); |
| 91 | } |
| 92 | |
| 93 | /** |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 94 | * @hide |
| 95 | */ |
| 96 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 97 | @IntDef({ |
| 98 | MODE_SMALL, MODE_LARGE, MODE_SHORTCUT |
| 99 | }) |
| 100 | public @interface SliceMode {} |
| 101 | |
| 102 | /** |
| 103 | * Mode indicating this slice should be presented in small template format. |
| 104 | */ |
| 105 | public static final int MODE_SMALL = 1; |
| 106 | /** |
| 107 | * Mode indicating this slice should be presented in large template format. |
| 108 | */ |
| 109 | public static final int MODE_LARGE = 2; |
| 110 | /** |
Mady Mellor | 24b4296 | 2017-11-08 13:27:24 -0800 | [diff] [blame] | 111 | * Mode indicating this slice should be presented as an icon. A shortcut requires an intent, |
Jason Monk | dcb5e2f | 2017-11-15 20:19:43 -0500 | [diff] [blame] | 112 | * icon, and label. This can be indicated by using {@link android.app.slice.Slice#HINT_TITLE} |
| 113 | * on an action in a slice. |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 114 | */ |
| 115 | public static final int MODE_SHORTCUT = 3; |
| 116 | |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 117 | private int mMode = MODE_LARGE; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 118 | private Slice mCurrentSlice; |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 119 | private SliceChildView mCurrentView; |
| 120 | private List<SliceItem> mActions; |
| 121 | private final ActionRow mActionRow; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 122 | |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 123 | private boolean mShowActions = false; |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 124 | private boolean mIsScrollable = true; |
| 125 | |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 126 | private final int mShortcutSize; |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 127 | private final int mMinLargeHeight; |
| 128 | |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 129 | private AttributeSet mAttrs; |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 130 | private int mThemeTintColor = -1; |
| 131 | |
| 132 | private OnSliceActionListener mSliceObserver; |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 133 | |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 134 | public SliceView(Context context) { |
| 135 | this(context, null); |
| 136 | } |
| 137 | |
| 138 | public SliceView(Context context, @Nullable AttributeSet attrs) { |
Mady Mellor | 0922d59 | 2018-01-23 16:30:59 -0800 | [diff] [blame] | 139 | this(context, attrs, R.attr.sliceViewStyle); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 140 | } |
| 141 | |
| 142 | public SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { |
Mady Mellor | 0922d59 | 2018-01-23 16:30:59 -0800 | [diff] [blame] | 143 | this(context, attrs, defStyleAttr, R.style.Widget_SliceView); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 144 | } |
| 145 | |
| 146 | public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
| 147 | super(context, attrs, defStyleAttr, defStyleRes); |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 148 | mAttrs = attrs; |
Mady Mellor | 0922d59 | 2018-01-23 16:30:59 -0800 | [diff] [blame] | 149 | TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView, |
| 150 | defStyleAttr, defStyleRes); |
| 151 | try { |
| 152 | mThemeTintColor = a.getColor(R.styleable.SliceView_tintColor, -1); |
| 153 | } finally { |
| 154 | a.recycle(); |
| 155 | } |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 156 | // TODO: action row background should support light / dark / maybe presenter customization |
| 157 | mActionRow = new ActionRow(getContext(), true); |
| 158 | mActionRow.setBackground(new ColorDrawable(0xffeeeeee)); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 159 | mCurrentView = new LargeTemplateView(getContext()); |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 160 | mCurrentView.setMode(getMode()); |
Mady Mellor | 5b2c0ce | 2017-12-08 12:16:37 -0800 | [diff] [blame] | 161 | addView(mCurrentView.getView(), getChildLp(mCurrentView.getView())); |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 162 | addView(mActionRow, getChildLp(mActionRow)); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 163 | mShortcutSize = getContext().getResources() |
| 164 | .getDimensionPixelSize(R.dimen.abc_slice_shortcut_size); |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 165 | mMinLargeHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_large_height); |
| 166 | } |
| 167 | |
| 168 | private int getHeightForMode() { |
| 169 | int mode = getMode(); |
| 170 | if (mode == MODE_SHORTCUT) { |
| 171 | return mShortcutSize; |
| 172 | } |
| 173 | return mode == MODE_LARGE |
| 174 | ? mCurrentView.getActualHeight() |
| 175 | : mCurrentView.getSmallHeight(); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 176 | } |
| 177 | |
| 178 | @Override |
| 179 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
Mady Mellor | 06dc141 | 2017-11-07 14:34:57 -0800 | [diff] [blame] | 180 | int width = MeasureSpec.getSize(widthMeasureSpec); |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 181 | int childWidth = MeasureSpec.getSize(widthMeasureSpec); |
Mady Mellor | 06dc141 | 2017-11-07 14:34:57 -0800 | [diff] [blame] | 182 | if (MODE_SHORTCUT == mMode) { |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 183 | // TODO: consider scaling the shortcut to fit if too small |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 184 | childWidth = mShortcutSize; |
Mady Mellor | 06dc141 | 2017-11-07 14:34:57 -0800 | [diff] [blame] | 185 | width = mShortcutSize; |
| 186 | } |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 187 | |
| 188 | final int actionHeight = mActionRow.getVisibility() != View.GONE |
| 189 | ? mActionRow.getMeasuredHeight() |
| 190 | : 0; |
| 191 | final int sliceHeight = getHeightForMode() + actionHeight; |
| 192 | final int heightAvailable = MeasureSpec.getSize(heightMeasureSpec); |
| 193 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
| 194 | int height = heightAvailable; |
| 195 | if (heightAvailable >= sliceHeight) { |
| 196 | // Available space is larger than the slice |
| 197 | if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { |
| 198 | height = sliceHeight; |
| 199 | } |
| 200 | } else { |
| 201 | // Not enough space available for slice in current mode |
| 202 | if (getMode() == MODE_LARGE && heightAvailable >= mMinLargeHeight + actionHeight) { |
| 203 | // It's just a slice with scrolling content; cap it to height available. |
| 204 | height = heightAvailable; |
| 205 | } else if (getMode() == MODE_SHORTCUT) { |
| 206 | // TODO: consider scaling the shortcut to fit if too small |
| 207 | height = mShortcutSize; |
| 208 | } |
| 209 | } |
| 210 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); |
| 211 | |
| 212 | // Measure the children without the padding |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 213 | final int left = getPaddingLeft(); |
| 214 | final int top = getPaddingTop(); |
| 215 | final int right = getPaddingRight(); |
| 216 | final int bot = getPaddingBottom(); |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 217 | int childHeight = MeasureSpec.getSize(heightMeasureSpec); |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 218 | childWidth -= left + right; |
| 219 | childHeight -= top + bot; |
| 220 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); |
| 221 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY); |
| 222 | measureChildren(childWidthMeasureSpec, childHeightMeasureSpec); |
| 223 | |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 224 | // Figure out parent width |
| 225 | width += left + right; |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 226 | setMeasuredDimension(width, heightMeasureSpec); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 227 | } |
| 228 | |
| 229 | @Override |
| 230 | protected void onLayout(boolean changed, int l, int t, int r, int b) { |
Mady Mellor | 5b2c0ce | 2017-12-08 12:16:37 -0800 | [diff] [blame] | 231 | View v = mCurrentView.getView(); |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 232 | final int left = getPaddingLeft(); |
| 233 | final int top = getPaddingTop(); |
| 234 | final int right = getPaddingRight(); |
| 235 | final int bottom = getPaddingBottom(); |
| 236 | v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight()); |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 237 | if (mActionRow.getVisibility() != View.GONE) { |
| 238 | mActionRow.layout(left, |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 239 | top + v.getMeasuredHeight() + bottom, |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 240 | left + mActionRow.getMeasuredWidth() + right, |
| 241 | top + v.getMeasuredHeight() + bottom + mActionRow.getMeasuredHeight()); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 242 | } |
| 243 | } |
| 244 | |
Jason Monk | 698ad8c | 2017-11-10 10:31:19 -0500 | [diff] [blame] | 245 | @Override |
| 246 | public void onChanged(@Nullable Slice slice) { |
| 247 | setSlice(slice); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 248 | } |
| 249 | |
| 250 | /** |
| 251 | * Populates this view to the provided {@link Slice}. |
Jason Monk | 698ad8c | 2017-11-10 10:31:19 -0500 | [diff] [blame] | 252 | * |
| 253 | * This will not update automatically if the slice content changes, for live |
| 254 | * content see {@link SliceLiveData}. |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 255 | */ |
Jason Monk | 698ad8c | 2017-11-10 10:31:19 -0500 | [diff] [blame] | 256 | public void setSlice(@Nullable Slice slice) { |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 257 | if (slice != null) { |
| 258 | if (mCurrentSlice == null || mCurrentSlice.getUri() != slice.getUri()) { |
| 259 | // New slice, new actions |
| 260 | mActions = SliceUtils.getSliceActions(slice); |
| 261 | mCurrentView.resetView(); |
| 262 | } |
| 263 | } else { |
| 264 | // No slice, no actions |
| 265 | mActions = null; |
Mady Mellor | 93ac8cd | 2018-02-01 11:06:42 -0800 | [diff] [blame] | 266 | } |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 267 | mCurrentSlice = slice; |
| 268 | reinflate(); |
| 269 | } |
| 270 | |
| 271 | /** |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 272 | * Returns the slice actions presented in this view. |
| 273 | */ |
| 274 | @Nullable |
| 275 | public List<SliceItem> getSliceActions() { |
| 276 | return mActions; |
| 277 | } |
| 278 | |
| 279 | /** |
| 280 | * Sets the slice actions to display for the slice contained in this view. Normally SliceView |
| 281 | * will automatically show actions, however, it is possible to reorder or omit actions on the |
| 282 | * view using this method. This is generally discouraged. |
| 283 | * <p> |
| 284 | * It is required that the slice be set on this view before actions can be set, otherwise |
| 285 | * this will throw {@link IllegalStateException}. If any of the actions supplied are not |
| 286 | * available for the slice set on this view (i.e. the action is not returned by |
| 287 | * {@link SliceUtils#getSliceActions(Slice)} this will throw {@link IllegalArgumentException}. |
| 288 | */ |
| 289 | public void setSliceActions(@Nullable List<SliceItem> newActions) { |
| 290 | // Check that these actions are part of available set |
| 291 | if (mCurrentSlice == null) { |
| 292 | throw new IllegalStateException("Trying to set actions on a view without a slice"); |
| 293 | } |
| 294 | List<SliceItem> availableActions = SliceUtils.getSliceActions(mCurrentSlice); |
| 295 | if (availableActions != null && newActions != null) { |
| 296 | for (int i = 0; i < newActions.size(); i++) { |
| 297 | if (!availableActions.contains(newActions.get(i))) { |
| 298 | throw new IllegalArgumentException( |
| 299 | "Trying to set an action that isn't available: " + newActions.get(i)); |
| 300 | } |
| 301 | } |
| 302 | } |
| 303 | mActions = newActions; |
| 304 | updateActions(); |
| 305 | } |
| 306 | |
| 307 | /** |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 308 | * Set the mode this view should present in. |
| 309 | */ |
| 310 | public void setMode(@SliceMode int mode) { |
| 311 | setMode(mode, false /* animate */); |
| 312 | } |
| 313 | |
| 314 | /** |
| 315 | * Set whether this view should allow scrollable content when presenting in {@link #MODE_LARGE}. |
| 316 | */ |
| 317 | public void setScrollable(boolean isScrollable) { |
| 318 | mIsScrollable = isScrollable; |
| 319 | reinflate(); |
| 320 | } |
| 321 | |
| 322 | /** |
Mady Mellor | abd7ffd | 2018-01-10 14:03:45 -0800 | [diff] [blame] | 323 | * Sets the listener to notify when an interaction events occur on the view. |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 324 | * @see EventInfo |
| 325 | */ |
Mady Mellor | abd7ffd | 2018-01-10 14:03:45 -0800 | [diff] [blame] | 326 | public void setOnSliceActionListener(@Nullable OnSliceActionListener observer) { |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 327 | mSliceObserver = observer; |
Mady Mellor | abd7ffd | 2018-01-10 14:03:45 -0800 | [diff] [blame] | 328 | mCurrentView.setSliceActionListener(mSliceObserver); |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 329 | } |
| 330 | |
| 331 | /** |
Mady Mellor | 0922d59 | 2018-01-23 16:30:59 -0800 | [diff] [blame] | 332 | * Contents of a slice such as icons, text, and controls (e.g. toggle) can be tinted. Normally |
| 333 | * a color for tinting will be provided by the slice. Using this method will override |
| 334 | * this color information and instead tint elements with the provided color. |
| 335 | * |
| 336 | * @param tintColor the color to use for tinting contents of this view. |
| 337 | */ |
| 338 | public void setTint(int tintColor) { |
| 339 | mThemeTintColor = tintColor; |
| 340 | mCurrentView.setTint(tintColor); |
| 341 | } |
| 342 | |
| 343 | /** |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 344 | * @hide |
| 345 | */ |
| 346 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
| 347 | public void setMode(@SliceMode int mode, boolean animate) { |
| 348 | if (animate) { |
| 349 | Log.e(TAG, "Animation not supported yet"); |
| 350 | } |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 351 | if (mMode == mode) { |
| 352 | return; |
| 353 | } |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 354 | mMode = mode; |
| 355 | reinflate(); |
| 356 | } |
| 357 | |
| 358 | /** |
| 359 | * @return the mode this view is presenting in. |
| 360 | */ |
| 361 | public @SliceMode int getMode() { |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 362 | return mMode; |
| 363 | } |
| 364 | |
| 365 | /** |
| 366 | * @hide |
| 367 | * |
| 368 | * Whether this view should show a row of actions with it. |
| 369 | */ |
| 370 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
| 371 | public void setShowActionRow(boolean show) { |
| 372 | mShowActions = show; |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 373 | updateActions(); |
| 374 | } |
| 375 | |
| 376 | /** |
| 377 | * @return whether this view is showing a row of actions. |
| 378 | * @hide |
| 379 | */ |
| 380 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
| 381 | public boolean isShowingActionRow() { |
| 382 | return mShowActions; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 383 | } |
| 384 | |
Mady Mellor | 9b0a49d | 2018-02-13 13:55:06 -0800 | [diff] [blame] | 385 | private SliceChildView createView(int mode, boolean isGrid) { |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 386 | switch (mode) { |
| 387 | case MODE_SHORTCUT: |
| 388 | return new ShortcutView(getContext()); |
| 389 | case MODE_SMALL: |
Mady Mellor | 6078df5 | 2018-02-12 17:57:33 -0800 | [diff] [blame] | 390 | return isGrid ? new GridRowView(getContext()) : new RowView(getContext()); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 391 | } |
Mady Mellor | 9b0a49d | 2018-02-13 13:55:06 -0800 | [diff] [blame] | 392 | return new LargeTemplateView(getContext()); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 393 | } |
| 394 | |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 395 | private void reinflate() { |
| 396 | if (mCurrentSlice == null) { |
Mady Mellor | df26970 | 2017-12-20 15:40:00 -0800 | [diff] [blame] | 397 | mCurrentView.resetView(); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 398 | return; |
| 399 | } |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 400 | ListContent lc = new ListContent(getContext(), mCurrentSlice); |
Mady Mellor | 9b0a49d | 2018-02-13 13:55:06 -0800 | [diff] [blame] | 401 | if (!lc.isValid()) { |
| 402 | mCurrentView.resetView(); |
| 403 | mCurrentView.setVisibility(View.GONE); |
| 404 | return; |
| 405 | } |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 406 | // TODO: Smarter mapping here from one state to the next. |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 407 | int mode = getMode(); |
Mady Mellor | 9b0a49d | 2018-02-13 13:55:06 -0800 | [diff] [blame] | 408 | boolean reuseView = mode == mCurrentView.getMode(); |
| 409 | SliceItem header = lc.getHeaderItem(); |
| 410 | boolean isSmallGrid = header != null && SliceQuery.hasHints(header, HINT_HORIZONTAL); |
| 411 | if (reuseView && mode == MODE_SMALL) { |
| 412 | reuseView = (mCurrentView instanceof GridRowView) == isSmallGrid; |
| 413 | } |
| 414 | if (!reuseView) { |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 415 | removeAllViews(); |
Mady Mellor | 9b0a49d | 2018-02-13 13:55:06 -0800 | [diff] [blame] | 416 | mCurrentView = createView(mode, isSmallGrid); |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 417 | if (mSliceObserver != null) { |
Mady Mellor | abd7ffd | 2018-01-10 14:03:45 -0800 | [diff] [blame] | 418 | mCurrentView.setSliceActionListener(mSliceObserver); |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 419 | } |
Mady Mellor | 5b2c0ce | 2017-12-08 12:16:37 -0800 | [diff] [blame] | 420 | addView(mCurrentView.getView(), getChildLp(mCurrentView.getView())); |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 421 | addView(mActionRow, getChildLp(mActionRow)); |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 422 | mCurrentView.setMode(mode); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 423 | } |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 424 | // Scrolling |
Mady Mellor | 6078df5 | 2018-02-12 17:57:33 -0800 | [diff] [blame] | 425 | if (mode == MODE_LARGE && (mCurrentView instanceof LargeTemplateView)) { |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 426 | ((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable); |
| 427 | } |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 428 | // Styles |
| 429 | mCurrentView.setStyle(mAttrs); |
Mady Mellor | 0922d59 | 2018-01-23 16:30:59 -0800 | [diff] [blame] | 430 | mCurrentView.setTint(getTintColor()); |
Mady Mellor | 9b0a49d | 2018-02-13 13:55:06 -0800 | [diff] [blame] | 431 | mCurrentView.setVisibility(lc.isValid() ? View.VISIBLE : View.GONE); |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 432 | // Set the slice |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 433 | mCurrentView.setSlice(mCurrentSlice); |
| 434 | updateActions(); |
| 435 | } |
| 436 | |
| 437 | private void updateActions() { |
| 438 | if (mActions == null || mActions.isEmpty()) { |
| 439 | // No actions, hide the row, clear out the view |
| 440 | mActionRow.setVisibility(View.GONE); |
| 441 | mCurrentView.setSliceActions(null); |
| 442 | return; |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 443 | } |
Mady Mellor | b794b5b | 2018-02-07 11:06:48 -0800 | [diff] [blame] | 444 | |
| 445 | // TODO: take priority attached to actions into account |
| 446 | if (mShowActions && mMode != MODE_SHORTCUT && mActions.size() >= 2) { |
| 447 | // Show in action row if available |
| 448 | mActionRow.setActions(mActions, getTintColor()); |
| 449 | mActionRow.setVisibility(View.VISIBLE); |
| 450 | // Hide them on the template |
| 451 | mCurrentView.setSliceActions(null); |
| 452 | } else if (mActions.size() > 0) { |
| 453 | // Otherwise set them on the template |
| 454 | mCurrentView.setSliceActions(mActions); |
| 455 | mActionRow.setVisibility(View.GONE); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 456 | } |
| 457 | } |
| 458 | |
Mady Mellor | dc05204 | 2018-01-04 17:11:07 -0800 | [diff] [blame] | 459 | private int getTintColor() { |
| 460 | if (mThemeTintColor != -1) { |
| 461 | // Theme has specified a color, use that |
| 462 | return mThemeTintColor; |
| 463 | } else { |
| 464 | final SliceItem colorItem = SliceQuery.findSubtype( |
| 465 | mCurrentSlice, FORMAT_INT, SUBTYPE_COLOR); |
| 466 | return colorItem != null |
| 467 | ? colorItem.getInt() |
| 468 | : SliceViewUtil.getColorAccent(getContext()); |
| 469 | } |
| 470 | } |
| 471 | |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 472 | private LayoutParams getChildLp(View child) { |
| 473 | if (child instanceof ShortcutView) { |
| 474 | return new LayoutParams(mShortcutSize, mShortcutSize); |
| 475 | } else { |
Mady Mellor | 8a2763f | 2018-02-16 13:39:25 -0800 | [diff] [blame^] | 476 | return new LayoutParams(LayoutParams.MATCH_PARENT, |
| 477 | LayoutParams.MATCH_PARENT); |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 478 | } |
| 479 | } |
| 480 | |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 481 | /** |
| 482 | * @return String representation of the provided mode. |
| 483 | * @hide |
| 484 | */ |
| 485 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
| 486 | public static String modeToString(@SliceMode int mode) { |
| 487 | switch(mode) { |
Mady Mellor | 238b9b6 | 2018-01-09 16:15:40 -0800 | [diff] [blame] | 488 | case MODE_SHORTCUT: |
| 489 | return "MODE SHORTCUT"; |
| 490 | case MODE_SMALL: |
| 491 | return "MODE SMALL"; |
| 492 | case MODE_LARGE: |
| 493 | return "MODE LARGE"; |
| 494 | default: |
| 495 | return "unknown mode: " + mode; |
| 496 | } |
| 497 | } |
Jason Monk | 8a452e9 | 2017-10-31 19:21:47 -0400 | [diff] [blame] | 498 | } |