Workspace.java revision a5902524d4403885eb4c50360bf3465c6be796ef
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.launcher2;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.ComponentName;
22import android.content.res.TypedArray;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.graphics.Paint;
26import android.graphics.RectF;
27import android.graphics.Rect;
28import android.graphics.Region;
29import android.graphics.drawable.Drawable;
30import android.util.AttributeSet;
31import android.view.MotionEvent;
32import android.view.VelocityTracker;
33import android.view.View;
34import android.view.ViewConfiguration;
35import android.view.ViewGroup;
36import android.view.ViewParent;
37import android.widget.Scroller;
38import android.widget.TextView;
39import android.os.Parcelable;
40import android.os.Parcel;
41
42import java.util.ArrayList;
43
44/**
45 * The workspace is a wide area with a wallpaper and a finite number of screens. Each
46 * screen contains a number of icons, folders or widgets the user can interact with.
47 * A workspace is meant to be used with a fixed width only.
48 */
49public class Workspace extends ViewGroup implements DropTarget, DragSource, DragScroller {
50    private static final int INVALID_SCREEN = -1;
51
52    /**
53     * The velocity at which a fling gesture will cause us to snap to the next screen
54     */
55    private static final int SNAP_VELOCITY = 1000;
56
57    private int mDefaultScreen;
58
59    private Paint mPaint;
60    private Bitmap mWallpaper;
61
62    private int mWallpaperWidth;
63    private int mWallpaperHeight;
64    private float mWallpaperOffset;
65    private boolean mWallpaperLoaded;
66
67    private boolean mFirstLayout = true;
68
69    private int mCurrentScreen;
70    private int mNextScreen = INVALID_SCREEN;
71    private Scroller mScroller;
72    private VelocityTracker mVelocityTracker;
73
74    /**
75     * CellInfo for the cell that is currently being dragged
76     */
77    private CellLayout.CellInfo mDragInfo;
78
79    /**
80     * Target drop area calculated during last acceptDrop call.
81     */
82    private int[] mTargetCell = null;
83
84    private float mLastMotionX;
85    private float mLastMotionY;
86
87    private final static int TOUCH_STATE_REST = 0;
88    private final static int TOUCH_STATE_SCROLLING = 1;
89
90    private int mTouchState = TOUCH_STATE_REST;
91
92    private OnLongClickListener mLongClickListener;
93
94    private Launcher mLauncher;
95    private DragController mDragger;
96
97    /**
98     * Cache of vacant cells, used during drag events and invalidated as needed.
99     */
100    private CellLayout.CellInfo mVacantCache = null;
101
102    private int[] mTempCell = new int[2];
103    private int[] mTempEstimate = new int[2];
104
105    private boolean mAllowLongPress;
106    private boolean mLocked;
107
108    private int mTouchSlop;
109    private int mMaximumVelocity;
110
111    final Rect mDrawerBounds = new Rect();
112    final Rect mClipBounds = new Rect();
113    int mDrawerContentHeight;
114    int mDrawerContentWidth;
115
116    /**
117     * Used to inflate the Workspace from XML.
118     *
119     * @param context The application's context.
120     * @param attrs The attribtues set containing the Workspace's customization values.
121     */
122    public Workspace(Context context, AttributeSet attrs) {
123        this(context, attrs, 0);
124    }
125
126    /**
127     * Used to inflate the Workspace from XML.
128     *
129     * @param context The application's context.
130     * @param attrs The attribtues set containing the Workspace's customization values.
131     * @param defStyle Unused.
132     */
133    public Workspace(Context context, AttributeSet attrs, int defStyle) {
134        super(context, attrs, defStyle);
135
136        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace, defStyle, 0);
137        mDefaultScreen = a.getInt(R.styleable.Workspace_defaultScreen, 1);
138        a.recycle();
139
140        initWorkspace();
141    }
142
143    /**
144     * Initializes various states for this workspace.
145     */
146    private void initWorkspace() {
147        mScroller = new Scroller(getContext());
148        mCurrentScreen = mDefaultScreen;
149        Launcher.setScreen(mCurrentScreen);
150
151        mPaint = new Paint();
152        mPaint.setDither(false);
153
154        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
155        mTouchSlop = configuration.getScaledTouchSlop();
156        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
157    }
158
159    /**
160     * Set the background's wallpaper.
161     */
162    void loadWallpaper(Bitmap bitmap) {
163        mWallpaper = bitmap;
164        mWallpaperLoaded = true;
165        requestLayout();
166        invalidate();
167    }
168
169    @Override
170    public void addView(View child, int index, LayoutParams params) {
171        if (!(child instanceof CellLayout)) {
172            throw new IllegalArgumentException("A Workspace can only have CellLayout children.");
173        }
174        super.addView(child, index, params);
175    }
176
177    @Override
178    public void addView(View child) {
179        if (!(child instanceof CellLayout)) {
180            throw new IllegalArgumentException("A Workspace can only have CellLayout children.");
181        }
182        super.addView(child);
183    }
184
185    @Override
186    public void addView(View child, int index) {
187        if (!(child instanceof CellLayout)) {
188            throw new IllegalArgumentException("A Workspace can only have CellLayout children.");
189        }
190        super.addView(child, index);
191    }
192
193    @Override
194    public void addView(View child, int width, int height) {
195        if (!(child instanceof CellLayout)) {
196            throw new IllegalArgumentException("A Workspace can only have CellLayout children.");
197        }
198        super.addView(child, width, height);
199    }
200
201    @Override
202    public void addView(View child, LayoutParams params) {
203        if (!(child instanceof CellLayout)) {
204            throw new IllegalArgumentException("A Workspace can only have CellLayout children.");
205        }
206        super.addView(child, params);
207    }
208
209    /**
210     * @return The open folder on the current screen, or null if there is none
211     */
212    Folder getOpenFolder() {
213        CellLayout currentScreen = (CellLayout) getChildAt(mCurrentScreen);
214        int count = currentScreen.getChildCount();
215        for (int i = 0; i < count; i++) {
216            View child = currentScreen.getChildAt(i);
217            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
218            if (lp.cellHSpan == 4 && lp.cellVSpan == 4 && child instanceof Folder) {
219                return (Folder) child;
220            }
221        }
222        return null;
223    }
224
225    ArrayList<Folder> getOpenFolders() {
226        final int screens = getChildCount();
227        ArrayList<Folder> folders = new ArrayList<Folder>(screens);
228
229        for (int screen = 0; screen < screens; screen++) {
230            CellLayout currentScreen = (CellLayout) getChildAt(screen);
231            int count = currentScreen.getChildCount();
232            for (int i = 0; i < count; i++) {
233                View child = currentScreen.getChildAt(i);
234                CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
235                if (lp.cellHSpan == 4 && lp.cellVSpan == 4 && child instanceof Folder) {
236                    folders.add((Folder) child);
237                    break;
238                }
239            }
240        }
241
242        return folders;
243    }
244
245    boolean isDefaultScreenShowing() {
246        return mCurrentScreen == mDefaultScreen;
247    }
248
249    /**
250     * Returns the index of the currently displayed screen.
251     *
252     * @return The index of the currently displayed screen.
253     */
254    int getCurrentScreen() {
255        return mCurrentScreen;
256    }
257
258    /**
259     * Computes a bounding rectangle for a range of cells
260     *
261     * @param cellX X coordinate of upper left corner expressed as a cell position
262     * @param cellY Y coordinate of upper left corner expressed as a cell position
263     * @param cellHSpan Width in cells
264     * @param cellVSpan Height in cells
265     * @param rect Rectnagle into which to put the results
266     */
267    public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, RectF rect) {
268        ((CellLayout)getChildAt(mCurrentScreen)).cellToRect(cellX, cellY,
269                cellHSpan, cellVSpan, rect);
270    }
271
272    /**
273     * Sets the current screen.
274     *
275     * @param currentScreen
276     */
277    void setCurrentScreen(int currentScreen) {
278        clearVacantCache();
279        mCurrentScreen = Math.max(0, Math.min(currentScreen, getChildCount() - 1));
280        scrollTo(mCurrentScreen * getWidth(), 0);
281        invalidate();
282    }
283
284    /**
285     * Shows the default screen (defined by the firstScreen attribute in XML.)
286     */
287    void showDefaultScreen() {
288        setCurrentScreen(mDefaultScreen);
289    }
290
291    /**
292     * Adds the specified child in the current screen. The position and dimension of
293     * the child are defined by x, y, spanX and spanY.
294     *
295     * @param child The child to add in one of the workspace's screens.
296     * @param x The X position of the child in the screen's grid.
297     * @param y The Y position of the child in the screen's grid.
298     * @param spanX The number of cells spanned horizontally by the child.
299     * @param spanY The number of cells spanned vertically by the child.
300     */
301    void addInCurrentScreen(View child, int x, int y, int spanX, int spanY) {
302        addInScreen(child, mCurrentScreen, x, y, spanX, spanY, false);
303    }
304
305    /**
306     * Adds the specified child in the current screen. The position and dimension of
307     * the child are defined by x, y, spanX and spanY.
308     *
309     * @param child The child to add in one of the workspace's screens.
310     * @param x The X position of the child in the screen's grid.
311     * @param y The Y position of the child in the screen's grid.
312     * @param spanX The number of cells spanned horizontally by the child.
313     * @param spanY The number of cells spanned vertically by the child.
314     * @param insert When true, the child is inserted at the beginning of the children list.
315     */
316    void addInCurrentScreen(View child, int x, int y, int spanX, int spanY, boolean insert) {
317        addInScreen(child, mCurrentScreen, x, y, spanX, spanY, insert);
318    }
319
320    /**
321     * Adds the specified child in the specified screen. The position and dimension of
322     * the child are defined by x, y, spanX and spanY.
323     *
324     * @param child The child to add in one of the workspace's screens.
325     * @param screen The screen in which to add the child.
326     * @param x The X position of the child in the screen's grid.
327     * @param y The Y position of the child in the screen's grid.
328     * @param spanX The number of cells spanned horizontally by the child.
329     * @param spanY The number of cells spanned vertically by the child.
330     */
331    void addInScreen(View child, int screen, int x, int y, int spanX, int spanY) {
332        addInScreen(child, screen, x, y, spanX, spanY, false);
333    }
334
335    /**
336     * Adds the specified child in the specified screen. The position and dimension of
337     * the child are defined by x, y, spanX and spanY.
338     *
339     * @param child The child to add in one of the workspace's screens.
340     * @param screen The screen in which to add the child.
341     * @param x The X position of the child in the screen's grid.
342     * @param y The Y position of the child in the screen's grid.
343     * @param spanX The number of cells spanned horizontally by the child.
344     * @param spanY The number of cells spanned vertically by the child.
345     * @param insert When true, the child is inserted at the beginning of the children list.
346     */
347    void addInScreen(View child, int screen, int x, int y, int spanX, int spanY, boolean insert) {
348        if (screen < 0 || screen >= getChildCount()) {
349            throw new IllegalStateException("The screen must be >= 0 and < " + getChildCount());
350        }
351
352        clearVacantCache();
353
354        final CellLayout group = (CellLayout) getChildAt(screen);
355        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
356        if (lp == null) {
357            lp = new CellLayout.LayoutParams(x, y, spanX, spanY);
358        } else {
359            lp.cellX = x;
360            lp.cellY = y;
361            lp.cellHSpan = spanX;
362            lp.cellVSpan = spanY;
363        }
364        group.addView(child, insert ? 0 : -1, lp);
365        if (!(child instanceof Folder)) {
366            child.setOnLongClickListener(mLongClickListener);
367        }
368    }
369
370    void addWidget(View view, Widget widget) {
371        addInScreen(view, widget.screen, widget.cellX, widget.cellY, widget.spanX,
372                widget.spanY, false);
373    }
374
375    void addWidget(View view, Widget widget, boolean insert) {
376        addInScreen(view, widget.screen, widget.cellX, widget.cellY, widget.spanX,
377                widget.spanY, insert);
378    }
379
380    CellLayout.CellInfo findAllVacantCells(boolean[] occupied) {
381        CellLayout group = (CellLayout) getChildAt(mCurrentScreen);
382        if (group != null) {
383            return group.findAllVacantCells(occupied, null);
384        }
385        return null;
386    }
387
388    private void clearVacantCache() {
389        if (mVacantCache != null) {
390            mVacantCache.clearVacantCells();
391            mVacantCache = null;
392        }
393    }
394
395    /**
396     * Returns the coordinate of a vacant cell for the current screen.
397     */
398    boolean getVacantCell(int[] vacant, int spanX, int spanY) {
399        CellLayout group = (CellLayout) getChildAt(mCurrentScreen);
400        if (group != null) {
401            return group.getVacantCell(vacant, spanX, spanY);
402        }
403        return false;
404    }
405
406    /**
407     * Adds the specified child in the current screen. The position and dimension of
408     * the child are defined by x, y, spanX and spanY.
409     *
410     * @param child The child to add in one of the workspace's screens.
411     * @param spanX The number of cells spanned horizontally by the child.
412     * @param spanY The number of cells spanned vertically by the child.
413     */
414    void fitInCurrentScreen(View child, int spanX, int spanY) {
415        fitInScreen(child, mCurrentScreen, spanX, spanY);
416    }
417
418    /**
419     * Adds the specified child in the specified screen. The position and dimension of
420     * the child are defined by x, y, spanX and spanY.
421     *
422     * @param child The child to add in one of the workspace's screens.
423     * @param screen The screen in which to add the child.
424     * @param spanX The number of cells spanned horizontally by the child.
425     * @param spanY The number of cells spanned vertically by the child.
426     */
427    void fitInScreen(View child, int screen, int spanX, int spanY) {
428        if (screen < 0 || screen >= getChildCount()) {
429            throw new IllegalStateException("The screen must be >= 0 and < " + getChildCount());
430        }
431
432        final CellLayout group = (CellLayout) getChildAt(screen);
433        boolean vacant = group.getVacantCell(mTempCell, spanX, spanY);
434        if (vacant) {
435            group.addView(child,
436                    new CellLayout.LayoutParams(mTempCell[0], mTempCell[1], spanX, spanY));
437            child.setOnLongClickListener(mLongClickListener);
438            if (!(child instanceof Folder)) {
439                child.setOnLongClickListener(mLongClickListener);
440            }
441        }
442    }
443
444    /**
445     * Registers the specified listener on each screen contained in this workspace.
446     *
447     * @param l The listener used to respond to long clicks.
448     */
449    @Override
450    public void setOnLongClickListener(OnLongClickListener l) {
451        mLongClickListener = l;
452        final int count = getChildCount();
453        for (int i = 0; i < count; i++) {
454            getChildAt(i).setOnLongClickListener(l);
455        }
456    }
457
458    @Override
459    public void computeScroll() {
460        if (mScroller.computeScrollOffset()) {
461            mScrollX = mScroller.getCurrX();
462            mScrollY = mScroller.getCurrY();
463            postInvalidate();
464        } else if (mNextScreen != INVALID_SCREEN) {
465            mCurrentScreen = Math.max(0, Math.min(mNextScreen, getChildCount() - 1));
466            Launcher.setScreen(mCurrentScreen);
467            mNextScreen = INVALID_SCREEN;
468            clearChildrenCache();
469        }
470    }
471
472    @Override
473    public boolean isOpaque() {
474        return !mWallpaper.hasAlpha();
475    }
476
477    @Override
478    protected void dispatchDraw(Canvas canvas) {
479        boolean restore = false;
480
481        // If the all apps drawer is open and the drawing region for the workspace
482        // is contained within the drawer's bounds, we skip the drawing. This requires
483        // the drawer to be fully opaque.
484        if (mLauncher.isDrawerUp()) {
485            final Rect clipBounds = mClipBounds;
486            canvas.getClipBounds(clipBounds);
487            clipBounds.offset(-mScrollX, -mScrollY);
488            if (mDrawerBounds.contains(clipBounds)) {
489                return;
490            }
491        } else if (mLauncher.isDrawerMoving()) {
492            restore = true;
493            canvas.save(Canvas.CLIP_SAVE_FLAG);
494
495            final View view = mLauncher.getDrawerHandle();
496            final int top = view.getTop() + view.getHeight();
497
498            canvas.clipRect(mScrollX, top, mScrollX + mDrawerContentWidth,
499                    top + mDrawerContentHeight, Region.Op.DIFFERENCE);
500        }
501
502        float x = mScrollX * mWallpaperOffset;
503        if (x + mWallpaperWidth < mRight - mLeft) {
504            x = mRight - mLeft - mWallpaperWidth;
505        }
506
507        canvas.drawBitmap(mWallpaper, x, (mBottom - mTop - mWallpaperHeight) / 2, mPaint);
508
509        // ViewGroup.dispatchDraw() supports many features we don't need:
510        // clip to padding, layout animation, animation listener, disappearing
511        // children, etc. The following implementation attempts to fast-track
512        // the drawing dispatch by drawing only what we know needs to be drawn.
513
514        boolean fastDraw = mTouchState != TOUCH_STATE_SCROLLING && mNextScreen == INVALID_SCREEN;
515        // If we are not scrolling or flinging, draw only the current screen
516        if (fastDraw) {
517            drawChild(canvas, getChildAt(mCurrentScreen), getDrawingTime());
518        } else {
519            final long drawingTime = getDrawingTime();
520            // If we are flinging, draw only the current screen and the target screen
521            if (mNextScreen >= 0 && mNextScreen < getChildCount() &&
522                    Math.abs(mCurrentScreen - mNextScreen) == 1) {
523                drawChild(canvas, getChildAt(mCurrentScreen), drawingTime);
524                drawChild(canvas, getChildAt(mNextScreen), drawingTime);
525            } else {
526                // If we are scrolling, draw all of our children
527                final int count = getChildCount();
528                for (int i = 0; i < count; i++) {
529                    drawChild(canvas, getChildAt(i), drawingTime);
530                }
531            }
532        }
533
534        if (restore) {
535            canvas.restore();
536        }
537    }
538
539    @Override
540    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
541        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
542
543        final int width = MeasureSpec.getSize(widthMeasureSpec);
544        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
545        if (widthMode != MeasureSpec.EXACTLY) {
546            throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
547        }
548
549        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
550        if (heightMode != MeasureSpec.EXACTLY) {
551            throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
552        }
553
554        // The children are given the same width and height as the workspace
555        final int count = getChildCount();
556        for (int i = 0; i < count; i++) {
557            getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
558        }
559
560        if (mWallpaperLoaded) {
561            mWallpaperLoaded = false;
562            mWallpaper = Utilities.centerToFit(mWallpaper, width,
563                    MeasureSpec.getSize(heightMeasureSpec), getContext());
564            mWallpaperWidth = mWallpaper.getWidth();
565            mWallpaperHeight = mWallpaper.getHeight();
566        }
567
568        final int wallpaperWidth = mWallpaperWidth;
569        mWallpaperOffset = wallpaperWidth > width ? (count * width - wallpaperWidth) /
570                ((count - 1) * (float) width) : 1.0f;
571
572        if (mFirstLayout) {
573            scrollTo(mCurrentScreen * width, 0);
574            mFirstLayout = false;
575        }
576    }
577
578    @Override
579    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
580        int childLeft = 0;
581
582        final int count = getChildCount();
583        for (int i = 0; i < count; i++) {
584            final View child = getChildAt(i);
585            if (child.getVisibility() != View.GONE) {
586                final int childWidth = child.getMeasuredWidth();
587                child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
588                childLeft += childWidth;
589            }
590        }
591    }
592
593    @Override
594    public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
595        int screen = indexOfChild(child);
596        if (screen != mCurrentScreen || !mScroller.isFinished()) {
597            if (!mLauncher.isWorkspaceLocked()) {
598                snapToScreen(screen);
599            }
600            return true;
601        }
602        return false;
603    }
604
605    @Override
606    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
607        if (mLauncher.isDrawerDown()) {
608            final Folder openFolder = getOpenFolder();
609            if (openFolder != null) {
610                return openFolder.requestFocus(direction, previouslyFocusedRect);
611            } else {
612                int focusableScreen;
613                if (mNextScreen != INVALID_SCREEN) {
614                    focusableScreen = mNextScreen;
615                } else {
616                    focusableScreen = mCurrentScreen;
617                }
618                getChildAt(focusableScreen).requestFocus(direction, previouslyFocusedRect);
619            }
620        }
621        return false;
622    }
623
624    @Override
625    public boolean dispatchUnhandledMove(View focused, int direction) {
626        if (direction == View.FOCUS_LEFT) {
627            if (getCurrentScreen() > 0) {
628                snapToScreen(getCurrentScreen() - 1);
629                return true;
630            }
631        } else if (direction == View.FOCUS_RIGHT) {
632            if (getCurrentScreen() < getChildCount() - 1) {
633                snapToScreen(getCurrentScreen() + 1);
634                return true;
635            }
636        }
637        return super.dispatchUnhandledMove(focused, direction);
638    }
639
640    @Override
641    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
642        if (mLauncher.isDrawerDown()) {
643            final Folder openFolder = getOpenFolder();
644            if (openFolder == null) {
645                getChildAt(mCurrentScreen).addFocusables(views, direction);
646                if (direction == View.FOCUS_LEFT) {
647                    if (mCurrentScreen > 0) {
648                        getChildAt(mCurrentScreen - 1).addFocusables(views, direction);
649                    }
650                } else if (direction == View.FOCUS_RIGHT){
651                    if (mCurrentScreen < getChildCount() - 1) {
652                        getChildAt(mCurrentScreen + 1).addFocusables(views, direction);
653                    }
654                }
655            } else {
656                openFolder.addFocusables(views, direction);
657            }
658        }
659    }
660
661    @Override
662    public boolean onInterceptTouchEvent(MotionEvent ev) {
663        if (mLocked || !mLauncher.isDrawerDown()) {
664            return true;
665        }
666
667        /*
668         * This method JUST determines whether we want to intercept the motion.
669         * If we return true, onTouchEvent will be called and we do the actual
670         * scrolling there.
671         */
672
673        /*
674         * Shortcut the most recurring case: the user is in the dragging
675         * state and he is moving his finger.  We want to intercept this
676         * motion.
677         */
678        final int action = ev.getAction();
679        if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {
680            return true;
681        }
682
683        final float x = ev.getX();
684        final float y = ev.getY();
685
686        switch (action) {
687            case MotionEvent.ACTION_MOVE:
688                /*
689                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
690                 * whether the user has moved far enough from his original down touch.
691                 */
692
693                /*
694                 * Locally do absolute value. mLastMotionX is set to the y value
695                 * of the down event.
696                 */
697                final int xDiff = (int) Math.abs(x - mLastMotionX);
698                final int yDiff = (int) Math.abs(y - mLastMotionY);
699
700                final int touchSlop = mTouchSlop;
701                boolean xMoved = xDiff > touchSlop;
702                boolean yMoved = yDiff > touchSlop;
703
704                if (xMoved || yMoved) {
705
706                    if (xMoved) {
707                        // Scroll if the user moved far enough along the X axis
708                        mTouchState = TOUCH_STATE_SCROLLING;
709                        enableChildrenCache();
710                    }
711                    // Either way, cancel any pending longpress
712                    if (mAllowLongPress) {
713                        mAllowLongPress = false;
714                        // Try canceling the long press. It could also have been scheduled
715                        // by a distant descendant, so use the mAllowLongPress flag to block
716                        // everything
717                        final View currentScreen = getChildAt(mCurrentScreen);
718                        currentScreen.cancelLongPress();
719                    }
720                }
721                break;
722
723            case MotionEvent.ACTION_DOWN:
724                // Remember location of down touch
725                mLastMotionX = x;
726                mLastMotionY = y;
727                mAllowLongPress = true;
728
729                /*
730                 * If being flinged and user touches the screen, initiate drag;
731                 * otherwise don't.  mScroller.isFinished should be false when
732                 * being flinged.
733                 */
734                mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
735                break;
736
737            case MotionEvent.ACTION_CANCEL:
738            case MotionEvent.ACTION_UP:
739                // Release the drag
740                clearChildrenCache();
741                mTouchState = TOUCH_STATE_REST;
742                mAllowLongPress = false;
743                break;
744        }
745
746        /*
747         * The only time we want to intercept motion events is if we are in the
748         * drag mode.
749         */
750        return mTouchState != TOUCH_STATE_REST;
751    }
752
753    void enableChildrenCache() {
754        final int count = getChildCount();
755        for (int i = 0; i < count; i++) {
756            final CellLayout layout = (CellLayout) getChildAt(i);
757            layout.setChildrenDrawnWithCacheEnabled(true);
758            layout.setChildrenDrawingCacheEnabled(true);
759        }
760    }
761
762    void clearChildrenCache() {
763        final int count = getChildCount();
764        for (int i = 0; i < count; i++) {
765            final CellLayout layout = (CellLayout) getChildAt(i);
766            layout.setChildrenDrawnWithCacheEnabled(false);
767        }
768    }
769
770    @Override
771    public boolean onTouchEvent(MotionEvent ev) {
772        if (mLocked || !mLauncher.isDrawerDown()) {
773            return true;
774        }
775
776        if (mVelocityTracker == null) {
777            mVelocityTracker = VelocityTracker.obtain();
778        }
779        mVelocityTracker.addMovement(ev);
780
781        final int action = ev.getAction();
782        final float x = ev.getX();
783
784        switch (action) {
785        case MotionEvent.ACTION_DOWN:
786            /*
787             * If being flinged and user touches, stop the fling. isFinished
788             * will be false if being flinged.
789             */
790            if (!mScroller.isFinished()) {
791                mScroller.abortAnimation();
792            }
793
794            // Remember where the motion event started
795            mLastMotionX = x;
796            break;
797        case MotionEvent.ACTION_MOVE:
798            if (mTouchState == TOUCH_STATE_SCROLLING) {
799                // Scroll to follow the motion event
800                final int deltaX = (int) (mLastMotionX - x);
801                mLastMotionX = x;
802
803                if (deltaX < 0) {
804                    if (mScrollX > 0) {
805                        scrollBy(Math.max(-mScrollX, deltaX), 0);
806                    }
807                } else if (deltaX > 0) {
808                    final int availableToScroll = getChildAt(getChildCount() - 1).getRight() -
809                            mScrollX - getWidth();
810                    if (availableToScroll > 0) {
811                        scrollBy(Math.min(availableToScroll, deltaX), 0);
812                    }
813                }
814            }
815            break;
816        case MotionEvent.ACTION_UP:
817            if (mTouchState == TOUCH_STATE_SCROLLING) {
818                final VelocityTracker velocityTracker = mVelocityTracker;
819                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
820                int velocityX = (int) velocityTracker.getXVelocity();
821
822                if (velocityX > SNAP_VELOCITY && mCurrentScreen > 0) {
823                    // Fling hard enough to move left
824                    snapToScreen(mCurrentScreen - 1);
825                } else if (velocityX < -SNAP_VELOCITY && mCurrentScreen < getChildCount() - 1) {
826                    // Fling hard enough to move right
827                    snapToScreen(mCurrentScreen + 1);
828                } else {
829                    snapToDestination();
830                }
831
832                if (mVelocityTracker != null) {
833                    mVelocityTracker.recycle();
834                    mVelocityTracker = null;
835                }
836            }
837            mTouchState = TOUCH_STATE_REST;
838            break;
839        case MotionEvent.ACTION_CANCEL:
840            mTouchState = TOUCH_STATE_REST;
841        }
842
843        return true;
844    }
845
846    private void snapToDestination() {
847        final int screenWidth = getWidth();
848        final int whichScreen = (mScrollX + (screenWidth / 2)) / screenWidth;
849
850        snapToScreen(whichScreen);
851    }
852
853    void snapToScreen(int whichScreen) {
854        if (!mScroller.isFinished()) return;
855
856        clearVacantCache();
857        enableChildrenCache();
858
859        whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
860        boolean changingScreens = whichScreen != mCurrentScreen;
861
862        mNextScreen = whichScreen;
863
864        View focusedChild = getFocusedChild();
865        if (focusedChild != null && changingScreens && focusedChild == getChildAt(mCurrentScreen)) {
866            focusedChild.clearFocus();
867        }
868
869        final int newX = whichScreen * getWidth();
870        final int delta = newX - mScrollX;
871        mScroller.startScroll(mScrollX, 0, delta, 0, Math.abs(delta) * 2);
872        invalidate();
873    }
874
875    void startDrag(CellLayout.CellInfo cellInfo) {
876        View child = cellInfo.cell;
877
878        // Make sure the drag was started by a long press as opposed to a long click.
879        // Note that Search takes focus when clicked rather than entering touch mode
880        if (!child.isInTouchMode() && !(child instanceof Search)) {
881            return;
882        }
883
884        mDragInfo = cellInfo;
885        mDragInfo.screen = mCurrentScreen;
886
887        CellLayout current = ((CellLayout) getChildAt(mCurrentScreen));
888
889        current.onDragChild(child);
890        mDragger.startDrag(child, this, child.getTag(), DragController.DRAG_ACTION_MOVE);
891        invalidate();
892    }
893
894    @Override
895    protected Parcelable onSaveInstanceState() {
896        final SavedState state = new SavedState(super.onSaveInstanceState());
897        state.currentScreen = mCurrentScreen;
898        return state;
899    }
900
901    @Override
902    protected void onRestoreInstanceState(Parcelable state) {
903        SavedState savedState = (SavedState) state;
904        super.onRestoreInstanceState(savedState.getSuperState());
905        if (savedState.currentScreen != -1) {
906            mCurrentScreen = savedState.currentScreen;
907            Launcher.setScreen(mCurrentScreen);
908        }
909    }
910
911    void addApplicationShortcut(ApplicationInfo info, CellLayout.CellInfo cellInfo) {
912        addApplicationShortcut(info, cellInfo, false);
913    }
914
915    void addApplicationShortcut(ApplicationInfo info, CellLayout.CellInfo cellInfo,
916            boolean insertAtFirst) {
917        final CellLayout layout = (CellLayout) getChildAt(cellInfo.screen);
918        final int[] result = new int[2];
919
920        layout.cellToPoint(cellInfo.cellX, cellInfo.cellY, result);
921        onDropExternal(result[0], result[1], info, layout, insertAtFirst);
922    }
923
924    public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, Object dragInfo) {
925        final CellLayout cellLayout = getCurrentDropLayout();
926        if (source != this) {
927            onDropExternal(x - xOffset, y - yOffset, dragInfo, cellLayout);
928        } else {
929            // Move internally
930            if (mDragInfo != null) {
931                final View cell = mDragInfo.cell;
932                if (mCurrentScreen != mDragInfo.screen) {
933                    final CellLayout originalCellLayout = (CellLayout) getChildAt(mDragInfo.screen);
934                    originalCellLayout.removeView(cell);
935                    cellLayout.addView(cell);
936                }
937                mTargetCell = estimateDropCell(x - xOffset, y - yOffset,
938                        mDragInfo.spanX, mDragInfo.spanY, cell, cellLayout, mTargetCell);
939                cellLayout.onDropChild(cell, mTargetCell);
940
941                final ItemInfo info = (ItemInfo)cell.getTag();
942                CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
943                LauncherModel.moveItemInDatabase(mLauncher, info,
944                        LauncherSettings.Favorites.CONTAINER_DESKTOP, mCurrentScreen, lp.cellX, lp.cellY);
945            }
946        }
947    }
948
949    public void onDragEnter(DragSource source, int x, int y, int xOffset, int yOffset,
950            Object dragInfo) {
951        clearVacantCache();
952    }
953
954    public void onDragOver(DragSource source, int x, int y, int xOffset, int yOffset,
955            Object dragInfo) {
956    }
957
958    public void onDragExit(DragSource source, int x, int y, int xOffset, int yOffset,
959            Object dragInfo) {
960        clearVacantCache();
961    }
962
963    private void onDropExternal(int x, int y, Object dragInfo, CellLayout cellLayout) {
964        onDropExternal(x, y, dragInfo, cellLayout, false);
965    }
966
967    private void onDropExternal(int x, int y, Object dragInfo, CellLayout cellLayout,
968            boolean insertAtFirst) {
969        // Drag from somewhere else
970        ItemInfo info = (ItemInfo) dragInfo;
971
972        View view;
973
974        switch (info.itemType) {
975        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
976        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
977            if (info.container == NO_ID) {
978                // Came from all apps -- make a copy
979                info = new ApplicationInfo((ApplicationInfo) info);
980            }
981            view = mLauncher.createShortcut(R.layout.application, cellLayout,
982                    (ApplicationInfo) info);
983            break;
984        case LauncherSettings.Favorites.ITEM_TYPE_USER_FOLDER:
985            view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher,
986                    (ViewGroup) getChildAt(mCurrentScreen), ((UserFolderInfo) info));
987            break;
988        default:
989            throw new IllegalStateException("Unknown item type: " + info.itemType);
990        }
991
992        cellLayout.addView(view, insertAtFirst ? 0 : -1);
993        view.setOnLongClickListener(mLongClickListener);
994        mTargetCell = estimateDropCell(x, y, 1, 1, view, cellLayout, mTargetCell);
995        cellLayout.onDropChild(view, mTargetCell);
996        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
997
998        final LauncherModel model = Launcher.getModel();
999        model.addDesktopItem(info);
1000        LauncherModel.addOrMoveItemInDatabase(mLauncher, info,
1001                LauncherSettings.Favorites.CONTAINER_DESKTOP, mCurrentScreen, lp.cellX, lp.cellY);
1002    }
1003
1004    /**
1005     * Return the current {@link CellLayout}, correctly picking the destination
1006     * screen while a scroll is in progress.
1007     */
1008    private CellLayout getCurrentDropLayout() {
1009        int index = mScroller.isFinished() ? mCurrentScreen : mNextScreen;
1010        return (CellLayout) getChildAt(index);
1011    }
1012
1013    /**
1014     * {@inheritDoc}
1015     */
1016    public boolean acceptDrop(DragSource source, int x, int y,
1017            int xOffset, int yOffset, Object dragInfo) {
1018        final CellLayout layout = getCurrentDropLayout();
1019        final CellLayout.CellInfo cellInfo = mDragInfo;
1020        final int spanX = cellInfo == null ? 1 : cellInfo.spanX;
1021        final int spanY = cellInfo == null ? 1 : cellInfo.spanY;
1022
1023        if (mVacantCache == null) {
1024            final View ignoreView = cellInfo == null ? null : cellInfo.cell;
1025            mVacantCache = layout.findAllVacantCells(null, ignoreView);
1026        }
1027
1028        return mVacantCache.findCellForSpan(mTempEstimate, spanX, spanY, false);
1029    }
1030
1031    /**
1032     * {@inheritDoc}
1033     */
1034    public Rect estimateDropLocation(DragSource source, int x, int y,
1035            int xOffset, int yOffset, Object dragInfo, Rect recycle) {
1036        final CellLayout layout = getCurrentDropLayout();
1037
1038        final CellLayout.CellInfo cellInfo = mDragInfo;
1039        final int spanX = cellInfo == null ? 1 : cellInfo.spanX;
1040        final int spanY = cellInfo == null ? 1 : cellInfo.spanY;
1041        final View ignoreView = cellInfo == null ? null : cellInfo.cell;
1042
1043        final Rect location = recycle != null ? recycle : new Rect();
1044
1045        // Find drop cell and convert into rectangle
1046        int[] dropCell = estimateDropCell(x - xOffset, y - yOffset,
1047                spanX, spanY, ignoreView, layout, mTempCell);
1048
1049        if (dropCell == null) {
1050            return null;
1051        }
1052
1053        layout.cellToPoint(dropCell[0], dropCell[1], mTempEstimate);
1054        location.left = mTempEstimate[0];
1055        location.top = mTempEstimate[1];
1056
1057        layout.cellToPoint(dropCell[0] + spanX, dropCell[1] + spanY, mTempEstimate);
1058        location.right = mTempEstimate[0];
1059        location.bottom = mTempEstimate[1];
1060
1061        return location;
1062    }
1063
1064    /**
1065     * Calculate the nearest cell where the given object would be dropped.
1066     */
1067    private int[] estimateDropCell(int pixelX, int pixelY,
1068            int spanX, int spanY, View ignoreView, CellLayout layout, int[] recycle) {
1069        // Create vacant cell cache if none exists
1070        if (mVacantCache == null) {
1071            mVacantCache = layout.findAllVacantCells(null, ignoreView);
1072        }
1073
1074        // Find the best target drop location
1075        return layout.findNearestVacantArea(pixelX, pixelY,
1076                spanX, spanY, mVacantCache, recycle);
1077    }
1078
1079    void setLauncher(Launcher launcher) {
1080        mLauncher = launcher;
1081    }
1082
1083    public void setDragger(DragController dragger) {
1084        mDragger = dragger;
1085    }
1086
1087    public void onDropCompleted(View target, boolean success) {
1088        if (success){
1089            if (target != this && mDragInfo != null) {
1090                final CellLayout cellLayout = (CellLayout) getChildAt(mDragInfo.screen);
1091                cellLayout.removeView(mDragInfo.cell);
1092                final Object tag = mDragInfo.cell.getTag();
1093                Launcher.getModel().removeDesktopItem((ItemInfo) tag);
1094            }
1095        } else {
1096            if (mDragInfo != null) {
1097                final CellLayout cellLayout = (CellLayout) getChildAt(mDragInfo.screen);
1098                cellLayout.onDropAborted(mDragInfo.cell);
1099            }
1100        }
1101
1102        mDragInfo = null;
1103    }
1104
1105    public void scrollLeft() {
1106        clearVacantCache();
1107        if (mNextScreen == INVALID_SCREEN && mCurrentScreen > 0 && mScroller.isFinished()) {
1108            snapToScreen(mCurrentScreen - 1);
1109        }
1110    }
1111
1112    public void scrollRight() {
1113        clearVacantCache();
1114        if (mNextScreen == INVALID_SCREEN && mCurrentScreen < getChildCount() -1 &&
1115                mScroller.isFinished()) {
1116            snapToScreen(mCurrentScreen + 1);
1117        }
1118    }
1119
1120    public int getScreenForView(View v) {
1121        int result = -1;
1122        if (v != null) {
1123            ViewParent vp = v.getParent();
1124            int count = getChildCount();
1125            for (int i = 0; i < count; i++) {
1126                if (vp == getChildAt(i)) {
1127                    return i;
1128                }
1129            }
1130        }
1131        return result;
1132    }
1133
1134    /**
1135     * Find a search widget on the given screen
1136     */
1137    private Search findSearchWidget(CellLayout screen) {
1138        final int count = screen.getChildCount();
1139        for (int i = 0; i < count; i++) {
1140            View v = screen.getChildAt(i);
1141            if (v instanceof Search) {
1142                return (Search) v;
1143            }
1144        }
1145        return null;
1146    }
1147
1148    /**
1149     * Gets the first search widget on the current screen, if there is one.
1150     * Returns <code>null</code> otherwise.
1151     */
1152    public Search findSearchWidgetOnCurrentScreen() {
1153        CellLayout currentScreen = (CellLayout)getChildAt(mCurrentScreen);
1154        return findSearchWidget(currentScreen);
1155    }
1156
1157    public Folder getFolderForTag(Object tag) {
1158        int screenCount = getChildCount();
1159        for (int screen = 0; screen < screenCount; screen++) {
1160            CellLayout currentScreen = ((CellLayout) getChildAt(screen));
1161            int count = currentScreen.getChildCount();
1162            for (int i = 0; i < count; i++) {
1163                View child = currentScreen.getChildAt(i);
1164                CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
1165                if (lp.cellHSpan == 4 && lp.cellVSpan == 4 && child instanceof Folder) {
1166                    Folder f = (Folder) child;
1167                    if (f.getInfo() == tag) {
1168                        return f;
1169                    }
1170                }
1171            }
1172        }
1173        return null;
1174    }
1175
1176    public View getViewForTag(Object tag) {
1177        int screenCount = getChildCount();
1178        for (int screen = 0; screen < screenCount; screen++) {
1179            CellLayout currentScreen = ((CellLayout) getChildAt(screen));
1180            int count = currentScreen.getChildCount();
1181            for (int i = 0; i < count; i++) {
1182                View child = currentScreen.getChildAt(i);
1183                if (child.getTag() == tag) {
1184                    return child;
1185                }
1186            }
1187        }
1188        return null;
1189    }
1190
1191    /**
1192     * Unlocks the SlidingDrawer so that touch events are processed.
1193     *
1194     * @see #lock()
1195     */
1196    public void unlock() {
1197        mLocked = false;
1198    }
1199
1200    /**
1201     * Locks the SlidingDrawer so that touch events are ignores.
1202     *
1203     * @see #unlock()
1204     */
1205    public void lock() {
1206        mLocked = true;
1207    }
1208
1209    /**
1210     * @return True is long presses are still allowed for the current touch
1211     */
1212    public boolean allowLongPress() {
1213        return mAllowLongPress;
1214    }
1215
1216    /**
1217     * Set true to allow long-press events to be triggered, usually checked by
1218     * {@link Launcher} to accept or block dpad-initiated long-presses.
1219     */
1220    public void setAllowLongPress(boolean allowLongPress) {
1221        mAllowLongPress = allowLongPress;
1222    }
1223
1224    void removeShortcutsForPackage(String packageName) {
1225        final ArrayList<View> childrenToRemove = new ArrayList<View>();
1226        final LauncherModel model = Launcher.getModel();
1227        final int count = getChildCount();
1228
1229        for (int i = 0; i < count; i++) {
1230            final CellLayout layout = (CellLayout) getChildAt(i);
1231            int childCount = layout.getChildCount();
1232
1233            childrenToRemove.clear();
1234
1235            for (int j = 0; j < childCount; j++) {
1236                final View view = layout.getChildAt(j);
1237                Object tag = view.getTag();
1238
1239                if (tag instanceof ApplicationInfo) {
1240                    final ApplicationInfo info = (ApplicationInfo) tag;
1241                    // We need to check for ACTION_MAIN otherwise getComponent() might
1242                    // return null for some shortcuts (for instance, for shortcuts to
1243                    // web pages.)
1244                    final Intent intent = info.intent;
1245                    final ComponentName name = intent.getComponent();
1246
1247                    if (Intent.ACTION_MAIN.equals(intent.getAction()) &&
1248                            name != null && packageName.equals(name.getPackageName())) {
1249                        model.removeDesktopItem(info);
1250                        LauncherModel.deleteItemFromDatabase(mLauncher, info);
1251                        childrenToRemove.add(view);
1252                    }
1253                } else if (tag instanceof UserFolderInfo) {
1254                    final UserFolderInfo info = (UserFolderInfo) tag;
1255                    final ArrayList<ApplicationInfo> contents = info.contents;
1256                    final ArrayList<ApplicationInfo> toRemove = new ArrayList<ApplicationInfo>(1);
1257                    final int contentsCount = contents.size();
1258                    boolean removedFromFolder = false;
1259
1260                    for (int k = 0; k < contentsCount; k++) {
1261                        final ApplicationInfo appInfo = contents.get(k);
1262                        final Intent intent = appInfo.intent;
1263                        final ComponentName name = intent.getComponent();
1264
1265                        if (Intent.ACTION_MAIN.equals(intent.getAction()) &&
1266                                name != null && packageName.equals(name.getPackageName())) {
1267                            toRemove.add(appInfo);
1268                            LauncherModel.deleteItemFromDatabase(mLauncher, appInfo);
1269                            removedFromFolder = true;
1270                        }
1271                    }
1272
1273                    contents.removeAll(toRemove);
1274                    if (removedFromFolder) {
1275                        final Folder folder = getOpenFolder();
1276                        if (folder != null) folder.notifyDataSetChanged();
1277                    }
1278                }
1279            }
1280
1281            childCount = childrenToRemove.size();
1282            for (int j = 0; j < childCount; j++) {
1283                layout.removeViewInLayout(childrenToRemove.get(j));
1284            }
1285
1286            if (childCount > 0) {
1287                layout.requestLayout();
1288                layout.invalidate();
1289            }
1290        }
1291    }
1292
1293    void updateShortcutsForPackage(String packageName) {
1294        final int count = getChildCount();
1295        for (int i = 0; i < count; i++) {
1296            final CellLayout layout = (CellLayout) getChildAt(i);
1297            int childCount = layout.getChildCount();
1298            for (int j = 0; j < childCount; j++) {
1299                final View view = layout.getChildAt(j);
1300                Object tag = view.getTag();
1301                if (tag instanceof ApplicationInfo) {
1302                    ApplicationInfo info = (ApplicationInfo) tag;
1303                    // We need to check for ACTION_MAIN otherwise getComponent() might
1304                    // return null for some shortcuts (for instance, for shortcuts to
1305                    // web pages.)
1306                    final Intent intent = info.intent;
1307                    final ComponentName name = intent.getComponent();
1308                    if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
1309                            Intent.ACTION_MAIN.equals(intent.getAction()) && name != null &&
1310                            packageName.equals(name.getPackageName())) {
1311
1312                        final Drawable icon = Launcher.getModel().getApplicationInfoIcon(
1313                                mLauncher.getPackageManager(), info);
1314                        if (icon != null && icon != info.icon) {
1315                            info.icon.setCallback(null);
1316                            info.icon = Utilities.createIconThumbnail(icon, mContext);
1317                            info.filtered = true;
1318                            ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(null,
1319                                    info.icon, null, null);
1320                        }
1321                    }
1322                }
1323            }
1324        }
1325    }
1326
1327    void moveToDefaultScreen() {
1328        snapToScreen(mDefaultScreen);
1329        getChildAt(mDefaultScreen).requestFocus();
1330    }
1331
1332    public static class SavedState extends BaseSavedState {
1333        int currentScreen = -1;
1334
1335        SavedState(Parcelable superState) {
1336            super(superState);
1337        }
1338
1339        private SavedState(Parcel in) {
1340            super(in);
1341            currentScreen = in.readInt();
1342        }
1343
1344        @Override
1345        public void writeToParcel(Parcel out, int flags) {
1346            super.writeToParcel(out, flags);
1347            out.writeInt(currentScreen);
1348        }
1349
1350        public static final Parcelable.Creator<SavedState> CREATOR =
1351                new Parcelable.Creator<SavedState>() {
1352            public SavedState createFromParcel(Parcel in) {
1353                return new SavedState(in);
1354            }
1355
1356            public SavedState[] newArray(int size) {
1357                return new SavedState[size];
1358            }
1359        };
1360    }
1361}
1362