diff options
Diffstat (limited to 'src/com/android/launcher3/popup')
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); + } + } +} |