1 /* 2 * Copyright (C) 2016 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 com.android.systemui.statusbar.notification.row; 18 19 import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE; 20 import static android.view.HapticFeedbackConstants.CLOCK_TICK; 21 22 import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.ValueAnimator; 27 import android.annotation.Nullable; 28 import android.content.Context; 29 import android.content.res.Resources; 30 import android.graphics.Point; 31 import android.graphics.drawable.Drawable; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.provider.Settings; 35 import android.service.notification.StatusBarNotification; 36 import android.util.ArrayMap; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.FrameLayout; 41 import android.widget.FrameLayout.LayoutParams; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.systemui.R; 45 import com.android.systemui.animation.Interpolators; 46 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 47 import com.android.systemui.statusbar.AlphaOptimizedImageView; 48 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 49 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; 50 import com.android.systemui.statusbar.notification.row.NotificationGuts.GutsContent; 51 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; 52 53 import java.util.ArrayList; 54 import java.util.List; 55 import java.util.Map; 56 57 public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener, 58 ExpandableNotificationRow.LayoutListener { 59 60 // Notification must be swiped at least this fraction of a single menu item to show menu 61 private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f; 62 private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f; 63 64 // When the menu is displayed, the notification must be swiped within this fraction of a single 65 // menu item to snap back to menu (else it will cover the menu or it'll be dismissed) 66 private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f; 67 68 private static final int ICON_ALPHA_ANIM_DURATION = 200; 69 private static final long SHOW_MENU_DELAY = 60; 70 71 private ExpandableNotificationRow mParent; 72 73 private Context mContext; 74 private FrameLayout mMenuContainer; 75 private NotificationMenuItem mInfoItem; 76 private MenuItem mFeedbackItem; 77 private MenuItem mSnoozeItem; 78 private ArrayList<MenuItem> mLeftMenuItems; 79 private ArrayList<MenuItem> mRightMenuItems; 80 private final Map<View, MenuItem> mMenuItemsByView = new ArrayMap<>(); 81 private OnMenuEventListener mMenuListener; 82 83 private ValueAnimator mFadeAnimator; 84 private boolean mAnimating; 85 private boolean mMenuFadedIn; 86 87 private boolean mOnLeft; 88 private boolean mIconsPlaced; 89 90 private boolean mDismissing; 91 private boolean mSnapping; 92 private float mTranslation; 93 94 private int[] mIconLocation = new int[2]; 95 private int[] mParentLocation = new int[2]; 96 97 private int mHorizSpaceForIcon = -1; 98 private int mVertSpaceForIcons = -1; 99 private int mIconPadding = -1; 100 private int mSidePadding; 101 102 private float mAlpha = 0f; 103 104 private CheckForDrag mCheckForDrag; 105 private Handler mHandler; 106 107 private boolean mMenuSnapped; 108 private boolean mMenuSnappedOnLeft; 109 private boolean mShouldShowMenu; 110 111 private boolean mIsUserTouching; 112 113 private boolean mSnappingToDismiss; 114 115 private final PeopleNotificationIdentifier mPeopleNotificationIdentifier; 116 NotificationMenuRow(Context context, PeopleNotificationIdentifier peopleNotificationIdentifier)117 public NotificationMenuRow(Context context, 118 PeopleNotificationIdentifier peopleNotificationIdentifier) { 119 mContext = context; 120 mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear); 121 mHandler = new Handler(Looper.getMainLooper()); 122 mLeftMenuItems = new ArrayList<>(); 123 mRightMenuItems = new ArrayList<>(); 124 mPeopleNotificationIdentifier = peopleNotificationIdentifier; 125 } 126 127 @Override getMenuItems(Context context)128 public ArrayList<MenuItem> getMenuItems(Context context) { 129 return mOnLeft ? mLeftMenuItems : mRightMenuItems; 130 } 131 132 @Override getLongpressMenuItem(Context context)133 public MenuItem getLongpressMenuItem(Context context) { 134 return mInfoItem; 135 } 136 137 @Override getFeedbackMenuItem(Context context)138 public MenuItem getFeedbackMenuItem(Context context) { 139 return mFeedbackItem; 140 } 141 142 @Override getSnoozeMenuItem(Context context)143 public MenuItem getSnoozeMenuItem(Context context) { 144 return mSnoozeItem; 145 } 146 147 @VisibleForTesting getParent()148 protected ExpandableNotificationRow getParent() { 149 return mParent; 150 } 151 152 @VisibleForTesting isMenuOnLeft()153 protected boolean isMenuOnLeft() { 154 return mOnLeft; 155 } 156 157 @VisibleForTesting isMenuSnappedOnLeft()158 protected boolean isMenuSnappedOnLeft() { 159 return mMenuSnappedOnLeft; 160 } 161 162 @VisibleForTesting isMenuSnapped()163 protected boolean isMenuSnapped() { 164 return mMenuSnapped; 165 } 166 167 @VisibleForTesting isDismissing()168 protected boolean isDismissing() { 169 return mDismissing; 170 } 171 172 @VisibleForTesting isSnapping()173 protected boolean isSnapping() { 174 return mSnapping; 175 } 176 177 @VisibleForTesting isSnappingToDismiss()178 protected boolean isSnappingToDismiss() { 179 return mSnappingToDismiss; 180 } 181 182 @Override setMenuClickListener(OnMenuEventListener listener)183 public void setMenuClickListener(OnMenuEventListener listener) { 184 mMenuListener = listener; 185 } 186 187 @Override createMenu(ViewGroup parent, StatusBarNotification sbn)188 public void createMenu(ViewGroup parent, StatusBarNotification sbn) { 189 mParent = (ExpandableNotificationRow) parent; 190 createMenuViews(true /* resetState */); 191 } 192 193 @Override isMenuVisible()194 public boolean isMenuVisible() { 195 return mAlpha > 0; 196 } 197 198 @VisibleForTesting isUserTouching()199 protected boolean isUserTouching() { 200 return mIsUserTouching; 201 } 202 203 @Override shouldShowMenu()204 public boolean shouldShowMenu() { 205 return mShouldShowMenu; 206 } 207 208 @Override getMenuView()209 public View getMenuView() { 210 return mMenuContainer; 211 } 212 213 @VisibleForTesting getTranslation()214 protected float getTranslation() { 215 return mTranslation; 216 } 217 218 @Override resetMenu()219 public void resetMenu() { 220 resetState(true); 221 } 222 223 @Override onTouchEnd()224 public void onTouchEnd() { 225 mIsUserTouching = false; 226 } 227 228 @Override onNotificationUpdated(StatusBarNotification sbn)229 public void onNotificationUpdated(StatusBarNotification sbn) { 230 if (mMenuContainer == null) { 231 // Menu hasn't been created yet, no need to do anything. 232 return; 233 } 234 createMenuViews(!isMenuVisible() /* resetState */); 235 } 236 237 @Override onConfigurationChanged()238 public void onConfigurationChanged() { 239 mParent.setLayoutListener(this); 240 } 241 242 @Override onLayout()243 public void onLayout() { 244 mIconsPlaced = false; // Force icons to be re-placed 245 setMenuLocation(); 246 mParent.removeListener(); 247 } 248 createMenuViews(boolean resetState)249 private void createMenuViews(boolean resetState) { 250 final Resources res = mContext.getResources(); 251 mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size); 252 mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height); 253 mLeftMenuItems.clear(); 254 mRightMenuItems.clear(); 255 256 boolean showSnooze = Settings.Secure.getInt(mContext.getContentResolver(), 257 SHOW_NOTIFICATION_SNOOZE, 0) == 1; 258 259 // Construct the menu items based on the notification 260 if (showSnooze) { 261 // Only show snooze for non-foreground notifications, and if the setting is on 262 mSnoozeItem = createSnoozeItem(mContext); 263 } 264 mFeedbackItem = createFeedbackItem(mContext); 265 NotificationEntry entry = mParent.getEntry(); 266 int personNotifType = mPeopleNotificationIdentifier.getPeopleNotificationType(entry); 267 if (personNotifType == PeopleNotificationIdentifier.TYPE_PERSON) { 268 mInfoItem = createPartialConversationItem(mContext); 269 } else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) { 270 mInfoItem = createConversationItem(mContext); 271 } else { 272 mInfoItem = createInfoItem(mContext); 273 } 274 275 if (showSnooze) { 276 mRightMenuItems.add(mSnoozeItem); 277 } 278 mRightMenuItems.add(mInfoItem); 279 mRightMenuItems.add(mFeedbackItem); 280 mLeftMenuItems.addAll(mRightMenuItems); 281 282 populateMenuViews(); 283 if (resetState) { 284 resetState(false /* notify */); 285 } else { 286 mIconsPlaced = false; 287 setMenuLocation(); 288 if (!mIsUserTouching) { 289 onSnapOpen(); 290 } 291 } 292 } 293 populateMenuViews()294 private void populateMenuViews() { 295 if (mMenuContainer != null) { 296 mMenuContainer.removeAllViews(); 297 mMenuItemsByView.clear(); 298 } else { 299 mMenuContainer = new FrameLayout(mContext); 300 } 301 final int showDismissSetting = Settings.Global.getInt(mContext.getContentResolver(), 302 Settings.Global.SHOW_NEW_NOTIF_DISMISS, /* default = */ 1); 303 final boolean newFlowHideShelf = showDismissSetting == 1; 304 if (newFlowHideShelf) { 305 return; 306 } 307 List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems; 308 for (int i = 0; i < menuItems.size(); i++) { 309 addMenuView(menuItems.get(i), mMenuContainer); 310 } 311 } 312 resetState(boolean notify)313 private void resetState(boolean notify) { 314 setMenuAlpha(0f); 315 mIconsPlaced = false; 316 mMenuFadedIn = false; 317 mAnimating = false; 318 mSnapping = false; 319 mDismissing = false; 320 mMenuSnapped = false; 321 setMenuLocation(); 322 if (mMenuListener != null && notify) { 323 mMenuListener.onMenuReset(mParent); 324 } 325 } 326 327 @Override onTouchMove(float delta)328 public void onTouchMove(float delta) { 329 mSnapping = false; 330 331 if (!isTowardsMenu(delta) && isMenuLocationChange()) { 332 // Don't consider it "snapped" if location has changed. 333 mMenuSnapped = false; 334 335 // Changed directions, make sure we check to fade in icon again. 336 if (!mHandler.hasCallbacks(mCheckForDrag)) { 337 // No check scheduled, set null to schedule a new one. 338 mCheckForDrag = null; 339 } else { 340 // Check scheduled, reset alpha and update location; check will fade it in 341 setMenuAlpha(0f); 342 setMenuLocation(); 343 } 344 } 345 if (mShouldShowMenu 346 && !NotificationStackScrollLayout.isPinnedHeadsUp(getParent()) 347 && !mParent.areGutsExposed() 348 && !mParent.showingPulsing() 349 && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) { 350 // Only show the menu if we're not a heads up view and guts aren't exposed. 351 mCheckForDrag = new CheckForDrag(); 352 mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY); 353 } 354 if (canBeDismissed()) { 355 final float dismissThreshold = getDismissThreshold(); 356 final boolean snappingToDismiss = delta < -dismissThreshold || delta > dismissThreshold; 357 if (mSnappingToDismiss != snappingToDismiss) { 358 getMenuView().performHapticFeedback(CLOCK_TICK); 359 } 360 mSnappingToDismiss = snappingToDismiss; 361 } 362 } 363 364 @VisibleForTesting beginDrag()365 protected void beginDrag() { 366 mSnapping = false; 367 if (mFadeAnimator != null) { 368 mFadeAnimator.cancel(); 369 } 370 mHandler.removeCallbacks(mCheckForDrag); 371 mCheckForDrag = null; 372 mIsUserTouching = true; 373 } 374 375 @Override onTouchStart()376 public void onTouchStart() { 377 beginDrag(); 378 mSnappingToDismiss = false; 379 } 380 381 @Override onSnapOpen()382 public void onSnapOpen() { 383 mMenuSnapped = true; 384 mMenuSnappedOnLeft = isMenuOnLeft(); 385 if (mAlpha == 0f && mParent != null) { 386 fadeInMenu(mParent.getWidth()); 387 } 388 if (mMenuListener != null) { 389 mMenuListener.onMenuShown(getParent()); 390 } 391 } 392 393 @Override onSnapClosed()394 public void onSnapClosed() { 395 cancelDrag(); 396 mMenuSnapped = false; 397 mSnapping = true; 398 } 399 400 @Override onDismiss()401 public void onDismiss() { 402 cancelDrag(); 403 mMenuSnapped = false; 404 mDismissing = true; 405 } 406 407 @VisibleForTesting cancelDrag()408 protected void cancelDrag() { 409 if (mFadeAnimator != null) { 410 mFadeAnimator.cancel(); 411 } 412 mHandler.removeCallbacks(mCheckForDrag); 413 } 414 415 @VisibleForTesting getMinimumSwipeDistance()416 protected float getMinimumSwipeDistance() { 417 final float multiplier = getParent().canViewBeDismissed() 418 ? SWIPED_FAR_ENOUGH_MENU_FRACTION 419 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION; 420 return mHorizSpaceForIcon * multiplier; 421 } 422 423 @VisibleForTesting getMaximumSwipeDistance()424 protected float getMaximumSwipeDistance() { 425 return mHorizSpaceForIcon * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION; 426 } 427 428 /** 429 * Returns whether the gesture is towards the menu location or not. 430 */ 431 @Override isTowardsMenu(float movement)432 public boolean isTowardsMenu(float movement) { 433 return isMenuVisible() 434 && ((isMenuOnLeft() && movement <= 0) 435 || (!isMenuOnLeft() && movement >= 0)); 436 } 437 438 @Override setAppName(String appName)439 public void setAppName(String appName) { 440 if (appName == null) { 441 return; 442 } 443 setAppName(appName, mLeftMenuItems); 444 setAppName(appName, mRightMenuItems); 445 } 446 setAppName(String appName, ArrayList<MenuItem> menuItems)447 private void setAppName(String appName, 448 ArrayList<MenuItem> menuItems) { 449 Resources res = mContext.getResources(); 450 final int count = menuItems.size(); 451 for (int i = 0; i < count; i++) { 452 MenuItem item = menuItems.get(i); 453 String description = String.format( 454 res.getString(R.string.notification_menu_accessibility), 455 appName, item.getContentDescription()); 456 View menuView = item.getMenuView(); 457 if (menuView != null) { 458 menuView.setContentDescription(description); 459 } 460 } 461 } 462 463 @Override onParentHeightUpdate()464 public void onParentHeightUpdate() { 465 if (mParent == null 466 || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty()) 467 || mMenuContainer == null) { 468 return; 469 } 470 int parentHeight = mParent.getActualHeight(); 471 float translationY; 472 if (parentHeight < mVertSpaceForIcons) { 473 translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2); 474 } else { 475 translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2; 476 } 477 mMenuContainer.setTranslationY(translationY); 478 } 479 480 @Override onParentTranslationUpdate(float translation)481 public void onParentTranslationUpdate(float translation) { 482 mTranslation = translation; 483 if (mAnimating || !mMenuFadedIn) { 484 // Don't adjust when animating, or if the menu hasn't been shown yet. 485 return; 486 } 487 final float fadeThreshold = mParent.getWidth() * 0.3f; 488 final float absTrans = Math.abs(translation); 489 float desiredAlpha = 0; 490 if (absTrans == 0) { 491 desiredAlpha = 0; 492 } else if (absTrans <= fadeThreshold) { 493 desiredAlpha = 1; 494 } else { 495 desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold)); 496 } 497 setMenuAlpha(desiredAlpha); 498 } 499 500 @Override onClick(View v)501 public void onClick(View v) { 502 if (mMenuListener == null) { 503 // Nothing to do 504 return; 505 } 506 v.getLocationOnScreen(mIconLocation); 507 mParent.getLocationOnScreen(mParentLocation); 508 final int centerX = mHorizSpaceForIcon / 2; 509 final int centerY = v.getHeight() / 2; 510 final int x = mIconLocation[0] - mParentLocation[0] + centerX; 511 final int y = mIconLocation[1] - mParentLocation[1] + centerY; 512 if (mMenuItemsByView.containsKey(v)) { 513 mMenuListener.onMenuClicked(mParent, x, y, mMenuItemsByView.get(v)); 514 } 515 } 516 isMenuLocationChange()517 private boolean isMenuLocationChange() { 518 boolean onLeft = mTranslation > mIconPadding; 519 boolean onRight = mTranslation < -mIconPadding; 520 if ((isMenuOnLeft() && onRight) || (!isMenuOnLeft() && onLeft)) { 521 return true; 522 } 523 return false; 524 } 525 526 private void setMenuLocation() { 527 boolean showOnLeft = mTranslation > 0; 528 if ((mIconsPlaced && showOnLeft == isMenuOnLeft()) || isSnapping() || mMenuContainer == null 529 || !mMenuContainer.isAttachedToWindow()) { 530 // Do nothing 531 return; 532 } 533 boolean wasOnLeft = mOnLeft; 534 mOnLeft = showOnLeft; 535 if (wasOnLeft != showOnLeft) { 536 populateMenuViews(); 537 } 538 final int count = mMenuContainer.getChildCount(); 539 for (int i = 0; i < count; i++) { 540 final View v = mMenuContainer.getChildAt(i); 541 final float left = i * mHorizSpaceForIcon; 542 final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1)); 543 v.setX(showOnLeft ? left : right); 544 } 545 mIconsPlaced = true; 546 } 547 548 @VisibleForTesting setMenuAlpha(float alpha)549 protected void setMenuAlpha(float alpha) { 550 mAlpha = alpha; 551 if (mMenuContainer == null) { 552 return; 553 } 554 if (alpha == 0) { 555 mMenuFadedIn = false; // Can fade in again once it's gone. 556 mMenuContainer.setVisibility(View.INVISIBLE); 557 } else { 558 mMenuContainer.setVisibility(View.VISIBLE); 559 } 560 final int count = mMenuContainer.getChildCount(); 561 for (int i = 0; i < count; i++) { 562 mMenuContainer.getChildAt(i).setAlpha(mAlpha); 563 } 564 } 565 566 /** 567 * Returns the horizontal space in pixels required to display the menu. 568 */ 569 @VisibleForTesting getSpaceForMenu()570 protected int getSpaceForMenu() { 571 return mHorizSpaceForIcon * mMenuContainer.getChildCount(); 572 } 573 574 private final class CheckForDrag implements Runnable { 575 @Override run()576 public void run() { 577 final float absTransX = Math.abs(mTranslation); 578 final float bounceBackToMenuWidth = getSpaceForMenu(); 579 final float notiThreshold = mParent.getWidth() * 0.4f; 580 if ((!isMenuVisible() || isMenuLocationChange()) 581 && absTransX >= bounceBackToMenuWidth * 0.4 582 && absTransX < notiThreshold) { 583 fadeInMenu(notiThreshold); 584 } 585 } 586 } 587 fadeInMenu(final float notiThreshold)588 private void fadeInMenu(final float notiThreshold) { 589 if (mDismissing || mAnimating) { 590 return; 591 } 592 if (isMenuLocationChange()) { 593 setMenuAlpha(0f); 594 } 595 final float transX = mTranslation; 596 final boolean fromLeft = mTranslation > 0; 597 setMenuLocation(); 598 mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1); 599 mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 600 @Override 601 public void onAnimationUpdate(ValueAnimator animation) { 602 final float absTrans = Math.abs(transX); 603 604 boolean pastMenu = (fromLeft && transX <= notiThreshold) 605 || (!fromLeft && absTrans <= notiThreshold); 606 if (pastMenu && !mMenuFadedIn) { 607 setMenuAlpha((float) animation.getAnimatedValue()); 608 } 609 } 610 }); 611 mFadeAnimator.addListener(new AnimatorListenerAdapter() { 612 @Override 613 public void onAnimationStart(Animator animation) { 614 mAnimating = true; 615 } 616 617 @Override 618 public void onAnimationCancel(Animator animation) { 619 // TODO should animate back to 0f from current alpha 620 setMenuAlpha(0f); 621 } 622 623 @Override 624 public void onAnimationEnd(Animator animation) { 625 mAnimating = false; 626 mMenuFadedIn = mAlpha == 1; 627 } 628 }); 629 mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN); 630 mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION); 631 mFadeAnimator.start(); 632 } 633 634 @Override setMenuItems(ArrayList<MenuItem> items)635 public void setMenuItems(ArrayList<MenuItem> items) { 636 // Do nothing we use our own for now. 637 // TODO -- handle / allow custom menu items! 638 } 639 640 @Override shouldShowGutsOnSnapOpen()641 public boolean shouldShowGutsOnSnapOpen() { 642 return false; 643 } 644 645 @Override menuItemToExposeOnSnap()646 public MenuItem menuItemToExposeOnSnap() { 647 return null; 648 } 649 650 @Override getRevealAnimationOrigin()651 public Point getRevealAnimationOrigin() { 652 View v = mInfoItem.getMenuView(); 653 int menuX = v.getLeft() + v.getPaddingLeft() + (v.getWidth() / 2); 654 int menuY = v.getTop() + v.getPaddingTop() + (v.getHeight() / 2); 655 if (isMenuOnLeft()) { 656 return new Point(menuX, menuY); 657 } else { 658 menuX = mParent.getRight() - menuX; 659 return new Point(menuX, menuY); 660 } 661 } 662 createSnoozeItem(Context context)663 static MenuItem createSnoozeItem(Context context) { 664 Resources res = context.getResources(); 665 NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context) 666 .inflate(R.layout.notification_snooze, null, false); 667 String snoozeDescription = res.getString(R.string.notification_menu_snooze_description); 668 MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content, 669 R.drawable.ic_snooze); 670 return snooze; 671 } 672 createConversationItem(Context context)673 static NotificationMenuItem createConversationItem(Context context) { 674 Resources res = context.getResources(); 675 String infoDescription = res.getString(R.string.notification_menu_gear_description); 676 NotificationConversationInfo infoContent = 677 (NotificationConversationInfo) LayoutInflater.from(context).inflate( 678 R.layout.notification_conversation_info, null, false); 679 return new NotificationMenuItem(context, infoDescription, infoContent, 680 R.drawable.ic_settings); 681 } 682 createPartialConversationItem(Context context)683 static NotificationMenuItem createPartialConversationItem(Context context) { 684 Resources res = context.getResources(); 685 String infoDescription = res.getString(R.string.notification_menu_gear_description); 686 PartialConversationInfo infoContent = 687 (PartialConversationInfo) LayoutInflater.from(context).inflate( 688 R.layout.partial_conversation_info, null, false); 689 return new NotificationMenuItem(context, infoDescription, infoContent, 690 R.drawable.ic_settings); 691 } 692 createInfoItem(Context context)693 static NotificationMenuItem createInfoItem(Context context) { 694 Resources res = context.getResources(); 695 String infoDescription = res.getString(R.string.notification_menu_gear_description); 696 NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate( 697 R.layout.notification_info, null, false); 698 return new NotificationMenuItem(context, infoDescription, infoContent, 699 R.drawable.ic_settings); 700 } 701 createFeedbackItem(Context context)702 static MenuItem createFeedbackItem(Context context) { 703 FeedbackInfo feedbackContent = (FeedbackInfo) LayoutInflater.from(context).inflate( 704 R.layout.feedback_info, null, false); 705 MenuItem info = new NotificationMenuItem(context, null, feedbackContent, 706 -1 /*don't show in slow swipe menu */); 707 return info; 708 } 709 addMenuView(MenuItem item, ViewGroup parent)710 private void addMenuView(MenuItem item, ViewGroup parent) { 711 View menuView = item.getMenuView(); 712 if (menuView != null) { 713 menuView.setAlpha(mAlpha); 714 parent.addView(menuView); 715 menuView.setOnClickListener(this); 716 FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams(); 717 lp.width = mHorizSpaceForIcon; 718 lp.height = mHorizSpaceForIcon; 719 menuView.setLayoutParams(lp); 720 } 721 mMenuItemsByView.put(menuView, item); 722 } 723 724 @VisibleForTesting 725 /** 726 * Determine the minimum offset below which the menu should snap back closed. 727 */ getSnapBackThreshold()728 protected float getSnapBackThreshold() { 729 return getSpaceForMenu() - getMaximumSwipeDistance(); 730 } 731 732 /** 733 * Determine the maximum offset above which the parent notification should be dismissed. 734 * @return 735 */ 736 @VisibleForTesting getDismissThreshold()737 protected float getDismissThreshold() { 738 return getParent().getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION; 739 } 740 741 @Override isWithinSnapMenuThreshold()742 public boolean isWithinSnapMenuThreshold() { 743 float translation = getTranslation(); 744 float snapBackThreshold = getSnapBackThreshold(); 745 float targetRight = getDismissThreshold(); 746 return isMenuOnLeft() 747 ? translation > snapBackThreshold && translation < targetRight 748 : translation < -snapBackThreshold && translation > -targetRight; 749 } 750 751 @Override isSwipedEnoughToShowMenu()752 public boolean isSwipedEnoughToShowMenu() { 753 final float minimumSwipeDistance = getMinimumSwipeDistance(); 754 final float translation = getTranslation(); 755 return isMenuVisible() && (isMenuOnLeft() ? 756 translation > minimumSwipeDistance 757 : translation < -minimumSwipeDistance); 758 } 759 760 @Override getMenuSnapTarget()761 public int getMenuSnapTarget() { 762 return isMenuOnLeft() ? getSpaceForMenu() : -getSpaceForMenu(); 763 } 764 765 @Override shouldSnapBack()766 public boolean shouldSnapBack() { 767 float translation = getTranslation(); 768 float targetLeft = getSnapBackThreshold(); 769 return isMenuOnLeft() ? translation < targetLeft : translation > -targetLeft; 770 } 771 772 @Override isSnappedAndOnSameSide()773 public boolean isSnappedAndOnSameSide() { 774 return isMenuSnapped() && isMenuVisible() 775 && isMenuSnappedOnLeft() == isMenuOnLeft(); 776 } 777 778 @Override canBeDismissed()779 public boolean canBeDismissed() { 780 return getParent().canViewBeDismissed(); 781 } 782 783 public static class NotificationMenuItem implements MenuItem { 784 View mMenuView; 785 GutsContent mGutsContent; 786 String mContentDescription; 787 788 /** 789 * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu 790 * but can still be exposed via other affordances. 791 */ NotificationMenuItem(Context context, String contentDescription, GutsContent content, int iconResId)792 public NotificationMenuItem(Context context, String contentDescription, GutsContent content, 793 int iconResId) { 794 Resources res = context.getResources(); 795 int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); 796 int tint = res.getColor(R.color.notification_gear_color); 797 if (iconResId >= 0) { 798 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context); 799 iv.setPadding(padding, padding, padding, padding); 800 Drawable icon = context.getResources().getDrawable(iconResId); 801 iv.setImageDrawable(icon); 802 iv.setColorFilter(tint); 803 iv.setAlpha(1f); 804 mMenuView = iv; 805 } 806 mContentDescription = contentDescription; 807 mGutsContent = content; 808 } 809 810 @Override 811 @Nullable getMenuView()812 public View getMenuView() { 813 return mMenuView; 814 } 815 816 @Override getGutsView()817 public View getGutsView() { 818 return mGutsContent.getContentView(); 819 } 820 821 @Override getContentDescription()822 public String getContentDescription() { 823 return mContentDescription; 824 } 825 } 826 } 827