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.SUBTYPE_COLOR; 20 import static android.app.slice.SliceItem.FORMAT_INT; 21 22 import android.app.PendingIntent; 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.graphics.drawable.ColorDrawable; 26 import android.os.Handler; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.HapticFeedbackConstants; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewConfiguration; 33 import android.view.ViewGroup; 34 35 import androidx.annotation.ColorInt; 36 import androidx.annotation.IntDef; 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 import androidx.annotation.RequiresApi; 40 import androidx.annotation.RestrictTo; 41 import androidx.lifecycle.Observer; 42 import androidx.slice.Slice; 43 import androidx.slice.SliceItem; 44 import androidx.slice.SliceMetadata; 45 import androidx.slice.core.SliceActionImpl; 46 import androidx.slice.core.SliceHints; 47 import androidx.slice.core.SliceQuery; 48 import androidx.slice.view.R; 49 50 import java.lang.annotation.Retention; 51 import java.lang.annotation.RetentionPolicy; 52 import java.util.List; 53 54 /** 55 * A view for displaying a {@link Slice} which is a piece of app content and actions. SliceView is 56 * able to present slice content in a templated format outside of the associated app. The way this 57 * content is displayed depends on the structure of the slice, the hints associated with the 58 * content, and the mode that SliceView is configured for. The modes that SliceView supports are: 59 * <ul> 60 * <li><b>Shortcut</b>: A shortcut is presented as an icon and a text label representing the main 61 * content or action associated with the slice.</li> 62 * <li><b>Small</b>: The small format has a restricted height and can present a single 63 * {@link SliceItem} or a limited collection of items.</li> 64 * <li><b>Large</b>: The large format displays multiple small templates in a list, if scrolling is 65 * not enabled (see {@link #setScrollable(boolean)}) the view will show as many items as it can 66 * comfortably fit.</li> 67 * </ul> 68 * <p> 69 * When constructing a slice, the contents of it can be annotated with hints, these provide the OS 70 * with some information on how the content should be displayed. For example, text annotated with 71 * {@link android.app.slice.Slice#HINT_TITLE} would be placed in the title position of a template. 72 * A slice annotated with {@link android.app.slice.Slice#HINT_LIST} would present the child items 73 * of that slice in a list. 74 * <p> 75 * Example usage: 76 * 77 * <pre class="prettyprint"> 78 * SliceView v = new SliceView(getContext()); 79 * v.setMode(desiredMode); 80 * LiveData<Slice> liveData = SliceLiveData.fromUri(sliceUri); 81 * liveData.observe(lifecycleOwner, v); 82 * </pre> 83 * @see SliceLiveData 84 */ 85 public class SliceView extends ViewGroup implements Observer<Slice>, View.OnClickListener { 86 87 private static final String TAG = "SliceView"; 88 89 /** 90 * Implement this interface to be notified of interactions with the slice displayed 91 * in this view. 92 * @see EventInfo 93 */ 94 public interface OnSliceActionListener { 95 /** 96 * Called when an interaction has occurred with an element in this view. 97 * @param info the type of event that occurred. 98 * @param item the specific item within the {@link Slice} that was interacted with. 99 */ onSliceAction(@onNull EventInfo info, @NonNull SliceItem item)100 void onSliceAction(@NonNull EventInfo info, @NonNull SliceItem item); 101 } 102 103 /** 104 * @hide 105 */ 106 @RestrictTo(RestrictTo.Scope.LIBRARY) 107 @IntDef({ 108 MODE_SMALL, MODE_LARGE, MODE_SHORTCUT 109 }) 110 @Retention(RetentionPolicy.SOURCE) 111 public @interface SliceMode {} 112 113 /** 114 * Mode indicating this slice should be presented in small template format. 115 */ 116 public static final int MODE_SMALL = 1; 117 /** 118 * Mode indicating this slice should be presented in large template format. 119 */ 120 public static final int MODE_LARGE = 2; 121 /** 122 * Mode indicating this slice should be presented as an icon. A shortcut requires an intent, 123 * icon, and label. This can be indicated by using {@link android.app.slice.Slice#HINT_TITLE} 124 * on an action in a slice. 125 */ 126 public static final int MODE_SHORTCUT = 3; 127 128 private int mMode = MODE_LARGE; 129 private Slice mCurrentSlice; 130 private ListContent mListContent; 131 private SliceChildView mCurrentView; 132 private List<SliceItem> mActions; 133 private ActionRow mActionRow; 134 135 private boolean mShowActions = false; 136 private boolean mIsScrollable = true; 137 private boolean mShowLastUpdated = true; 138 139 private int mShortcutSize; 140 private int mMinLargeHeight; 141 private int mMaxLargeHeight; 142 private int mActionRowHeight; 143 144 private AttributeSet mAttrs; 145 private int mDefStyleAttr; 146 private int mDefStyleRes; 147 private int mThemeTintColor = -1; 148 149 private OnSliceActionListener mSliceObserver; 150 private int mTouchSlopSquared; 151 private View.OnLongClickListener mLongClickListener; 152 private View.OnClickListener mOnClickListener; 153 private int mDownX; 154 private int mDownY; 155 private boolean mPressing; 156 private boolean mInLongpress; 157 private Handler mHandler; 158 int[] mClickInfo; 159 SliceView(Context context)160 public SliceView(Context context) { 161 this(context, null); 162 } 163 SliceView(Context context, @Nullable AttributeSet attrs)164 public SliceView(Context context, @Nullable AttributeSet attrs) { 165 this(context, attrs, R.attr.sliceViewStyle); 166 } 167 SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)168 public SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 169 super(context, attrs, defStyleAttr); 170 init(context, attrs, defStyleAttr, R.style.Widget_SliceView); 171 } 172 173 @RequiresApi(21) SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)174 public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 175 super(context, attrs, defStyleAttr, defStyleRes); 176 init(context, attrs, defStyleAttr, defStyleRes); 177 } 178 init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)179 private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 180 mAttrs = attrs; 181 mDefStyleAttr = defStyleAttr; 182 mDefStyleRes = defStyleRes; 183 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView, 184 defStyleAttr, defStyleRes); 185 186 try { 187 mThemeTintColor = a.getColor(R.styleable.SliceView_tintColor, -1); 188 } finally { 189 a.recycle(); 190 } 191 mShortcutSize = getContext().getResources() 192 .getDimensionPixelSize(R.dimen.abc_slice_shortcut_size); 193 mMinLargeHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_large_height); 194 mMaxLargeHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_max_large_height); 195 mActionRowHeight = getResources().getDimensionPixelSize( 196 R.dimen.abc_slice_action_row_height); 197 198 mCurrentView = new LargeTemplateView(getContext()); 199 mCurrentView.setMode(getMode()); 200 addView(mCurrentView, getChildLp(mCurrentView)); 201 202 // TODO: action row background should support light / dark / maybe presenter customization 203 mActionRow = new ActionRow(getContext(), true); 204 mActionRow.setBackground(new ColorDrawable(0xffeeeeee)); 205 addView(mActionRow, getChildLp(mActionRow)); 206 207 final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 208 mTouchSlopSquared = slop * slop; 209 mHandler = new Handler(); 210 211 super.setOnClickListener(this); 212 } 213 214 /** 215 * Indicates whether this view reacts to click events or not. 216 * @hide 217 */ 218 @RestrictTo(RestrictTo.Scope.LIBRARY) isSliceViewClickable()219 public boolean isSliceViewClickable() { 220 return mOnClickListener != null 221 || (mListContent != null && mListContent.getPrimaryAction() != null); 222 } 223 224 /** 225 * Sets the event info for logging a click. 226 * @hide 227 */ 228 @RestrictTo(RestrictTo.Scope.LIBRARY) setClickInfo(int[] info)229 public void setClickInfo(int[] info) { 230 mClickInfo = info; 231 } 232 233 @Override onClick(View v)234 public void onClick(View v) { 235 if (mListContent != null && mListContent.getPrimaryAction() != null) { 236 try { 237 SliceActionImpl sa = new SliceActionImpl(mListContent.getPrimaryAction()); 238 sa.getAction().send(); 239 if (mSliceObserver != null && mClickInfo != null && mClickInfo.length > 1) { 240 EventInfo eventInfo = new EventInfo(getMode(), 241 EventInfo.ACTION_TYPE_CONTENT, mClickInfo[0], mClickInfo[1]); 242 mSliceObserver.onSliceAction(eventInfo, mListContent.getPrimaryAction()); 243 } 244 } catch (PendingIntent.CanceledException e) { 245 Log.e(TAG, "PendingIntent for slice cannot be sent", e); 246 } 247 } else if (mOnClickListener != null) { 248 mOnClickListener.onClick(this); 249 } 250 } 251 252 @Override setOnClickListener(View.OnClickListener listener)253 public void setOnClickListener(View.OnClickListener listener) { 254 mOnClickListener = listener; 255 } 256 257 @Override setOnLongClickListener(View.OnLongClickListener listener)258 public void setOnLongClickListener(View.OnLongClickListener listener) { 259 super.setOnLongClickListener(listener); 260 mLongClickListener = listener; 261 } 262 263 @Override onInterceptTouchEvent(MotionEvent ev)264 public boolean onInterceptTouchEvent(MotionEvent ev) { 265 boolean ret = super.onInterceptTouchEvent(ev); 266 if (mLongClickListener != null) { 267 return handleTouchForLongpress(ev); 268 } 269 return ret; 270 } 271 272 @Override onTouchEvent(MotionEvent ev)273 public boolean onTouchEvent(MotionEvent ev) { 274 boolean ret = super.onTouchEvent(ev); 275 if (mLongClickListener != null) { 276 return handleTouchForLongpress(ev); 277 } 278 return ret; 279 } 280 handleTouchForLongpress(MotionEvent ev)281 private boolean handleTouchForLongpress(MotionEvent ev) { 282 int action = ev.getActionMasked(); 283 switch (action) { 284 case MotionEvent.ACTION_DOWN: 285 mHandler.removeCallbacks(mLongpressCheck); 286 mDownX = (int) ev.getRawX(); 287 mDownY = (int) ev.getRawY(); 288 mPressing = true; 289 mInLongpress = false; 290 mHandler.postDelayed(mLongpressCheck, ViewConfiguration.getLongPressTimeout()); 291 break; 292 293 case MotionEvent.ACTION_MOVE: 294 final int deltaX = (int) ev.getRawX() - mDownX; 295 final int deltaY = (int) ev.getRawY() - mDownY; 296 int distance = (deltaX * deltaX) + (deltaY * deltaY); 297 if (distance > mTouchSlopSquared) { 298 mPressing = false; 299 mHandler.removeCallbacks(mLongpressCheck); 300 } 301 break; 302 303 case MotionEvent.ACTION_CANCEL: 304 case MotionEvent.ACTION_UP: 305 mPressing = false; 306 mInLongpress = false; 307 mHandler.removeCallbacks(mLongpressCheck); 308 break; 309 } 310 return mInLongpress; 311 } 312 getHeightForMode()313 private int getHeightForMode() { 314 int mode = getMode(); 315 if (mode == MODE_SHORTCUT) { 316 return mListContent != null && mListContent.isValid() ? mShortcutSize : 0; 317 } 318 return mode == MODE_LARGE 319 ? mCurrentView.getActualHeight() 320 : mCurrentView.getSmallHeight(); 321 } 322 323 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)324 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 325 int width = MeasureSpec.getSize(widthMeasureSpec); 326 int childWidth = MeasureSpec.getSize(widthMeasureSpec); 327 if (MODE_SHORTCUT == mMode) { 328 // TODO: consider scaling the shortcut to fit if too small 329 childWidth = mShortcutSize; 330 width = mShortcutSize + getPaddingLeft() + getPaddingRight(); 331 } 332 final int actionHeight = mActionRow.getVisibility() != View.GONE 333 ? mActionRowHeight 334 : 0; 335 final int sliceHeight = getHeightForMode(); 336 final int heightAvailable = MeasureSpec.getSize(heightMeasureSpec); 337 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 338 // Remove the padding from our available height 339 int height = heightAvailable - getPaddingTop() - getPaddingBottom(); 340 if (heightAvailable >= sliceHeight + actionHeight 341 || heightMode == MeasureSpec.UNSPECIFIED) { 342 // Available space is larger than the slice or we be what we want 343 if (heightMode != MeasureSpec.EXACTLY) { 344 if (!mIsScrollable) { 345 height = Math.min(mMaxLargeHeight, sliceHeight); 346 } else { 347 // If we want to be bigger than max, then we can be a good scrollable at min 348 // large height, if it's not larger lets just use its desired height 349 height = sliceHeight > mMaxLargeHeight ? mMinLargeHeight : sliceHeight; 350 } 351 } 352 } else { 353 // Not enough space available for slice in current mode 354 if (getMode() == MODE_LARGE && heightAvailable >= mMinLargeHeight + actionHeight) { 355 // It's just a slice with scrolling content; cap it to height available. 356 height = Math.min(mMinLargeHeight, heightAvailable); 357 } else if (getMode() == MODE_SHORTCUT) { 358 // TODO: consider scaling the shortcut to fit if too small 359 height = mShortcutSize; 360 } 361 } 362 363 int childHeight = height + getPaddingTop() + getPaddingBottom(); 364 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); 365 int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY); 366 measureChild(mCurrentView, childWidthMeasureSpec, childHeightMeasureSpec); 367 368 int actionPaddedHeight = actionHeight + getPaddingTop() + getPaddingBottom(); 369 int actionHeightSpec = MeasureSpec.makeMeasureSpec(actionPaddedHeight, MeasureSpec.EXACTLY); 370 measureChild(mActionRow, childWidthMeasureSpec, actionHeightSpec); 371 372 // Total height should include action row and our padding 373 height += actionHeight + getPaddingTop() + getPaddingBottom(); 374 setMeasuredDimension(width, height); 375 } 376 377 @Override onLayout(boolean changed, int l, int t, int r, int b)378 protected void onLayout(boolean changed, int l, int t, int r, int b) { 379 View v = mCurrentView; 380 final int left = getPaddingLeft(); 381 final int top = getPaddingTop(); 382 v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight()); 383 if (mActionRow.getVisibility() != View.GONE) { 384 mActionRow.layout(left, 385 top + v.getMeasuredHeight(), 386 left + mActionRow.getMeasuredWidth(), 387 top + v.getMeasuredHeight() + mActionRow.getMeasuredHeight()); 388 } 389 } 390 391 @Override onChanged(@ullable Slice slice)392 public void onChanged(@Nullable Slice slice) { 393 setSlice(slice); 394 } 395 396 /** 397 * Populates this view to the provided {@link Slice}. 398 * 399 * This will not update automatically if the slice content changes, for live 400 * content see {@link SliceLiveData}. 401 */ setSlice(@ullable Slice slice)402 public void setSlice(@Nullable Slice slice) { 403 if (slice != null) { 404 if (mCurrentSlice == null || !mCurrentSlice.getUri().equals(slice.getUri())) { 405 mCurrentView.resetView(); 406 } 407 } else { 408 // No slice, no actions 409 mActions = null; 410 } 411 mActions = SliceMetadata.getSliceActions(slice); 412 mCurrentSlice = slice; 413 reinflate(); 414 } 415 416 /** 417 * @return the slice being used to populate this view. 418 */ 419 @Nullable getSlice()420 public Slice getSlice() { 421 return mCurrentSlice; 422 } 423 424 /** 425 * Returns the slice actions presented in this view. 426 * <p> 427 * Note that these may be different from {@link SliceMetadata#getSliceActions()} if the actions 428 * set on the view have been adjusted using {@link #setSliceActions(List)}. 429 */ 430 @Nullable getSliceActions()431 public List<SliceItem> getSliceActions() { 432 return mActions; 433 } 434 435 /** 436 * Sets the slice actions to display for the slice contained in this view. Normally SliceView 437 * will automatically show actions, however, it is possible to reorder or omit actions on the 438 * view using this method. This is generally discouraged. 439 * <p> 440 * It is required that the slice be set on this view before actions can be set, otherwise 441 * this will throw {@link IllegalStateException}. If any of the actions supplied are not 442 * available for the slice set on this view (i.e. the action is not returned by 443 * {@link SliceMetadata#getSliceActions()} this will throw {@link IllegalArgumentException}. 444 */ setSliceActions(@ullable List<SliceItem> newActions)445 public void setSliceActions(@Nullable List<SliceItem> newActions) { 446 // Check that these actions are part of available set 447 if (mCurrentSlice == null) { 448 throw new IllegalStateException("Trying to set actions on a view without a slice"); 449 } 450 List<SliceItem> availableActions = SliceMetadata.getSliceActions(mCurrentSlice); 451 if (availableActions != null && newActions != null) { 452 for (int i = 0; i < newActions.size(); i++) { 453 if (!availableActions.contains(newActions.get(i))) { 454 throw new IllegalArgumentException( 455 "Trying to set an action that isn't available: " + newActions.get(i)); 456 } 457 } 458 } 459 mActions = newActions; 460 updateActions(); 461 } 462 463 /** 464 * Set the mode this view should present in. 465 */ setMode(@liceMode int mode)466 public void setMode(@SliceMode int mode) { 467 setMode(mode, false /* animate */); 468 } 469 470 /** 471 * Set whether this view should allow scrollable content when presenting in {@link #MODE_LARGE}. 472 */ setScrollable(boolean isScrollable)473 public void setScrollable(boolean isScrollable) { 474 mIsScrollable = isScrollable; 475 reinflate(); 476 } 477 478 /** 479 * Sets the listener to notify when an interaction events occur on the view. 480 * @see EventInfo 481 */ setOnSliceActionListener(@ullable OnSliceActionListener observer)482 public void setOnSliceActionListener(@Nullable OnSliceActionListener observer) { 483 mSliceObserver = observer; 484 mCurrentView.setSliceActionListener(mSliceObserver); 485 } 486 487 /** 488 * @deprecated TO BE REMOVED; use {@link #setAccentColor(int)} instead. 489 */ 490 @Deprecated setTint(int tintColor)491 public void setTint(int tintColor) { 492 setAccentColor(tintColor); 493 } 494 495 /** 496 * Contents of a slice such as icons, text, and controls (e.g. toggle) can be tinted. Normally 497 * a color for tinting will be provided by the slice. Using this method will override 498 * the slice-provided color information and instead tint elements with the color set here. 499 * 500 * @param accentColor the color to use for tinting contents of this view. 501 */ setAccentColor(@olorInt int accentColor)502 public void setAccentColor(@ColorInt int accentColor) { 503 mThemeTintColor = accentColor; 504 mCurrentView.setTint(accentColor); 505 } 506 507 /** 508 * @hide 509 */ 510 @RestrictTo(RestrictTo.Scope.LIBRARY) setMode(@liceMode int mode, boolean animate)511 public void setMode(@SliceMode int mode, boolean animate) { 512 if (animate) { 513 Log.e(TAG, "Animation not supported yet"); 514 } 515 if (mMode == mode) { 516 return; 517 } 518 mMode = mode; 519 reinflate(); 520 } 521 522 /** 523 * @return the mode this view is presenting in. 524 */ getMode()525 public @SliceMode int getMode() { 526 return mMode; 527 } 528 529 /** 530 * @hide 531 * 532 * Whether this view should show a row of actions with it. 533 */ 534 @RestrictTo(RestrictTo.Scope.LIBRARY) setShowActionRow(boolean show)535 public void setShowActionRow(boolean show) { 536 mShowActions = show; 537 updateActions(); 538 } 539 540 /** 541 * @return whether this view is showing a row of actions. 542 * @hide 543 */ 544 @RestrictTo(RestrictTo.Scope.LIBRARY) isShowingActionRow()545 public boolean isShowingActionRow() { 546 return mShowActions; 547 } 548 reinflate()549 private void reinflate() { 550 if (mCurrentSlice == null) { 551 mCurrentView.resetView(); 552 updateActions(); 553 return; 554 } 555 mListContent = new ListContent(getContext(), mCurrentSlice, mAttrs, mDefStyleAttr, 556 mDefStyleRes); 557 if (!mListContent.isValid()) { 558 mCurrentView.resetView(); 559 updateActions(); 560 return; 561 } 562 563 // TODO: Smarter mapping here from one state to the next. 564 int mode = getMode(); 565 boolean isCurrentViewShortcut = mCurrentView instanceof ShortcutView; 566 if (mode == MODE_SHORTCUT && !isCurrentViewShortcut) { 567 removeAllViews(); 568 mCurrentView = new ShortcutView(getContext()); 569 addView(mCurrentView, getChildLp(mCurrentView)); 570 } else if (mode != MODE_SHORTCUT && isCurrentViewShortcut) { 571 removeAllViews(); 572 mCurrentView = new LargeTemplateView(getContext()); 573 addView(mCurrentView, getChildLp(mCurrentView)); 574 } 575 mCurrentView.setMode(mode); 576 577 mCurrentView.setSliceActionListener(mSliceObserver); 578 if (mCurrentView instanceof LargeTemplateView) { 579 ((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable); 580 } 581 mCurrentView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes); 582 mCurrentView.setTint(getTintColor()); 583 584 // Check if the slice content is expired and show when it was last updated 585 SliceMetadata sliceMetadata = SliceMetadata.from(getContext(), mCurrentSlice); 586 long lastUpdated = sliceMetadata.getLastUpdatedTime(); 587 long expiry = sliceMetadata.getExpiry(); 588 long now = System.currentTimeMillis(); 589 mCurrentView.setLastUpdated(lastUpdated); 590 boolean expired = expiry != 0 && expiry != SliceHints.INFINITY && now > expiry; 591 mCurrentView.setShowLastUpdated(mShowLastUpdated && expired); 592 593 // Set the slice 594 mCurrentView.setSliceContent(mListContent); 595 updateActions(); 596 } 597 updateActions()598 private void updateActions() { 599 if (mActions == null || mActions.isEmpty()) { 600 // No actions, hide the row, clear out the view 601 mActionRow.setVisibility(View.GONE); 602 mCurrentView.setSliceActions(null); 603 return; 604 } 605 606 // TODO: take priority attached to actions into account 607 if (mShowActions && mMode != MODE_SHORTCUT && mActions.size() >= 2) { 608 // Show in action row if available 609 mActionRow.setActions(mActions, getTintColor()); 610 mActionRow.setVisibility(View.VISIBLE); 611 // Hide them on the template 612 mCurrentView.setSliceActions(null); 613 } else if (mActions.size() > 0) { 614 // Otherwise set them on the template 615 mCurrentView.setSliceActions(mActions); 616 mActionRow.setVisibility(View.GONE); 617 } 618 } 619 getTintColor()620 private int getTintColor() { 621 if (mThemeTintColor != -1) { 622 // Theme has specified a color, use that 623 return mThemeTintColor; 624 } else { 625 final SliceItem colorItem = SliceQuery.findSubtype( 626 mCurrentSlice, FORMAT_INT, SUBTYPE_COLOR); 627 return colorItem != null 628 ? colorItem.getInt() 629 : SliceViewUtil.getColorAccent(getContext()); 630 } 631 } 632 getChildLp(View child)633 private LayoutParams getChildLp(View child) { 634 if (child instanceof ShortcutView) { 635 return new LayoutParams(mShortcutSize, mShortcutSize); 636 } else { 637 return new LayoutParams(LayoutParams.MATCH_PARENT, 638 LayoutParams.MATCH_PARENT); 639 } 640 } 641 642 /** 643 * @return String representation of the provided mode. 644 * @hide 645 */ 646 @RestrictTo(RestrictTo.Scope.LIBRARY) modeToString(@liceMode int mode)647 public static String modeToString(@SliceMode int mode) { 648 switch(mode) { 649 case MODE_SHORTCUT: 650 return "MODE SHORTCUT"; 651 case MODE_SMALL: 652 return "MODE SMALL"; 653 case MODE_LARGE: 654 return "MODE LARGE"; 655 default: 656 return "unknown mode: " + mode; 657 } 658 } 659 660 Runnable mLongpressCheck = new Runnable() { 661 @Override 662 public void run() { 663 if (mPressing && mLongClickListener != null) { 664 mInLongpress = true; 665 mLongClickListener.onLongClick(SliceView.this); 666 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 667 } 668 } 669 }; 670 } 671