| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.launcher3.popup; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.LayoutTransition; |
| import android.animation.ObjectAnimator; |
| import android.animation.TimeInterpolator; |
| import android.animation.ValueAnimator; |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.CornerPathEffect; |
| import android.graphics.Outline; |
| import android.graphics.Paint; |
| import android.graphics.Point; |
| import android.graphics.PointF; |
| import android.graphics.Rect; |
| import android.graphics.drawable.ShapeDrawable; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.util.AttributeSet; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.ViewOutlineProvider; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.animation.AccelerateDecelerateInterpolator; |
| import android.widget.ImageView; |
| |
| import com.android.launcher3.AbstractFloatingView; |
| import com.android.launcher3.BubbleTextView; |
| import com.android.launcher3.DragSource; |
| import com.android.launcher3.DropTarget; |
| import com.android.launcher3.DropTarget.DragObject; |
| import com.android.launcher3.ItemInfo; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.LauncherAnimUtils; |
| import com.android.launcher3.LauncherModel; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; |
| import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate; |
| import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; |
| import com.android.launcher3.badge.BadgeInfo; |
| import com.android.launcher3.dragndrop.DragController; |
| import com.android.launcher3.dragndrop.DragLayer; |
| import com.android.launcher3.dragndrop.DragOptions; |
| import com.android.launcher3.dragndrop.DragView; |
| import com.android.launcher3.graphics.IconPalette; |
| import com.android.launcher3.graphics.TriangleShape; |
| import com.android.launcher3.logging.LoggerUtils; |
| import com.android.launcher3.notification.NotificationInfo; |
| import com.android.launcher3.notification.NotificationItemView; |
| import com.android.launcher3.notification.NotificationKeyData; |
| import com.android.launcher3.shortcuts.DeepShortcutManager; |
| import com.android.launcher3.shortcuts.DeepShortcutView; |
| import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; |
| import com.android.launcher3.util.PackageUserKey; |
| import com.android.launcher3.util.Themes; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; |
| import static com.android.launcher3.notification.NotificationMainView.NOTIFICATION_ITEM_INFO; |
| import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS; |
| import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS; |
| import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; |
| import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType; |
| import static com.android.launcher3.userevent.nano.LauncherLogProto.Target; |
| |
| /** |
| * A container for shortcuts to deep links and notifications associated with an app. |
| */ |
| @TargetApi(Build.VERSION_CODES.N) |
| public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource, |
| DragController.DragListener, View.OnLongClickListener, |
| View.OnTouchListener { |
| |
| private final List<DeepShortcutView> mShortcuts = new ArrayList<>(); |
| private final PointF mInterceptTouchDown = new PointF(); |
| private final Rect mTempRect = new Rect(); |
| private final Point mIconLastTouchPos = new Point(); |
| |
| private final int mStartDragThreshold; |
| private final LayoutInflater mInflater; |
| private final float mOutlineRadius; |
| private final Launcher mLauncher; |
| private final LauncherAccessibilityDelegate mAccessibilityDelegate; |
| private final boolean mIsRtl; |
| |
| private final int mArrayOffset; |
| private final View mArrow; |
| |
| private BubbleTextView mOriginalIcon; |
| private NotificationItemView mNotificationItemView; |
| |
| private ViewGroup mSystemShortcutContainer; |
| |
| private boolean mIsLeftAligned; |
| protected boolean mIsAboveIcon; |
| private int mNumNotifications; |
| private int mGravity; |
| |
| protected Animator mOpenCloseAnimator; |
| protected boolean mDeferContainerRemoval; |
| private final Rect mStartRect = new Rect(); |
| private final Rect mEndRect = new Rect(); |
| |
| public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| mStartDragThreshold = getResources().getDimensionPixelSize( |
| R.dimen.deep_shortcuts_start_drag_threshold); |
| mInflater = LayoutInflater.from(context); |
| mOutlineRadius = getResources().getDimension(R.dimen.bg_round_rect_radius); |
| mLauncher = Launcher.getLauncher(context); |
| mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher); |
| mIsRtl = Utilities.isRtl(getResources()); |
| |
| setClipToOutline(true); |
| setOutlineProvider(new ViewOutlineProvider() { |
| @Override |
| public void getOutline(View view, Outline outline) { |
| outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius); |
| } |
| }); |
| |
| // Initialize arrow view |
| final Resources resources = getResources(); |
| final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width); |
| final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height); |
| mArrow = new View(context); |
| mArrow.setLayoutParams(new DragLayer.LayoutParams(arrowWidth, arrowHeight)); |
| mArrayOffset = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset); |
| } |
| |
| public PopupContainerWithArrow(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public PopupContainerWithArrow(Context context) { |
| this(context, null, 0); |
| } |
| |
| public LauncherAccessibilityDelegate getAccessibilityDelegate() { |
| return mAccessibilityDelegate; |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| if (ev.getAction() == MotionEvent.ACTION_DOWN) { |
| mInterceptTouchDown.set(ev.getX(), ev.getY()); |
| } |
| if (mNotificationItemView != null |
| && mNotificationItemView.onInterceptTouchEvent(ev)) { |
| return true; |
| } |
| // Stop sending touch events to deep shortcut views if user moved beyond touch slop. |
| return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY()) |
| > ViewConfiguration.get(getContext()).getScaledTouchSlop(); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| if (mNotificationItemView != null) { |
| return mNotificationItemView.onTouchEvent(ev); |
| } |
| return super.onTouchEvent(ev); |
| } |
| |
| @Override |
| protected boolean isOfType(int type) { |
| return (type & TYPE_ACTION_POPUP) != 0; |
| } |
| |
| @Override |
| public void logActionCommand(int command) { |
| mLauncher.getUserEventDispatcher().logActionCommand( |
| command, mOriginalIcon, ContainerType.DEEPSHORTCUTS); |
| } |
| |
| @Override |
| public boolean onControllerInterceptTouchEvent(MotionEvent ev) { |
| if (ev.getAction() == MotionEvent.ACTION_DOWN) { |
| DragLayer dl = mLauncher.getDragLayer(); |
| if (!dl.isEventOverView(this, ev)) { |
| mLauncher.getUserEventDispatcher().logActionTapOutside( |
| LoggerUtils.newContainerTarget(ContainerType.DEEPSHORTCUTS)); |
| close(true); |
| |
| // We let touches on the original icon go through so that users can launch |
| // the app with one tap if they don't find a shortcut they want. |
| return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev); |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| protected void handleClose(boolean animate) { |
| if (animate) { |
| animateClose(); |
| } else { |
| closeComplete(); |
| } |
| } |
| |
| public <T extends View> T inflateAndAdd(int resId) { |
| View view = mInflater.inflate(resId, this, false); |
| addView(view); |
| return (T) view; |
| } |
| |
| /** |
| * Shows the notifications and deep shortcuts associated with {@param icon}. |
| * @return the container if shown or null. |
| */ |
| public static PopupContainerWithArrow showForIcon(BubbleTextView icon) { |
| Launcher launcher = Launcher.getLauncher(icon.getContext()); |
| if (getOpen(launcher) != null) { |
| // There is already an items container open, so don't open this one. |
| icon.clearFocus(); |
| return null; |
| } |
| ItemInfo itemInfo = (ItemInfo) icon.getTag(); |
| if (!DeepShortcutManager.supportsShortcuts(itemInfo)) { |
| return null; |
| } |
| |
| PopupDataProvider popupDataProvider = launcher.getPopupDataProvider(); |
| List<String> shortcutIds = popupDataProvider.getShortcutIdsForItem(itemInfo); |
| List<NotificationKeyData> notificationKeys = popupDataProvider |
| .getNotificationKeysForItem(itemInfo); |
| List<SystemShortcut> systemShortcuts = popupDataProvider |
| .getEnabledSystemShortcutsForItem(itemInfo); |
| |
| final PopupContainerWithArrow container = |
| (PopupContainerWithArrow) launcher.getLayoutInflater().inflate( |
| R.layout.popup_container, launcher.getDragLayer(), false); |
| container.populateAndShow(icon, shortcutIds, notificationKeys, systemShortcuts); |
| return container; |
| } |
| |
| private void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds, |
| final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) { |
| mNumNotifications = notificationKeys.size(); |
| |
| setVisibility(View.INVISIBLE); |
| mLauncher.getDragLayer().addView(this); |
| |
| mOriginalIcon = originalIcon; |
| |
| // Add views |
| if (mNumNotifications > 0) { |
| // Add notification entries |
| View.inflate(getContext(), R.layout.notification_content, this); |
| mNotificationItemView = new NotificationItemView(this); |
| if (mNumNotifications == 1) { |
| mNotificationItemView.removeFooter(); |
| } |
| updateNotificationHeader(); |
| } |
| int viewsToFlip = getChildCount(); |
| mSystemShortcutContainer = this; |
| |
| if (!shortcutIds.isEmpty()) { |
| if (mNotificationItemView != null) { |
| mNotificationItemView.addGutter(); |
| } |
| |
| for (int i = shortcutIds.size(); i > 0; i--) { |
| mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut)); |
| } |
| updateHiddenShortcuts(); |
| |
| if (!systemShortcuts.isEmpty()) { |
| mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons); |
| for (SystemShortcut shortcut : systemShortcuts) { |
| View view = mInflater.inflate(R.layout.system_shortcut_icon_only, |
| mSystemShortcutContainer, false); |
| mSystemShortcutContainer.addView(view); |
| initializeSystemShortcut(view, shortcut); |
| } |
| } |
| } else if (!systemShortcuts.isEmpty()) { |
| if (mNotificationItemView != null) { |
| mNotificationItemView.addGutter(); |
| } |
| |
| for (SystemShortcut shortcut : systemShortcuts) { |
| initializeSystemShortcut(inflateAndAdd(R.layout.system_shortcut), shortcut); |
| } |
| } |
| orientAboutIcon(); |
| |
| boolean reverseOrder = mIsAboveIcon; |
| if (reverseOrder) { |
| int count = getChildCount(); |
| ArrayList<View> allViews = new ArrayList<>(count); |
| for (int i = 0; i < count; i++) { |
| if (i == viewsToFlip) { |
| Collections.reverse(allViews); |
| } |
| allViews.add(getChildAt(i)); |
| } |
| Collections.reverse(allViews); |
| removeAllViews(); |
| for (int i = 0; i < count; i++) { |
| addView(allViews.get(i)); |
| } |
| if (mNotificationItemView != null) { |
| mNotificationItemView.inverseGutterMargin(); |
| } |
| |
| orientAboutIcon(); |
| } |
| updateDividers(); |
| |
| // Add the arrow. |
| final int arrowHorizontalOffset = getResources().getDimensionPixelSize(isAlignedWithStart() |
| ? R.dimen.popup_arrow_horizontal_offset_start |
| : R.dimen.popup_arrow_horizontal_offset_end); |
| mLauncher.getDragLayer().addView(mArrow); |
| DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams(); |
| if (mIsLeftAligned) { |
| mArrow.setX(getX() + arrowHorizontalOffset); |
| } else { |
| mArrow.setX(getX() + getMeasuredWidth() - arrowHorizontalOffset); |
| } |
| |
| if (Gravity.isVertical(mGravity)) { |
| // This is only true if there wasn't room for the container next to the icon, |
| // so we centered it instead. In that case we don't want to show the arrow. |
| mArrow.setVisibility(INVISIBLE); |
| } else { |
| ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create( |
| arrowLp.width, arrowLp.height, !mIsAboveIcon)); |
| Paint arrowPaint = arrowDrawable.getPaint(); |
| arrowPaint.setColor(Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary)); |
| // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable. |
| int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius); |
| arrowPaint.setPathEffect(new CornerPathEffect(radius)); |
| mArrow.setBackground(arrowDrawable); |
| mArrow.setElevation(getElevation()); |
| } |
| |
| mArrow.setPivotX(arrowLp.width / 2); |
| mArrow.setPivotY(mIsAboveIcon ? 0 : arrowLp.height); |
| |
| animateOpen(); |
| |
| ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag(); |
| int numShortcuts = mShortcuts.size() + systemShortcuts.size(); |
| if (mNumNotifications == 0) { |
| setContentDescription(getContext().getString(R.string.shortcuts_menu_description, |
| numShortcuts, originalIcon.getContentDescription().toString())); |
| } else { |
| setContentDescription(getContext().getString( |
| R.string.shortcuts_menu_with_notifications_description, numShortcuts, |
| mNumNotifications, originalIcon.getContentDescription().toString())); |
| } |
| |
| mLauncher.getDragController().addDragListener(this); |
| mOriginalIcon.forceHideBadge(true); |
| |
| // All views are added. Animate layout from now on. |
| setLayoutTransition(new LayoutTransition()); |
| |
| // Load the shortcuts on a background thread and update the container as it animates. |
| final Looper workerLooper = LauncherModel.getWorkerLooper(); |
| new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable( |
| mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()), |
| this, shortcutIds, mShortcuts, notificationKeys)); |
| } |
| |
| protected boolean isAlignedWithStart() { |
| return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl; |
| } |
| |
| /** |
| * Orients this container above or below the given icon, aligning with the left or right. |
| * |
| * These are the preferred orientations, in order (RTL prefers right-aligned over left): |
| * - Above and left-aligned |
| * - Above and right-aligned |
| * - Below and left-aligned |
| * - Below and right-aligned |
| * |
| * So we always align left if there is enough horizontal space |
| * and align above if there is enough vertical space. |
| */ |
| protected void orientAboutIcon() { |
| measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); |
| int width = getMeasuredWidth(); |
| int extraVerticalSpace = mArrow.getLayoutParams().height + mArrayOffset |
| + getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding); |
| int height = getMeasuredHeight() + extraVerticalSpace; |
| |
| DragLayer dragLayer = mLauncher.getDragLayer(); |
| dragLayer.getDescendantRectRelativeToSelf(mOriginalIcon, mTempRect); |
| Rect insets = dragLayer.getInsets(); |
| |
| // Align left (right in RTL) if there is room. |
| int leftAlignedX = mTempRect.left + mOriginalIcon.getPaddingLeft(); |
| int rightAlignedX = mTempRect.right - width - mOriginalIcon.getPaddingRight(); |
| int x = leftAlignedX; |
| boolean canBeLeftAligned = leftAlignedX + width + insets.left |
| < dragLayer.getRight() - insets.right; |
| boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left; |
| if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) { |
| x = rightAlignedX; |
| } |
| mIsLeftAligned = x == leftAlignedX; |
| if (mIsRtl) { |
| x -= dragLayer.getWidth() - width; |
| } |
| |
| // Offset x so that the arrow and shortcut icons are center-aligned with the original icon. |
| int iconWidth = mOriginalIcon.getWidth() |
| - mOriginalIcon.getTotalPaddingLeft() - mOriginalIcon.getTotalPaddingRight(); |
| iconWidth *= mOriginalIcon.getScaleX(); |
| Resources resources = getResources(); |
| int xOffset; |
| if (isAlignedWithStart()) { |
| // Aligning with the shortcut icon. |
| int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size); |
| int shortcutPaddingStart = resources.getDimensionPixelSize( |
| R.dimen.popup_padding_start); |
| xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart; |
| } else { |
| // Aligning with the drag handle. |
| int shortcutDragHandleWidth = resources.getDimensionPixelSize( |
| R.dimen.deep_shortcut_drag_handle_size); |
| int shortcutPaddingEnd = resources.getDimensionPixelSize( |
| R.dimen.popup_padding_end); |
| xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd; |
| } |
| x += mIsLeftAligned ? xOffset : -xOffset; |
| |
| // Open above icon if there is room. |
| int iconHeight = getIconHeightForPopupPlacement(); |
| int y = mTempRect.top + mOriginalIcon.getPaddingTop() - height; |
| mIsAboveIcon = y > dragLayer.getTop() + insets.top; |
| if (!mIsAboveIcon) { |
| y = mTempRect.top + mOriginalIcon.getPaddingTop() + iconHeight + extraVerticalSpace; |
| } |
| |
| // Insets are added later, so subtract them now. |
| if (mIsRtl) { |
| x += insets.right; |
| } else { |
| x -= insets.left; |
| } |
| y -= insets.top; |
| |
| mGravity = 0; |
| if (y + height > dragLayer.getBottom() - insets.bottom) { |
| // The container is opening off the screen, so just center it in the drag layer instead. |
| mGravity = Gravity.CENTER_VERTICAL; |
| // Put the container next to the icon, preferring the right side in ltr (left in rtl). |
| int rightSide = leftAlignedX + iconWidth - insets.left; |
| int leftSide = rightAlignedX - iconWidth - insets.left; |
| if (!mIsRtl) { |
| if (rightSide + width < dragLayer.getRight()) { |
| x = rightSide; |
| mIsLeftAligned = true; |
| } else { |
| x = leftSide; |
| mIsLeftAligned = false; |
| } |
| } else { |
| if (leftSide > dragLayer.getLeft()) { |
| x = leftSide; |
| mIsLeftAligned = false; |
| } else { |
| x = rightSide; |
| mIsLeftAligned = true; |
| } |
| } |
| mIsAboveIcon = true; |
| } |
| |
| setX(x); |
| if (Gravity.isVertical(mGravity)) { |
| return; |
| } |
| |
| DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); |
| DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams(); |
| if (mIsAboveIcon) { |
| arrowLp.gravity = lp.gravity = Gravity.BOTTOM; |
| lp.bottomMargin = |
| mLauncher.getDragLayer().getHeight() - y - getMeasuredHeight() - insets.top; |
| arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrayOffset - insets.bottom; |
| } else { |
| arrowLp.gravity = lp.gravity = Gravity.TOP; |
| lp.topMargin = y + insets.top; |
| arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrayOffset; |
| } |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| super.onLayout(changed, l, t, r, b); |
| |
| // enforce contained is within screen |
| DragLayer dragLayer = mLauncher.getDragLayer(); |
| if (getTranslationX() + l < 0 || |
| getTranslationX() + r > dragLayer.getWidth()) { |
| // If we are still off screen, center horizontally too. |
| mGravity |= Gravity.CENTER_HORIZONTAL; |
| } |
| |
| if (Gravity.isHorizontal(mGravity)) { |
| setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2); |
| mArrow.setVisibility(INVISIBLE); |
| } |
| if (Gravity.isVertical(mGravity)) { |
| setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2); |
| } |
| } |
| |
| protected void animateOpen() { |
| setVisibility(View.VISIBLE); |
| mIsOpen = true; |
| |
| final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet(); |
| final Resources res = getResources(); |
| final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration); |
| final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator(); |
| |
| // Rectangular reveal. |
| final ValueAnimator revealAnim = createOpenCloseOutlineProvider() |
| .createRevealAnimator(this, false); |
| revealAnim.setDuration(revealDuration); |
| revealAnim.setInterpolator(revealInterpolator); |
| |
| Animator fadeIn = ObjectAnimator.ofFloat(this, ALPHA, 0, 1); |
| fadeIn.setDuration(revealDuration); |
| fadeIn.setInterpolator(revealInterpolator); |
| openAnim.play(fadeIn); |
| |
| // Animate the arrow. |
| mArrow.setScaleX(0); |
| mArrow.setScaleY(0); |
| Animator arrowScale = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 1) |
| .setDuration(res.getInteger(R.integer.config_popupArrowOpenDuration)); |
| |
| openAnim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mOpenCloseAnimator = null; |
| sendCustomAccessibilityEvent( |
| PopupContainerWithArrow.this, |
| AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, |
| getContext().getString(R.string.action_deep_shortcut)); |
| } |
| }); |
| |
| mOpenCloseAnimator = openAnim; |
| openAnim.playSequentially(revealAnim, arrowScale); |
| openAnim.start(); |
| } |
| |
| public void applyNotificationInfos(List<NotificationInfo> notificationInfos) { |
| mNotificationItemView.applyNotificationInfos(notificationInfos); |
| } |
| |
| private void updateHiddenShortcuts() { |
| int allowedCount = mNotificationItemView != null |
| ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS; |
| int originalHeight = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height); |
| int itemHeight = mNotificationItemView != null ? |
| getResources().getDimensionPixelSize(R.dimen.bg_popup_item_condensed_height) |
| : originalHeight; |
| float iconScale = ((float) itemHeight) / originalHeight; |
| |
| int total = mShortcuts.size(); |
| for (int i = 0; i < total; i++) { |
| DeepShortcutView view = mShortcuts.get(i); |
| view.setVisibility(i >= allowedCount ? GONE : VISIBLE); |
| view.getLayoutParams().height = itemHeight; |
| view.getIconView().setScaleX(iconScale); |
| view.getIconView().setScaleY(iconScale); |
| } |
| } |
| |
| private void updateDividers() { |
| int count = getChildCount(); |
| DeepShortcutView lastView = null; |
| for (int i = 0; i < count; i++) { |
| View view = getChildAt(i); |
| if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) { |
| if (lastView != null) { |
| lastView.setDividerVisibility(VISIBLE); |
| } |
| lastView = (DeepShortcutView) view; |
| lastView.setDividerVisibility(INVISIBLE); |
| } |
| } |
| } |
| |
| @Override |
| protected void onWidgetsBound() { |
| ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); |
| SystemShortcut widgetInfo = new SystemShortcut.Widgets(); |
| View.OnClickListener onClickListener = widgetInfo.getOnClickListener(mLauncher, itemInfo); |
| View widgetsView = null; |
| int count = mSystemShortcutContainer.getChildCount(); |
| for (int i = 0; i < count; i++) { |
| View systemShortcutView = mSystemShortcutContainer.getChildAt(i); |
| if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) { |
| widgetsView = systemShortcutView; |
| break; |
| } |
| } |
| |
| if (onClickListener != null && widgetsView == null) { |
| // We didn't have any widgets cached but now there are some, so enable the shortcut. |
| if (mSystemShortcutContainer != this) { |
| View view = mInflater.inflate(R.layout.system_shortcut_icon_only, |
| mSystemShortcutContainer, false); |
| mSystemShortcutContainer.addView(view); |
| initializeSystemShortcut(view, widgetInfo); |
| } else { |
| // If using the expanded system shortcut (as opposed to just the icon), we need to |
| // reopen the container to ensure measurements etc. all work out. While this could |
| // be quite janky, in practice the user would typically see a small flicker as the |
| // animation restarts partway through, and this is a very rare edge case anyway. |
| ((PopupContainerWithArrow) getParent()).close(false); |
| PopupContainerWithArrow.showForIcon(mOriginalIcon); |
| } |
| } else if (onClickListener == null && widgetsView != null) { |
| // No widgets exist, but we previously added the shortcut so remove it. |
| if (mSystemShortcutContainer != this) { |
| mSystemShortcutContainer.removeView(widgetsView); |
| } else { |
| ((PopupContainerWithArrow) getParent()).close(false); |
| PopupContainerWithArrow.showForIcon(mOriginalIcon); |
| } |
| } |
| } |
| |
| private void initializeSystemShortcut(View view, SystemShortcut info) { |
| if (view instanceof DeepShortcutView) { |
| // Expanded system shortcut, with both icon and text shown on white background. |
| final DeepShortcutView shortcutView = (DeepShortcutView) view; |
| shortcutView.getIconView().setBackgroundResource(info.iconResId); |
| shortcutView.getBubbleText().setText(info.labelResId); |
| } else if (view instanceof ImageView) { |
| // Only the system shortcut icon shows on a gray background header. |
| final ImageView shortcutIcon = (ImageView) view; |
| shortcutIcon.setImageResource(info.iconResId); |
| shortcutIcon.setContentDescription(getContext().getText(info.labelResId)); |
| } |
| view.setTag(info); |
| view.setOnClickListener(info.getOnClickListener(mLauncher, |
| (ItemInfo) mOriginalIcon.getTag())); |
| } |
| |
| protected int getIconHeightForPopupPlacement() { |
| return mOriginalIcon.getIcon() != null |
| ? mOriginalIcon.getIcon().getBounds().height() |
| : mOriginalIcon.getHeight(); |
| } |
| |
| /** |
| * Determines when the deferred drag should be started. |
| * |
| * Current behavior: |
| * - Start the drag if the touch passes a certain distance from the original touch down. |
| */ |
| public DragOptions.PreDragCondition createPreDragCondition() { |
| return new DragOptions.PreDragCondition() { |
| |
| @Override |
| public boolean shouldStartDrag(double distanceDragged) { |
| return distanceDragged > mStartDragThreshold; |
| } |
| |
| @Override |
| public void onPreDragStart(DropTarget.DragObject dragObject) { |
| if (mIsAboveIcon) { |
| // Hide only the icon, keep the text visible. |
| mOriginalIcon.setIconVisible(false); |
| mOriginalIcon.setVisibility(VISIBLE); |
| } else { |
| // Hide both the icon and text. |
| mOriginalIcon.setVisibility(INVISIBLE); |
| } |
| } |
| |
| @Override |
| public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) { |
| mOriginalIcon.setIconVisible(true); |
| if (dragStarted) { |
| // Make sure we keep the original icon hidden while it is being dragged. |
| mOriginalIcon.setVisibility(INVISIBLE); |
| } else { |
| mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon); |
| if (!mIsAboveIcon) { |
| // Show the icon but keep the text hidden. |
| mOriginalIcon.setVisibility(VISIBLE); |
| mOriginalIcon.setTextVisibility(false); |
| } |
| } |
| } |
| }; |
| } |
| |
| /** |
| * Updates the notification header if the original icon's badge updated. |
| */ |
| public void updateNotificationHeader(Set<PackageUserKey> updatedBadges) { |
| ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); |
| PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo); |
| if (updatedBadges.contains(packageUser)) { |
| updateNotificationHeader(); |
| } |
| } |
| |
| private void updateNotificationHeader() { |
| ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); |
| BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo); |
| if (mNotificationItemView != null && badgeInfo != null) { |
| IconPalette palette = mOriginalIcon.getBadgePalette(); |
| mNotificationItemView.updateHeader(badgeInfo.getNotificationCount(), palette); |
| } |
| } |
| |
| public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) { |
| if (mNotificationItemView == null) { |
| return; |
| } |
| ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag(); |
| BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo)); |
| if (badgeInfo == null || badgeInfo.getNotificationKeys().size() == 0) { |
| // No more notifications, remove the notification views and expand all shortcuts. |
| mNotificationItemView.removeAllViews(); |
| mNotificationItemView = null; |
| updateHiddenShortcuts(); |
| updateDividers(); |
| } else { |
| mNotificationItemView.trimNotifications( |
| NotificationKeyData.extractKeysOnly(badgeInfo.getNotificationKeys())); |
| } |
| } |
| |
| @Override |
| public void onDropCompleted(View target, DragObject d, boolean success) { } |
| |
| @Override |
| public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { |
| // Either the original icon or one of the shortcuts was dragged. |
| // Hide the container, but don't remove it yet because that interferes with touch events. |
| mDeferContainerRemoval = true; |
| animateClose(); |
| } |
| |
| @Override |
| public void onDragEnd() { |
| if (!mIsOpen) { |
| if (mOpenCloseAnimator != null) { |
| // Close animation is running. |
| mDeferContainerRemoval = false; |
| } else { |
| // Close animation is not running. |
| if (mDeferContainerRemoval) { |
| closeComplete(); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { |
| if (info == NOTIFICATION_ITEM_INFO) { |
| target.itemType = ItemType.NOTIFICATION; |
| } else { |
| target.itemType = ItemType.DEEPSHORTCUT; |
| target.rank = info.rank; |
| } |
| targetParent.containerType = ContainerType.DEEPSHORTCUTS; |
| } |
| |
| protected void animateClose() { |
| if (!mIsOpen) { |
| return; |
| } |
| mEndRect.setEmpty(); |
| if (mOpenCloseAnimator != null) { |
| Outline outline = new Outline(); |
| getOutlineProvider().getOutline(this, outline); |
| outline.getRect(mEndRect); |
| mOpenCloseAnimator.cancel(); |
| } |
| mIsOpen = false; |
| |
| final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet(); |
| // Hide the arrow |
| closeAnim.play(ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 0)); |
| closeAnim.play(ObjectAnimator.ofFloat(mArrow, ALPHA, 0)); |
| |
| // Animate original icon's text back in. |
| closeAnim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */)); |
| mOriginalIcon.forceHideBadge(false); |
| |
| final Resources res = getResources(); |
| final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator(); |
| |
| // Rectangular reveal (reversed). |
| final ValueAnimator revealAnim = createOpenCloseOutlineProvider() |
| .createRevealAnimator(this, true); |
| revealAnim.setInterpolator(revealInterpolator); |
| closeAnim.play(revealAnim); |
| |
| Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0); |
| fadeOut.setInterpolator(revealInterpolator); |
| closeAnim.play(fadeOut); |
| closeAnim.setDuration((long) res.getInteger(R.integer.config_popupOpenCloseDuration)); |
| |
| closeAnim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mOpenCloseAnimator = null; |
| if (mDeferContainerRemoval) { |
| setVisibility(INVISIBLE); |
| } else { |
| closeComplete(); |
| } |
| } |
| }); |
| mOpenCloseAnimator = closeAnim; |
| closeAnim.start(); |
| } |
| |
| private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { |
| int arrowCenterX = getResources().getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ? |
| R.dimen.popup_arrow_horizontal_center_start: |
| R.dimen.popup_arrow_horizontal_center_end); |
| if (!mIsLeftAligned) { |
| arrowCenterX = getMeasuredWidth() - arrowCenterX; |
| } |
| int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0; |
| |
| mStartRect.set(arrowCenterX, arrowCenterY, arrowCenterX, arrowCenterY); |
| if (mEndRect.isEmpty()) { |
| mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); |
| } |
| |
| return new RoundedRectRevealOutlineProvider |
| (mOutlineRadius, mOutlineRadius, mStartRect, mEndRect); |
| } |
| |
| /** |
| * Closes the popup without animation. |
| */ |
| private void closeComplete() { |
| mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible()); |
| mOriginalIcon.forceHideBadge(false); |
| |
| mLauncher.getDragController().removeDragListener(this); |
| if (mOpenCloseAnimator != null) { |
| mOpenCloseAnimator.cancel(); |
| mOpenCloseAnimator = null; |
| } |
| mIsOpen = false; |
| mDeferContainerRemoval = false; |
| mLauncher.getDragLayer().removeView(this); |
| mLauncher.getDragLayer().removeView(mArrow); |
| } |
| |
| @Override |
| public boolean onTouch(View v, MotionEvent ev) { |
| // Touched a shortcut, update where it was touched so we can drag from there on long click. |
| switch (ev.getAction()) { |
| case MotionEvent.ACTION_DOWN: |
| case MotionEvent.ACTION_MOVE: |
| mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); |
| break; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean onLongClick(View v) { |
| // Return early if not the correct view |
| if (!(v.getParent() instanceof DeepShortcutView)) return false; |
| // Return early if global dragging is not enabled |
| if (!mLauncher.isDraggingEnabled()) return false; |
| // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts) |
| if (mLauncher.getDragController().isDragging()) return false; |
| |
| // Long clicked on a shortcut. |
| DeepShortcutView sv = (DeepShortcutView) v.getParent(); |
| sv.setWillDrawIcon(false); |
| |
| // Move the icon to align with the center-top of the touch point |
| Point iconShift = new Point(); |
| iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; |
| iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx; |
| |
| DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(), |
| this, sv.getFinalInfo(), |
| new ShortcutDragPreviewProvider(sv.getIconView(), iconShift), new DragOptions()); |
| dv.animateShift(-iconShift.x, -iconShift.y); |
| |
| // TODO: support dragging from within folder without having to close it |
| AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER); |
| return false; |
| } |
| |
| /** |
| * Returns a PopupContainerWithArrow which is already open or null |
| */ |
| public static PopupContainerWithArrow getOpen(Launcher launcher) { |
| return getOpenView(launcher, TYPE_ACTION_POPUP); |
| } |
| } |