mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-18 10:18:20 +00:00
> Do not allow empty cells on the last page to be click targets > Make drop targets accessible > Do not close folder on tap outside Change-Id: I7cae20e45097092f41408b888e20b6c40c17d981
999 lines
38 KiB
Java
999 lines
38 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 android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.TimeInterpolator;
|
|
import android.animation.ValueAnimator;
|
|
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Rect;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.util.AttributeSet;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.accessibility.AccessibilityEvent;
|
|
import android.view.accessibility.AccessibilityManager;
|
|
import android.view.animation.DecelerateInterpolator;
|
|
import android.view.animation.Interpolator;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.launcher3.util.Thunk;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
/**
|
|
* A ViewGroup that coordinates dragging across its descendants
|
|
*/
|
|
public class DragLayer extends InsettableFrameLayout {
|
|
@Thunk DragController mDragController;
|
|
private int[] mTmpXY = new int[2];
|
|
|
|
private int mXDown, mYDown;
|
|
private Launcher mLauncher;
|
|
|
|
// Variables relating to resizing widgets
|
|
private final ArrayList<AppWidgetResizeFrame> mResizeFrames =
|
|
new ArrayList<AppWidgetResizeFrame>();
|
|
private AppWidgetResizeFrame mCurrentResizeFrame;
|
|
|
|
// Variables relating to animation of views after drop
|
|
private ValueAnimator mDropAnim = null;
|
|
private ValueAnimator mFadeOutAnim = null;
|
|
private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f);
|
|
@Thunk DragView mDropView = null;
|
|
@Thunk int mAnchorViewInitialScrollX = 0;
|
|
@Thunk View mAnchorView = null;
|
|
|
|
private boolean mHoverPointClosesFolder = false;
|
|
private Rect mHitRect = new Rect();
|
|
public static final int ANIMATION_END_DISAPPEAR = 0;
|
|
public static final int ANIMATION_END_FADE_OUT = 1;
|
|
public static final int ANIMATION_END_REMAIN_VISIBLE = 2;
|
|
|
|
private TouchCompleteListener mTouchCompleteListener;
|
|
|
|
private View mOverlayView;
|
|
private int mTopViewIndex;
|
|
private int mChildCountOnLastUpdate = -1;
|
|
|
|
// Darkening scrim
|
|
private Drawable mBackground;
|
|
private float mBackgroundAlpha = 0;
|
|
|
|
// Related to adjacent page hints
|
|
private boolean mInScrollArea;
|
|
private boolean mShowPageHints;
|
|
private Drawable mLeftHoverDrawable;
|
|
private Drawable mRightHoverDrawable;
|
|
private Drawable mLeftHoverDrawableActive;
|
|
private Drawable mRightHoverDrawableActive;
|
|
|
|
private boolean mBlockTouches = false;
|
|
|
|
/**
|
|
* Used to create a new DragLayer from XML.
|
|
*
|
|
* @param context The application's context.
|
|
* @param attrs The attributes set containing the Workspace's customization values.
|
|
*/
|
|
public DragLayer(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
|
|
// Disable multitouch across the workspace/all apps/customize tray
|
|
setMotionEventSplittingEnabled(false);
|
|
setChildrenDrawingOrderEnabled(true);
|
|
|
|
final Resources res = getResources();
|
|
mLeftHoverDrawable = res.getDrawable(R.drawable.page_hover_left);
|
|
mRightHoverDrawable = res.getDrawable(R.drawable.page_hover_right);
|
|
mLeftHoverDrawableActive = res.getDrawable(R.drawable.page_hover_left_active);
|
|
mRightHoverDrawableActive = res.getDrawable(R.drawable.page_hover_right_active);
|
|
mBackground = res.getDrawable(R.drawable.apps_customize_bg);
|
|
}
|
|
|
|
public void setup(Launcher launcher, DragController controller) {
|
|
mLauncher = launcher;
|
|
mDragController = controller;
|
|
}
|
|
|
|
@Override
|
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
|
|
}
|
|
|
|
public void showOverlayView(View overlayView) {
|
|
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
|
mOverlayView = overlayView;
|
|
addView(overlayView, lp);
|
|
|
|
// ensure that the overlay view stays on top. we can't use drawing order for this
|
|
// because in API level 16 touch dispatch doesn't respect drawing order.
|
|
mOverlayView.bringToFront();
|
|
}
|
|
|
|
public void dismissOverlayView() {
|
|
removeView(mOverlayView);
|
|
}
|
|
|
|
private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) {
|
|
getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect);
|
|
if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean isEventOverFolder(Folder folder, MotionEvent ev) {
|
|
getDescendantRectRelativeToSelf(folder, mHitRect);
|
|
if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean isEventOverDropTargetBar(MotionEvent ev) {
|
|
getDescendantRectRelativeToSelf(mLauncher.getSearchBar(), mHitRect);
|
|
if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void setBlockTouch(boolean block) {
|
|
mBlockTouches = block;
|
|
}
|
|
|
|
private boolean handleTouchDown(MotionEvent ev, boolean intercept) {
|
|
Rect hitRect = new Rect();
|
|
int x = (int) ev.getX();
|
|
int y = (int) ev.getY();
|
|
|
|
if (mBlockTouches) {
|
|
return true;
|
|
}
|
|
|
|
for (AppWidgetResizeFrame child: mResizeFrames) {
|
|
child.getHitRect(hitRect);
|
|
if (hitRect.contains(x, y)) {
|
|
if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) {
|
|
mCurrentResizeFrame = child;
|
|
mXDown = x;
|
|
mYDown = y;
|
|
requestDisallowInterceptTouchEvent(true);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
|
|
if (currentFolder != null && intercept) {
|
|
if (currentFolder.isEditingName()) {
|
|
if (!isEventOverFolderTextRegion(currentFolder, ev)) {
|
|
currentFolder.dismissEditingName();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!isEventOverFolder(currentFolder, ev)) {
|
|
if (isInAccessibleDrag()) {
|
|
// Do not close the folder if in drag and drop.
|
|
if (!isEventOverDropTargetBar(ev)) {
|
|
return true;
|
|
}
|
|
} else {
|
|
mLauncher.closeFolder();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
int action = ev.getAction();
|
|
|
|
if (action == MotionEvent.ACTION_DOWN) {
|
|
if (handleTouchDown(ev, true)) {
|
|
return true;
|
|
}
|
|
} else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
if (mTouchCompleteListener != null) {
|
|
mTouchCompleteListener.onTouchComplete();
|
|
}
|
|
mTouchCompleteListener = null;
|
|
}
|
|
clearAllResizeFrames();
|
|
return mDragController.onInterceptTouchEvent(ev);
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptHoverEvent(MotionEvent ev) {
|
|
if (mLauncher == null || mLauncher.getWorkspace() == null) {
|
|
return false;
|
|
}
|
|
Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
|
|
if (currentFolder == null) {
|
|
return false;
|
|
} else {
|
|
AccessibilityManager accessibilityManager = (AccessibilityManager)
|
|
getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
|
|
if (accessibilityManager.isTouchExplorationEnabled()) {
|
|
final int action = ev.getAction();
|
|
boolean isOverFolderOrSearchBar;
|
|
switch (action) {
|
|
case MotionEvent.ACTION_HOVER_ENTER:
|
|
isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) ||
|
|
(isInAccessibleDrag() && isEventOverDropTargetBar(ev));
|
|
if (!isOverFolderOrSearchBar) {
|
|
sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
|
|
mHoverPointClosesFolder = true;
|
|
return true;
|
|
}
|
|
mHoverPointClosesFolder = false;
|
|
break;
|
|
case MotionEvent.ACTION_HOVER_MOVE:
|
|
isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) ||
|
|
(isInAccessibleDrag() && isEventOverDropTargetBar(ev));
|
|
if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) {
|
|
sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
|
|
mHoverPointClosesFolder = true;
|
|
return true;
|
|
} else if (!isOverFolderOrSearchBar) {
|
|
return true;
|
|
}
|
|
mHoverPointClosesFolder = false;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) {
|
|
AccessibilityManager accessibilityManager = (AccessibilityManager)
|
|
getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
|
|
if (accessibilityManager.isEnabled()) {
|
|
int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close;
|
|
AccessibilityEvent event = AccessibilityEvent.obtain(
|
|
AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
|
onInitializeAccessibilityEvent(event);
|
|
event.getText().add(getContext().getString(stringId));
|
|
accessibilityManager.sendAccessibilityEvent(event);
|
|
}
|
|
}
|
|
|
|
private boolean isInAccessibleDrag() {
|
|
LauncherAccessibilityDelegate delegate = LauncherAppState
|
|
.getInstance().getAccessibilityDelegate();
|
|
return delegate != null && delegate.isInAccessibleDrag();
|
|
}
|
|
|
|
@Override
|
|
public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
|
|
Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
|
|
if (currentFolder != null) {
|
|
if (child == currentFolder) {
|
|
return super.onRequestSendAccessibilityEvent(child, event);
|
|
}
|
|
|
|
if (isInAccessibleDrag() && child instanceof SearchDropTargetBar) {
|
|
return super.onRequestSendAccessibilityEvent(child, event);
|
|
}
|
|
// Skip propagating onRequestSendAccessibilityEvent all for other children
|
|
// when a folder is open
|
|
return false;
|
|
}
|
|
return super.onRequestSendAccessibilityEvent(child, event);
|
|
}
|
|
|
|
@Override
|
|
public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) {
|
|
Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
|
|
if (currentFolder != null) {
|
|
// Only add the folder as a child for accessibility when it is open
|
|
childrenForAccessibility.add(currentFolder);
|
|
|
|
if (isInAccessibleDrag()) {
|
|
childrenForAccessibility.add(mLauncher.getSearchBar());
|
|
}
|
|
} else {
|
|
super.addChildrenForAccessibility(childrenForAccessibility);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onHoverEvent(MotionEvent ev) {
|
|
// If we've received this, we've already done the necessary handling
|
|
// in onInterceptHoverEvent. Return true to consume the event.
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
|
boolean handled = false;
|
|
int action = ev.getAction();
|
|
|
|
int x = (int) ev.getX();
|
|
int y = (int) ev.getY();
|
|
|
|
if (mBlockTouches) {
|
|
return true;
|
|
}
|
|
|
|
if (action == MotionEvent.ACTION_DOWN) {
|
|
if (handleTouchDown(ev, false)) {
|
|
return true;
|
|
}
|
|
} else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
if (mTouchCompleteListener != null) {
|
|
mTouchCompleteListener.onTouchComplete();
|
|
}
|
|
mTouchCompleteListener = null;
|
|
}
|
|
|
|
if (mCurrentResizeFrame != null) {
|
|
handled = true;
|
|
switch (action) {
|
|
case MotionEvent.ACTION_MOVE:
|
|
mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
|
|
break;
|
|
case MotionEvent.ACTION_CANCEL:
|
|
case MotionEvent.ACTION_UP:
|
|
mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
|
|
mCurrentResizeFrame.onTouchUp();
|
|
mCurrentResizeFrame = null;
|
|
}
|
|
}
|
|
if (handled) return true;
|
|
return mDragController.onTouchEvent(ev);
|
|
}
|
|
|
|
/**
|
|
* Determine the rect of the descendant in this DragLayer's coordinates
|
|
*
|
|
* @param descendant The descendant whose coordinates we want to find.
|
|
* @param r The rect into which to place the results.
|
|
* @return The factor by which this descendant is scaled relative to this DragLayer.
|
|
*/
|
|
public float getDescendantRectRelativeToSelf(View descendant, Rect r) {
|
|
mTmpXY[0] = 0;
|
|
mTmpXY[1] = 0;
|
|
float scale = getDescendantCoordRelativeToSelf(descendant, mTmpXY);
|
|
|
|
r.set(mTmpXY[0], mTmpXY[1],
|
|
(int) (mTmpXY[0] + scale * descendant.getMeasuredWidth()),
|
|
(int) (mTmpXY[1] + scale * descendant.getMeasuredHeight()));
|
|
return scale;
|
|
}
|
|
|
|
public float getLocationInDragLayer(View child, int[] loc) {
|
|
loc[0] = 0;
|
|
loc[1] = 0;
|
|
return getDescendantCoordRelativeToSelf(child, loc);
|
|
}
|
|
|
|
public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) {
|
|
return getDescendantCoordRelativeToSelf(descendant, coord, false);
|
|
}
|
|
|
|
/**
|
|
* Given a coordinate relative to the descendant, find the coordinate in this DragLayer's
|
|
* coordinates.
|
|
*
|
|
* @param descendant The descendant to which the passed coordinate is relative.
|
|
* @param coord The coordinate that we want mapped.
|
|
* @param includeRootScroll Whether or not to account for the scroll of the root descendant:
|
|
* sometimes this is relevant as in a child's coordinates within the root descendant.
|
|
* @return The factor by which this descendant is scaled relative to this DragLayer. Caution
|
|
* this scale factor is assumed to be equal in X and Y, and so if at any point this
|
|
* assumption fails, we will need to return a pair of scale factors.
|
|
*/
|
|
public float getDescendantCoordRelativeToSelf(View descendant, int[] coord,
|
|
boolean includeRootScroll) {
|
|
return Utilities.getDescendantCoordRelativeToParent(descendant, this,
|
|
coord, includeRootScroll);
|
|
}
|
|
|
|
/**
|
|
* Inverse of {@link #getDescendantCoordRelativeToSelf(View, int[])}.
|
|
*/
|
|
public float mapCoordInSelfToDescendent(View descendant, int[] coord) {
|
|
return Utilities.mapCoordInSelfToDescendent(descendant, this, coord);
|
|
}
|
|
|
|
public void getViewRectRelativeToSelf(View v, Rect r) {
|
|
int[] loc = new int[2];
|
|
getLocationInWindow(loc);
|
|
int x = loc[0];
|
|
int y = loc[1];
|
|
|
|
v.getLocationInWindow(loc);
|
|
int vX = loc[0];
|
|
int vY = loc[1];
|
|
|
|
int left = vX - x;
|
|
int top = vY - y;
|
|
r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
|
|
}
|
|
|
|
@Override
|
|
public boolean dispatchUnhandledMove(View focused, int direction) {
|
|
return mDragController.dispatchUnhandledMove(focused, direction);
|
|
}
|
|
|
|
@Override
|
|
public LayoutParams generateLayoutParams(AttributeSet attrs) {
|
|
return new LayoutParams(getContext(), attrs);
|
|
}
|
|
|
|
@Override
|
|
protected LayoutParams generateDefaultLayoutParams() {
|
|
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
|
}
|
|
|
|
// Override to allow type-checking of LayoutParams.
|
|
@Override
|
|
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
|
|
return p instanceof LayoutParams;
|
|
}
|
|
|
|
@Override
|
|
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
|
|
return new LayoutParams(p);
|
|
}
|
|
|
|
public static class LayoutParams extends InsettableFrameLayout.LayoutParams {
|
|
public int x, y;
|
|
public boolean customPosition = false;
|
|
|
|
public LayoutParams(Context c, AttributeSet attrs) {
|
|
super(c, attrs);
|
|
}
|
|
|
|
public LayoutParams(int width, int height) {
|
|
super(width, height);
|
|
}
|
|
|
|
public LayoutParams(ViewGroup.LayoutParams lp) {
|
|
super(lp);
|
|
}
|
|
|
|
public void setWidth(int width) {
|
|
this.width = width;
|
|
}
|
|
|
|
public int getWidth() {
|
|
return width;
|
|
}
|
|
|
|
public void setHeight(int height) {
|
|
this.height = height;
|
|
}
|
|
|
|
public int getHeight() {
|
|
return height;
|
|
}
|
|
|
|
public void setX(int x) {
|
|
this.x = x;
|
|
}
|
|
|
|
public int getX() {
|
|
return x;
|
|
}
|
|
|
|
public void setY(int y) {
|
|
this.y = y;
|
|
}
|
|
|
|
public int getY() {
|
|
return y;
|
|
}
|
|
}
|
|
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
super.onLayout(changed, l, t, r, b);
|
|
int count = getChildCount();
|
|
for (int i = 0; i < count; i++) {
|
|
View child = getChildAt(i);
|
|
final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams();
|
|
if (flp instanceof LayoutParams) {
|
|
final LayoutParams lp = (LayoutParams) flp;
|
|
if (lp.customPosition) {
|
|
child.layout(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void clearAllResizeFrames() {
|
|
if (mResizeFrames.size() > 0) {
|
|
for (AppWidgetResizeFrame frame: mResizeFrames) {
|
|
frame.commitResize();
|
|
removeView(frame);
|
|
}
|
|
mResizeFrames.clear();
|
|
}
|
|
}
|
|
|
|
public boolean hasResizeFrames() {
|
|
return mResizeFrames.size() > 0;
|
|
}
|
|
|
|
public boolean isWidgetBeingResized() {
|
|
return mCurrentResizeFrame != null;
|
|
}
|
|
|
|
public void addResizeFrame(ItemInfo itemInfo, LauncherAppWidgetHostView widget,
|
|
CellLayout cellLayout) {
|
|
AppWidgetResizeFrame resizeFrame = new AppWidgetResizeFrame(getContext(),
|
|
widget, cellLayout, this);
|
|
|
|
LayoutParams lp = new LayoutParams(-1, -1);
|
|
lp.customPosition = true;
|
|
|
|
addView(resizeFrame, lp);
|
|
mResizeFrames.add(resizeFrame);
|
|
|
|
resizeFrame.snapToWidget(false);
|
|
}
|
|
|
|
public void animateViewIntoPosition(DragView dragView, final View child) {
|
|
animateViewIntoPosition(dragView, child, null, null);
|
|
}
|
|
|
|
public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha,
|
|
float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable,
|
|
int duration) {
|
|
Rect r = new Rect();
|
|
getViewRectRelativeToSelf(dragView, r);
|
|
final int fromX = r.left;
|
|
final int fromY = r.top;
|
|
|
|
animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY,
|
|
onFinishRunnable, animationEndStyle, duration, null);
|
|
}
|
|
|
|
public void animateViewIntoPosition(DragView dragView, final View child,
|
|
final Runnable onFinishAnimationRunnable, View anchorView) {
|
|
animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable, anchorView);
|
|
}
|
|
|
|
public void animateViewIntoPosition(DragView dragView, final View child, int duration,
|
|
final Runnable onFinishAnimationRunnable, View anchorView) {
|
|
ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent();
|
|
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
|
|
parentChildren.measureChild(child);
|
|
|
|
Rect r = new Rect();
|
|
getViewRectRelativeToSelf(dragView, r);
|
|
|
|
int coord[] = new int[2];
|
|
float childScale = child.getScaleX();
|
|
coord[0] = lp.x + (int) (child.getMeasuredWidth() * (1 - childScale) / 2);
|
|
coord[1] = lp.y + (int) (child.getMeasuredHeight() * (1 - childScale) / 2);
|
|
|
|
// Since the child hasn't necessarily been laid out, we force the lp to be updated with
|
|
// the correct coordinates (above) and use these to determine the final location
|
|
float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord);
|
|
// We need to account for the scale of the child itself, as the above only accounts for
|
|
// for the scale in parents.
|
|
scale *= childScale;
|
|
int toX = coord[0];
|
|
int toY = coord[1];
|
|
float toScale = scale;
|
|
if (child instanceof TextView) {
|
|
TextView tv = (TextView) child;
|
|
// Account for the source scale of the icon (ie. from AllApps to Workspace, in which
|
|
// the workspace may have smaller icon bounds).
|
|
toScale = scale / dragView.getIntrinsicIconScaleFactor();
|
|
|
|
// The child may be scaled (always about the center of the view) so to account for it,
|
|
// we have to offset the position by the scaled size. Once we do that, we can center
|
|
// the drag view about the scaled child view.
|
|
toY += Math.round(toScale * tv.getPaddingTop());
|
|
toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2;
|
|
if (dragView.getDragVisualizeOffset() != null) {
|
|
toY -= Math.round(toScale * dragView.getDragVisualizeOffset().y);
|
|
}
|
|
|
|
toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
|
|
} else if (child instanceof FolderIcon) {
|
|
// Account for holographic blur padding on the drag view
|
|
toY += Math.round(scale * (child.getPaddingTop() - dragView.getDragRegionTop()));
|
|
toY -= scale * Workspace.DRAG_BITMAP_PADDING / 2;
|
|
toY -= (1 - scale) * dragView.getMeasuredHeight() / 2;
|
|
// Center in the x coordinate about the target's drawable
|
|
toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
|
|
} else {
|
|
toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2;
|
|
toX -= (Math.round(scale * (dragView.getMeasuredWidth()
|
|
- child.getMeasuredWidth()))) / 2;
|
|
}
|
|
|
|
final int fromX = r.left;
|
|
final int fromY = r.top;
|
|
child.setVisibility(INVISIBLE);
|
|
Runnable onCompleteRunnable = new Runnable() {
|
|
public void run() {
|
|
child.setVisibility(VISIBLE);
|
|
if (onFinishAnimationRunnable != null) {
|
|
onFinishAnimationRunnable.run();
|
|
}
|
|
}
|
|
};
|
|
animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale,
|
|
onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView);
|
|
}
|
|
|
|
public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY,
|
|
final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY,
|
|
float finalScaleX, float finalScaleY, Runnable onCompleteRunnable,
|
|
int animationEndStyle, int duration, View anchorView) {
|
|
Rect from = new Rect(fromX, fromY, fromX +
|
|
view.getMeasuredWidth(), fromY + view.getMeasuredHeight());
|
|
Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
|
|
animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration,
|
|
null, null, onCompleteRunnable, animationEndStyle, anchorView);
|
|
}
|
|
|
|
/**
|
|
* This method animates a view at the end of a drag and drop animation.
|
|
*
|
|
* @param view The view to be animated. This view is drawn directly into DragLayer, and so
|
|
* doesn't need to be a child of DragLayer.
|
|
* @param from The initial location of the view. Only the left and top parameters are used.
|
|
* @param to The final location of the view. Only the left and top parameters are used. This
|
|
* location doesn't account for scaling, and so should be centered about the desired
|
|
* final location (including scaling).
|
|
* @param finalAlpha The final alpha of the view, in case we want it to fade as it animates.
|
|
* @param finalScale The final scale of the view. The view is scaled about its center.
|
|
* @param duration The duration of the animation.
|
|
* @param motionInterpolator The interpolator to use for the location of the view.
|
|
* @param alphaInterpolator The interpolator to use for the alpha of the view.
|
|
* @param onCompleteRunnable Optional runnable to run on animation completion.
|
|
* @param fadeOut Whether or not to fade out the view once the animation completes. If true,
|
|
* the runnable will execute after the view is faded out.
|
|
* @param anchorView If not null, this represents the view which the animated view stays
|
|
* anchored to in case scrolling is currently taking place. Note: currently this is
|
|
* only used for the X dimension for the case of the workspace.
|
|
*/
|
|
public void animateView(final DragView view, final Rect from, final Rect to,
|
|
final float finalAlpha, final float initScaleX, final float initScaleY,
|
|
final float finalScaleX, final float finalScaleY, int duration,
|
|
final Interpolator motionInterpolator, final Interpolator alphaInterpolator,
|
|
final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) {
|
|
|
|
// Calculate the duration of the animation based on the object's distance
|
|
final float dist = (float) Math.hypot(to.left - from.left, to.top - from.top);
|
|
final Resources res = getResources();
|
|
final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist);
|
|
|
|
// If duration < 0, this is a cue to compute the duration based on the distance
|
|
if (duration < 0) {
|
|
duration = res.getInteger(R.integer.config_dropAnimMaxDuration);
|
|
if (dist < maxDist) {
|
|
duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist);
|
|
}
|
|
duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration));
|
|
}
|
|
|
|
// Fall back to cubic ease out interpolator for the animation if none is specified
|
|
TimeInterpolator interpolator = null;
|
|
if (alphaInterpolator == null || motionInterpolator == null) {
|
|
interpolator = mCubicEaseOutInterpolator;
|
|
}
|
|
|
|
// Animate the view
|
|
final float initAlpha = view.getAlpha();
|
|
final float dropViewScale = view.getScaleX();
|
|
AnimatorUpdateListener updateCb = new AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
final float percent = (Float) animation.getAnimatedValue();
|
|
final int width = view.getMeasuredWidth();
|
|
final int height = view.getMeasuredHeight();
|
|
|
|
float alphaPercent = alphaInterpolator == null ? percent :
|
|
alphaInterpolator.getInterpolation(percent);
|
|
float motionPercent = motionInterpolator == null ? percent :
|
|
motionInterpolator.getInterpolation(percent);
|
|
|
|
float initialScaleX = initScaleX * dropViewScale;
|
|
float initialScaleY = initScaleY * dropViewScale;
|
|
float scaleX = finalScaleX * percent + initialScaleX * (1 - percent);
|
|
float scaleY = finalScaleY * percent + initialScaleY * (1 - percent);
|
|
float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent);
|
|
|
|
float fromLeft = from.left + (initialScaleX - 1f) * width / 2;
|
|
float fromTop = from.top + (initialScaleY - 1f) * height / 2;
|
|
|
|
int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent)));
|
|
int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent)));
|
|
|
|
int anchorAdjust = mAnchorView == null ? 0 : (int) (mAnchorView.getScaleX() *
|
|
(mAnchorViewInitialScrollX - mAnchorView.getScrollX()));
|
|
|
|
int xPos = x - mDropView.getScrollX() + anchorAdjust;
|
|
int yPos = y - mDropView.getScrollY();
|
|
|
|
mDropView.setTranslationX(xPos);
|
|
mDropView.setTranslationY(yPos);
|
|
mDropView.setScaleX(scaleX);
|
|
mDropView.setScaleY(scaleY);
|
|
mDropView.setAlpha(alpha);
|
|
}
|
|
};
|
|
animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle,
|
|
anchorView);
|
|
}
|
|
|
|
public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration,
|
|
TimeInterpolator interpolator, final Runnable onCompleteRunnable,
|
|
final int animationEndStyle, View anchorView) {
|
|
// Clean up the previous animations
|
|
if (mDropAnim != null) mDropAnim.cancel();
|
|
if (mFadeOutAnim != null) mFadeOutAnim.cancel();
|
|
|
|
// Show the drop view if it was previously hidden
|
|
mDropView = view;
|
|
mDropView.cancelAnimation();
|
|
mDropView.resetLayoutParams();
|
|
|
|
// Set the anchor view if the page is scrolling
|
|
if (anchorView != null) {
|
|
mAnchorViewInitialScrollX = anchorView.getScrollX();
|
|
}
|
|
mAnchorView = anchorView;
|
|
|
|
// Create and start the animation
|
|
mDropAnim = new ValueAnimator();
|
|
mDropAnim.setInterpolator(interpolator);
|
|
mDropAnim.setDuration(duration);
|
|
mDropAnim.setFloatValues(0f, 1f);
|
|
mDropAnim.addUpdateListener(updateCb);
|
|
mDropAnim.addListener(new AnimatorListenerAdapter() {
|
|
public void onAnimationEnd(Animator animation) {
|
|
if (onCompleteRunnable != null) {
|
|
onCompleteRunnable.run();
|
|
}
|
|
switch (animationEndStyle) {
|
|
case ANIMATION_END_DISAPPEAR:
|
|
clearAnimatedView();
|
|
break;
|
|
case ANIMATION_END_FADE_OUT:
|
|
fadeOutDragView();
|
|
break;
|
|
case ANIMATION_END_REMAIN_VISIBLE:
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
mDropAnim.start();
|
|
}
|
|
|
|
public void clearAnimatedView() {
|
|
if (mDropAnim != null) {
|
|
mDropAnim.cancel();
|
|
}
|
|
if (mDropView != null) {
|
|
mDragController.onDeferredEndDrag(mDropView);
|
|
}
|
|
mDropView = null;
|
|
invalidate();
|
|
}
|
|
|
|
public View getAnimatedView() {
|
|
return mDropView;
|
|
}
|
|
|
|
@Thunk void fadeOutDragView() {
|
|
mFadeOutAnim = new ValueAnimator();
|
|
mFadeOutAnim.setDuration(150);
|
|
mFadeOutAnim.setFloatValues(0f, 1f);
|
|
mFadeOutAnim.removeAllUpdateListeners();
|
|
mFadeOutAnim.addUpdateListener(new AnimatorUpdateListener() {
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
final float percent = (Float) animation.getAnimatedValue();
|
|
|
|
float alpha = 1 - percent;
|
|
mDropView.setAlpha(alpha);
|
|
}
|
|
});
|
|
mFadeOutAnim.addListener(new AnimatorListenerAdapter() {
|
|
public void onAnimationEnd(Animator animation) {
|
|
if (mDropView != null) {
|
|
mDragController.onDeferredEndDrag(mDropView);
|
|
}
|
|
mDropView = null;
|
|
invalidate();
|
|
}
|
|
});
|
|
mFadeOutAnim.start();
|
|
}
|
|
|
|
@Override
|
|
public void onChildViewAdded(View parent, View child) {
|
|
super.onChildViewAdded(parent, child);
|
|
if (mOverlayView != null) {
|
|
// ensure that the overlay view stays on top. we can't use drawing order for this
|
|
// because in API level 16 touch dispatch doesn't respect drawing order.
|
|
mOverlayView.bringToFront();
|
|
}
|
|
updateChildIndices();
|
|
}
|
|
|
|
@Override
|
|
public void onChildViewRemoved(View parent, View child) {
|
|
updateChildIndices();
|
|
}
|
|
|
|
@Override
|
|
public void bringChildToFront(View child) {
|
|
super.bringChildToFront(child);
|
|
if (child != mOverlayView && mOverlayView != null) {
|
|
// ensure that the overlay view stays on top. we can't use drawing order for this
|
|
// because in API level 16 touch dispatch doesn't respect drawing order.
|
|
mOverlayView.bringToFront();
|
|
}
|
|
updateChildIndices();
|
|
}
|
|
|
|
private void updateChildIndices() {
|
|
mTopViewIndex = -1;
|
|
int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
if (getChildAt(i) instanceof DragView) {
|
|
mTopViewIndex = i;
|
|
}
|
|
}
|
|
mChildCountOnLastUpdate = childCount;
|
|
}
|
|
|
|
@Override
|
|
protected int getChildDrawingOrder(int childCount, int i) {
|
|
if (mChildCountOnLastUpdate != childCount) {
|
|
// between platform versions 17 and 18, behavior for onChildViewRemoved / Added changed.
|
|
// Pre-18, the child was not added / removed by the time of those callbacks. We need to
|
|
// force update our representation of things here to avoid crashing on pre-18 devices
|
|
// in certain instances.
|
|
updateChildIndices();
|
|
}
|
|
|
|
// i represents the current draw iteration
|
|
if (mTopViewIndex == -1) {
|
|
// in general we do nothing
|
|
return i;
|
|
} else if (i == childCount - 1) {
|
|
// if we have a top index, we return it when drawing last item (highest z-order)
|
|
return mTopViewIndex;
|
|
} else if (i < mTopViewIndex) {
|
|
return i;
|
|
} else {
|
|
// for indexes greater than the top index, we fetch one item above to shift for the
|
|
// displacement of the top index
|
|
return i + 1;
|
|
}
|
|
}
|
|
|
|
void onEnterScrollArea(int direction) {
|
|
mInScrollArea = true;
|
|
invalidate();
|
|
}
|
|
|
|
void onExitScrollArea() {
|
|
mInScrollArea = false;
|
|
invalidate();
|
|
}
|
|
|
|
void showPageHints() {
|
|
mShowPageHints = true;
|
|
invalidate();
|
|
}
|
|
|
|
void hidePageHints() {
|
|
mShowPageHints = false;
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api.
|
|
*/
|
|
private boolean isLayoutRtl() {
|
|
return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
|
|
}
|
|
|
|
@Override
|
|
protected void dispatchDraw(Canvas canvas) {
|
|
// Draw the background gradient below children.
|
|
if (mBackground != null && mBackgroundAlpha > 0.0f) {
|
|
int alpha = (int) (mBackgroundAlpha * 255);
|
|
mBackground.setAlpha(alpha);
|
|
mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
|
|
mBackground.draw(canvas);
|
|
}
|
|
|
|
super.dispatchDraw(canvas);
|
|
}
|
|
|
|
private void drawPageHints(Canvas canvas) {
|
|
if (mShowPageHints) {
|
|
Workspace workspace = mLauncher.getWorkspace();
|
|
int width = getMeasuredWidth();
|
|
Rect childRect = new Rect();
|
|
getDescendantRectRelativeToSelf(workspace.getChildAt(workspace.getChildCount() - 1),
|
|
childRect);
|
|
|
|
int page = workspace.getNextPage();
|
|
final boolean isRtl = isLayoutRtl();
|
|
CellLayout leftPage = (CellLayout) workspace.getChildAt(isRtl ? page + 1 : page - 1);
|
|
CellLayout rightPage = (CellLayout) workspace.getChildAt(isRtl ? page - 1 : page + 1);
|
|
|
|
if (leftPage != null && leftPage.isDragTarget()) {
|
|
Drawable left = mInScrollArea && leftPage.getIsDragOverlapping() ?
|
|
mLeftHoverDrawableActive : mLeftHoverDrawable;
|
|
left.setBounds(0, childRect.top,
|
|
left.getIntrinsicWidth(), childRect.bottom);
|
|
left.draw(canvas);
|
|
}
|
|
if (rightPage != null && rightPage.isDragTarget()) {
|
|
Drawable right = mInScrollArea && rightPage.getIsDragOverlapping() ?
|
|
mRightHoverDrawableActive : mRightHoverDrawable;
|
|
right.setBounds(width - right.getIntrinsicWidth(),
|
|
childRect.top, width, childRect.bottom);
|
|
right.draw(canvas);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
|
|
boolean ret = super.drawChild(canvas, child, drawingTime);
|
|
|
|
// We want to draw the page hints above the workspace, but below the drag view.
|
|
if (child instanceof Workspace) {
|
|
drawPageHints(canvas);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
public void setBackgroundAlpha(float alpha) {
|
|
if (alpha != mBackgroundAlpha) {
|
|
mBackgroundAlpha = alpha;
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
public float getBackgroundAlpha() {
|
|
return mBackgroundAlpha;
|
|
}
|
|
|
|
public void setTouchCompleteListener(TouchCompleteListener listener) {
|
|
mTouchCompleteListener = listener;
|
|
}
|
|
|
|
public interface TouchCompleteListener {
|
|
public void onTouchComplete();
|
|
}
|
|
}
|