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