Folder.java revision badf71e11fba2d6efa1d1bcca9542001f90a3777
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 java.util.ArrayList;
20
21import android.animation.Animator;
22import android.animation.AnimatorListenerAdapter;
23import android.animation.ObjectAnimator;
24import android.animation.PropertyValuesHolder;
25import android.animation.ValueAnimator;
26import android.animation.ValueAnimator.AnimatorUpdateListener;
27import android.content.Context;
28import android.graphics.Rect;
29import android.graphics.drawable.Drawable;
30import android.util.AttributeSet;
31import android.view.LayoutInflater;
32import android.view.MotionEvent;
33import android.view.View;
34import android.view.View.OnClickListener;
35import android.view.animation.AccelerateInterpolator;
36import android.view.animation.DecelerateInterpolator;
37import android.widget.AdapterView;
38import android.widget.LinearLayout;
39import android.widget.TextView;
40import android.widget.AdapterView.OnItemClickListener;
41import android.widget.AdapterView.OnItemLongClickListener;
42
43import com.android.launcher.R;
44import com.android.launcher2.FolderInfo.FolderListener;
45
46/**
47 * Represents a set of icons chosen by the user or generated by the system.
48 */
49public class Folder extends LinearLayout implements DragSource, OnItemLongClickListener,
50        OnItemClickListener, OnClickListener, View.OnLongClickListener, DropTarget, FolderListener {
51
52    protected DragController mDragController;
53
54    protected Launcher mLauncher;
55
56    protected FolderInfo mInfo;
57
58    /**
59     * Which item is being dragged
60     */
61    protected ShortcutInfo mDragItem;
62
63    private static final String TAG = "Launcher.Folder";
64
65    static final int STATE_NONE = -1;
66    static final int STATE_SMALL = 0;
67    static final int STATE_ANIMATING = 1;
68    static final int STATE_OPEN = 2;
69
70    private int mExpandDuration;
71    protected CellLayout mContent;
72    private final LayoutInflater mInflater;
73    private final IconCache mIconCache;
74    private int mState = STATE_NONE;
75    private int[] mDragItemPosition = new int[2];
76    private static final int FULL_GROW = 0;
77    private static final int PARTIAL_GROW = 1;
78    private int mMode = PARTIAL_GROW;
79    private boolean mRearrangeOnClose = false;
80    private FolderIcon mFolderIcon;
81    private int mMaxCountX;
82    private int mMaxCountY;
83    private Rect mNewSize = new Rect();
84    private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
85    private Drawable mIconDrawable;
86    boolean mItemsInvalidated = false;
87
88    /**
89     * Used to inflate the Workspace from XML.
90     *
91     * @param context The application's context.
92     * @param attrs The attribtues set containing the Workspace's customization values.
93     */
94    public Folder(Context context, AttributeSet attrs) {
95        super(context, attrs);
96        setAlwaysDrawnWithCacheEnabled(false);
97        mInflater = LayoutInflater.from(context);
98        mIconCache = ((LauncherApplication)context.getApplicationContext()).getIconCache();
99        mExpandDuration = getResources().getInteger(R.integer.config_folderAnimDuration);
100
101        mMaxCountX = LauncherModel.getCellCountX() - 1;
102        mMaxCountY = LauncherModel.getCellCountY() - 1;
103    }
104
105    @Override
106    protected void onFinishInflate() {
107        super.onFinishInflate();
108        mContent = (CellLayout) findViewById(R.id.folder_content);
109        mContent.setGridSize(0, 0);
110        mContent.enableHardwareLayers();
111    }
112
113    public void onItemClick(AdapterView parent, View v, int position, long id) {
114        ShortcutInfo app = (ShortcutInfo) parent.getItemAtPosition(position);
115        int[] pos = new int[2];
116        v.getLocationOnScreen(pos);
117        app.intent.setSourceBounds(new Rect(pos[0], pos[1],
118                pos[0] + v.getWidth(), pos[1] + v.getHeight()));
119        mLauncher.startActivitySafely(app.intent, app);
120    }
121
122    public void onClick(View v) {
123        Object tag = v.getTag();
124        if (tag instanceof ShortcutInfo) {
125            // refactor this code from Folder
126            ShortcutInfo item = (ShortcutInfo) tag;
127            int[] pos = new int[2];
128            v.getLocationOnScreen(pos);
129            item.intent.setSourceBounds(new Rect(pos[0], pos[1],
130                    pos[0] + v.getWidth(), pos[1] + v.getHeight()));
131            mLauncher.startActivitySafely(item.intent, item);
132        }
133    }
134
135    public boolean onLongClick(View v) {
136        Object tag = v.getTag();
137        if (tag instanceof ShortcutInfo) {
138            mLauncher.closeFolder(this);
139
140            ShortcutInfo item = (ShortcutInfo) tag;
141            if (!v.isInTouchMode()) {
142                return false;
143            }
144
145            mLauncher.getWorkspace().onDragStartedWithItem(v);
146            mDragController.startDrag(v, this, item, DragController.DRAG_ACTION_COPY);
147            mDragItemPosition[0] = item.cellX;
148            mDragItemPosition[1] = item.cellY;
149            mIconDrawable = ((TextView) v).getCompoundDrawables()[1];
150            mInfo.remove(item);
151
152            mDragItem = item;
153        } else {
154            mLauncher.closeFolder(this);
155            mLauncher.showRenameDialog(mInfo);
156        }
157        return true;
158    }
159
160    public Drawable getDragDrawable() {
161        return mIconDrawable;
162    }
163
164    /**
165     * We need to handle touch events to prevent them from falling through to the workspace below.
166     */
167    @Override
168    public boolean onTouchEvent(MotionEvent ev) {
169        return true;
170    }
171
172    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
173        if (!view.isInTouchMode()) {
174            return false;
175        }
176
177        ShortcutInfo app = (ShortcutInfo) parent.getItemAtPosition(position);
178
179        mDragController.startDrag(view, this, app, DragController.DRAG_ACTION_COPY);
180        mLauncher.closeFolder(this);
181        mDragItem = app;
182
183        return true;
184    }
185
186    public void setDragController(DragController dragController) {
187        mDragController = dragController;
188    }
189
190    public void onDragViewVisible() {
191    }
192
193    void setLauncher(Launcher launcher) {
194        mLauncher = launcher;
195    }
196
197    void setFolderIcon(FolderIcon icon) {
198        mFolderIcon = icon;
199    }
200
201    /**
202     * @return the FolderInfo object associated with this folder
203     */
204    FolderInfo getInfo() {
205        return mInfo;
206    }
207
208    void onOpen() {
209        // When the folder opens, we need to refresh the GridView's selection by
210        // forcing a layout
211        // TODO: find out if this is still necessary
212        mContent.requestLayout();
213        requestFocus();
214    }
215
216    void onClose() {
217        final Workspace workspace = mLauncher.getWorkspace();
218        workspace.getChildAt(workspace.getCurrentPage()).requestFocus();
219    }
220
221    void bind(FolderInfo info) {
222        mInfo = info;
223        ArrayList<ShortcutInfo> children = info.contents;
224        setupContentForNumItems(children.size());
225        for (int i = 0; i < children.size(); i++) {
226            ShortcutInfo child = (ShortcutInfo) children.get(i);
227            createAndAddShortcut(child);
228        }
229        mItemsInvalidated = true;
230        mInfo.addListener(this);
231    }
232
233    /**
234     * Creates a new UserFolder, inflated from R.layout.user_folder.
235     *
236     * @param context The application's context.
237     *
238     * @return A new UserFolder.
239     */
240    static Folder fromXml(Context context) {
241        return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null);
242    }
243
244    /**
245     * This method is intended to make the UserFolder to be visually identical in size and position
246     * to its associated FolderIcon. This allows for a seamless transition into the expanded state.
247     */
248    private void positionAndSizeAsIcon() {
249        if (!(getParent() instanceof CellLayoutChildren)) return;
250
251        CellLayout.LayoutParams iconLp = (CellLayout.LayoutParams) mFolderIcon.getLayoutParams();
252        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
253
254        if (mMode == PARTIAL_GROW) {
255            setScaleX(0.8f);
256            setScaleY(0.8f);
257            setAlpha(0f);
258        } else {
259            lp.width = iconLp.width;
260            lp.height = iconLp.height;
261            lp.x = iconLp.x;
262            lp.y = iconLp.y;
263            mContent.setAlpha(0);
264        }
265        mState = STATE_SMALL;
266    }
267
268    public void animateOpen() {
269        if (mState != STATE_SMALL) {
270            positionAndSizeAsIcon();
271        }
272        if (!(getParent() instanceof CellLayoutChildren)) return;
273
274        ObjectAnimator oa;
275        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
276
277        centerAboutIcon();
278        if (mMode == PARTIAL_GROW) {
279            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1);
280            PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f);
281            PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f);
282            oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
283        } else {
284            PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", mNewSize.width());
285            PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", mNewSize.height());
286            PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", mNewSize.left);
287            PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", mNewSize.top);
288            oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y);
289            oa.addUpdateListener(new AnimatorUpdateListener() {
290                public void onAnimationUpdate(ValueAnimator animation) {
291                    requestLayout();
292                }
293            });
294
295            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f);
296            ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha);
297            alphaOa.setDuration(mExpandDuration);
298            alphaOa.setInterpolator(new AccelerateInterpolator(2.0f));
299            alphaOa.start();
300        }
301
302        oa.addListener(new AnimatorListenerAdapter() {
303            @Override
304            public void onAnimationStart(Animator animation) {
305                mState = STATE_ANIMATING;
306            }
307            @Override
308            public void onAnimationEnd(Animator animation) {
309                mState = STATE_OPEN;
310            }
311        });
312        oa.setDuration(mExpandDuration);
313        oa.start();
314    }
315
316    public void animateClosed() {
317        if (!(getParent() instanceof CellLayoutChildren)) return;
318
319        CellLayoutChildren clc = (CellLayoutChildren) getParent();
320        final CellLayout cellLayout = (CellLayout) clc.getParent();
321        ObjectAnimator oa;
322
323        if (mMode == PARTIAL_GROW) {
324            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0);
325            PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f);
326            PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f);
327            oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
328        } else {
329            CellLayout.LayoutParams iconLp = (CellLayout.LayoutParams) mFolderIcon.getLayoutParams();
330            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
331
332            PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", iconLp.width);
333            PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", iconLp.height);
334            PropertyValuesHolder x = PropertyValuesHolder.ofInt("x",iconLp.x);
335            PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", iconLp.y);
336            oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y);
337            oa.addUpdateListener(new AnimatorUpdateListener() {
338                public void onAnimationUpdate(ValueAnimator animation) {
339                    requestLayout();
340                }
341            });
342
343            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f);
344            ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha);
345            alphaOa.setDuration(mExpandDuration);
346            alphaOa.setInterpolator(new DecelerateInterpolator(2.0f));
347            alphaOa.start();
348        }
349
350        oa.addListener(new AnimatorListenerAdapter() {
351            @Override
352            public void onAnimationEnd(Animator animation) {
353                onCloseComplete();
354                cellLayout.removeViewWithoutMarkingCells(Folder.this);
355                mState = STATE_SMALL;
356            }
357            @Override
358            public void onAnimationStart(Animator animation) {
359                mState = STATE_ANIMATING;
360            }
361        });
362        oa.setDuration(mExpandDuration);
363        oa.start();
364    }
365
366    void notifyDataSetChanged() {
367        // recreate all the children if the data set changes under us. We may want to do this more
368        // intelligently (ie just removing the views that should no longer exist)
369        mContent.removeAllViewsInLayout();
370        bind(mInfo);
371    }
372
373    public boolean acceptDrop(DragObject d) {
374        final ItemInfo item = (ItemInfo) d.dragInfo;
375        final int itemType = item.itemType;
376        return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
377                    itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
378                    !isFull());
379    }
380
381    public void onDrop(DragObject d) {
382        ShortcutInfo item;
383        if (d.dragInfo instanceof ApplicationInfo) {
384            // Came from all apps -- make a copy
385            item = ((ApplicationInfo) d.dragInfo).makeShortcut();
386            item.spanX = 1;
387            item.spanY = 1;
388        } else {
389            item = (ShortcutInfo) d.dragInfo;
390        }
391        mInfo.add(item);
392        LauncherModel.addOrMoveItemInDatabase(mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
393    }
394
395    protected boolean findAndSetEmptyCells(ShortcutInfo item) {
396        int[] emptyCell = new int[2];
397        if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) {
398            item.cellX = emptyCell[0];
399            item.cellY = emptyCell[1];
400            LauncherModel.addOrMoveItemInDatabase(
401                    mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
402            return true;
403        } else {
404            return false;
405        }
406    }
407
408    protected void createAndAddShortcut(ShortcutInfo item) {
409        final TextView textView =
410            (TextView) mInflater.inflate(R.layout.application_boxed, this, false);
411        textView.setCompoundDrawablesWithIntrinsicBounds(null,
412                new FastBitmapDrawable(item.getIcon(mIconCache)), null, null);
413        textView.setText(item.title);
414        textView.setTag(item);
415
416        textView.setOnClickListener(this);
417        textView.setOnLongClickListener(this);
418
419        CellLayout.LayoutParams lp =
420            new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY);
421        boolean insert = false;
422        mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true);
423    }
424
425    public void onDragEnter(DragObject d) {
426        mContent.onDragEnter();
427    }
428
429    public void onDragOver(DragObject d) {
430        float[] r = mapPointFromScreenToContent(d.x, d.y, null);
431        mContent.visualizeDropLocation(null, null, (int) r[0], (int) r[1], 1, 1);
432    }
433
434    public void onDragExit(DragObject d) {
435        mContent.onDragExit();
436    }
437
438    public float[] mapPointFromScreenToContent(int x, int y, float[] r) {
439        if (r == null) {
440            r = new float[2];
441        }
442
443        int[] screenLocation = new int[2];
444        mContent.getLocationOnScreen(screenLocation);
445
446        r[0] = x - screenLocation[0];
447        r[1] = y - screenLocation[1];
448        return r;
449    }
450
451    public void onDropCompleted(View target, Object dragInfo, boolean success) {
452    }
453
454    public boolean isDropEnabled() {
455        return true;
456    }
457
458    public DropTarget getDropTargetDelegate(DragObject d) {
459        return null;
460    }
461
462    private void setupContentDimension(int count) {
463        ArrayList<View> list = getItemsInReadingOrder();
464
465        int countX = mContent.getCountX();
466        int countY = mContent.getCountY();
467        boolean done = false;
468
469        while (!done) {
470            int oldCountX = countX;
471            int oldCountY = countY;
472            if (countX * countY < count) {
473                // Current grid is too small, expand it
474                if (countX <= countY && countX < mMaxCountX) {
475                    countX++;
476                } else if (countY < mMaxCountY) {
477                    countY++;
478                }
479                if (countY == 0) countY++;
480            } else if ((countY - 1) * countX >= count && countY >= countX) {
481                countY = Math.max(0, countY - 1);
482            } else if ((countX - 1) * countY >= count) {
483                countX = Math.max(0, countX - 1);
484            }
485            done = countX == oldCountX && countY == oldCountY;
486        }
487        mContent.setGridSize(countX, countY);
488        arrangeChildren(list);
489    }
490
491    public boolean isFull() {
492        return getItemCount() >= mMaxCountX * mMaxCountY;
493    }
494
495    private void centerAboutIcon() {
496        CellLayout.LayoutParams iconLp = (CellLayout.LayoutParams) mFolderIcon.getLayoutParams();
497        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
498
499        int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
500        int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight();
501
502        int centerX = iconLp.x + iconLp.width / 2;
503        int centerY = iconLp.y + iconLp.height / 2;
504        int centeredLeft = centerX - width / 2;
505        int centeredTop = centerY - height / 2;
506
507        CellLayoutChildren clc = (CellLayoutChildren) getParent();
508        int parentWidth = 0;
509        int parentHeight = 0;
510        if (clc != null) {
511            parentWidth = clc.getMeasuredWidth();
512            parentHeight = clc.getMeasuredHeight();
513        }
514
515        int left = Math.min(Math.max(0, centeredLeft), parentWidth - width);
516        int top = Math.min(Math.max(0, centeredTop), parentHeight - height);
517
518        int folderPivotX = width / 2 + (centeredLeft - left);
519        int folderPivotY = height / 2 + (centeredTop - top);
520        setPivotX(folderPivotX);
521        setPivotY(folderPivotY);
522        int folderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() *
523                (1.0f * folderPivotX / width));
524        int folderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() *
525                (1.0f * folderPivotY / height));
526        mFolderIcon.setPivotX(folderIconPivotX);
527        mFolderIcon.setPivotY(folderIconPivotY);
528
529        if (mMode == PARTIAL_GROW) {
530            lp.width = width;
531            lp.height = height;
532            lp.x = left;
533            lp.y = top;
534        } else {
535            mNewSize.set(left, top, left + width, top + height);
536        }
537    }
538
539    private void setupContentForNumItems(int count) {
540        setupContentDimension(count);
541
542        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
543        if (lp == null) {
544            lp = new CellLayout.LayoutParams(0, 0, -1, -1);
545            lp.isLockedToGrid = false;
546            setLayoutParams(lp);
547        }
548        centerAboutIcon();
549    }
550
551    private void arrangeChildren(ArrayList<View> list) {
552        int[] vacant = new int[2];
553        if (list == null) {
554            list = getItemsInReadingOrder();
555        }
556        mContent.removeAllViews();
557
558        for (int i = 0; i < list.size(); i++) {
559            View v = list.get(i);
560            mContent.getVacantCell(vacant, 1, 1);
561            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
562            lp.cellX = vacant[0];
563            lp.cellY = vacant[1];
564            ItemInfo info = (ItemInfo) v.getTag();
565            info.cellX = vacant[0];
566            info.cellY = vacant[1];
567            boolean insert = false;
568            mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true);
569            LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0,
570                    info.cellX, info.cellY);
571        }
572        mItemsInvalidated = true;
573    }
574
575    public void onAdd(ShortcutInfo item) {
576        mItemsInvalidated = true;
577        if (!findAndSetEmptyCells(item)) {
578            // The current layout is full, can we expand it?
579            setupContentForNumItems(getItemCount() + 1);
580            findAndSetEmptyCells(item);
581        }
582        createAndAddShortcut(item);
583    }
584
585    public int getItemCount() {
586        return mContent.getChildrenLayout().getChildCount();
587    }
588
589    public View getItemAt(int index) {
590        return mContent.getChildrenLayout().getChildAt(index);
591    }
592
593    private void onCloseComplete() {
594        if (mRearrangeOnClose) {
595            setupContentForNumItems(getItemCount());
596            mRearrangeOnClose = false;
597        }
598    }
599
600    public void onRemove(ShortcutInfo item) {
601        mItemsInvalidated = true;
602        View v = mContent.getChildAt(mDragItemPosition[0], mDragItemPosition[1]);
603        mContent.removeView(v);
604        if (mState == STATE_ANIMATING) {
605            mRearrangeOnClose = true;
606        } else {
607            setupContentForNumItems(getItemCount());
608        }
609    }
610
611    public ArrayList<View> getItemsInReadingOrder() {
612        if (mItemsInvalidated) {
613            mItemsInReadingOrder.clear();
614            for (int j = 0; j < mContent.getCountY(); j++) {
615                for (int i = 0; i < mContent.getCountX(); i++) {
616                    View v = mContent.getChildAt(i, j);
617                    if (v != null) {
618                        mItemsInReadingOrder.add(v);
619                    }
620                }
621            }
622            mItemsInvalidated = false;
623        }
624        return mItemsInReadingOrder;
625    }
626}
627