summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/popup
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/launcher3/popup')
-rw-r--r--src/com/android/launcher3/popup/PopupContainerWithArrow.java732
-rw-r--r--src/com/android/launcher3/popup/PopupDataProvider.java2
-rw-r--r--src/com/android/launcher3/popup/PopupItemView.java208
-rw-r--r--src/com/android/launcher3/popup/PopupPopulator.java179
4 files changed, 1120 insertions, 1 deletions
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
new file mode 100644
index 000000000..95d51dc9c
--- /dev/null
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -0,0 +1,732 @@
+/*
+ * 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.TimeInterpolator;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Color;
+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.accessibility.AccessibilityEvent;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherViewPropertyAnimator;
+import com.android.launcher3.LogAccelerateInterpolator;
+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.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.shortcuts.DeepShortcutView;
+import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
+import com.android.launcher3.util.PackageUserKey;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import static com.android.launcher3.userevent.nano.LauncherLogProto.*;
+
+/**
+ * A container for shortcuts to deep links within apps.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class PopupContainerWithArrow extends AbstractFloatingView
+ implements View.OnLongClickListener, View.OnTouchListener, DragSource,
+ DragController.DragListener {
+
+ private final Point mIconShift = new Point();
+ private final Point mIconLastTouchPos = new Point();
+
+ protected final Launcher mLauncher;
+ private final int mStartDragThreshold;
+ private LauncherAccessibilityDelegate mAccessibilityDelegate;
+ private final boolean mIsRtl;
+
+ protected BubbleTextView mOriginalIcon;
+ private final Rect mTempRect = new Rect();
+ private PointF mInterceptTouchDown = new PointF();
+ private boolean mIsLeftAligned;
+ protected boolean mIsAboveIcon;
+ private View mArrow;
+
+ protected Animator mOpenCloseAnimator;
+ private boolean mDeferContainerRemoval;
+
+ public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mLauncher = Launcher.getLauncher(context);
+
+ mStartDragThreshold = getResources().getDimensionPixelSize(
+ R.dimen.deep_shortcuts_start_drag_threshold);
+ // TODO: make sure the delegate works for all items, not just shortcuts.
+ mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
+ mIsRtl = Utilities.isRtl(getResources());
+ }
+
+ public PopupContainerWithArrow(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PopupContainerWithArrow(Context context) {
+ this(context, null, 0);
+ }
+
+ public LauncherAccessibilityDelegate getAccessibilityDelegate() {
+ return mAccessibilityDelegate;
+ }
+
+ /**
+ * 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();
+ List<String> shortcutIds = launcher.getPopupDataProvider().getShortcutIdsForItem(itemInfo);
+ if (shortcutIds.size() > 0) {
+ final PopupContainerWithArrow container =
+ (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
+ R.layout.popup_container, launcher.getDragLayer(), false);
+ container.setVisibility(View.INVISIBLE);
+ launcher.getDragLayer().addView(container);
+ container.populateAndShow(icon, shortcutIds);
+ return container;
+ }
+ return null;
+ }
+
+ public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds) {
+ final Resources resources = getResources();
+ final int arrowWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_width);
+ final int arrowHeight = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_height);
+ final int arrowHorizontalOffset = resources.getDimensionPixelSize(
+ R.dimen.deep_shortcuts_arrow_horizontal_offset);
+ final int arrowVerticalOffset = resources.getDimensionPixelSize(
+ R.dimen.deep_shortcuts_arrow_vertical_offset);
+
+ // Add dummy views first, and populate with real info when ready.
+ PopupPopulator.Item[] itemsToPopulate = PopupPopulator.getItemsToPopulate(shortcutIds);
+ addDummyViews(originalIcon, itemsToPopulate);
+
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
+
+ boolean reverseOrder = mIsAboveIcon;
+ if (reverseOrder) {
+ removeAllViews();
+ itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
+ addDummyViews(originalIcon, itemsToPopulate);
+
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
+ }
+
+ List<DeepShortcutView> shortcutViews = new ArrayList<>();
+ for (int i = 0; i < getChildCount(); i++) {
+ View item = getChildAt(i);
+ switch (itemsToPopulate[i]) {
+ case SHORTCUT:
+ if (reverseOrder) {
+ shortcutViews.add(0, (DeepShortcutView) item);
+ } else {
+ shortcutViews.add((DeepShortcutView) item);
+ }
+ break;
+ }
+ }
+
+ // Add the arrow.
+ mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
+ mArrow.setPivotX(arrowWidth / 2);
+ mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
+
+ animateOpen();
+
+ mOriginalIcon = originalIcon;
+
+ mLauncher.getDragController().addDragListener(this);
+
+ // 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, (ItemInfo) originalIcon.getTag(), new Handler(Looper.getMainLooper()),
+ this, shortcutIds, shortcutViews));
+ }
+
+ private void addDummyViews(BubbleTextView originalIcon, PopupPopulator.Item[] itemsToPopulate) {
+ final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
+ final LayoutInflater inflater = mLauncher.getLayoutInflater();
+ int numItems = itemsToPopulate.length;
+ for (int i = 0; i < numItems; i++) {
+ final PopupItemView item = (PopupItemView) inflater.inflate(
+ itemsToPopulate[i].layoutId, this, false);
+ if (i < numItems - 1) {
+ ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
+ }
+ item.setAccessibilityDelegate(mAccessibilityDelegate);
+ addView(item);
+ }
+ // TODO: update this, since not all items are shortcuts
+ setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
+ numItems, originalIcon.getContentDescription().toString()));
+ }
+
+ protected PopupItemView getItemViewAt(int index) {
+ if (!mIsAboveIcon) {
+ // Opening down, so arrow is the first view.
+ index++;
+ }
+ return (PopupItemView) getChildAt(index);
+ }
+
+ protected int getItemCount() {
+ // All children except the arrow are items.
+ return getChildCount() - 1;
+ }
+
+ private void animateOpen() {
+ setVisibility(View.VISIBLE);
+ mIsOpen = true;
+
+ final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
+ final int itemCount = getItemCount();
+
+ final long duration = getResources().getInteger(
+ R.integer.config_deepShortcutOpenDuration);
+ final long arrowScaleDuration = getResources().getInteger(
+ R.integer.config_deepShortcutArrowOpenDuration);
+ final long arrowScaleDelay = duration - arrowScaleDuration;
+ final long stagger = getResources().getInteger(
+ R.integer.config_deepShortcutOpenStagger);
+ final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
+
+ // Animate shortcuts
+ DecelerateInterpolator interpolator = new DecelerateInterpolator();
+ for (int i = 0; i < itemCount; i++) {
+ final PopupItemView popupItemView = getItemViewAt(i);
+ popupItemView.setVisibility(INVISIBLE);
+ popupItemView.setAlpha(0);
+
+ Animator anim = popupItemView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ popupItemView.setVisibility(VISIBLE);
+ }
+ });
+ anim.setDuration(duration);
+ int animationIndex = mIsAboveIcon ? itemCount - i - 1 : i;
+ anim.setStartDelay(stagger * animationIndex);
+ anim.setInterpolator(interpolator);
+ shortcutAnims.play(anim);
+
+ Animator fadeAnim = new LauncherViewPropertyAnimator(popupItemView).alpha(1);
+ fadeAnim.setInterpolator(fadeInterpolator);
+ // We want the shortcut to be fully opaque before the arrow starts animating.
+ fadeAnim.setDuration(arrowScaleDelay);
+ shortcutAnims.play(fadeAnim);
+ }
+ shortcutAnims.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mOpenCloseAnimator = null;
+ Utilities.sendCustomAccessibilityEvent(
+ PopupContainerWithArrow.this,
+ AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
+ getContext().getString(R.string.action_deep_shortcut));
+ }
+ });
+
+ // Animate the arrow
+ mArrow.setScaleX(0);
+ mArrow.setScaleY(0);
+ Animator arrowScale = new LauncherViewPropertyAnimator(mArrow).scaleX(1).scaleY(1);
+ arrowScale.setStartDelay(arrowScaleDelay);
+ arrowScale.setDuration(arrowScaleDuration);
+ shortcutAnims.play(arrowScale);
+
+ mOpenCloseAnimator = shortcutAnims;
+ shortcutAnims.start();
+ }
+
+ /**
+ * 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.
+ */
+ private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
+ int width = getMeasuredWidth();
+ int height = getMeasuredHeight() + arrowHeight;
+
+ DragLayer dragLayer = mLauncher.getDragLayer();
+ dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
+ Rect insets = dragLayer.getInsets();
+
+ // Align left (right in RTL) if there is room.
+ int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
+ int rightAlignedX = mTempRect.right - width - icon.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 = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
+ iconWidth *= icon.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.deep_shortcut_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.deep_shortcut_padding_end);
+ xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
+ }
+ x += mIsLeftAligned ? xOffset : -xOffset;
+
+ // Open above icon if there is room.
+ int iconHeight = icon.getIcon().getBounds().height();
+ int y = mTempRect.top + icon.getPaddingTop() - height;
+ mIsAboveIcon = y > dragLayer.getTop() + insets.top;
+ if (!mIsAboveIcon) {
+ y = mTempRect.top + icon.getPaddingTop() + iconHeight;
+ }
+
+ // Insets are added later, so subtract them now.
+ if (mIsRtl) {
+ x += insets.right;
+ } else {
+ x -= insets.left;
+ }
+ y -= insets.top;
+
+ if (y < dragLayer.getTop() || y + height > dragLayer.getBottom()) {
+ // The container is opening off the screen, so just center it in the drag layer instead.
+ ((FrameLayout.LayoutParams) getLayoutParams()).gravity = 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;
+ }
+
+ if (x < dragLayer.getLeft() || x + width > dragLayer.getRight()) {
+ // If we are still off screen, center horizontally too.
+ ((FrameLayout.LayoutParams) getLayoutParams()).gravity |= Gravity.CENTER_HORIZONTAL;
+ }
+
+ int gravity = ((FrameLayout.LayoutParams) getLayoutParams()).gravity;
+ if (!Gravity.isHorizontal(gravity)) {
+ setX(x);
+ }
+ if (!Gravity.isVertical(gravity)) {
+ setY(y);
+ }
+ }
+
+ private boolean isAlignedWithStart() {
+ return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
+ }
+
+ /**
+ * Adds an arrow view pointing at the original icon.
+ * @param horizontalOffset the horizontal offset of the arrow, so that it
+ * points at the center of the original icon
+ */
+ private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
+ LayoutParams layoutParams = new LayoutParams(width, height);
+ if (mIsLeftAligned) {
+ layoutParams.gravity = Gravity.LEFT;
+ layoutParams.leftMargin = horizontalOffset;
+ } else {
+ layoutParams.gravity = Gravity.RIGHT;
+ layoutParams.rightMargin = horizontalOffset;
+ }
+ if (mIsAboveIcon) {
+ layoutParams.topMargin = verticalOffset;
+ } else {
+ layoutParams.bottomMargin = verticalOffset;
+ }
+
+ View arrowView = new View(getContext());
+ if (Gravity.isVertical(((FrameLayout.LayoutParams) getLayoutParams()).gravity)) {
+ // 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.
+ arrowView.setVisibility(INVISIBLE);
+ } else {
+ ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+ width, height, !mIsAboveIcon));
+ arrowDrawable.getPaint().setColor(Color.WHITE);
+ arrowView.setBackground(arrowDrawable);
+ arrowView.setElevation(getElevation());
+ }
+ addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
+ return arrowView;
+ }
+
+ @Override
+ public View getExtendedTouchView() {
+ return mOriginalIcon;
+ }
+
+ /**
+ * 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() {
+ mOriginalIcon.setVisibility(INVISIBLE);
+ }
+
+ @Override
+ public void onPreDragEnd(boolean dragStarted) {
+ if (!dragStarted) {
+ mOriginalIcon.setVisibility(VISIBLE);
+ mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
+ if (!mIsAboveIcon) {
+ mOriginalIcon.setTextVisibility(false);
+ }
+ }
+ }
+ };
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mInterceptTouchDown.set(ev.getX(), ev.getY());
+ return false;
+ }
+ // 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();
+ }
+
+ /**
+ * We need to handle touch events to prevent them from falling through to the workspace below.
+ */
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return true;
+ }
+
+ @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;
+ }
+
+ public boolean onLongClick(View v) {
+ // Return early if this is not initiated from a touch or not the correct view
+ if (!v.isInTouchMode() || !(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.
+ mDeferContainerRemoval = true;
+ DeepShortcutView sv = (DeepShortcutView) v.getParent();
+ sv.setWillDrawIcon(false);
+
+ // Move the icon to align with the center-top of the touch point
+ mIconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
+ mIconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
+
+ DragView dv = mLauncher.getWorkspace().beginDragShared(
+ sv.getBubbleText(), this, sv.getFinalInfo(),
+ new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift), new DragOptions());
+ dv.animateShift(-mIconShift.x, -mIconShift.y);
+
+ // TODO: support dragging from within folder without having to close it
+ AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER);
+ return false;
+ }
+
+ @Override
+ public boolean supportsAppInfoDropTarget() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsDeleteDropTarget() {
+ return false;
+ }
+
+ @Override
+ public float getIntrinsicIconScaleFactor() {
+ return 1f;
+ }
+
+ @Override
+ public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
+ boolean success) {
+ if (!success) {
+ d.dragView.remove();
+ mLauncher.showWorkspace(true);
+ mLauncher.getDropTargetBar().onDragEnd();
+ }
+ }
+
+ @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.
+ 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) {
+ target.itemType = ItemType.DEEPSHORTCUT;
+ // TODO: add target.rank
+ targetParent.containerType = ContainerType.DEEPSHORTCUTS;
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ if (animate) {
+ animateClose();
+ } else {
+ closeComplete();
+ }
+ }
+
+ protected void animateClose() {
+ if (!mIsOpen) {
+ return;
+ }
+ if (mOpenCloseAnimator != null) {
+ mOpenCloseAnimator.cancel();
+ }
+ mIsOpen = false;
+
+ final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
+ final int itemCount = getItemCount();
+ int numOpenShortcuts = 0;
+ for (int i = 0; i < itemCount; i++) {
+ if (getItemViewAt(i).isOpenOrOpening()) {
+ numOpenShortcuts++;
+ }
+ }
+ final long duration = getResources().getInteger(
+ R.integer.config_deepShortcutCloseDuration);
+ final long arrowScaleDuration = getResources().getInteger(
+ R.integer.config_deepShortcutArrowOpenDuration);
+ final long stagger = getResources().getInteger(
+ R.integer.config_deepShortcutCloseStagger);
+ final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
+
+ int firstOpenItemIndex = mIsAboveIcon ? itemCount - numOpenShortcuts : 0;
+ for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) {
+ final PopupItemView view = getItemViewAt(i);
+ Animator anim;
+ if (view.willDrawIcon()) {
+ anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
+ int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
+ : numOpenShortcuts - i - 1;
+ anim.setStartDelay(stagger * animationIndex);
+
+ Animator fadeAnim = new LauncherViewPropertyAnimator(view).alpha(0);
+ // Don't start fading until the arrow is gone.
+ fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
+ fadeAnim.setDuration(duration - arrowScaleDuration);
+ fadeAnim.setInterpolator(fadeInterpolator);
+ shortcutAnims.play(fadeAnim);
+ } else {
+ // The view is being dragged. Animate it such that it collapses with the drag view
+ anim = view.collapseToIcon();
+ anim.setDuration(DragView.VIEW_ZOOM_DURATION);
+
+ // Scale and translate the view to follow the drag view.
+ Point iconCenter = view.getIconCenter();
+ view.setPivotX(iconCenter.x);
+ view.setPivotY(iconCenter.y);
+
+ float scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / view.getHeight();
+ LauncherViewPropertyAnimator anim2 = new LauncherViewPropertyAnimator(view)
+ .scaleX(scale)
+ .scaleY(scale)
+ .translationX(mIconShift.x)
+ .translationY(mIconShift.y);
+ anim2.setDuration(DragView.VIEW_ZOOM_DURATION);
+ shortcutAnims.play(anim2);
+ }
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setVisibility(INVISIBLE);
+ }
+ });
+ shortcutAnims.play(anim);
+ }
+ Animator arrowAnim = new LauncherViewPropertyAnimator(mArrow)
+ .scaleX(0).scaleY(0).setDuration(arrowScaleDuration);
+ arrowAnim.setStartDelay(0);
+ shortcutAnims.play(arrowAnim);
+
+ shortcutAnims.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mOpenCloseAnimator = null;
+ if (mDeferContainerRemoval) {
+ setVisibility(INVISIBLE);
+ } else {
+ closeComplete();
+ }
+ }
+ });
+ mOpenCloseAnimator = shortcutAnims;
+ shortcutAnims.start();
+ }
+
+ /**
+ * Closes the folder without animation.
+ */
+ protected void closeComplete() {
+ if (mOpenCloseAnimator != null) {
+ mOpenCloseAnimator.cancel();
+ mOpenCloseAnimator = null;
+ }
+ mIsOpen = false;
+ mDeferContainerRemoval = false;
+ boolean isInHotseat = ((ItemInfo) mOriginalIcon.getTag()).container
+ == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+ mOriginalIcon.setTextVisibility(!isInHotseat);
+ mLauncher.getDragController().removeDragListener(this);
+ mLauncher.getDragLayer().removeView(this);
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
+ }
+
+ /**
+ * Returns a DeepShortcutsContainer which is already open or null
+ */
+ public static PopupContainerWithArrow getOpen(Launcher launcher) {
+ return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
+ }
+}
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 4ed32b543..b671c364f 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -23,7 +23,7 @@ import android.util.Log;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.badge.BadgeInfo;
-import com.android.launcher3.badging.NotificationListener;
+import com.android.launcher3.badge.NotificationListener;
import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.MultiHashMap;
diff --git a/src/com/android/launcher3/popup/PopupItemView.java b/src/com/android/launcher3/popup/PopupItemView.java
new file mode 100644
index 000000000..25d496a4b
--- /dev/null
+++ b/src/com/android/launcher3/popup/PopupItemView.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2017 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.ValueAnimator;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.LogAccelerateInterpolator;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.PillRevealOutlineProvider;
+import com.android.launcher3.util.PillWidthRevealOutlineProvider;
+
+/**
+ * An abstract {@link FrameLayout} that supports animating an item's content
+ * (e.g. icon and text) separate from the item's background.
+ */
+public abstract class PopupItemView extends FrameLayout
+ implements ValueAnimator.AnimatorUpdateListener {
+
+ protected static final Point sTempPoint = new Point();
+
+ private final Rect mPillRect;
+ private float mOpenAnimationProgress;
+
+ protected View mIconView;
+
+ public PopupItemView(Context context) {
+ this(context, null, 0);
+ }
+
+ public PopupItemView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PopupItemView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mPillRect = new Rect();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mIconView = findViewById(R.id.popup_item_icon);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
+ }
+
+ public boolean willDrawIcon() {
+ return true;
+ }
+
+ /**
+ * Creates an animator to play when the shortcut container is being opened.
+ */
+ public Animator createOpenAnimation(boolean isContainerAboveIcon, boolean pivotLeft) {
+ Point center = getIconCenter();
+ ValueAnimator openAnimator = new ZoomRevealOutlineProvider(center.x, center.y,
+ mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft)
+ .createRevealAnimator(this, false);
+ mOpenAnimationProgress = 0f;
+ openAnimator.addUpdateListener(this);
+ return openAnimator;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ mOpenAnimationProgress = valueAnimator.getAnimatedFraction();
+ }
+
+ public boolean isOpenOrOpening() {
+ return mOpenAnimationProgress > 0;
+ }
+
+ /**
+ * Creates an animator to play when the shortcut container is being closed.
+ */
+ public Animator createCloseAnimation(boolean isContainerAboveIcon, boolean pivotLeft,
+ long duration) {
+ Point center = getIconCenter();
+ ValueAnimator closeAnimator = new ZoomRevealOutlineProvider(center.x, center.y,
+ mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft)
+ .createRevealAnimator(this, true);
+ // Scale down the duration and interpolator according to the progress
+ // that the open animation was at when the close started.
+ closeAnimator.setDuration((long) (duration * mOpenAnimationProgress));
+ closeAnimator.setInterpolator(new CloseInterpolator(mOpenAnimationProgress));
+ closeAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mOpenAnimationProgress = 0;
+ }
+ });
+ return closeAnimator;
+ }
+
+ /**
+ * Creates an animator which clips the container to form a circle around the icon.
+ */
+ public Animator collapseToIcon() {
+ int halfHeight = getMeasuredHeight() / 2;
+ int iconCenterX = getIconCenter().x;
+ return new PillWidthRevealOutlineProvider(mPillRect,
+ iconCenterX - halfHeight, iconCenterX + halfHeight)
+ .createRevealAnimator(this, true);
+ }
+
+ /**
+ * Returns the position of the center of the icon relative to the container.
+ */
+ public Point getIconCenter() {
+ sTempPoint.y = sTempPoint.x = getMeasuredHeight() / 2;
+ if (Utilities.isRtl(getResources())) {
+ sTempPoint.x = getMeasuredWidth() - sTempPoint.x;
+ }
+ return sTempPoint;
+ }
+
+ /**
+ * Extension of {@link PillRevealOutlineProvider} which scales the icon based on the height.
+ */
+ private static class ZoomRevealOutlineProvider extends PillRevealOutlineProvider {
+
+ private final View mTranslateView;
+ private final View mZoomView;
+
+ private final float mFullHeight;
+ private final float mTranslateYMultiplier;
+
+ private final boolean mPivotLeft;
+ private final float mTranslateX;
+
+ public ZoomRevealOutlineProvider(int x, int y, Rect pillRect,
+ View translateView, View zoomView, boolean isContainerAboveIcon, boolean pivotLeft) {
+ super(x, y, pillRect);
+ mTranslateView = translateView;
+ mZoomView = zoomView;
+ mFullHeight = pillRect.height();
+
+ mTranslateYMultiplier = isContainerAboveIcon ? 0.5f : -0.5f;
+
+ mPivotLeft = pivotLeft;
+ mTranslateX = pivotLeft ? pillRect.height() / 2 : pillRect.right - pillRect.height() / 2;
+ }
+
+ @Override
+ public void setProgress(float progress) {
+ super.setProgress(progress);
+
+ mZoomView.setScaleX(progress);
+ mZoomView.setScaleY(progress);
+
+ float height = mOutline.height();
+ mTranslateView.setTranslationY(mTranslateYMultiplier * (mFullHeight - height));
+
+ float pivotX = mPivotLeft ? (mOutline.left + height / 2) : (mOutline.right - height / 2);
+ mTranslateView.setTranslationX(mTranslateX - pivotX);
+ }
+ }
+
+ /**
+ * An interpolator that reverses the current open animation progress.
+ */
+ private static class CloseInterpolator extends LogAccelerateInterpolator {
+ private float mStartProgress;
+ private float mRemainingProgress;
+
+ /**
+ * @param openAnimationProgress The progress that the open interpolator ended at.
+ */
+ public CloseInterpolator(float openAnimationProgress) {
+ super(100, 0);
+ mStartProgress = 1f - openAnimationProgress;
+ mRemainingProgress = openAnimationProgress;
+ }
+
+ @Override
+ public float getInterpolation(float v) {
+ return mStartProgress + super.getInterpolation(v) * mRemainingProgress;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
new file mode 100644
index 000000000..b5a59b02b
--- /dev/null
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2017 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.content.ComponentName;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.graphics.LauncherIcons;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Contains logic relevant to populating a {@link PopupContainerWithArrow}. In particular,
+ * this class determines which items appear in the container, and in what order.
+ */
+public class PopupPopulator {
+
+ public static final int MAX_ITEMS = 4;
+ @VisibleForTesting static final int NUM_DYNAMIC = 2;
+
+ public enum Item {
+ SHORTCUT(R.layout.deep_shortcut);
+
+ public final int layoutId;
+
+ Item(int layoutId) {
+ this.layoutId = layoutId;
+ }
+ }
+
+ public static Item[] getItemsToPopulate(List<String> shortcutIds) {
+ int numItems = Math.min(MAX_ITEMS, shortcutIds.size());
+ Item[] items = new Item[numItems];
+ for (int i = 0; i < numItems; i++) {
+ items[i] = Item.SHORTCUT;
+ }
+ return items;
+ }
+
+ public static Item[] reverseItems(Item[] items) {
+ if (items == null) return null;
+ int numItems = items.length;
+ Item[] reversedArray = new Item[numItems];
+ for (int i = 0; i < numItems; i++) {
+ reversedArray[i] = items[numItems - i - 1];
+ }
+ return reversedArray;
+ }
+
+ /**
+ * Sorts shortcuts in rank order, with manifest shortcuts coming before dynamic shortcuts.
+ */
+ private static final Comparator<ShortcutInfoCompat> SHORTCUT_RANK_COMPARATOR
+ = new Comparator<ShortcutInfoCompat>() {
+ @Override
+ public int compare(ShortcutInfoCompat a, ShortcutInfoCompat b) {
+ if (a.isDeclaredInManifest() && !b.isDeclaredInManifest()) {
+ return -1;
+ }
+ if (!a.isDeclaredInManifest() && b.isDeclaredInManifest()) {
+ return 1;
+ }
+ return Integer.compare(a.getRank(), b.getRank());
+ }
+ };
+
+ /**
+ * Filters the shortcuts so that only MAX_ITEMS or fewer shortcuts are retained.
+ * We want the filter to include both static and dynamic shortcuts, so we always
+ * include NUM_DYNAMIC dynamic shortcuts, if at least that many are present.
+ *
+ * @return a subset of shortcuts, in sorted order, with size <= MAX_ITEMS.
+ */
+ public static List<ShortcutInfoCompat> sortAndFilterShortcuts(
+ List<ShortcutInfoCompat> shortcuts) {
+ Collections.sort(shortcuts, SHORTCUT_RANK_COMPARATOR);
+ if (shortcuts.size() <= MAX_ITEMS) {
+ return shortcuts;
+ }
+
+ // The list of shortcuts is now sorted with static shortcuts followed by dynamic
+ // shortcuts. We want to preserve this order, but only keep MAX_ITEMS.
+ List<ShortcutInfoCompat> filteredShortcuts = new ArrayList<>(MAX_ITEMS);
+ int numDynamic = 0;
+ int size = shortcuts.size();
+ for (int i = 0; i < size; i++) {
+ ShortcutInfoCompat shortcut = shortcuts.get(i);
+ int filteredSize = filteredShortcuts.size();
+ if (filteredSize < MAX_ITEMS) {
+ // Always add the first MAX_ITEMS to the filtered list.
+ filteredShortcuts.add(shortcut);
+ if (shortcut.isDynamic()) {
+ numDynamic++;
+ }
+ continue;
+ }
+ // At this point, we have MAX_ITEMS already, but they may all be static.
+ // If there are dynamic shortcuts, remove static shortcuts to add them.
+ if (shortcut.isDynamic() && numDynamic < NUM_DYNAMIC) {
+ numDynamic++;
+ int lastStaticIndex = filteredSize - numDynamic;
+ filteredShortcuts.remove(lastStaticIndex);
+ filteredShortcuts.add(shortcut);
+ }
+ }
+ return filteredShortcuts;
+ }
+
+ public static Runnable createUpdateRunnable(final Launcher launcher, ItemInfo originalInfo,
+ final Handler uiHandler, final PopupContainerWithArrow container,
+ final List<String> shortcutIds, final List<DeepShortcutView> shortcutViews) {
+ final ComponentName activity = originalInfo.getTargetComponent();
+ final UserHandle user = originalInfo.user;
+ return new Runnable() {
+ @Override
+ public void run() {
+ final List<ShortcutInfoCompat> shortcuts = PopupPopulator.sortAndFilterShortcuts(
+ DeepShortcutManager.getInstance(launcher).queryForShortcutsContainer(
+ activity, shortcutIds, user));
+ for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) {
+ final ShortcutInfoCompat shortcut = shortcuts.get(i);
+ ShortcutInfo si = new ShortcutInfo(shortcut, launcher);
+ // Use unbadged icon for the menu.
+ si.iconBitmap = LauncherIcons.createShortcutIcon(
+ shortcut, launcher, false /* badged */);
+ uiHandler.post(new UpdateShortcutChild(container, shortcutViews.get(i),
+ si, shortcut));
+ }
+ }
+ };
+ }
+
+ /** Updates the child of this container at the given index based on the given shortcut info. */
+ private static class UpdateShortcutChild implements Runnable {
+ private final PopupContainerWithArrow mContainer;
+ private final DeepShortcutView mShortcutChild;
+ private final ShortcutInfo mShortcutChildInfo;
+ private final ShortcutInfoCompat mDetail;
+
+ public UpdateShortcutChild(PopupContainerWithArrow container, DeepShortcutView shortcutChild,
+ ShortcutInfo shortcutChildInfo, ShortcutInfoCompat detail) {
+ mContainer = container;
+ mShortcutChild = shortcutChild;
+ mShortcutChildInfo = shortcutChildInfo;
+ mDetail = detail;
+ }
+
+ @Override
+ public void run() {
+ mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail, mContainer);
+ }
+ }
+}