| /* |
| * 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.InsettableFrameLayout.LayoutParams; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * A ViewGroup that coordinates dragging across its descendants |
| */ |
| public class DragLayer extends InsettableFrameLayout { |
| private 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); |
| private DragView mDropView = null; |
| private int mAnchorViewInitialScrollX = 0; |
| private 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; |
| } |
| |
| 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; |
| } |
| } |
| |
| getDescendantRectRelativeToSelf(currentFolder, hitRect); |
| if (!isEventOverFolder(currentFolder, ev)) { |
| 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 isOverFolder; |
| switch (action) { |
| case MotionEvent.ACTION_HOVER_ENTER: |
| isOverFolder = isEventOverFolder(currentFolder, ev); |
| if (!isOverFolder) { |
| sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); |
| mHoverPointClosesFolder = true; |
| return true; |
| } |
| mHoverPointClosesFolder = false; |
| break; |
| case MotionEvent.ACTION_HOVER_MOVE: |
| isOverFolder = isEventOverFolder(currentFolder, ev); |
| if (!isOverFolder && !mHoverPointClosesFolder) { |
| sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); |
| mHoverPointClosesFolder = true; |
| return true; |
| } else if (!isOverFolder) { |
| 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); |
| } |
| } |
| |
| @Override |
| public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { |
| Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); |
| if (currentFolder != null) { |
| if (child == currentFolder) { |
| 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); |
| } 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.sqrt(Math.pow(to.left - from.left, 2) + |
| Math.pow(to.top - from.top, 2)); |
| 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; |
| } |
| |
| private 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(); |
| } |
| } |