c77bc45fa5
=> Also fix the widget tray in these non-default grids, and the external shortcut addition flow. The layout was broken as it was scaling with the icon size. Instead keep it fixed which looks much better, similar to how we maintain all apps. => This also fixes a small jump when dragging shortcuts external to Launcher. issue 154169001 Change-Id: Iad1e3859dd6fedccce9b5c6633e64426a4630c31
3343 lines
134 KiB
Java
3343 lines
134 KiB
Java
/*
|
|
* Copyright (C) 2008 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;
|
|
|
|
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
|
|
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
|
|
import static com.android.launcher3.LauncherState.ALL_APPS;
|
|
import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
|
|
import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED;
|
|
import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_INACCESSIBLE;
|
|
import static com.android.launcher3.LauncherState.NORMAL;
|
|
import static com.android.launcher3.LauncherState.OVERVIEW;
|
|
import static com.android.launcher3.LauncherState.SPRING_LOADED;
|
|
import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
|
|
import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY;
|
|
import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.LayoutTransition;
|
|
import android.animation.ValueAnimator;
|
|
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
|
import android.annotation.SuppressLint;
|
|
import android.app.WallpaperManager;
|
|
import android.appwidget.AppWidgetHostView;
|
|
import android.appwidget.AppWidgetProviderInfo;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Point;
|
|
import android.graphics.Rect;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import android.os.Parcelable;
|
|
import android.os.UserHandle;
|
|
import android.text.TextUtils;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.util.SparseArray;
|
|
import android.view.LayoutInflater;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.ViewTreeObserver;
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
|
import android.widget.Toast;
|
|
|
|
import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
|
|
import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
|
|
import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
|
|
import com.android.launcher3.anim.Interpolators;
|
|
import com.android.launcher3.anim.PendingAnimation;
|
|
import com.android.launcher3.config.FeatureFlags;
|
|
import com.android.launcher3.dot.FolderDotInfo;
|
|
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.dragndrop.DraggableView;
|
|
import com.android.launcher3.dragndrop.SpringLoadedDragController;
|
|
import com.android.launcher3.folder.Folder;
|
|
import com.android.launcher3.folder.FolderIcon;
|
|
import com.android.launcher3.folder.PreviewBackground;
|
|
import com.android.launcher3.graphics.DragPreviewProvider;
|
|
import com.android.launcher3.graphics.PreloadIconDrawable;
|
|
import com.android.launcher3.icons.BitmapRenderer;
|
|
import com.android.launcher3.logging.StatsLogManager;
|
|
import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
|
|
import com.android.launcher3.logging.UserEventDispatcher;
|
|
import com.android.launcher3.model.data.AppInfo;
|
|
import com.android.launcher3.model.data.FolderInfo;
|
|
import com.android.launcher3.model.data.ItemInfo;
|
|
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
|
|
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
|
import com.android.launcher3.pageindicators.WorkspacePageIndicator;
|
|
import com.android.launcher3.popup.PopupContainerWithArrow;
|
|
import com.android.launcher3.states.StateAnimationConfig;
|
|
import com.android.launcher3.touch.WorkspaceTouchListener;
|
|
import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
|
|
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
|
|
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
|
|
import com.android.launcher3.util.Executors;
|
|
import com.android.launcher3.util.IntArray;
|
|
import com.android.launcher3.util.IntSparseArrayMap;
|
|
import com.android.launcher3.util.ItemInfoMatcher;
|
|
import com.android.launcher3.util.PackageUserKey;
|
|
import com.android.launcher3.util.Thunk;
|
|
import com.android.launcher3.util.WallpaperOffsetInterpolator;
|
|
import com.android.launcher3.widget.LauncherAppWidgetHostView;
|
|
import com.android.launcher3.widget.PendingAddShortcutInfo;
|
|
import com.android.launcher3.widget.PendingAddWidgetInfo;
|
|
import com.android.launcher3.widget.PendingAppWidgetHostView;
|
|
import com.android.launcher3.widget.WidgetManagerHelper;
|
|
import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.function.Predicate;
|
|
|
|
/**
|
|
* The workspace is a wide area with a wallpaper and a finite number of pages.
|
|
* Each page contains a number of icons, folders or widgets the user can
|
|
* interact with. A workspace is meant to be used with a fixed width only.
|
|
*/
|
|
public class Workspace extends PagedView<WorkspacePageIndicator>
|
|
implements DropTarget, DragSource, View.OnTouchListener,
|
|
DragController.DragListener, Insettable, LauncherStateManager.StateHandler,
|
|
WorkspaceLayoutManager {
|
|
|
|
/** The value that {@link #mTransitionProgress} must be greater than for
|
|
* {@link #transitionStateShouldAllowDrop()} to return true. */
|
|
private static final float ALLOW_DROP_TRANSITION_PROGRESS = 0.25f;
|
|
|
|
/** The value that {@link #mTransitionProgress} must be greater than for
|
|
* {@link #isFinishedSwitchingState()} ()} to return true. */
|
|
private static final float FINISHED_SWITCHING_STATE_TRANSITION_PROGRESS = 0.5f;
|
|
|
|
private static final boolean ENFORCE_DRAG_EVENT_ORDER = false;
|
|
|
|
private static final int ADJACENT_SCREEN_DROP_DURATION = 300;
|
|
|
|
public static final int DEFAULT_PAGE = 0;
|
|
|
|
private LayoutTransition mLayoutTransition;
|
|
@Thunk final WallpaperManager mWallpaperManager;
|
|
|
|
private ShortcutAndWidgetContainer mDragSourceInternal;
|
|
|
|
@Thunk final IntSparseArrayMap<CellLayout> mWorkspaceScreens = new IntSparseArrayMap<>();
|
|
@Thunk final IntArray mScreenOrder = new IntArray();
|
|
|
|
@Thunk boolean mDeferRemoveExtraEmptyScreen = false;
|
|
|
|
/**
|
|
* CellInfo for the cell that is currently being dragged
|
|
*/
|
|
private CellLayout.CellInfo mDragInfo;
|
|
|
|
/**
|
|
* Target drop area calculated during last acceptDrop call.
|
|
*/
|
|
@Thunk int[] mTargetCell = new int[2];
|
|
private int mDragOverX = -1;
|
|
private int mDragOverY = -1;
|
|
|
|
/**
|
|
* The CellLayout that is currently being dragged over
|
|
*/
|
|
@Thunk CellLayout mDragTargetLayout = null;
|
|
/**
|
|
* The CellLayout that we will show as highlighted
|
|
*/
|
|
private CellLayout mDragOverlappingLayout = null;
|
|
|
|
/**
|
|
* The CellLayout which will be dropped to
|
|
*/
|
|
private CellLayout mDropToLayout = null;
|
|
|
|
@Thunk final Launcher mLauncher;
|
|
@Thunk DragController mDragController;
|
|
|
|
private final Rect mTempRect = new Rect();
|
|
private final int[] mTempXY = new int[2];
|
|
private final float[] mTempFXY = new float[2];
|
|
@Thunk float[] mDragViewVisualCenter = new float[2];
|
|
private final float[] mTempTouchCoordinates = new float[2];
|
|
|
|
private SpringLoadedDragController mSpringLoadedDragController;
|
|
|
|
private boolean mIsSwitchingState = false;
|
|
|
|
boolean mChildrenLayersEnabled = true;
|
|
|
|
private boolean mStripScreensOnPageStopMoving = false;
|
|
|
|
private DragPreviewProvider mOutlineProvider = null;
|
|
private boolean mWorkspaceFadeInAdjacentScreens;
|
|
|
|
final WallpaperOffsetInterpolator mWallpaperOffset;
|
|
private boolean mUnlockWallpaperFromDefaultPageOnLayout;
|
|
|
|
public static final int REORDER_TIMEOUT = 650;
|
|
private final Alarm mReorderAlarm = new Alarm();
|
|
private PreviewBackground mFolderCreateBg;
|
|
private FolderIcon mDragOverFolderIcon = null;
|
|
private boolean mCreateUserFolderOnDrop = false;
|
|
private boolean mAddToExistingFolderOnDrop = false;
|
|
private float mMaxDistanceForFolderCreation;
|
|
|
|
// Variables relating to touch disambiguation (scrolling workspace vs. scrolling a widget)
|
|
private float mXDown;
|
|
private float mYDown;
|
|
final static float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6;
|
|
final static float MAX_SWIPE_ANGLE = (float) Math.PI / 3;
|
|
final static float TOUCH_SLOP_DAMPING_FACTOR = 4;
|
|
|
|
// Relating to the animation of items being dropped externally
|
|
public static final int ANIMATE_INTO_POSITION_AND_DISAPPEAR = 0;
|
|
public static final int ANIMATE_INTO_POSITION_AND_REMAIN = 1;
|
|
public static final int ANIMATE_INTO_POSITION_AND_RESIZE = 2;
|
|
public static final int COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION = 3;
|
|
public static final int CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION = 4;
|
|
|
|
// Related to dragging, folder creation and reordering
|
|
private static final int DRAG_MODE_NONE = 0;
|
|
private static final int DRAG_MODE_CREATE_FOLDER = 1;
|
|
private static final int DRAG_MODE_ADD_TO_FOLDER = 2;
|
|
private static final int DRAG_MODE_REORDER = 3;
|
|
private int mDragMode = DRAG_MODE_NONE;
|
|
@Thunk int mLastReorderX = -1;
|
|
@Thunk int mLastReorderY = -1;
|
|
|
|
private SparseArray<Parcelable> mSavedStates;
|
|
private final IntArray mRestoredPages = new IntArray();
|
|
|
|
private float mCurrentScale;
|
|
private float mTransitionProgress;
|
|
|
|
// State related to Launcher Overlay
|
|
LauncherOverlay mLauncherOverlay;
|
|
boolean mScrollInteractionBegan;
|
|
boolean mStartedSendingScrollEvents;
|
|
float mLastOverlayScroll = 0;
|
|
boolean mOverlayShown = false;
|
|
private Runnable mOnOverlayHiddenCallback;
|
|
|
|
private boolean mForceDrawAdjacentPages = false;
|
|
|
|
// Total over scrollX in the overlay direction.
|
|
private float mOverlayTranslation;
|
|
|
|
// Handles workspace state transitions
|
|
private final WorkspaceStateTransitionAnimation mStateTransitionAnimation;
|
|
|
|
private final StatsLogManager mStatsLogManager;
|
|
|
|
/**
|
|
* Used to inflate the Workspace from XML.
|
|
*
|
|
* @param context The application's context.
|
|
* @param attrs The attributes set containing the Workspace's customization values.
|
|
*/
|
|
public Workspace(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
/**
|
|
* Used to inflate the Workspace from XML.
|
|
*
|
|
* @param context The application's context.
|
|
* @param attrs The attributes set containing the Workspace's customization values.
|
|
* @param defStyle Unused.
|
|
*/
|
|
public Workspace(Context context, AttributeSet attrs, int defStyle) {
|
|
super(context, attrs, defStyle);
|
|
|
|
mLauncher = Launcher.getLauncher(context);
|
|
mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this);
|
|
mWallpaperManager = WallpaperManager.getInstance(context);
|
|
|
|
mWallpaperOffset = new WallpaperOffsetInterpolator(this);
|
|
|
|
setHapticFeedbackEnabled(false);
|
|
initWorkspace();
|
|
|
|
// Disable multitouch across the workspace/all apps/customize tray
|
|
setMotionEventSplittingEnabled(true);
|
|
setOnTouchListener(new WorkspaceTouchListener(mLauncher, this));
|
|
mStatsLogManager = StatsLogManager.newInstance(context);
|
|
}
|
|
|
|
@Override
|
|
public void setInsets(Rect insets) {
|
|
DeviceProfile grid = mLauncher.getDeviceProfile();
|
|
|
|
mMaxDistanceForFolderCreation = grid.isTablet
|
|
? 0.75f * grid.iconSizePx : 0.55f * grid.iconSizePx;
|
|
mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
|
|
|
|
Rect padding = grid.workspacePadding;
|
|
setPadding(padding.left, padding.top, padding.right, padding.bottom);
|
|
mInsets.set(insets);
|
|
|
|
if (mWorkspaceFadeInAdjacentScreens) {
|
|
// In landscape mode the page spacing is set to the default.
|
|
setPageSpacing(grid.edgeMarginPx);
|
|
} else {
|
|
// In portrait, we want the pages spaced such that there is no
|
|
// overhang of the previous / next page into the current page viewport.
|
|
// We assume symmetrical padding in portrait mode.
|
|
setPageSpacing(Math.max(grid.edgeMarginPx, padding.left + 1));
|
|
}
|
|
|
|
|
|
int paddingLeftRight = grid.cellLayoutPaddingLeftRightPx;
|
|
int paddingBottom = grid.cellLayoutBottomPaddingPx;
|
|
for (int i = mWorkspaceScreens.size() - 1; i >= 0; i--) {
|
|
mWorkspaceScreens.valueAt(i)
|
|
.setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Estimates the size of an item using spans: hSpan, vSpan.
|
|
*
|
|
* @return MAX_VALUE for each dimension if unsuccessful.
|
|
*/
|
|
public int[] estimateItemSize(ItemInfo itemInfo) {
|
|
int[] size = new int[2];
|
|
if (getChildCount() > 0) {
|
|
// Use the first page to estimate the child position
|
|
CellLayout cl = (CellLayout) getChildAt(0);
|
|
boolean isWidget = itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
|
|
|
|
Rect r = estimateItemPosition(cl, 0, 0, itemInfo.spanX, itemInfo.spanY);
|
|
|
|
float scale = 1;
|
|
if (isWidget) {
|
|
DeviceProfile profile = mLauncher.getDeviceProfile();
|
|
scale = Utilities.shrinkRect(r, profile.appWidgetScale.x, profile.appWidgetScale.y);
|
|
}
|
|
size[0] = r.width();
|
|
size[1] = r.height();
|
|
|
|
if (isWidget) {
|
|
size[0] /= scale;
|
|
size[1] /= scale;
|
|
}
|
|
return size;
|
|
} else {
|
|
size[0] = Integer.MAX_VALUE;
|
|
size[1] = Integer.MAX_VALUE;
|
|
return size;
|
|
}
|
|
}
|
|
|
|
public float getWallpaperOffsetForCenterPage() {
|
|
int pageScroll = getScrollForPage(getPageNearestToCenterOfScreen());
|
|
return mWallpaperOffset.wallpaperOffsetForScroll(pageScroll);
|
|
}
|
|
|
|
public Rect estimateItemPosition(CellLayout cl, int hCell, int vCell, int hSpan, int vSpan) {
|
|
Rect r = new Rect();
|
|
cl.cellToRect(hCell, vCell, hSpan, vSpan, r);
|
|
return r;
|
|
}
|
|
|
|
@Override
|
|
public void onDragStart(DragObject dragObject, DragOptions options) {
|
|
if (ENFORCE_DRAG_EVENT_ORDER) {
|
|
enforceDragParity("onDragStart", 0, 0);
|
|
}
|
|
|
|
if (mDragInfo != null && mDragInfo.cell != null) {
|
|
CellLayout layout = (CellLayout) mDragInfo.cell.getParent().getParent();
|
|
layout.markCellsAsUnoccupiedForView(mDragInfo.cell);
|
|
}
|
|
|
|
if (mOutlineProvider != null) {
|
|
if (dragObject.dragView != null) {
|
|
Bitmap preview = dragObject.dragView.getPreviewBitmap();
|
|
|
|
// The outline is used to visualize where the item will land if dropped
|
|
mOutlineProvider.generateDragOutline(preview);
|
|
}
|
|
}
|
|
|
|
updateChildrenLayersEnabled();
|
|
|
|
// Do not add a new page if it is a accessible drag which was not started by the workspace.
|
|
// We do not support accessibility drag from other sources and instead provide a direct
|
|
// action for move/add to homescreen.
|
|
// When a accessible drag is started by the folder, we only allow rearranging withing the
|
|
// folder.
|
|
boolean addNewPage = !(options.isAccessibleDrag && dragObject.dragSource != this);
|
|
|
|
if (addNewPage) {
|
|
mDeferRemoveExtraEmptyScreen = false;
|
|
addExtraEmptyScreenOnDrag();
|
|
|
|
if (dragObject.dragInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
|
|
&& dragObject.dragSource != this) {
|
|
// When dragging a widget from different source, move to a page which has
|
|
// enough space to place this widget (after rearranging/resizing). We special case
|
|
// widgets as they cannot be placed inside a folder.
|
|
// Start at the current page and search right (on LTR) until finding a page with
|
|
// enough space. Since an empty screen is the furthest right, a page must be found.
|
|
int currentPage = getPageNearestToCenterOfScreen();
|
|
for (int pageIndex = currentPage; pageIndex < getPageCount(); pageIndex++) {
|
|
CellLayout page = (CellLayout) getPageAt(pageIndex);
|
|
if (page.hasReorderSolution(dragObject.dragInfo)) {
|
|
setCurrentPage(pageIndex);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Always enter the spring loaded mode
|
|
mLauncher.getStateManager().goToState(SPRING_LOADED);
|
|
mStatsLogManager.log(
|
|
LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED,
|
|
dragObject.logInstanceId,
|
|
dragObject.originalDragInfo.buildProto(
|
|
dragObject.dragSource instanceof Folder
|
|
? ((Folder) dragObject.dragSource).mInfo
|
|
: null)
|
|
);
|
|
}
|
|
|
|
public void deferRemoveExtraEmptyScreen() {
|
|
mDeferRemoveExtraEmptyScreen = true;
|
|
}
|
|
|
|
@Override
|
|
public void onDragEnd() {
|
|
if (ENFORCE_DRAG_EVENT_ORDER) {
|
|
enforceDragParity("onDragEnd", 0, 0);
|
|
}
|
|
|
|
if (!mDeferRemoveExtraEmptyScreen) {
|
|
removeExtraEmptyScreen(mDragSourceInternal != null);
|
|
}
|
|
|
|
updateChildrenLayersEnabled();
|
|
mDragInfo = null;
|
|
mOutlineProvider = null;
|
|
mDragSourceInternal = null;
|
|
}
|
|
|
|
/**
|
|
* Initializes various states for this workspace.
|
|
*/
|
|
protected void initWorkspace() {
|
|
mCurrentPage = DEFAULT_PAGE;
|
|
setClipToPadding(false);
|
|
|
|
setupLayoutTransition();
|
|
|
|
// Set the wallpaper dimensions when Launcher starts up
|
|
setWallpaperDimension();
|
|
}
|
|
|
|
private void setupLayoutTransition() {
|
|
// We want to show layout transitions when pages are deleted, to close the gap.
|
|
mLayoutTransition = new LayoutTransition();
|
|
|
|
mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING);
|
|
mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
|
|
// Change the interpolators such that the fade animation plays before the move animation.
|
|
// This prevents empty adjacent pages to overlay during animation
|
|
mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING,
|
|
Interpolators.clampToProgress(Interpolators.ACCEL_DEACCEL, 0, 0.5f));
|
|
mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING,
|
|
Interpolators.clampToProgress(Interpolators.ACCEL_DEACCEL, 0.5f, 1));
|
|
|
|
mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
|
|
mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
|
|
setLayoutTransition(mLayoutTransition);
|
|
}
|
|
|
|
void enableLayoutTransitions() {
|
|
setLayoutTransition(mLayoutTransition);
|
|
}
|
|
void disableLayoutTransitions() {
|
|
setLayoutTransition(null);
|
|
}
|
|
|
|
@Override
|
|
public void onViewAdded(View child) {
|
|
if (!(child instanceof CellLayout)) {
|
|
throw new IllegalArgumentException("A Workspace can only have CellLayout children.");
|
|
}
|
|
CellLayout cl = ((CellLayout) child);
|
|
cl.setOnInterceptTouchListener(this);
|
|
cl.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
super.onViewAdded(child);
|
|
}
|
|
|
|
/**
|
|
* Initializes and binds the first page
|
|
* @param qsb an existing qsb to recycle or null.
|
|
*/
|
|
public void bindAndInitFirstWorkspaceScreen(View qsb) {
|
|
if (!FeatureFlags.QSB_ON_FIRST_SCREEN) {
|
|
return;
|
|
}
|
|
// Add the first page
|
|
CellLayout firstPage = insertNewWorkspaceScreen(Workspace.FIRST_SCREEN_ID, 0);
|
|
// Always add a QSB on the first screen.
|
|
if (qsb == null) {
|
|
// In transposed layout, we add the QSB in the Grid. As workspace does not touch the
|
|
// edges, we do not need a full width QSB.
|
|
qsb = LayoutInflater.from(getContext())
|
|
.inflate(R.layout.search_container_workspace, firstPage, false);
|
|
}
|
|
|
|
CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, firstPage.getCountX(), 1);
|
|
lp.canReorder = false;
|
|
if (!firstPage.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true)) {
|
|
Log.e(TAG, "Failed to add to item at (0, 0) to CellLayout");
|
|
}
|
|
}
|
|
|
|
public void removeAllWorkspaceScreens() {
|
|
// Disable all layout transitions before removing all pages to ensure that we don't get the
|
|
// transition animations competing with us changing the scroll when we add pages
|
|
disableLayoutTransitions();
|
|
|
|
// Recycle the QSB widget
|
|
View qsb = findViewById(R.id.search_container_workspace);
|
|
if (qsb != null) {
|
|
((ViewGroup) qsb.getParent()).removeView(qsb);
|
|
}
|
|
|
|
// Remove the pages and clear the screen models
|
|
removeFolderListeners();
|
|
removeAllViews();
|
|
mScreenOrder.clear();
|
|
mWorkspaceScreens.clear();
|
|
|
|
// Remove any deferred refresh callbacks
|
|
mLauncher.mHandler.removeCallbacksAndMessages(DeferredWidgetRefresh.class);
|
|
|
|
// Ensure that the first page is always present
|
|
bindAndInitFirstWorkspaceScreen(qsb);
|
|
|
|
// Re-enable the layout transitions
|
|
enableLayoutTransitions();
|
|
}
|
|
|
|
public void insertNewWorkspaceScreenBeforeEmptyScreen(int screenId) {
|
|
// Find the index to insert this view into. If the empty screen exists, then
|
|
// insert it before that.
|
|
int insertIndex = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
|
|
if (insertIndex < 0) {
|
|
insertIndex = mScreenOrder.size();
|
|
}
|
|
insertNewWorkspaceScreen(screenId, insertIndex);
|
|
}
|
|
|
|
public void insertNewWorkspaceScreen(int screenId) {
|
|
insertNewWorkspaceScreen(screenId, getChildCount());
|
|
}
|
|
|
|
public CellLayout insertNewWorkspaceScreen(int screenId, int insertIndex) {
|
|
if (mWorkspaceScreens.containsKey(screenId)) {
|
|
throw new RuntimeException("Screen id " + screenId + " already exists!");
|
|
}
|
|
|
|
// Inflate the cell layout, but do not add it automatically so that we can get the newly
|
|
// created CellLayout.
|
|
CellLayout newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
|
|
R.layout.workspace_screen, this, false /* attachToRoot */);
|
|
DeviceProfile grid = mLauncher.getDeviceProfile();
|
|
int paddingLeftRight = grid.cellLayoutPaddingLeftRightPx;
|
|
int paddingBottom = grid.cellLayoutBottomPaddingPx;
|
|
newScreen.setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom);
|
|
|
|
mWorkspaceScreens.put(screenId, newScreen);
|
|
mScreenOrder.add(insertIndex, screenId);
|
|
addView(newScreen, insertIndex);
|
|
mStateTransitionAnimation.applyChildState(
|
|
mLauncher.getStateManager().getState(), newScreen, insertIndex);
|
|
return newScreen;
|
|
}
|
|
|
|
public void addExtraEmptyScreenOnDrag() {
|
|
boolean lastChildOnScreen = false;
|
|
boolean childOnFinalScreen = false;
|
|
|
|
if (mDragSourceInternal != null) {
|
|
if (mDragSourceInternal.getChildCount() == 1) {
|
|
lastChildOnScreen = true;
|
|
}
|
|
CellLayout cl = (CellLayout) mDragSourceInternal.getParent();
|
|
if (indexOfChild(cl) == getChildCount() - 1) {
|
|
childOnFinalScreen = true;
|
|
}
|
|
}
|
|
|
|
// If this is the last item on the final screen
|
|
if (lastChildOnScreen && childOnFinalScreen) {
|
|
return;
|
|
}
|
|
if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) {
|
|
insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID);
|
|
}
|
|
}
|
|
|
|
public boolean addExtraEmptyScreen() {
|
|
if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) {
|
|
insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void convertFinalScreenToEmptyScreenIfNecessary() {
|
|
if (mLauncher.isWorkspaceLoading()) {
|
|
// Invalid and dangerous operation if workspace is loading
|
|
return;
|
|
}
|
|
|
|
if (hasExtraEmptyScreen() || mScreenOrder.size() == 0) return;
|
|
int finalScreenId = mScreenOrder.get(mScreenOrder.size() - 1);
|
|
|
|
CellLayout finalScreen = mWorkspaceScreens.get(finalScreenId);
|
|
|
|
// If the final screen is empty, convert it to the extra empty screen
|
|
if (finalScreen.getShortcutsAndWidgets().getChildCount() == 0 &&
|
|
!finalScreen.isDropPending()) {
|
|
mWorkspaceScreens.remove(finalScreenId);
|
|
mScreenOrder.removeValue(finalScreenId);
|
|
|
|
// if this is the last screen, convert it to the empty screen
|
|
mWorkspaceScreens.put(EXTRA_EMPTY_SCREEN_ID, finalScreen);
|
|
mScreenOrder.add(EXTRA_EMPTY_SCREEN_ID);
|
|
}
|
|
}
|
|
|
|
public void removeExtraEmptyScreen(boolean stripEmptyScreens) {
|
|
removeExtraEmptyScreenDelayed(0, stripEmptyScreens, null);
|
|
}
|
|
|
|
public void removeExtraEmptyScreenDelayed(
|
|
int delay, boolean stripEmptyScreens, Runnable onComplete) {
|
|
if (mLauncher.isWorkspaceLoading()) {
|
|
// Don't strip empty screens if the workspace is still loading
|
|
return;
|
|
}
|
|
|
|
if (delay > 0) {
|
|
postDelayed(
|
|
() -> removeExtraEmptyScreenDelayed(0, stripEmptyScreens, onComplete), delay);
|
|
return;
|
|
}
|
|
|
|
convertFinalScreenToEmptyScreenIfNecessary();
|
|
if (hasExtraEmptyScreen()) {
|
|
removeView(mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID));
|
|
mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID);
|
|
mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID);
|
|
|
|
// Update the page indicator to reflect the removed page.
|
|
showPageIndicatorAtCurrentScroll();
|
|
}
|
|
|
|
if (stripEmptyScreens) {
|
|
stripEmptyScreens();
|
|
}
|
|
|
|
if (onComplete != null) {
|
|
onComplete.run();
|
|
}
|
|
}
|
|
|
|
public boolean hasExtraEmptyScreen() {
|
|
return mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID) && getChildCount() > 1;
|
|
}
|
|
|
|
public int commitExtraEmptyScreen() {
|
|
if (mLauncher.isWorkspaceLoading()) {
|
|
// Invalid and dangerous operation if workspace is loading
|
|
return -1;
|
|
}
|
|
|
|
CellLayout cl = mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID);
|
|
mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID);
|
|
mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID);
|
|
|
|
int newId = LauncherSettings.Settings.call(getContext().getContentResolver(),
|
|
LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
|
|
.getInt(LauncherSettings.Settings.EXTRA_VALUE);
|
|
mWorkspaceScreens.put(newId, cl);
|
|
mScreenOrder.add(newId);
|
|
|
|
return newId;
|
|
}
|
|
|
|
@Override
|
|
public Hotseat getHotseat() {
|
|
return mLauncher.getHotseat();
|
|
}
|
|
|
|
@Override
|
|
public void onAddDropTarget(DropTarget target) {
|
|
mDragController.addDropTarget(target);
|
|
}
|
|
|
|
@Override
|
|
public CellLayout getScreenWithId(int screenId) {
|
|
return mWorkspaceScreens.get(screenId);
|
|
}
|
|
|
|
public int getIdForScreen(CellLayout layout) {
|
|
int index = mWorkspaceScreens.indexOfValue(layout);
|
|
if (index != -1) {
|
|
return mWorkspaceScreens.keyAt(index);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
public int getPageIndexForScreenId(int screenId) {
|
|
return indexOfChild(mWorkspaceScreens.get(screenId));
|
|
}
|
|
|
|
public int getScreenIdForPageIndex(int index) {
|
|
if (0 <= index && index < mScreenOrder.size()) {
|
|
return mScreenOrder.get(index);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
public IntArray getScreenOrder() {
|
|
return mScreenOrder;
|
|
}
|
|
|
|
public void stripEmptyScreens() {
|
|
if (mLauncher.isWorkspaceLoading()) {
|
|
// Don't strip empty screens if the workspace is still loading.
|
|
// This is dangerous and can result in data loss.
|
|
return;
|
|
}
|
|
|
|
if (isPageInTransition()) {
|
|
mStripScreensOnPageStopMoving = true;
|
|
return;
|
|
}
|
|
|
|
int currentPage = getNextPage();
|
|
IntArray removeScreens = new IntArray();
|
|
int total = mWorkspaceScreens.size();
|
|
for (int i = 0; i < total; i++) {
|
|
int id = mWorkspaceScreens.keyAt(i);
|
|
CellLayout cl = mWorkspaceScreens.valueAt(i);
|
|
// FIRST_SCREEN_ID can never be removed.
|
|
if ((!FeatureFlags.QSB_ON_FIRST_SCREEN || id > FIRST_SCREEN_ID)
|
|
&& cl.getShortcutsAndWidgets().getChildCount() == 0) {
|
|
removeScreens.add(id);
|
|
}
|
|
}
|
|
|
|
// We enforce at least one page to add new items to. In the case that we remove the last
|
|
// such screen, we convert the last screen to the empty screen
|
|
int minScreens = 1;
|
|
|
|
int pageShift = 0;
|
|
for (int i = 0; i < removeScreens.size(); i++) {
|
|
int id = removeScreens.get(i);
|
|
CellLayout cl = mWorkspaceScreens.get(id);
|
|
mWorkspaceScreens.remove(id);
|
|
mScreenOrder.removeValue(id);
|
|
|
|
if (getChildCount() > minScreens) {
|
|
if (indexOfChild(cl) < currentPage) {
|
|
pageShift++;
|
|
}
|
|
removeView(cl);
|
|
} else {
|
|
// if this is the last screen, convert it to the empty screen
|
|
mWorkspaceScreens.put(EXTRA_EMPTY_SCREEN_ID, cl);
|
|
mScreenOrder.add(EXTRA_EMPTY_SCREEN_ID);
|
|
}
|
|
}
|
|
|
|
if (pageShift >= 0) {
|
|
setCurrentPage(currentPage - pageShift);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called directly from a CellLayout (not by the framework), after we've been added as a
|
|
* listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout
|
|
* that it should intercept touch events, which is not something that is normally supported.
|
|
*/
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
@Override
|
|
public boolean onTouch(View v, MotionEvent event) {
|
|
return shouldConsumeTouch(v);
|
|
}
|
|
|
|
private boolean shouldConsumeTouch(View v) {
|
|
return !workspaceIconsCanBeDragged()
|
|
|| (!workspaceInModalState() && indexOfChild(v) != mCurrentPage);
|
|
}
|
|
|
|
public boolean isSwitchingState() {
|
|
return mIsSwitchingState;
|
|
}
|
|
|
|
/** This differs from isSwitchingState in that we take into account how far the transition
|
|
* has completed. */
|
|
public boolean isFinishedSwitchingState() {
|
|
return !mIsSwitchingState
|
|
|| (mTransitionProgress > FINISHED_SWITCHING_STATE_TRANSITION_PROGRESS);
|
|
}
|
|
|
|
@Override
|
|
public boolean dispatchUnhandledMove(View focused, int direction) {
|
|
if (workspaceInModalState() || !isFinishedSwitchingState()) {
|
|
// when the home screens are shrunken, shouldn't allow side-scrolling
|
|
return false;
|
|
}
|
|
return super.dispatchUnhandledMove(focused, direction);
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
|
mXDown = ev.getX();
|
|
mYDown = ev.getY();
|
|
}
|
|
return super.onInterceptTouchEvent(ev);
|
|
}
|
|
|
|
@Override
|
|
protected void determineScrollingStart(MotionEvent ev) {
|
|
if (!isFinishedSwitchingState()) return;
|
|
|
|
float deltaX = ev.getX() - mXDown;
|
|
float absDeltaX = Math.abs(deltaX);
|
|
float absDeltaY = Math.abs(ev.getY() - mYDown);
|
|
|
|
if (Float.compare(absDeltaX, 0f) == 0) return;
|
|
|
|
float slope = absDeltaY / absDeltaX;
|
|
float theta = (float) Math.atan(slope);
|
|
|
|
if (absDeltaX > mTouchSlop || absDeltaY > mTouchSlop) {
|
|
cancelCurrentPageLongPress();
|
|
}
|
|
|
|
if (theta > MAX_SWIPE_ANGLE) {
|
|
// Above MAX_SWIPE_ANGLE, we don't want to ever start scrolling the workspace
|
|
return;
|
|
} else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) {
|
|
// Above START_DAMPING_TOUCH_SLOP_ANGLE and below MAX_SWIPE_ANGLE, we want to
|
|
// increase the touch slop to make it harder to begin scrolling the workspace. This
|
|
// results in vertically scrolling widgets to more easily. The higher the angle, the
|
|
// more we increase touch slop.
|
|
theta -= START_DAMPING_TOUCH_SLOP_ANGLE;
|
|
float extraRatio = (float)
|
|
Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE)));
|
|
super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio);
|
|
} else {
|
|
// Below START_DAMPING_TOUCH_SLOP_ANGLE, we don't do anything special
|
|
super.determineScrollingStart(ev);
|
|
}
|
|
}
|
|
|
|
protected void onPageBeginTransition() {
|
|
super.onPageBeginTransition();
|
|
updateChildrenLayersEnabled();
|
|
}
|
|
|
|
protected void onPageEndTransition() {
|
|
super.onPageEndTransition();
|
|
updateChildrenLayersEnabled();
|
|
|
|
if (mDragController.isDragging()) {
|
|
if (workspaceInModalState()) {
|
|
// If we are in springloaded mode, then force an event to check if the current touch
|
|
// is under a new page (to scroll to)
|
|
mDragController.forceTouchMove();
|
|
}
|
|
}
|
|
|
|
if (mStripScreensOnPageStopMoving) {
|
|
stripEmptyScreens();
|
|
mStripScreensOnPageStopMoving = false;
|
|
}
|
|
}
|
|
|
|
protected void onScrollInteractionBegin() {
|
|
super.onScrollInteractionBegin();
|
|
mScrollInteractionBegan = true;
|
|
}
|
|
|
|
protected void onScrollInteractionEnd() {
|
|
super.onScrollInteractionEnd();
|
|
mScrollInteractionBegan = false;
|
|
if (mStartedSendingScrollEvents) {
|
|
mStartedSendingScrollEvents = false;
|
|
mLauncherOverlay.onScrollInteractionEnd();
|
|
}
|
|
}
|
|
|
|
public void setLauncherOverlay(LauncherOverlay overlay) {
|
|
mLauncherOverlay = overlay;
|
|
// A new overlay has been set. Reset event tracking
|
|
mStartedSendingScrollEvents = false;
|
|
onOverlayScrollChanged(0);
|
|
}
|
|
|
|
public boolean hasOverlay() {
|
|
return mLauncherOverlay != null;
|
|
}
|
|
|
|
private boolean isScrollingOverlay() {
|
|
return mLauncherOverlay != null &&
|
|
((mIsRtl && getUnboundedScroll() > mMaxScroll)
|
|
|| (!mIsRtl && getUnboundedScroll() < mMinScroll));
|
|
}
|
|
|
|
@Override
|
|
protected void snapToDestination() {
|
|
// If we're overscrolling the overlay, we make sure to immediately reset the PagedView
|
|
// to it's baseline position instead of letting the overscroll settle. The overlay handles
|
|
// it's own settling, and every gesture to the overlay should be self-contained and start
|
|
// from 0, so we zero it out here.
|
|
if (isScrollingOverlay()) {
|
|
// We reset mWasInOverscroll so that PagedView doesn't zero out the overscroll
|
|
// interaction when we call snapToPageImmediately.
|
|
mWasInOverscroll = false;
|
|
snapToPageImmediately(0);
|
|
} else {
|
|
super.snapToDestination();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
|
|
super.onScrollChanged(l, t, oldl, oldt);
|
|
|
|
// Update the page indicator progress.
|
|
boolean isTransitioning = mIsSwitchingState
|
|
|| (getLayoutTransition() != null && getLayoutTransition().isRunning());
|
|
if (!isTransitioning) {
|
|
showPageIndicatorAtCurrentScroll();
|
|
}
|
|
|
|
updatePageAlphaValues();
|
|
enableHwLayersOnVisiblePages();
|
|
}
|
|
|
|
public void showPageIndicatorAtCurrentScroll() {
|
|
if (mPageIndicator != null) {
|
|
mPageIndicator.setScroll(getScrollX(), computeMaxScroll());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void overScroll(int amount) {
|
|
boolean shouldScrollOverlay = mLauncherOverlay != null && !mScroller.isSpringing() &&
|
|
((amount <= 0 && !mIsRtl) || (amount >= 0 && mIsRtl));
|
|
|
|
boolean shouldZeroOverlay = mLauncherOverlay != null && mLastOverlayScroll != 0 &&
|
|
((amount >= 0 && !mIsRtl) || (amount <= 0 && mIsRtl));
|
|
|
|
if (shouldScrollOverlay) {
|
|
if (!mStartedSendingScrollEvents && mScrollInteractionBegan) {
|
|
mStartedSendingScrollEvents = true;
|
|
mLauncherOverlay.onScrollInteractionBegin();
|
|
}
|
|
|
|
mLastOverlayScroll = Math.abs(((float) amount) / getMeasuredWidth());
|
|
mLauncherOverlay.onScrollChange(mLastOverlayScroll, mIsRtl);
|
|
} else {
|
|
dampedOverScroll(amount);
|
|
}
|
|
|
|
if (shouldZeroOverlay) {
|
|
mLauncherOverlay.onScrollChange(0, mIsRtl);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected boolean onOverscroll(int amount) {
|
|
// Enforce overscroll on -1 direction
|
|
if ((amount > 0 && !mIsRtl) || (amount < 0 && mIsRtl)) return false;
|
|
return super.onOverscroll(amount);
|
|
}
|
|
|
|
@Override
|
|
protected boolean shouldFlingForVelocity(int velocityX) {
|
|
// When the overlay is moving, the fling or settle transition is controlled by the overlay.
|
|
return Float.compare(Math.abs(mOverlayTranslation), 0) == 0 &&
|
|
super.shouldFlingForVelocity(velocityX);
|
|
}
|
|
|
|
/**
|
|
* The overlay scroll is being controlled locally, just update our overlay effect
|
|
*/
|
|
public void onOverlayScrollChanged(float scroll) {
|
|
if (Float.compare(scroll, 1f) == 0) {
|
|
if (!mOverlayShown) {
|
|
mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
|
|
Action.Direction.LEFT, ContainerType.WORKSPACE, 0);
|
|
}
|
|
mOverlayShown = true;
|
|
// Not announcing the overlay page for accessibility since it announces itself.
|
|
} else if (Float.compare(scroll, 0f) == 0) {
|
|
if (mOverlayShown) {
|
|
UserEventDispatcher ued = mLauncher.getUserEventDispatcher();
|
|
if (!ued.isPreviousHomeGesture()) {
|
|
mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
|
|
Action.Direction.RIGHT, ContainerType.WORKSPACE, -1);
|
|
}
|
|
} else if (Float.compare(mOverlayTranslation, 0f) != 0) {
|
|
// When arriving to 0 overscroll from non-zero overscroll, announce page for
|
|
// accessibility since default announcements were disabled while in overscroll
|
|
// state.
|
|
// Not doing this if mOverlayShown because in that case the accessibility service
|
|
// will announce the launcher window description upon regaining focus after
|
|
// switching from the overlay screen.
|
|
announcePageForAccessibility();
|
|
}
|
|
mOverlayShown = false;
|
|
tryRunOverlayCallback();
|
|
}
|
|
|
|
float offset = 0f;
|
|
|
|
scroll = Math.max(scroll - offset, 0);
|
|
scroll = Math.min(1, scroll / (1 - offset));
|
|
|
|
float alpha = 1 - Interpolators.DEACCEL_3.getInterpolation(scroll);
|
|
float transX = mLauncher.getDragLayer().getMeasuredWidth() * scroll;
|
|
|
|
if (mIsRtl) {
|
|
transX = -transX;
|
|
}
|
|
mOverlayTranslation = transX;
|
|
|
|
// TODO(adamcohen): figure out a final effect here. We may need to recommend
|
|
// different effects based on device performance. On at least one relatively high-end
|
|
// device I've tried, translating the launcher causes things to get quite laggy.
|
|
mLauncher.getDragLayer().setTranslationX(transX);
|
|
mLauncher.getDragLayer().getAlphaProperty(ALPHA_INDEX_OVERLAY).setValue(alpha);
|
|
}
|
|
|
|
/**
|
|
* @return false if the callback is still pending
|
|
*/
|
|
private boolean tryRunOverlayCallback() {
|
|
if (mOnOverlayHiddenCallback == null) {
|
|
// Return true as no callback is pending. This is used by OnWindowFocusChangeListener
|
|
// to remove itself if multiple focus handles were added.
|
|
return true;
|
|
}
|
|
if (mOverlayShown || !hasWindowFocus()) {
|
|
return false;
|
|
}
|
|
|
|
mOnOverlayHiddenCallback.run();
|
|
mOnOverlayHiddenCallback = null;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Runs the given callback when the minus one overlay is hidden. Specifically, it is run
|
|
* when launcher's window has focus and the overlay is no longer being shown. If a callback
|
|
* is already present, the new callback will chain off it so both are run.
|
|
*
|
|
* @return Whether the callback was deferred.
|
|
*/
|
|
public boolean runOnOverlayHidden(Runnable callback) {
|
|
if (mOnOverlayHiddenCallback == null) {
|
|
mOnOverlayHiddenCallback = callback;
|
|
} else {
|
|
// Chain the new callback onto the previous callback(s).
|
|
Runnable oldCallback = mOnOverlayHiddenCallback;
|
|
mOnOverlayHiddenCallback = () -> {
|
|
oldCallback.run();
|
|
callback.run();
|
|
};
|
|
}
|
|
if (!tryRunOverlayCallback()) {
|
|
ViewTreeObserver observer = getViewTreeObserver();
|
|
if (observer != null && observer.isAlive()) {
|
|
observer.addOnWindowFocusChangeListener(
|
|
new ViewTreeObserver.OnWindowFocusChangeListener() {
|
|
@Override
|
|
public void onWindowFocusChanged(boolean hasFocus) {
|
|
if (tryRunOverlayCallback() && observer.isAlive()) {
|
|
observer.removeOnWindowFocusChangeListener(this);
|
|
}
|
|
}});
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
protected void notifyPageSwitchListener(int prevPage) {
|
|
super.notifyPageSwitchListener(prevPage);
|
|
if (prevPage != mCurrentPage) {
|
|
int swipeDirection = (prevPage < mCurrentPage) ? Action.Direction.RIGHT : Action.Direction.LEFT;
|
|
mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
|
|
swipeDirection, ContainerType.WORKSPACE, prevPage);
|
|
}
|
|
}
|
|
|
|
protected void setWallpaperDimension() {
|
|
Executors.THREAD_POOL_EXECUTOR.execute(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
final Point size = LauncherAppState.getIDP(getContext()).defaultWallpaperSize;
|
|
if (size.x != mWallpaperManager.getDesiredMinimumWidth()
|
|
|| size.y != mWallpaperManager.getDesiredMinimumHeight()) {
|
|
mWallpaperManager.suggestDesiredDimensions(size.x, size.y);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
public void lockWallpaperToDefaultPage() {
|
|
mWallpaperOffset.setLockToDefaultPage(true);
|
|
}
|
|
|
|
public void unlockWallpaperFromDefaultPageOnNextLayout() {
|
|
if (mWallpaperOffset.isLockedToDefaultPage()) {
|
|
mUnlockWallpaperFromDefaultPageOnLayout = true;
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void computeScroll() {
|
|
super.computeScroll();
|
|
mWallpaperOffset.syncWithScroll();
|
|
}
|
|
|
|
public void computeScrollWithoutInvalidation() {
|
|
computeScrollHelper(false);
|
|
}
|
|
|
|
@Override
|
|
protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
|
|
if (!isSwitchingState()) {
|
|
super.determineScrollingStart(ev, touchSlopScale);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void announceForAccessibility(CharSequence text) {
|
|
// Don't announce if apps is on top of us.
|
|
if (!mLauncher.isInState(ALL_APPS)) {
|
|
super.announceForAccessibility(text);
|
|
}
|
|
}
|
|
|
|
private void updatePageAlphaValues() {
|
|
// We need to check the isDragging case because updatePageAlphaValues is called between
|
|
// goToState(SPRING_LOADED) and onStartStateTransition.
|
|
if (!workspaceInModalState() && !mIsSwitchingState && !mDragController.isDragging()) {
|
|
int screenCenter = getScrollX() + getMeasuredWidth() / 2;
|
|
for (int i = 0; i < getChildCount(); i++) {
|
|
CellLayout child = (CellLayout) getChildAt(i);
|
|
if (child != null) {
|
|
float scrollProgress = getScrollProgress(screenCenter, child, i);
|
|
float alpha = 1 - Math.abs(scrollProgress);
|
|
if (mWorkspaceFadeInAdjacentScreens) {
|
|
child.getShortcutsAndWidgets().setAlpha(alpha);
|
|
} else {
|
|
// Pages that are off-screen aren't important for accessibility.
|
|
child.getShortcutsAndWidgets().setImportantForAccessibility(
|
|
alpha > 0 ? IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
: IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
mWallpaperOffset.setWindowToken(getWindowToken());
|
|
computeScroll();
|
|
}
|
|
|
|
protected void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
mWallpaperOffset.setWindowToken(null);
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
if (mUnlockWallpaperFromDefaultPageOnLayout) {
|
|
mWallpaperOffset.setLockToDefaultPage(false);
|
|
mUnlockWallpaperFromDefaultPageOnLayout = false;
|
|
}
|
|
if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
|
|
mWallpaperOffset.syncWithScroll();
|
|
mWallpaperOffset.jumpToFinal();
|
|
}
|
|
super.onLayout(changed, left, top, right, bottom);
|
|
updatePageAlphaValues();
|
|
}
|
|
|
|
@Override
|
|
public int getDescendantFocusability() {
|
|
if (workspaceInModalState()) {
|
|
return ViewGroup.FOCUS_BLOCK_DESCENDANTS;
|
|
}
|
|
return super.getDescendantFocusability();
|
|
}
|
|
|
|
private boolean workspaceInModalState() {
|
|
return !mLauncher.isInState(NORMAL);
|
|
}
|
|
|
|
private boolean workspaceInScrollableState() {
|
|
return mLauncher.isInState(SPRING_LOADED) || !workspaceInModalState();
|
|
}
|
|
|
|
/** Returns whether a drag should be allowed to be started from the current workspace state. */
|
|
public boolean workspaceIconsCanBeDragged() {
|
|
return mLauncher.getStateManager().getState().hasFlag(FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED);
|
|
}
|
|
|
|
private void updateChildrenLayersEnabled() {
|
|
boolean enableChildrenLayers = mIsSwitchingState || isPageInTransition();
|
|
|
|
if (enableChildrenLayers != mChildrenLayersEnabled) {
|
|
mChildrenLayersEnabled = enableChildrenLayers;
|
|
if (mChildrenLayersEnabled) {
|
|
enableHwLayersOnVisiblePages();
|
|
} else {
|
|
for (int i = 0; i < getPageCount(); i++) {
|
|
final CellLayout cl = (CellLayout) getChildAt(i);
|
|
cl.enableHardwareLayer(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void enableHwLayersOnVisiblePages() {
|
|
if (mChildrenLayersEnabled) {
|
|
final int screenCount = getChildCount();
|
|
|
|
final int[] visibleScreens = getVisibleChildrenRange();
|
|
int leftScreen = visibleScreens[0];
|
|
int rightScreen = visibleScreens[1];
|
|
if (mForceDrawAdjacentPages) {
|
|
// In overview mode, make sure that the two side pages are visible.
|
|
leftScreen = Utilities.boundToRange(getCurrentPage() - 1, 0, rightScreen);
|
|
rightScreen = Utilities.boundToRange(getCurrentPage() + 1,
|
|
leftScreen, getPageCount() - 1);
|
|
}
|
|
|
|
if (leftScreen == rightScreen) {
|
|
// make sure we're caching at least two pages always
|
|
if (rightScreen < screenCount - 1) {
|
|
rightScreen++;
|
|
} else if (leftScreen > 0) {
|
|
leftScreen--;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < screenCount; i++) {
|
|
final CellLayout layout = (CellLayout) getPageAt(i);
|
|
// enable layers between left and right screen inclusive.
|
|
boolean enableLayer = leftScreen <= i && i <= rightScreen;
|
|
layout.enableHardwareLayer(enableLayer);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void onWallpaperTap(MotionEvent ev) {
|
|
final int[] position = mTempXY;
|
|
getLocationOnScreen(position);
|
|
|
|
int pointerIndex = ev.getActionIndex();
|
|
position[0] += (int) ev.getX(pointerIndex);
|
|
position[1] += (int) ev.getY(pointerIndex);
|
|
|
|
mWallpaperManager.sendWallpaperCommand(getWindowToken(),
|
|
ev.getAction() == MotionEvent.ACTION_UP
|
|
? WallpaperManager.COMMAND_TAP : WallpaperManager.COMMAND_SECONDARY_TAP,
|
|
position[0], position[1], 0, null);
|
|
}
|
|
|
|
public void prepareDragWithProvider(DragPreviewProvider outlineProvider) {
|
|
mOutlineProvider = outlineProvider;
|
|
}
|
|
|
|
public void snapToPageFromOverView(int whichPage) {
|
|
snapToPage(whichPage, OVERVIEW.getTransitionDuration(mLauncher), Interpolators.ZOOM_IN);
|
|
}
|
|
|
|
private void onStartStateTransition(LauncherState state) {
|
|
mIsSwitchingState = true;
|
|
mTransitionProgress = 0;
|
|
|
|
updateChildrenLayersEnabled();
|
|
}
|
|
|
|
private void onEndStateTransition() {
|
|
mIsSwitchingState = false;
|
|
mForceDrawAdjacentPages = false;
|
|
mTransitionProgress = 1;
|
|
|
|
updateChildrenLayersEnabled();
|
|
updateAccessibilityFlags();
|
|
}
|
|
|
|
/**
|
|
* Sets the current workspace {@link LauncherState} and updates the UI without any animations
|
|
*/
|
|
@Override
|
|
public void setState(LauncherState toState) {
|
|
onStartStateTransition(toState);
|
|
mStateTransitionAnimation.setState(toState);
|
|
onEndStateTransition();
|
|
}
|
|
|
|
/**
|
|
* Sets the current workspace {@link LauncherState}, then animates the UI
|
|
*/
|
|
@Override
|
|
public void setStateWithAnimation(
|
|
LauncherState toState, StateAnimationConfig config, PendingAnimation animation) {
|
|
StateTransitionListener listener = new StateTransitionListener(toState);
|
|
mStateTransitionAnimation.setStateWithAnimation(toState, config, animation);
|
|
|
|
// Invalidate the pages now, so that we have the visible pages before the
|
|
// animation is started
|
|
if (toState.hasFlag(FLAG_MULTI_PAGE)) {
|
|
mForceDrawAdjacentPages = true;
|
|
}
|
|
invalidate(); // This will call dispatchDraw(), which calls getVisiblePages().
|
|
|
|
ValueAnimator stepAnimator = ValueAnimator.ofFloat(0, 1);
|
|
stepAnimator.addUpdateListener(listener);
|
|
stepAnimator.setDuration(config.duration);
|
|
stepAnimator.addListener(listener);
|
|
animation.add(stepAnimator);
|
|
}
|
|
|
|
public WorkspaceStateTransitionAnimation getStateTransitionAnimation() {
|
|
return mStateTransitionAnimation;
|
|
}
|
|
|
|
public void updateAccessibilityFlags() {
|
|
// TODO: Update the accessibility flags appropriately when dragging.
|
|
int accessibilityFlag =
|
|
mLauncher.getStateManager().getState().hasFlag(FLAG_WORKSPACE_INACCESSIBLE)
|
|
? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
: IMPORTANT_FOR_ACCESSIBILITY_AUTO;
|
|
if (!mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
|
|
int total = getPageCount();
|
|
for (int i = 0; i < total; i++) {
|
|
updateAccessibilityFlags(accessibilityFlag, (CellLayout) getPageAt(i));
|
|
}
|
|
setImportantForAccessibility(accessibilityFlag);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public AccessibilityNodeInfo createAccessibilityNodeInfo() {
|
|
if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
|
// TAPL tests verify that workspace is not present in Overview and AllApps states.
|
|
// TAPL can work only if UIDevice is set up as setCompressedLayoutHeirarchy(false).
|
|
// Hiding workspace from the tests when it's
|
|
// IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS.
|
|
return null;
|
|
}
|
|
return super.createAccessibilityNodeInfo();
|
|
}
|
|
|
|
private void updateAccessibilityFlags(int accessibilityFlag, CellLayout page) {
|
|
page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
|
page.getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
|
|
page.setContentDescription(null);
|
|
page.setAccessibilityDelegate(null);
|
|
}
|
|
|
|
public void startDrag(CellLayout.CellInfo cellInfo, DragOptions options) {
|
|
View child = cellInfo.cell;
|
|
|
|
mDragInfo = cellInfo;
|
|
child.setVisibility(INVISIBLE);
|
|
|
|
if (options.isAccessibleDrag) {
|
|
mDragController.addDragListener(
|
|
new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
|
|
@Override
|
|
protected void enableAccessibleDrag(boolean enable) {
|
|
super.enableAccessibleDrag(enable);
|
|
setEnableForLayout(mLauncher.getHotseat(), enable);
|
|
}
|
|
});
|
|
}
|
|
|
|
beginDragShared(child, this, options);
|
|
}
|
|
|
|
public void beginDragShared(View child, DragSource source, DragOptions options) {
|
|
Object dragObject = child.getTag();
|
|
if (!(dragObject instanceof ItemInfo)) {
|
|
String msg = "Drag started with a view that has no tag set. This "
|
|
+ "will cause a crash (issue 11627249) down the line. "
|
|
+ "View: " + child + " tag: " + child.getTag();
|
|
throw new IllegalStateException(msg);
|
|
}
|
|
beginDragShared(child, null, source, (ItemInfo) dragObject,
|
|
new DragPreviewProvider(child), options);
|
|
}
|
|
|
|
/**
|
|
* Core functionality for beginning a drag operation for an item that will be dropped within
|
|
* the workspace
|
|
*/
|
|
public DragView beginDragShared(View child, DraggableView draggableView, DragSource source,
|
|
ItemInfo dragObject, DragPreviewProvider previewProvider, DragOptions dragOptions) {
|
|
|
|
float iconScale = 1f;
|
|
if (child instanceof BubbleTextView) {
|
|
Drawable icon = ((BubbleTextView) child).getIcon();
|
|
if (icon instanceof FastBitmapDrawable) {
|
|
iconScale = ((FastBitmapDrawable) icon).getAnimatedScale();
|
|
}
|
|
}
|
|
|
|
// Clear the pressed state if necessary
|
|
child.clearFocus();
|
|
child.setPressed(false);
|
|
if (child instanceof BubbleTextView) {
|
|
BubbleTextView icon = (BubbleTextView) child;
|
|
icon.clearPressedBackground();
|
|
}
|
|
|
|
mOutlineProvider = previewProvider;
|
|
|
|
if (draggableView == null && child instanceof DraggableView) {
|
|
draggableView = (DraggableView) child;
|
|
}
|
|
|
|
// The drag bitmap follows the touch point around on the screen
|
|
final Bitmap b = previewProvider.createDragBitmap();
|
|
int halfPadding = previewProvider.previewPadding / 2;
|
|
float scale = previewProvider.getScaleAndPosition(b, mTempXY);
|
|
int dragLayerX = mTempXY[0];
|
|
int dragLayerY = mTempXY[1];
|
|
|
|
Point dragVisualizeOffset = null;
|
|
Rect dragRect = new Rect();
|
|
|
|
if (draggableView != null) {
|
|
draggableView.getSourceVisualDragBounds(dragRect);
|
|
dragLayerY += dragRect.top;
|
|
dragVisualizeOffset = new Point(- halfPadding, halfPadding);
|
|
}
|
|
|
|
|
|
if (child.getParent() instanceof ShortcutAndWidgetContainer) {
|
|
mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
|
|
}
|
|
|
|
if (child instanceof BubbleTextView && !dragOptions.isAccessibleDrag) {
|
|
PopupContainerWithArrow popupContainer = PopupContainerWithArrow
|
|
.showForIcon((BubbleTextView) child);
|
|
if (popupContainer != null) {
|
|
dragOptions.preDragCondition = popupContainer.createPreDragCondition();
|
|
mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("dragging started");
|
|
}
|
|
}
|
|
|
|
DragView dv = mDragController.startDrag(b, draggableView, dragLayerX, dragLayerY, source,
|
|
dragObject, dragVisualizeOffset, dragRect, scale * iconScale,
|
|
scale, dragOptions);
|
|
dv.setIntrinsicIconScaleFactor(dragOptions.intrinsicIconScaleFactor);
|
|
return dv;
|
|
}
|
|
|
|
private boolean transitionStateShouldAllowDrop() {
|
|
return (!isSwitchingState() || mTransitionProgress > ALLOW_DROP_TRANSITION_PROGRESS) &&
|
|
workspaceIconsCanBeDragged();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public boolean acceptDrop(DragObject d) {
|
|
// If it's an external drop (e.g. from All Apps), check if it should be accepted
|
|
CellLayout dropTargetLayout = mDropToLayout;
|
|
if (d.dragSource != this) {
|
|
// Don't accept the drop if we're not over a screen at time of drop
|
|
if (dropTargetLayout == null) {
|
|
return false;
|
|
}
|
|
if (!transitionStateShouldAllowDrop()) return false;
|
|
|
|
mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
|
|
|
|
// We want the point to be mapped to the dragTarget.
|
|
mapPointFromDropLayout(dropTargetLayout, mDragViewVisualCenter);
|
|
|
|
int spanX;
|
|
int spanY;
|
|
if (mDragInfo != null) {
|
|
final CellLayout.CellInfo dragCellInfo = mDragInfo;
|
|
spanX = dragCellInfo.spanX;
|
|
spanY = dragCellInfo.spanY;
|
|
} else {
|
|
spanX = d.dragInfo.spanX;
|
|
spanY = d.dragInfo.spanY;
|
|
}
|
|
|
|
int minSpanX = spanX;
|
|
int minSpanY = spanY;
|
|
if (d.dragInfo instanceof PendingAddWidgetInfo) {
|
|
minSpanX = ((PendingAddWidgetInfo) d.dragInfo).minSpanX;
|
|
minSpanY = ((PendingAddWidgetInfo) d.dragInfo).minSpanY;
|
|
}
|
|
|
|
mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, dropTargetLayout,
|
|
mTargetCell);
|
|
float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
|
|
mDragViewVisualCenter[1], mTargetCell);
|
|
if (mCreateUserFolderOnDrop && willCreateUserFolder(d.dragInfo,
|
|
dropTargetLayout, mTargetCell, distance, true)) {
|
|
return true;
|
|
}
|
|
|
|
if (mAddToExistingFolderOnDrop && willAddToExistingUserFolder(d.dragInfo,
|
|
dropTargetLayout, mTargetCell, distance)) {
|
|
return true;
|
|
}
|
|
|
|
int[] resultSpan = new int[2];
|
|
mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
|
|
null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP);
|
|
boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;
|
|
|
|
// Don't accept the drop if there's no room for the item
|
|
if (!foundCell) {
|
|
onNoCellFound(dropTargetLayout);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int screenId = getIdForScreen(dropTargetLayout);
|
|
if (screenId == EXTRA_EMPTY_SCREEN_ID) {
|
|
commitExtraEmptyScreen();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell,
|
|
float distance, boolean considerTimeout) {
|
|
if (distance > mMaxDistanceForFolderCreation) return false;
|
|
View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
|
|
return willCreateUserFolder(info, dropOverView, considerTimeout);
|
|
}
|
|
|
|
boolean willCreateUserFolder(ItemInfo info, View dropOverView, boolean considerTimeout) {
|
|
if (dropOverView != null) {
|
|
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams();
|
|
if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
boolean hasntMoved = false;
|
|
if (mDragInfo != null) {
|
|
hasntMoved = dropOverView == mDragInfo.cell;
|
|
}
|
|
|
|
if (dropOverView == null || hasntMoved || (considerTimeout && !mCreateUserFolderOnDrop)) {
|
|
return false;
|
|
}
|
|
|
|
boolean aboveShortcut = (dropOverView.getTag() instanceof WorkspaceItemInfo
|
|
&& ((WorkspaceItemInfo) dropOverView.getTag()).container
|
|
!= LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
|
|
boolean willBecomeShortcut =
|
|
(info.itemType == ITEM_TYPE_APPLICATION ||
|
|
info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
|
|
info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT);
|
|
|
|
return (aboveShortcut && willBecomeShortcut);
|
|
}
|
|
|
|
boolean willAddToExistingUserFolder(ItemInfo dragInfo, CellLayout target, int[] targetCell,
|
|
float distance) {
|
|
if (distance > mMaxDistanceForFolderCreation) return false;
|
|
View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
|
|
return willAddToExistingUserFolder(dragInfo, dropOverView);
|
|
|
|
}
|
|
boolean willAddToExistingUserFolder(ItemInfo dragInfo, View dropOverView) {
|
|
if (dropOverView != null) {
|
|
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams();
|
|
if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (dropOverView instanceof FolderIcon) {
|
|
FolderIcon fi = (FolderIcon) dropOverView;
|
|
if (fi.acceptDrop(dragInfo)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
boolean createUserFolderIfNecessary(View newView, int container, CellLayout target,
|
|
int[] targetCell, float distance, boolean external, DragObject d) {
|
|
if (distance > mMaxDistanceForFolderCreation) return false;
|
|
View v = target.getChildAt(targetCell[0], targetCell[1]);
|
|
|
|
boolean hasntMoved = false;
|
|
if (mDragInfo != null) {
|
|
CellLayout cellParent = getParentCellLayoutForView(mDragInfo.cell);
|
|
hasntMoved = (mDragInfo.cellX == targetCell[0] &&
|
|
mDragInfo.cellY == targetCell[1]) && (cellParent == target);
|
|
}
|
|
|
|
if (v == null || hasntMoved || !mCreateUserFolderOnDrop) return false;
|
|
mCreateUserFolderOnDrop = false;
|
|
final int screenId = getIdForScreen(target);
|
|
|
|
boolean aboveShortcut = (v.getTag() instanceof WorkspaceItemInfo);
|
|
boolean willBecomeShortcut = (newView.getTag() instanceof WorkspaceItemInfo);
|
|
|
|
if (aboveShortcut && willBecomeShortcut) {
|
|
WorkspaceItemInfo sourceInfo = (WorkspaceItemInfo) newView.getTag();
|
|
WorkspaceItemInfo destInfo = (WorkspaceItemInfo) v.getTag();
|
|
// if the drag started here, we need to remove it from the workspace
|
|
if (!external) {
|
|
getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
|
|
}
|
|
|
|
Rect folderLocation = new Rect();
|
|
float scale = mLauncher.getDragLayer().getDescendantRectRelativeToSelf(v, folderLocation);
|
|
target.removeView(v);
|
|
mStatsLogManager.log(
|
|
LauncherEvent.LAUNCHER_ITEM_DROP_FOLDER_CREATED,
|
|
d.logInstanceId,
|
|
destInfo.buildProto(null));
|
|
FolderIcon fi = mLauncher.addFolder(target, container, screenId, targetCell[0],
|
|
targetCell[1]);
|
|
destInfo.cellX = -1;
|
|
destInfo.cellY = -1;
|
|
sourceInfo.cellX = -1;
|
|
sourceInfo.cellY = -1;
|
|
|
|
// If the dragView is null, we can't animate
|
|
boolean animate = d != null;
|
|
if (animate) {
|
|
// In order to keep everything continuous, we hand off the currently rendered
|
|
// folder background to the newly created icon. This preserves animation state.
|
|
fi.setFolderBackground(mFolderCreateBg);
|
|
mFolderCreateBg = new PreviewBackground();
|
|
fi.performCreateAnimation(destInfo, v, sourceInfo, d, folderLocation, scale);
|
|
} else {
|
|
fi.prepareCreateAnimation(v);
|
|
fi.addItem(destInfo);
|
|
fi.addItem(sourceInfo);
|
|
}
|
|
mLauncher.folderCreatedFromItem(fi.getFolder(), destInfo);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
boolean addToExistingFolderIfNecessary(View newView, CellLayout target, int[] targetCell,
|
|
float distance, DragObject d, boolean external) {
|
|
if (distance > mMaxDistanceForFolderCreation) return false;
|
|
|
|
View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
|
|
if (!mAddToExistingFolderOnDrop) return false;
|
|
mAddToExistingFolderOnDrop = false;
|
|
|
|
if (dropOverView instanceof FolderIcon) {
|
|
FolderIcon fi = (FolderIcon) dropOverView;
|
|
if (fi.acceptDrop(d.dragInfo)) {
|
|
mStatsLogManager.log(
|
|
LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED,
|
|
d.logInstanceId,
|
|
fi.mInfo.buildProto(null));
|
|
fi.onDrop(d, false /* itemReturnedOnFailedDrop */);
|
|
|
|
// if the drag started here, we need to remove it from the workspace
|
|
if (!external) {
|
|
getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void prepareAccessibilityDrop() { }
|
|
|
|
@Override
|
|
public void onDrop(final DragObject d, DragOptions options) {
|
|
mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
|
|
CellLayout dropTargetLayout = mDropToLayout;
|
|
|
|
// We want the point to be mapped to the dragTarget.
|
|
if (dropTargetLayout != null) {
|
|
mapPointFromDropLayout(dropTargetLayout, mDragViewVisualCenter);
|
|
}
|
|
|
|
boolean droppedOnOriginalCell = false;
|
|
|
|
int snapScreen = -1;
|
|
boolean resizeOnDrop = false;
|
|
if (d.dragSource != this || mDragInfo == null) {
|
|
final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1] };
|
|
onDropExternal(touchXY, dropTargetLayout, d);
|
|
} else {
|
|
final View cell = mDragInfo.cell;
|
|
boolean droppedOnOriginalCellDuringTransition = false;
|
|
Runnable onCompleteRunnable = null;
|
|
|
|
if (dropTargetLayout != null && !d.cancelled) {
|
|
// Move internally
|
|
boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout);
|
|
boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
|
|
int container = hasMovedIntoHotseat ?
|
|
LauncherSettings.Favorites.CONTAINER_HOTSEAT :
|
|
LauncherSettings.Favorites.CONTAINER_DESKTOP;
|
|
int screenId = (mTargetCell[0] < 0) ?
|
|
mDragInfo.screenId : getIdForScreen(dropTargetLayout);
|
|
int spanX = mDragInfo != null ? mDragInfo.spanX : 1;
|
|
int spanY = mDragInfo != null ? mDragInfo.spanY : 1;
|
|
// First we find the cell nearest to point at which the item is
|
|
// dropped, without any consideration to whether there is an item there.
|
|
|
|
mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int)
|
|
mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell);
|
|
float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
|
|
mDragViewVisualCenter[1], mTargetCell);
|
|
|
|
// If the item being dropped is a shortcut and the nearest drop
|
|
// cell also contains a shortcut, then create a folder with the two shortcuts.
|
|
if (createUserFolderIfNecessary(cell, container,
|
|
dropTargetLayout, mTargetCell, distance, false, d)
|
|
|| addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
|
|
distance, d, false)) {
|
|
mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
|
|
return;
|
|
}
|
|
|
|
// Aside from the special case where we're dropping a shortcut onto a shortcut,
|
|
// we need to find the nearest cell location that is vacant
|
|
ItemInfo item = d.dragInfo;
|
|
int minSpanX = item.spanX;
|
|
int minSpanY = item.spanY;
|
|
if (item.minSpanX > 0 && item.minSpanY > 0) {
|
|
minSpanX = item.minSpanX;
|
|
minSpanY = item.minSpanY;
|
|
}
|
|
|
|
droppedOnOriginalCell = item.screenId == screenId && item.container == container
|
|
&& item.cellX == mTargetCell[0] && item.cellY == mTargetCell[1];
|
|
droppedOnOriginalCellDuringTransition = droppedOnOriginalCell && mIsSwitchingState;
|
|
|
|
// When quickly moving an item, a user may accidentally rearrange their
|
|
// workspace. So instead we move the icon back safely to its original position.
|
|
boolean returnToOriginalCellToPreventShuffling = !isFinishedSwitchingState()
|
|
&& !droppedOnOriginalCellDuringTransition && !dropTargetLayout
|
|
.isRegionVacant(mTargetCell[0], mTargetCell[1], spanX, spanY);
|
|
int[] resultSpan = new int[2];
|
|
if (returnToOriginalCellToPreventShuffling) {
|
|
mTargetCell[0] = mTargetCell[1] = -1;
|
|
} else {
|
|
mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell,
|
|
mTargetCell, resultSpan, CellLayout.MODE_ON_DROP);
|
|
}
|
|
|
|
boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;
|
|
|
|
// if the widget resizes on drop
|
|
if (foundCell && (cell instanceof AppWidgetHostView) &&
|
|
(resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) {
|
|
resizeOnDrop = true;
|
|
item.spanX = resultSpan[0];
|
|
item.spanY = resultSpan[1];
|
|
AppWidgetHostView awhv = (AppWidgetHostView) cell;
|
|
AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0],
|
|
resultSpan[1]);
|
|
}
|
|
|
|
if (foundCell) {
|
|
if (getScreenIdForPageIndex(mCurrentPage) != screenId && !hasMovedIntoHotseat) {
|
|
snapScreen = getPageIndexForScreenId(screenId);
|
|
snapToPage(snapScreen);
|
|
}
|
|
|
|
final ItemInfo info = (ItemInfo) cell.getTag();
|
|
if (hasMovedLayouts) {
|
|
// Reparent the view
|
|
CellLayout parentCell = getParentCellLayoutForView(cell);
|
|
if (parentCell != null) {
|
|
parentCell.removeView(cell);
|
|
} else if (FeatureFlags.IS_STUDIO_BUILD) {
|
|
throw new NullPointerException("mDragInfo.cell has null parent");
|
|
}
|
|
addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1],
|
|
info.spanX, info.spanY);
|
|
}
|
|
|
|
// update the item's position after drop
|
|
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
|
|
lp.cellX = lp.tmpCellX = mTargetCell[0];
|
|
lp.cellY = lp.tmpCellY = mTargetCell[1];
|
|
lp.cellHSpan = item.spanX;
|
|
lp.cellVSpan = item.spanY;
|
|
lp.isLockedToGrid = true;
|
|
|
|
if (container != LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
|
|
cell instanceof LauncherAppWidgetHostView) {
|
|
final CellLayout cellLayout = dropTargetLayout;
|
|
// We post this call so that the widget has a chance to be placed
|
|
// in its final location
|
|
|
|
final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
|
|
AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
|
|
if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
|
|
&& !options.isAccessibleDrag) {
|
|
onCompleteRunnable = () -> {
|
|
if (!isPageInTransition()) {
|
|
AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
mLauncher.getModelWriter().modifyItemInDatabase(info, container, screenId,
|
|
lp.cellX, lp.cellY, item.spanX, item.spanY);
|
|
} else {
|
|
if (!returnToOriginalCellToPreventShuffling) {
|
|
onNoCellFound(dropTargetLayout);
|
|
}
|
|
|
|
// If we can't find a drop location, we return the item to its original position
|
|
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
|
|
mTargetCell[0] = lp.cellX;
|
|
mTargetCell[1] = lp.cellY;
|
|
CellLayout layout = (CellLayout) cell.getParent().getParent();
|
|
layout.markCellsAsOccupiedForView(cell);
|
|
}
|
|
}
|
|
|
|
final CellLayout parent = (CellLayout) cell.getParent().getParent();
|
|
if (d.dragView.hasDrawn()) {
|
|
if (droppedOnOriginalCellDuringTransition) {
|
|
// Animate the item to its original position, while simultaneously exiting
|
|
// spring-loaded mode so the page meets the icon where it was picked up.
|
|
mLauncher.getDragController().animateDragViewToOriginalPosition(
|
|
onCompleteRunnable, cell,
|
|
SPRING_LOADED.getTransitionDuration(mLauncher));
|
|
mLauncher.getStateManager().goToState(NORMAL);
|
|
mLauncher.getDropTargetBar().onDragEnd();
|
|
parent.onDropChild(cell);
|
|
return;
|
|
}
|
|
final ItemInfo info = (ItemInfo) cell.getTag();
|
|
boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
|
|
|| info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
|
|
if (isWidget) {
|
|
int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE :
|
|
ANIMATE_INTO_POSITION_AND_DISAPPEAR;
|
|
animateWidgetDrop(info, parent, d.dragView, null, animationType, cell, false);
|
|
} else {
|
|
int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION;
|
|
mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration,
|
|
this);
|
|
}
|
|
} else {
|
|
d.deferDragViewCleanupPostAnimation = false;
|
|
cell.setVisibility(VISIBLE);
|
|
}
|
|
parent.onDropChild(cell);
|
|
|
|
mLauncher.getStateManager().goToState(
|
|
NORMAL, SPRING_LOADED_EXIT_DELAY, onCompleteRunnable);
|
|
mStatsLogManager.log(
|
|
LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED,
|
|
d.logInstanceId,
|
|
d.dragInfo.buildProto(null));
|
|
}
|
|
|
|
if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
|
|
d.stateAnnouncer.completeAction(R.string.item_moved);
|
|
}
|
|
}
|
|
|
|
public void onNoCellFound(View dropTargetLayout) {
|
|
int strId = mLauncher.isHotseatLayout(dropTargetLayout)
|
|
? R.string.hotseat_out_of_space : R.string.out_of_space;
|
|
Toast.makeText(mLauncher, mLauncher.getString(strId), Toast.LENGTH_SHORT).show();
|
|
}
|
|
|
|
/**
|
|
* Computes the area relative to dragLayer which is used to display a page.
|
|
*/
|
|
public void getPageAreaRelativeToDragLayer(Rect outArea) {
|
|
CellLayout child = (CellLayout) getChildAt(getNextPage());
|
|
if (child == null) {
|
|
return;
|
|
}
|
|
|
|
ShortcutAndWidgetContainer boundingLayout = child.getShortcutsAndWidgets();
|
|
mLauncher.getDragLayer().getDescendantRectRelativeToSelf(boundingLayout, outArea);
|
|
}
|
|
|
|
@Override
|
|
public void onDragEnter(DragObject d) {
|
|
if (ENFORCE_DRAG_EVENT_ORDER) {
|
|
enforceDragParity("onDragEnter", 1, 1);
|
|
}
|
|
|
|
mCreateUserFolderOnDrop = false;
|
|
mAddToExistingFolderOnDrop = false;
|
|
|
|
mDropToLayout = null;
|
|
mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
|
|
setDropLayoutForDragObject(d, mDragViewVisualCenter[0], mDragViewVisualCenter[1]);
|
|
}
|
|
|
|
@Override
|
|
public void onDragExit(DragObject d) {
|
|
if (ENFORCE_DRAG_EVENT_ORDER) {
|
|
enforceDragParity("onDragExit", -1, 0);
|
|
}
|
|
|
|
// Here we store the final page that will be dropped to, if the workspace in fact
|
|
// receives the drop
|
|
mDropToLayout = mDragTargetLayout;
|
|
if (mDragMode == DRAG_MODE_CREATE_FOLDER) {
|
|
mCreateUserFolderOnDrop = true;
|
|
} else if (mDragMode == DRAG_MODE_ADD_TO_FOLDER) {
|
|
mAddToExistingFolderOnDrop = true;
|
|
}
|
|
|
|
// Reset the previous drag target
|
|
setCurrentDropLayout(null);
|
|
setCurrentDragOverlappingLayout(null);
|
|
|
|
mSpringLoadedDragController.cancel();
|
|
}
|
|
|
|
private void enforceDragParity(String event, int update, int expectedValue) {
|
|
enforceDragParity(this, event, update, expectedValue);
|
|
for (int i = 0; i < getChildCount(); i++) {
|
|
enforceDragParity(getChildAt(i), event, update, expectedValue);
|
|
}
|
|
}
|
|
|
|
private void enforceDragParity(View v, String event, int update, int expectedValue) {
|
|
Object tag = v.getTag(R.id.drag_event_parity);
|
|
int value = tag == null ? 0 : (Integer) tag;
|
|
value += update;
|
|
v.setTag(R.id.drag_event_parity, value);
|
|
|
|
if (value != expectedValue) {
|
|
Log.e(TAG, event + ": Drag contract violated: " + value);
|
|
}
|
|
}
|
|
|
|
void setCurrentDropLayout(CellLayout layout) {
|
|
if (mDragTargetLayout != null) {
|
|
mDragTargetLayout.revertTempState();
|
|
mDragTargetLayout.onDragExit();
|
|
}
|
|
mDragTargetLayout = layout;
|
|
if (mDragTargetLayout != null) {
|
|
mDragTargetLayout.onDragEnter();
|
|
}
|
|
cleanupReorder(true);
|
|
cleanupFolderCreation();
|
|
setCurrentDropOverCell(-1, -1);
|
|
}
|
|
|
|
void setCurrentDragOverlappingLayout(CellLayout layout) {
|
|
if (mDragOverlappingLayout != null) {
|
|
mDragOverlappingLayout.setIsDragOverlapping(false);
|
|
}
|
|
mDragOverlappingLayout = layout;
|
|
if (mDragOverlappingLayout != null) {
|
|
mDragOverlappingLayout.setIsDragOverlapping(true);
|
|
}
|
|
// Invalidating the scrim will also force this CellLayout
|
|
// to be invalidated so that it is highlighted if necessary.
|
|
mLauncher.getDragLayer().getScrim().invalidate();
|
|
}
|
|
|
|
public CellLayout getCurrentDragOverlappingLayout() {
|
|
return mDragOverlappingLayout;
|
|
}
|
|
|
|
void setCurrentDropOverCell(int x, int y) {
|
|
if (x != mDragOverX || y != mDragOverY) {
|
|
mDragOverX = x;
|
|
mDragOverY = y;
|
|
setDragMode(DRAG_MODE_NONE);
|
|
}
|
|
}
|
|
|
|
void setDragMode(int dragMode) {
|
|
if (dragMode != mDragMode) {
|
|
if (dragMode == DRAG_MODE_NONE) {
|
|
cleanupAddToFolder();
|
|
// We don't want to cancel the re-order alarm every time the target cell changes
|
|
// as this feels to slow / unresponsive.
|
|
cleanupReorder(false);
|
|
cleanupFolderCreation();
|
|
} else if (dragMode == DRAG_MODE_ADD_TO_FOLDER) {
|
|
cleanupReorder(true);
|
|
cleanupFolderCreation();
|
|
} else if (dragMode == DRAG_MODE_CREATE_FOLDER) {
|
|
cleanupAddToFolder();
|
|
cleanupReorder(true);
|
|
} else if (dragMode == DRAG_MODE_REORDER) {
|
|
cleanupAddToFolder();
|
|
cleanupFolderCreation();
|
|
}
|
|
mDragMode = dragMode;
|
|
}
|
|
}
|
|
|
|
private void cleanupFolderCreation() {
|
|
if (mFolderCreateBg != null) {
|
|
mFolderCreateBg.animateToRest();
|
|
}
|
|
}
|
|
|
|
private void cleanupAddToFolder() {
|
|
if (mDragOverFolderIcon != null) {
|
|
mDragOverFolderIcon.onDragExit();
|
|
mDragOverFolderIcon = null;
|
|
}
|
|
}
|
|
|
|
private void cleanupReorder(boolean cancelAlarm) {
|
|
// Any pending reorders are canceled
|
|
if (cancelAlarm) {
|
|
mReorderAlarm.cancelAlarm();
|
|
}
|
|
mLastReorderX = -1;
|
|
mLastReorderY = -1;
|
|
}
|
|
|
|
/*
|
|
*
|
|
* Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's
|
|
* coordinate space. The argument xy is modified with the return result.
|
|
*/
|
|
private void mapPointFromSelfToChild(View v, float[] xy) {
|
|
xy[0] = xy[0] - v.getLeft();
|
|
xy[1] = xy[1] - v.getTop();
|
|
}
|
|
|
|
boolean isPointInSelfOverHotseat(int x, int y) {
|
|
mTempFXY[0] = x;
|
|
mTempFXY[1] = y;
|
|
mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, mTempFXY, true);
|
|
View hotseat = mLauncher.getHotseat();
|
|
return mTempFXY[0] >= hotseat.getLeft() &&
|
|
mTempFXY[0] <= hotseat.getRight() &&
|
|
mTempFXY[1] >= hotseat.getTop() &&
|
|
mTempFXY[1] <= hotseat.getBottom();
|
|
}
|
|
|
|
/**
|
|
* Updates the point in {@param xy} to point to the co-ordinate space of {@param layout}
|
|
* @param layout either hotseat of a page in workspace
|
|
* @param xy the point location in workspace co-ordinate space
|
|
*/
|
|
private void mapPointFromDropLayout(CellLayout layout, float[] xy) {
|
|
if (mLauncher.isHotseatLayout(layout)) {
|
|
mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, xy, true);
|
|
mLauncher.getDragLayer().mapCoordInSelfToDescendant(layout, xy);
|
|
} else {
|
|
mapPointFromSelfToChild(layout, xy);
|
|
}
|
|
}
|
|
|
|
private boolean isDragWidget(DragObject d) {
|
|
return (d.dragInfo instanceof LauncherAppWidgetInfo ||
|
|
d.dragInfo instanceof PendingAddWidgetInfo);
|
|
}
|
|
|
|
public void onDragOver(DragObject d) {
|
|
// Skip drag over events while we are dragging over side pages
|
|
if (!transitionStateShouldAllowDrop()) return;
|
|
|
|
ItemInfo item = d.dragInfo;
|
|
if (item == null) {
|
|
if (FeatureFlags.IS_STUDIO_BUILD) {
|
|
throw new NullPointerException("DragObject has null info");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Ensure that we have proper spans for the item that we are dropping
|
|
if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found");
|
|
mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
|
|
|
|
final View child = (mDragInfo == null) ? null : mDragInfo.cell;
|
|
if (setDropLayoutForDragObject(d, mDragViewVisualCenter[0], mDragViewVisualCenter[1])) {
|
|
if (mLauncher.isHotseatLayout(mDragTargetLayout)) {
|
|
mSpringLoadedDragController.cancel();
|
|
} else {
|
|
mSpringLoadedDragController.setAlarm(mDragTargetLayout);
|
|
}
|
|
}
|
|
|
|
// Handle the drag over
|
|
if (mDragTargetLayout != null) {
|
|
// We want the point to be mapped to the dragTarget.
|
|
mapPointFromDropLayout(mDragTargetLayout, mDragViewVisualCenter);
|
|
|
|
int minSpanX = item.spanX;
|
|
int minSpanY = item.spanY;
|
|
if (item.minSpanX > 0 && item.minSpanY > 0) {
|
|
minSpanX = item.minSpanX;
|
|
minSpanY = item.minSpanY;
|
|
}
|
|
|
|
mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1], minSpanX, minSpanY,
|
|
mDragTargetLayout, mTargetCell);
|
|
int reorderX = mTargetCell[0];
|
|
int reorderY = mTargetCell[1];
|
|
|
|
setCurrentDropOverCell(mTargetCell[0], mTargetCell[1]);
|
|
|
|
float targetCellDistance = mDragTargetLayout.getDistanceFromCell(
|
|
mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
|
|
|
|
manageFolderFeedback(targetCellDistance, d);
|
|
|
|
boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
|
|
mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
|
|
item.spanY, child, mTargetCell);
|
|
|
|
if (!nearestDropOccupied) {
|
|
mDragTargetLayout.visualizeDropLocation(d.originalView, mOutlineProvider,
|
|
mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, d);
|
|
} else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
|
|
&& !mReorderAlarm.alarmPending() && (mLastReorderX != reorderX ||
|
|
mLastReorderY != reorderY)) {
|
|
|
|
int[] resultSpan = new int[2];
|
|
mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
|
|
child, mTargetCell, resultSpan, CellLayout.MODE_SHOW_REORDER_HINT);
|
|
|
|
// Otherwise, if we aren't adding to or creating a folder and there's no pending
|
|
// reorder, then we schedule a reorder
|
|
ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
|
|
minSpanX, minSpanY, item.spanX, item.spanY, d, child);
|
|
mReorderAlarm.setOnAlarmListener(listener);
|
|
mReorderAlarm.setAlarm(REORDER_TIMEOUT);
|
|
}
|
|
|
|
if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER ||
|
|
!nearestDropOccupied) {
|
|
if (mDragTargetLayout != null) {
|
|
mDragTargetLayout.revertTempState();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates {@link #mDragTargetLayout} and {@link #mDragOverlappingLayout}
|
|
* based on the DragObject's position.
|
|
*
|
|
* The layout will be:
|
|
* - The Hotseat if the drag object is over it
|
|
* - A side page if we are in spring-loaded mode and the drag object is over it
|
|
* - The current page otherwise
|
|
*
|
|
* @return whether the layout is different from the current {@link #mDragTargetLayout}.
|
|
*/
|
|
private boolean setDropLayoutForDragObject(DragObject d, float centerX, float centerY) {
|
|
CellLayout layout = null;
|
|
// Test to see if we are over the hotseat first
|
|
if (mLauncher.getHotseat() != null && !isDragWidget(d)) {
|
|
if (isPointInSelfOverHotseat(d.x, d.y)) {
|
|
layout = mLauncher.getHotseat();
|
|
}
|
|
}
|
|
|
|
int nextPage = getNextPage();
|
|
if (layout == null && !isPageInTransition()) {
|
|
// Check if the item is dragged over left page
|
|
mTempTouchCoordinates[0] = Math.min(centerX, d.x);
|
|
mTempTouchCoordinates[1] = d.y;
|
|
layout = verifyInsidePage(nextPage + (mIsRtl ? 1 : -1), mTempTouchCoordinates);
|
|
}
|
|
|
|
if (layout == null && !isPageInTransition()) {
|
|
// Check if the item is dragged over right page
|
|
mTempTouchCoordinates[0] = Math.max(centerX, d.x);
|
|
mTempTouchCoordinates[1] = d.y;
|
|
layout = verifyInsidePage(nextPage + (mIsRtl ? -1 : 1), mTempTouchCoordinates);
|
|
}
|
|
|
|
// Always pick the current page.
|
|
if (layout == null && nextPage >= 0 && nextPage < getPageCount()) {
|
|
layout = (CellLayout) getChildAt(nextPage);
|
|
}
|
|
if (layout != mDragTargetLayout) {
|
|
setCurrentDropLayout(layout);
|
|
setCurrentDragOverlappingLayout(layout);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the child CellLayout if the point is inside the page coordinates, null otherwise.
|
|
*/
|
|
private CellLayout verifyInsidePage(int pageNo, float[] touchXy) {
|
|
if (pageNo >= 0 && pageNo < getPageCount()) {
|
|
CellLayout cl = (CellLayout) getChildAt(pageNo);
|
|
mapPointFromSelfToChild(cl, touchXy);
|
|
if (touchXy[0] >= 0 && touchXy[0] <= cl.getWidth() &&
|
|
touchXy[1] >= 0 && touchXy[1] <= cl.getHeight()) {
|
|
// This point is inside the cell layout
|
|
return cl;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void manageFolderFeedback(float distance, DragObject dragObject) {
|
|
if (distance > mMaxDistanceForFolderCreation) {
|
|
if ((mDragMode == DRAG_MODE_ADD_TO_FOLDER
|
|
|| mDragMode == DRAG_MODE_CREATE_FOLDER)) {
|
|
setDragMode(DRAG_MODE_NONE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], mTargetCell[1]);
|
|
ItemInfo info = dragObject.dragInfo;
|
|
boolean userFolderPending = willCreateUserFolder(info, dragOverView, false);
|
|
if (mDragMode == DRAG_MODE_NONE && userFolderPending) {
|
|
|
|
mFolderCreateBg = new PreviewBackground();
|
|
mFolderCreateBg.setup(mLauncher, mLauncher, null,
|
|
dragOverView.getMeasuredWidth(), dragOverView.getPaddingTop());
|
|
|
|
// The full preview background should appear behind the icon
|
|
mFolderCreateBg.isClipping = false;
|
|
|
|
mFolderCreateBg.animateToAccept(mDragTargetLayout, mTargetCell[0], mTargetCell[1]);
|
|
mDragTargetLayout.clearDragOutlines();
|
|
setDragMode(DRAG_MODE_CREATE_FOLDER);
|
|
|
|
if (dragObject.stateAnnouncer != null) {
|
|
dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
|
|
.getDescriptionForDropOver(dragOverView, getContext()));
|
|
}
|
|
return;
|
|
}
|
|
|
|
boolean willAddToFolder = willAddToExistingUserFolder(info, dragOverView);
|
|
if (willAddToFolder && mDragMode == DRAG_MODE_NONE) {
|
|
mDragOverFolderIcon = ((FolderIcon) dragOverView);
|
|
mDragOverFolderIcon.onDragEnter(info);
|
|
if (mDragTargetLayout != null) {
|
|
mDragTargetLayout.clearDragOutlines();
|
|
}
|
|
setDragMode(DRAG_MODE_ADD_TO_FOLDER);
|
|
|
|
if (dragObject.stateAnnouncer != null) {
|
|
dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
|
|
.getDescriptionForDropOver(dragOverView, getContext()));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (mDragMode == DRAG_MODE_ADD_TO_FOLDER && !willAddToFolder) {
|
|
setDragMode(DRAG_MODE_NONE);
|
|
}
|
|
if (mDragMode == DRAG_MODE_CREATE_FOLDER && !userFolderPending) {
|
|
setDragMode(DRAG_MODE_NONE);
|
|
}
|
|
}
|
|
|
|
class ReorderAlarmListener implements OnAlarmListener {
|
|
final float[] dragViewCenter;
|
|
final int minSpanX, minSpanY, spanX, spanY;
|
|
final DragObject dragObject;
|
|
final View child;
|
|
|
|
public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX,
|
|
int spanY, DragObject dragObject, View child) {
|
|
this.dragViewCenter = dragViewCenter;
|
|
this.minSpanX = minSpanX;
|
|
this.minSpanY = minSpanY;
|
|
this.spanX = spanX;
|
|
this.spanY = spanY;
|
|
this.child = child;
|
|
this.dragObject = dragObject;
|
|
}
|
|
|
|
public void onAlarm(Alarm alarm) {
|
|
int[] resultSpan = new int[2];
|
|
mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, mDragTargetLayout,
|
|
mTargetCell);
|
|
mLastReorderX = mTargetCell[0];
|
|
mLastReorderY = mTargetCell[1];
|
|
|
|
mTargetCell = mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
|
|
child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER);
|
|
|
|
if (mTargetCell[0] < 0 || mTargetCell[1] < 0) {
|
|
mDragTargetLayout.revertTempState();
|
|
} else {
|
|
setDragMode(DRAG_MODE_REORDER);
|
|
}
|
|
|
|
boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
|
|
mDragTargetLayout.visualizeDropLocation(dragObject.originalView, mOutlineProvider,
|
|
mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, dragObject);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void getHitRectRelativeToDragLayer(Rect outRect) {
|
|
// We want the workspace to have the whole area of the display (it will find the correct
|
|
// cell layout to drop to in the existing drag/drop logic.
|
|
mLauncher.getDragLayer().getDescendantRectRelativeToSelf(this, outRect);
|
|
}
|
|
|
|
/**
|
|
* Drop an item that didn't originate on one of the workspace screens.
|
|
* It may have come from Launcher (e.g. from all apps or customize), or it may have
|
|
* come from another app altogether.
|
|
*
|
|
* NOTE: This can also be called when we are outside of a drag event, when we want
|
|
* to add an item to one of the workspace screens.
|
|
*/
|
|
private void onDropExternal(final int[] touchXY, final CellLayout cellLayout, DragObject d) {
|
|
if (d.dragInfo instanceof PendingAddShortcutInfo) {
|
|
WorkspaceItemInfo si = ((PendingAddShortcutInfo) d.dragInfo)
|
|
.activityInfo.createWorkspaceItemInfo();
|
|
if (si != null) {
|
|
d.dragInfo = si;
|
|
}
|
|
}
|
|
|
|
ItemInfo info = d.dragInfo;
|
|
int spanX = info.spanX;
|
|
int spanY = info.spanY;
|
|
if (mDragInfo != null) {
|
|
spanX = mDragInfo.spanX;
|
|
spanY = mDragInfo.spanY;
|
|
}
|
|
|
|
final int container = mLauncher.isHotseatLayout(cellLayout) ?
|
|
LauncherSettings.Favorites.CONTAINER_HOTSEAT :
|
|
LauncherSettings.Favorites.CONTAINER_DESKTOP;
|
|
final int screenId = getIdForScreen(cellLayout);
|
|
if (!mLauncher.isHotseatLayout(cellLayout)
|
|
&& screenId != getScreenIdForPageIndex(mCurrentPage)
|
|
&& !mLauncher.isInState(SPRING_LOADED)) {
|
|
snapToPage(getPageIndexForScreenId(screenId));
|
|
}
|
|
|
|
if (info instanceof PendingAddItemInfo) {
|
|
final PendingAddItemInfo pendingInfo = (PendingAddItemInfo) info;
|
|
|
|
boolean findNearestVacantCell = true;
|
|
if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
|
|
mTargetCell = findNearestArea(touchXY[0], touchXY[1], spanX, spanY,
|
|
cellLayout, mTargetCell);
|
|
float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0],
|
|
mDragViewVisualCenter[1], mTargetCell);
|
|
if (willCreateUserFolder(d.dragInfo, cellLayout, mTargetCell, distance, true)
|
|
|| willAddToExistingUserFolder(
|
|
d.dragInfo, cellLayout, mTargetCell, distance)) {
|
|
findNearestVacantCell = false;
|
|
}
|
|
}
|
|
|
|
final ItemInfo item = d.dragInfo;
|
|
boolean updateWidgetSize = false;
|
|
if (findNearestVacantCell) {
|
|
int minSpanX = item.spanX;
|
|
int minSpanY = item.spanY;
|
|
if (item.minSpanX > 0 && item.minSpanY > 0) {
|
|
minSpanX = item.minSpanX;
|
|
minSpanY = item.minSpanY;
|
|
}
|
|
int[] resultSpan = new int[2];
|
|
mTargetCell = cellLayout.performReorder((int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, info.spanX, info.spanY,
|
|
null, mTargetCell, resultSpan, CellLayout.MODE_ON_DROP_EXTERNAL);
|
|
|
|
if (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY) {
|
|
updateWidgetSize = true;
|
|
}
|
|
item.spanX = resultSpan[0];
|
|
item.spanY = resultSpan[1];
|
|
}
|
|
|
|
Runnable onAnimationCompleteRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
// Normally removeExtraEmptyScreen is called in Workspace#onDragEnd, but when
|
|
// adding an item that may not be dropped right away (due to a config activity)
|
|
// we defer the removal until the activity returns.
|
|
deferRemoveExtraEmptyScreen();
|
|
|
|
// When dragging and dropping from customization tray, we deal with creating
|
|
// widgets/shortcuts/folders in a slightly different way
|
|
mLauncher.addPendingItem(pendingInfo, container, screenId, mTargetCell,
|
|
item.spanX, item.spanY);
|
|
}
|
|
};
|
|
boolean isWidget = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
|
|
|| pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
|
|
|
|
AppWidgetHostView finalView = isWidget ?
|
|
((PendingAddWidgetInfo) pendingInfo).boundWidget : null;
|
|
|
|
if (finalView != null && updateWidgetSize) {
|
|
AppWidgetResizeFrame.updateWidgetSizeRanges(finalView, mLauncher, item.spanX,
|
|
item.spanY);
|
|
}
|
|
|
|
int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR;
|
|
if (isWidget && ((PendingAddWidgetInfo) pendingInfo).info != null &&
|
|
((PendingAddWidgetInfo) pendingInfo).getHandler().needsConfigure()) {
|
|
animationStyle = ANIMATE_INTO_POSITION_AND_REMAIN;
|
|
}
|
|
animateWidgetDrop(info, cellLayout, d.dragView, onAnimationCompleteRunnable,
|
|
animationStyle, finalView, true);
|
|
} else {
|
|
// This is for other drag/drop cases, like dragging from All Apps
|
|
mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
|
|
|
|
View view;
|
|
|
|
switch (info.itemType) {
|
|
case ITEM_TYPE_APPLICATION:
|
|
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
|
|
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
|
|
if (info instanceof AppInfo) {
|
|
// Came from all apps -- make a copy
|
|
info = ((AppInfo) info).makeWorkspaceItem();
|
|
d.dragInfo = info;
|
|
}
|
|
view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info);
|
|
break;
|
|
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
|
|
view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, mLauncher, cellLayout,
|
|
(FolderInfo) info);
|
|
break;
|
|
default:
|
|
throw new IllegalStateException("Unknown item type: " + info.itemType);
|
|
}
|
|
|
|
// First we find the cell nearest to point at which the item is
|
|
// dropped, without any consideration to whether there is an item there.
|
|
if (touchXY != null) {
|
|
mTargetCell = findNearestArea(touchXY[0], touchXY[1], spanX, spanY,
|
|
cellLayout, mTargetCell);
|
|
float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0],
|
|
mDragViewVisualCenter[1], mTargetCell);
|
|
if (createUserFolderIfNecessary(view, container, cellLayout, mTargetCell, distance,
|
|
true, d)) {
|
|
return;
|
|
}
|
|
if (addToExistingFolderIfNecessary(view, cellLayout, mTargetCell, distance, d,
|
|
true)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (touchXY != null) {
|
|
// when dragging and dropping, just find the closest free spot
|
|
mTargetCell = cellLayout.performReorder((int) mDragViewVisualCenter[0],
|
|
(int) mDragViewVisualCenter[1], 1, 1, 1, 1,
|
|
null, mTargetCell, null, CellLayout.MODE_ON_DROP_EXTERNAL);
|
|
} else {
|
|
cellLayout.findCellForSpan(mTargetCell, 1, 1);
|
|
}
|
|
// Add the item to DB before adding to screen ensures that the container and other
|
|
// values of the info is properly updated.
|
|
mLauncher.getModelWriter().addOrMoveItemInDatabase(info, container, screenId,
|
|
mTargetCell[0], mTargetCell[1]);
|
|
|
|
addInScreen(view, container, screenId, mTargetCell[0], mTargetCell[1],
|
|
info.spanX, info.spanY);
|
|
cellLayout.onDropChild(view);
|
|
cellLayout.getShortcutsAndWidgets().measureChild(view);
|
|
|
|
if (d.dragView != null) {
|
|
// We wrap the animation call in the temporary set and reset of the current
|
|
// cellLayout to its final transform -- this means we animate the drag view to
|
|
// the correct final location.
|
|
setFinalTransitionTransform();
|
|
mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, view, this);
|
|
resetTransitionTransform();
|
|
}
|
|
}
|
|
mStatsLogManager.log(
|
|
LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED,
|
|
d.logInstanceId,
|
|
d.dragInfo.buildProto(null));
|
|
}
|
|
|
|
public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) {
|
|
int[] unScaledSize = estimateItemSize(widgetInfo);
|
|
int visibility = layout.getVisibility();
|
|
layout.setVisibility(VISIBLE);
|
|
|
|
int width = MeasureSpec.makeMeasureSpec(unScaledSize[0], MeasureSpec.EXACTLY);
|
|
int height = MeasureSpec.makeMeasureSpec(unScaledSize[1], MeasureSpec.EXACTLY);
|
|
layout.measure(width, height);
|
|
layout.layout(0, 0, unScaledSize[0], unScaledSize[1]);
|
|
Bitmap b = BitmapRenderer.createHardwareBitmap(
|
|
unScaledSize[0], unScaledSize[1], layout::draw);
|
|
layout.setVisibility(visibility);
|
|
return b;
|
|
}
|
|
|
|
private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY,
|
|
DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, boolean scale) {
|
|
// Now we animate the dragView, (ie. the widget or shortcut preview) into its final
|
|
// location and size on the home screen.
|
|
int spanX = info.spanX;
|
|
int spanY = info.spanY;
|
|
|
|
Rect r = estimateItemPosition(layout, targetCell[0], targetCell[1], spanX, spanY);
|
|
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) {
|
|
DeviceProfile profile = mLauncher.getDeviceProfile();
|
|
Utilities.shrinkRect(r, profile.appWidgetScale.x, profile.appWidgetScale.y);
|
|
}
|
|
|
|
mTempFXY[0] = r.left;
|
|
mTempFXY[1] = r.top;
|
|
setFinalTransitionTransform();
|
|
float cellLayoutScale =
|
|
mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(layout, mTempFXY, true);
|
|
resetTransitionTransform();
|
|
Utilities.roundArray(mTempFXY, loc);
|
|
|
|
if (scale) {
|
|
float dragViewScaleX = (1.0f * r.width()) / dragView.getMeasuredWidth();
|
|
float dragViewScaleY = (1.0f * r.height()) / dragView.getMeasuredHeight();
|
|
|
|
// The animation will scale the dragView about its center, so we need to center about
|
|
// the final location.
|
|
loc[0] -= (dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2
|
|
- Math.ceil(layout.getUnusedHorizontalSpace() / 2f);
|
|
loc[1] -= (dragView.getMeasuredHeight() - cellLayoutScale * r.height()) / 2;
|
|
scaleXY[0] = dragViewScaleX * cellLayoutScale;
|
|
scaleXY[1] = dragViewScaleY * cellLayoutScale;
|
|
} else {
|
|
// Since we are not cross-fading the dragView, align the drag view to the
|
|
// final cell position.
|
|
float dragScale = dragView.getInitialScale() * cellLayoutScale;
|
|
loc[0] += (dragScale - 1) * dragView.getWidth() / 2;
|
|
loc[1] += (dragScale - 1) * dragView.getHeight() / 2;
|
|
scaleXY[0] = scaleXY[1] = dragScale;
|
|
|
|
// If a dragRegion was provided, offset the final position accordingly.
|
|
Rect dragRegion = dragView.getDragRegion();
|
|
if (dragRegion != null) {
|
|
loc[0] += cellLayoutScale * dragRegion.left;
|
|
loc[1] += cellLayoutScale * dragRegion.top;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView,
|
|
final Runnable onCompleteRunnable, int animationType, final View finalView,
|
|
boolean external) {
|
|
Rect from = new Rect();
|
|
mLauncher.getDragLayer().getViewRectRelativeToSelf(dragView, from);
|
|
|
|
int[] finalPos = new int[2];
|
|
float scaleXY[] = new float[2];
|
|
boolean scalePreview = !(info instanceof PendingAddShortcutInfo);
|
|
getFinalPositionForDropAnimation(finalPos, scaleXY, dragView, cellLayout, info, mTargetCell,
|
|
scalePreview);
|
|
|
|
Resources res = mLauncher.getResources();
|
|
final int duration = res.getInteger(R.integer.config_dropAnimMaxDuration) - 200;
|
|
|
|
boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET ||
|
|
info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
|
|
if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external) && finalView != null) {
|
|
Bitmap crossFadeBitmap = createWidgetBitmap(info, finalView);
|
|
dragView.setCrossFadeBitmap(crossFadeBitmap);
|
|
dragView.crossFade((int) (duration * 0.8f));
|
|
} else if (isWidget && external) {
|
|
scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]);
|
|
}
|
|
|
|
DragLayer dragLayer = mLauncher.getDragLayer();
|
|
if (animationType == CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION) {
|
|
mLauncher.getDragLayer().animateViewIntoPosition(dragView, finalPos, 0f, 0.1f, 0.1f,
|
|
DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration);
|
|
} else {
|
|
int endStyle;
|
|
if (animationType == ANIMATE_INTO_POSITION_AND_REMAIN) {
|
|
endStyle = DragLayer.ANIMATION_END_REMAIN_VISIBLE;
|
|
} else {
|
|
endStyle = DragLayer.ANIMATION_END_DISAPPEAR;
|
|
}
|
|
|
|
Runnable onComplete = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (finalView != null) {
|
|
finalView.setVisibility(VISIBLE);
|
|
}
|
|
if (onCompleteRunnable != null) {
|
|
onCompleteRunnable.run();
|
|
}
|
|
}
|
|
};
|
|
dragLayer.animateViewIntoPosition(dragView, from.left, from.top, finalPos[0],
|
|
finalPos[1], 1, 1, 1, scaleXY[0], scaleXY[1], onComplete, endStyle,
|
|
duration, this);
|
|
}
|
|
}
|
|
|
|
public void setFinalTransitionTransform() {
|
|
if (isSwitchingState()) {
|
|
mCurrentScale = getScaleX();
|
|
setScaleX(mStateTransitionAnimation.getFinalScale());
|
|
setScaleY(mStateTransitionAnimation.getFinalScale());
|
|
}
|
|
}
|
|
public void resetTransitionTransform() {
|
|
if (isSwitchingState()) {
|
|
setScaleX(mCurrentScale);
|
|
setScaleY(mCurrentScale);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the current CellInfo describing our current drag; this method exists
|
|
* so that Launcher can sync this object with the correct info when the activity is created/
|
|
* destroyed
|
|
*
|
|
*/
|
|
public CellLayout.CellInfo getDragInfo() {
|
|
return mDragInfo;
|
|
}
|
|
|
|
/**
|
|
* Calculate the nearest cell where the given object would be dropped.
|
|
*
|
|
* pixelX and pixelY should be in the coordinate system of layout
|
|
*/
|
|
@Thunk int[] findNearestArea(int pixelX, int pixelY,
|
|
int spanX, int spanY, CellLayout layout, int[] recycle) {
|
|
return layout.findNearestArea(
|
|
pixelX, pixelY, spanX, spanY, recycle);
|
|
}
|
|
|
|
void setup(DragController dragController) {
|
|
mSpringLoadedDragController = new SpringLoadedDragController(mLauncher);
|
|
mDragController = dragController;
|
|
|
|
// hardware layers on children are enabled on startup, but should be disabled until
|
|
// needed
|
|
updateChildrenLayersEnabled();
|
|
}
|
|
|
|
/**
|
|
* Called at the end of a drag which originated on the workspace.
|
|
*/
|
|
public void onDropCompleted(final View target, final DragObject d,
|
|
final boolean success) {
|
|
if (success) {
|
|
if (target != this && mDragInfo != null) {
|
|
removeWorkspaceItem(mDragInfo.cell);
|
|
}
|
|
} else if (mDragInfo != null) {
|
|
final CellLayout cellLayout = mLauncher.getCellLayout(
|
|
mDragInfo.container, mDragInfo.screenId);
|
|
if (cellLayout != null) {
|
|
cellLayout.onDropChild(mDragInfo.cell);
|
|
} else if (FeatureFlags.IS_STUDIO_BUILD) {
|
|
throw new RuntimeException("Invalid state: cellLayout == null in "
|
|
+ "Workspace#onDropCompleted. Please file a bug. ");
|
|
}
|
|
}
|
|
View cell = getHomescreenIconByItemId(d.originalDragInfo.id);
|
|
if (d.cancelled && cell != null) {
|
|
cell.setVisibility(VISIBLE);
|
|
}
|
|
mDragInfo = null;
|
|
}
|
|
|
|
/**
|
|
* For opposite operation. See {@link #addInScreen}.
|
|
*/
|
|
public void removeWorkspaceItem(View v) {
|
|
CellLayout parentCell = getParentCellLayoutForView(v);
|
|
if (parentCell != null) {
|
|
parentCell.removeView(v);
|
|
} else if (FeatureFlags.IS_STUDIO_BUILD) {
|
|
// When an app is uninstalled using the drop target, we wait until resume to remove
|
|
// the icon. We also remove all the corresponding items from the workspace at
|
|
// {@link Launcher#bindComponentsRemoved}. That call can come before or after
|
|
// {@link Launcher#mOnResumeCallbacks} depending on how busy the worker thread is.
|
|
Log.e(TAG, "mDragInfo.cell has null parent");
|
|
}
|
|
if (v instanceof DropTarget) {
|
|
mDragController.removeDropTarget((DropTarget) v);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removed widget from workspace by appWidgetId
|
|
* @param appWidgetId
|
|
*/
|
|
public void removeWidget(int appWidgetId) {
|
|
mapOverItems((info, view) -> {
|
|
if (info instanceof LauncherAppWidgetInfo) {
|
|
LauncherAppWidgetInfo appWidgetInfo = (LauncherAppWidgetInfo) info;
|
|
if (appWidgetInfo.appWidgetId == appWidgetId) {
|
|
mLauncher.removeItem(view, appWidgetInfo, true);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Removes all folder listeners
|
|
*/
|
|
public void removeFolderListeners() {
|
|
mapOverItems(new ItemOperator() {
|
|
@Override
|
|
public boolean evaluate(ItemInfo info, View view) {
|
|
if (view instanceof FolderIcon) {
|
|
((FolderIcon) view).removeListeners();
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
public boolean isDropEnabled() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
|
|
// We don't dispatch restoreInstanceState to our children using this code path.
|
|
// Some pages will be restored immediately as their items are bound immediately, and
|
|
// others we will need to wait until after their items are bound.
|
|
mSavedStates = container;
|
|
}
|
|
|
|
public void restoreInstanceStateForChild(int child) {
|
|
if (mSavedStates != null) {
|
|
mRestoredPages.add(child);
|
|
CellLayout cl = (CellLayout) getChildAt(child);
|
|
if (cl != null) {
|
|
cl.restoreInstanceState(mSavedStates);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void restoreInstanceStateForRemainingPages() {
|
|
int count = getChildCount();
|
|
for (int i = 0; i < count; i++) {
|
|
if (!mRestoredPages.contains(i)) {
|
|
restoreInstanceStateForChild(i);
|
|
}
|
|
}
|
|
mRestoredPages.clear();
|
|
mSavedStates = null;
|
|
}
|
|
|
|
@Override
|
|
public boolean scrollLeft() {
|
|
boolean result = false;
|
|
if (!mIsSwitchingState && workspaceInScrollableState()) {
|
|
result = super.scrollLeft();
|
|
}
|
|
Folder openFolder = Folder.getOpen(mLauncher);
|
|
if (openFolder != null) {
|
|
openFolder.completeDragExit();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public boolean scrollRight() {
|
|
boolean result = false;
|
|
if (!mIsSwitchingState && workspaceInScrollableState()) {
|
|
result = super.scrollRight();
|
|
}
|
|
Folder openFolder = Folder.getOpen(mLauncher);
|
|
if (openFolder != null) {
|
|
openFolder.completeDragExit();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Returns a specific CellLayout
|
|
*/
|
|
CellLayout getParentCellLayoutForView(View v) {
|
|
for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
|
|
if (layout.getShortcutsAndWidgets().indexOfChild(v) > -1) {
|
|
return layout;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of all the CellLayouts on the Homescreen.
|
|
*/
|
|
private CellLayout[] getWorkspaceAndHotseatCellLayouts() {
|
|
int screenCount = getChildCount();
|
|
final CellLayout[] layouts;
|
|
if (mLauncher.getHotseat() != null) {
|
|
layouts = new CellLayout[screenCount + 1];
|
|
layouts[screenCount] = mLauncher.getHotseat();
|
|
} else {
|
|
layouts = new CellLayout[screenCount];
|
|
}
|
|
for (int screen = 0; screen < screenCount; screen++) {
|
|
layouts[screen] = (CellLayout) getChildAt(screen);
|
|
}
|
|
return layouts;
|
|
}
|
|
|
|
/**
|
|
* Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close
|
|
* animation.
|
|
*
|
|
* @param packageName The package name of the app to match.
|
|
* @param user The user of the app to match.
|
|
*/
|
|
public View getFirstMatchForAppClose(String packageName, UserHandle user) {
|
|
final int curPage = getCurrentPage();
|
|
final CellLayout currentPage = (CellLayout) getPageAt(curPage);
|
|
final Workspace.ItemOperator packageAndUser = (ItemInfo info, View view) -> info != null
|
|
&& info.getTargetComponent() != null
|
|
&& TextUtils.equals(info.getTargetComponent().getPackageName(), packageName)
|
|
&& info.user.equals(user);
|
|
final Workspace.ItemOperator packageAndUserAndApp = (ItemInfo info, View view) ->
|
|
packageAndUser.evaluate(info, view) && info.itemType == ITEM_TYPE_APPLICATION;
|
|
final Workspace.ItemOperator packageAndUserAndAppInFolder = (info, view) -> {
|
|
if (info instanceof FolderInfo) {
|
|
FolderInfo folderInfo = (FolderInfo) info;
|
|
for (WorkspaceItemInfo shortcutInfo : folderInfo.contents) {
|
|
if (packageAndUserAndApp.evaluate(shortcutInfo, view)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Order: App icons, app in folder. Items in hotseat get returned first.
|
|
if (ADAPTIVE_ICON_WINDOW_ANIM.get()) {
|
|
return getFirstMatch(new CellLayout[] { getHotseat(), currentPage },
|
|
packageAndUserAndApp, packageAndUserAndAppInFolder);
|
|
} else {
|
|
// Do not use Folder as a criteria, since it'll cause a crash when trying to draw
|
|
// FolderAdaptiveIcon as the background.
|
|
return getFirstMatch(new CellLayout[] { getHotseat(), currentPage },
|
|
packageAndUserAndApp);
|
|
}
|
|
}
|
|
|
|
public View getHomescreenIconByItemId(final int id) {
|
|
return getFirstMatch((info, v) -> info != null && info.id == id);
|
|
}
|
|
|
|
public LauncherAppWidgetHostView getWidgetForAppWidgetId(final int appWidgetId) {
|
|
return (LauncherAppWidgetHostView) getFirstMatch((info, v) ->
|
|
(info instanceof LauncherAppWidgetInfo) &&
|
|
((LauncherAppWidgetInfo) info).appWidgetId == appWidgetId);
|
|
}
|
|
|
|
public View getFirstMatch(final ItemOperator operator) {
|
|
final View[] value = new View[1];
|
|
mapOverItems(new ItemOperator() {
|
|
@Override
|
|
public boolean evaluate(ItemInfo info, View v) {
|
|
if (operator.evaluate(info, v)) {
|
|
value[0] = v;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
return value[0];
|
|
}
|
|
|
|
/**
|
|
* @param cellLayouts List of CellLayouts to scan, in order of preference.
|
|
* @param operators List of operators, in order starting from best matching operator.
|
|
* @return
|
|
*/
|
|
private View getFirstMatch(CellLayout[] cellLayouts, final ItemOperator... operators) {
|
|
// This array is filled with the first match for each operator.
|
|
final View[] matches = new View[operators.length];
|
|
// For efficiency, the outer loop should be CellLayout.
|
|
for (CellLayout cellLayout : cellLayouts) {
|
|
mapOverCellLayout(cellLayout, (info, v) -> {
|
|
for (int i = 0; i < operators.length; ++i) {
|
|
if (matches[i] == null && operators[i].evaluate(info, v)) {
|
|
matches[i] = v;
|
|
if (i == 0) {
|
|
// We can return since this is the best match possible.
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
if (matches[0] != null) {
|
|
break;
|
|
}
|
|
}
|
|
for (View match : matches) {
|
|
if (match != null) {
|
|
return match;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void clearDropTargets() {
|
|
mapOverItems(new ItemOperator() {
|
|
@Override
|
|
public boolean evaluate(ItemInfo info, View v) {
|
|
if (v instanceof DropTarget) {
|
|
mDragController.removeDropTarget((DropTarget) v);
|
|
}
|
|
// not done, process all the shortcuts
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Removes items that match the {@param matcher}. When applications are removed
|
|
* as a part of an update, this is called to ensure that other widgets and application
|
|
* shortcuts are not removed.
|
|
*/
|
|
public void removeItemsByMatcher(final ItemInfoMatcher matcher) {
|
|
for (final CellLayout layoutParent: getWorkspaceAndHotseatCellLayouts()) {
|
|
final ViewGroup layout = layoutParent.getShortcutsAndWidgets();
|
|
|
|
IntSparseArrayMap<View> idToViewMap = new IntSparseArrayMap<>();
|
|
ArrayList<ItemInfo> items = new ArrayList<>();
|
|
for (int j = 0; j < layout.getChildCount(); j++) {
|
|
final View view = layout.getChildAt(j);
|
|
if (view.getTag() instanceof ItemInfo) {
|
|
ItemInfo item = (ItemInfo) view.getTag();
|
|
items.add(item);
|
|
idToViewMap.put(item.id, view);
|
|
}
|
|
}
|
|
|
|
for (ItemInfo itemToRemove : matcher.filterItemInfos(items)) {
|
|
View child = idToViewMap.get(itemToRemove.id);
|
|
|
|
if (child != null) {
|
|
// Note: We can not remove the view directly from CellLayoutChildren as this
|
|
// does not re-mark the spaces as unoccupied.
|
|
layoutParent.removeViewInLayout(child);
|
|
if (child instanceof DropTarget) {
|
|
mDragController.removeDropTarget((DropTarget) child);
|
|
}
|
|
} else if (itemToRemove.container >= 0) {
|
|
// The item may belong to a folder.
|
|
View parent = idToViewMap.get(itemToRemove.container);
|
|
if (parent instanceof FolderIcon) {
|
|
FolderInfo folderInfo = (FolderInfo) parent.getTag();
|
|
folderInfo.remove((WorkspaceItemInfo) itemToRemove, false);
|
|
if (((FolderIcon) parent).getFolder().isOpen()) {
|
|
((FolderIcon) parent).getFolder().close(false /* animate */);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Strip all the empty screens
|
|
stripEmptyScreens();
|
|
}
|
|
|
|
public interface ItemOperator {
|
|
/**
|
|
* Process the next itemInfo, possibly with side-effect on the next item.
|
|
*
|
|
* @param info info for the shortcut
|
|
* @param view view for the shortcut
|
|
* @return true if done, false to continue the map
|
|
*/
|
|
boolean evaluate(ItemInfo info, View view);
|
|
}
|
|
|
|
/**
|
|
* Map the operator over the shortcuts and widgets, return the first-non-null value.
|
|
*
|
|
* @param op the operator to map over the shortcuts
|
|
*/
|
|
public void mapOverItems(ItemOperator op) {
|
|
for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
|
|
if (mapOverCellLayout(layout, op)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean mapOverCellLayout(CellLayout layout, ItemOperator op) {
|
|
// TODO(b/128460496) Potential race condition where layout is not yet loaded
|
|
if (layout == null) {
|
|
return false;
|
|
}
|
|
ShortcutAndWidgetContainer container = layout.getShortcutsAndWidgets();
|
|
// map over all the shortcuts on the workspace
|
|
final int itemCount = container.getChildCount();
|
|
for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
|
|
View item = container.getChildAt(itemIdx);
|
|
if (op.evaluate((ItemInfo) item.getTag(), item)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void updateShortcuts(ArrayList<WorkspaceItemInfo> shortcuts) {
|
|
final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
|
|
ItemOperator op = (info, v) -> {
|
|
if (v instanceof BubbleTextView && updates.contains(info)) {
|
|
WorkspaceItemInfo si = (WorkspaceItemInfo) info;
|
|
BubbleTextView shortcut = (BubbleTextView) v;
|
|
Drawable oldIcon = shortcut.getIcon();
|
|
boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
|
|
&& ((PreloadIconDrawable) oldIcon).hasNotCompleted();
|
|
shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
|
|
} else if (info instanceof FolderInfo && v instanceof FolderIcon) {
|
|
((FolderIcon) v).updatePreviewItems(updates::contains);
|
|
}
|
|
|
|
// Iterate all items
|
|
return false;
|
|
};
|
|
|
|
mapOverItems(op);
|
|
Folder openFolder = Folder.getOpen(mLauncher);
|
|
if (openFolder != null) {
|
|
openFolder.iterateOverItems(op);
|
|
}
|
|
}
|
|
|
|
public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
|
|
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
|
|
Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
|
|
|| updatedDots.test(packageUserKey);
|
|
|
|
ItemOperator op = (info, v) -> {
|
|
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
|
|
if (matcher.test(info)) {
|
|
((BubbleTextView) v).applyDotState(info, true /* animate */);
|
|
}
|
|
} else if (info instanceof FolderInfo && v instanceof FolderIcon) {
|
|
FolderInfo fi = (FolderInfo) info;
|
|
if (fi.contents.stream().anyMatch(matcher)) {
|
|
FolderDotInfo folderDotInfo = new FolderDotInfo();
|
|
for (WorkspaceItemInfo si : fi.contents) {
|
|
folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si));
|
|
}
|
|
((FolderIcon) v).setDotInfo(folderDotInfo);
|
|
}
|
|
}
|
|
|
|
// process all the shortcuts
|
|
return false;
|
|
};
|
|
|
|
mapOverItems(op);
|
|
Folder folder = Folder.getOpen(mLauncher);
|
|
if (folder != null) {
|
|
folder.iterateOverItems(op);
|
|
}
|
|
}
|
|
|
|
public void removeAbandonedPromise(String packageName, UserHandle user) {
|
|
HashSet<String> packages = new HashSet<>(1);
|
|
packages.add(packageName);
|
|
ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(packages, user);
|
|
mLauncher.getModelWriter().deleteItemsFromDatabase(matcher);
|
|
removeItemsByMatcher(matcher);
|
|
}
|
|
|
|
public void updateRestoreItems(final HashSet<ItemInfo> updates) {
|
|
ItemOperator op = (info, v) -> {
|
|
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
|
|
&& updates.contains(info)) {
|
|
((BubbleTextView) v).applyPromiseState(false /* promiseStateChanged */);
|
|
} else if (v instanceof PendingAppWidgetHostView
|
|
&& info instanceof LauncherAppWidgetInfo
|
|
&& updates.contains(info)) {
|
|
((PendingAppWidgetHostView) v).applyState();
|
|
} else if (v instanceof FolderIcon && info instanceof FolderInfo) {
|
|
((FolderIcon) v).updatePreviewItems(updates::contains);
|
|
}
|
|
// process all the shortcuts
|
|
return false;
|
|
};
|
|
mapOverItems(op);
|
|
Folder folder = Folder.getOpen(mLauncher);
|
|
if (folder != null) {
|
|
folder.iterateOverItems(op);
|
|
}
|
|
}
|
|
|
|
public void widgetsRestored(final ArrayList<LauncherAppWidgetInfo> changedInfo) {
|
|
if (!changedInfo.isEmpty()) {
|
|
DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo,
|
|
mLauncher.getAppWidgetHost());
|
|
|
|
LauncherAppWidgetInfo item = changedInfo.get(0);
|
|
final AppWidgetProviderInfo widgetInfo;
|
|
WidgetManagerHelper widgetHelper = new WidgetManagerHelper(getContext());
|
|
if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
|
|
widgetInfo = widgetHelper.findProvider(item.providerName, item.user);
|
|
} else {
|
|
widgetInfo = widgetHelper.getLauncherAppWidgetInfo(item.appWidgetId);
|
|
}
|
|
|
|
if (widgetInfo != null) {
|
|
// Re-inflate the widgets which have changed status
|
|
widgetRefresh.run();
|
|
} else {
|
|
// widgetRefresh will automatically run when the packages are updated.
|
|
// For now just update the progress bars
|
|
mapOverItems(new ItemOperator() {
|
|
@Override
|
|
public boolean evaluate(ItemInfo info, View view) {
|
|
if (view instanceof PendingAppWidgetHostView
|
|
&& changedInfo.contains(info)) {
|
|
((LauncherAppWidgetInfo) info).installProgress = 100;
|
|
((PendingAppWidgetHostView) view).applyState();
|
|
}
|
|
// process all the shortcuts
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean isOverlayShown() {
|
|
return mOverlayShown;
|
|
}
|
|
|
|
/** Calls {@link #snapToPage(int)} on the {@link #DEFAULT_PAGE}, then requests focus on it. */
|
|
public void moveToDefaultScreen() {
|
|
int page = DEFAULT_PAGE;
|
|
if (!workspaceInModalState() && getNextPage() != page) {
|
|
snapToPage(page);
|
|
}
|
|
View child = getChildAt(page);
|
|
if (child != null) {
|
|
child.requestFocus();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int getExpectedHeight() {
|
|
return getMeasuredHeight() <= 0 || !mIsLayoutValid
|
|
? mLauncher.getDeviceProfile().heightPx : getMeasuredHeight();
|
|
}
|
|
|
|
@Override
|
|
public int getExpectedWidth() {
|
|
return getMeasuredWidth() <= 0 || !mIsLayoutValid
|
|
? mLauncher.getDeviceProfile().widthPx : getMeasuredWidth();
|
|
}
|
|
|
|
@Override
|
|
protected boolean canAnnouncePageDescription() {
|
|
// Disable announcements while overscrolling potentially to overlay screen because if we end
|
|
// up on the overlay screen, it will take care of announcing itself.
|
|
return Float.compare(mOverlayTranslation, 0f) == 0;
|
|
}
|
|
|
|
@Override
|
|
protected String getCurrentPageDescription() {
|
|
int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
|
|
return getPageDescription(page);
|
|
}
|
|
|
|
private String getPageDescription(int page) {
|
|
int nScreens = getChildCount();
|
|
int extraScreenId = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
|
|
if (extraScreenId >= 0 && nScreens > 1) {
|
|
if (page == extraScreenId) {
|
|
return getContext().getString(R.string.workspace_new_page);
|
|
}
|
|
nScreens--;
|
|
}
|
|
if (nScreens == 0) {
|
|
// When the workspace is not loaded, we do not know how many screen will be bound.
|
|
return getContext().getString(R.string.all_apps_home_button_label);
|
|
}
|
|
return getContext().getString(R.string.workspace_scroll_format, page + 1, nScreens);
|
|
}
|
|
|
|
@Override
|
|
public void fillInLogContainerData(ItemInfo childInfo, Target child,
|
|
ArrayList<Target> parents) {
|
|
if (childInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
|
|
|| childInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
|
|
getHotseat().fillInLogContainerData(childInfo, child, parents);
|
|
return;
|
|
} else if (childInfo.container >= 0) {
|
|
FolderIcon icon = (FolderIcon) getHomescreenIconByItemId(childInfo.container);
|
|
icon.getFolder().fillInLogContainerData(childInfo, child, parents);
|
|
return;
|
|
}
|
|
child.gridX = childInfo.cellX;
|
|
child.gridY = childInfo.cellY;
|
|
child.pageIndex = getCurrentPage();
|
|
parents.add(newContainerTarget(ContainerType.WORKSPACE));
|
|
}
|
|
|
|
/**
|
|
* Used as a workaround to ensure that the AppWidgetService receives the
|
|
* PACKAGE_ADDED broadcast before updating widgets.
|
|
*/
|
|
private class DeferredWidgetRefresh implements Runnable, ProviderChangedListener {
|
|
private final ArrayList<LauncherAppWidgetInfo> mInfos;
|
|
private final LauncherAppWidgetHost mHost;
|
|
private final Handler mHandler;
|
|
|
|
private boolean mRefreshPending;
|
|
|
|
DeferredWidgetRefresh(ArrayList<LauncherAppWidgetInfo> infos,
|
|
LauncherAppWidgetHost host) {
|
|
mInfos = infos;
|
|
mHost = host;
|
|
mHandler = mLauncher.mHandler;
|
|
mRefreshPending = true;
|
|
|
|
mHost.addProviderChangeListener(this);
|
|
// Force refresh after 10 seconds, if we don't get the provider changed event.
|
|
// This could happen when the provider is no longer available in the app.
|
|
Message msg = Message.obtain(mHandler, this);
|
|
msg.obj = DeferredWidgetRefresh.class;
|
|
mHandler.sendMessageDelayed(msg, 10000);
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
mHost.removeProviderChangeListener(this);
|
|
mHandler.removeCallbacks(this);
|
|
|
|
if (!mRefreshPending) {
|
|
return;
|
|
}
|
|
|
|
mRefreshPending = false;
|
|
|
|
ArrayList<PendingAppWidgetHostView> views = new ArrayList<>(mInfos.size());
|
|
mapOverItems((info, view) -> {
|
|
if (view instanceof PendingAppWidgetHostView && mInfos.contains(info)) {
|
|
views.add((PendingAppWidgetHostView) view);
|
|
}
|
|
// process all children
|
|
return false;
|
|
});
|
|
for (PendingAppWidgetHostView view : views) {
|
|
view.reInflate();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void notifyWidgetProvidersChanged() {
|
|
run();
|
|
}
|
|
}
|
|
|
|
private class StateTransitionListener extends AnimatorListenerAdapter
|
|
implements AnimatorUpdateListener {
|
|
|
|
private final LauncherState mToState;
|
|
|
|
StateTransitionListener(LauncherState toState) {
|
|
mToState = toState;
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator anim) {
|
|
mTransitionProgress = anim.getAnimatedFraction();
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
onStartStateTransition(mToState);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
onEndStateTransition();
|
|
}
|
|
}
|
|
}
|