1/*
2 * Copyright (C) 2010 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.launcher3;
18
19import android.content.Context;
20import android.util.AttributeSet;
21import android.view.MotionEvent;
22import android.view.View;
23import android.view.ViewDebug;
24import android.view.ViewGroup;
25
26/**
27 * An abstraction of the original CellLayout which supports laying out items
28 * which span multiple cells into a grid-like layout.  Also supports dimming
29 * to give a preview of its contents.
30 */
31public class PagedViewCellLayout extends ViewGroup implements Page {
32    static final String TAG = "PagedViewCellLayout";
33
34    private int mCellCountX;
35    private int mCellCountY;
36    private int mOriginalCellWidth;
37    private int mOriginalCellHeight;
38    private int mCellWidth;
39    private int mCellHeight;
40    private int mOriginalWidthGap;
41    private int mOriginalHeightGap;
42    private int mWidthGap;
43    private int mHeightGap;
44    protected PagedViewCellLayoutChildren mChildren;
45
46    public PagedViewCellLayout(Context context) {
47        this(context, null);
48    }
49
50    public PagedViewCellLayout(Context context, AttributeSet attrs) {
51        this(context, attrs, 0);
52    }
53
54    public PagedViewCellLayout(Context context, AttributeSet attrs, int defStyle) {
55        super(context, attrs, defStyle);
56
57        setAlwaysDrawnWithCacheEnabled(false);
58
59        // setup default cell parameters
60        LauncherAppState app = LauncherAppState.getInstance();
61        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
62        mOriginalCellWidth = mCellWidth = grid.cellWidthPx;
63        mOriginalCellHeight = mCellHeight = grid.cellHeightPx;
64        mCellCountX = (int) grid.numColumns;
65        mCellCountY = (int) grid.numRows;
66        mOriginalWidthGap = mOriginalHeightGap = mWidthGap = mHeightGap = -1;
67
68        mChildren = new PagedViewCellLayoutChildren(context);
69        mChildren.setCellDimensions(mCellWidth, mCellHeight);
70        mChildren.setGap(mWidthGap, mHeightGap);
71
72        addView(mChildren);
73    }
74
75    public int getCellWidth() {
76        return mCellWidth;
77    }
78
79    public int getCellHeight() {
80        return mCellHeight;
81    }
82
83    @Override
84    public void cancelLongPress() {
85        super.cancelLongPress();
86
87        // Cancel long press for all children
88        final int count = getChildCount();
89        for (int i = 0; i < count; i++) {
90            final View child = getChildAt(i);
91            child.cancelLongPress();
92        }
93    }
94
95    public boolean addViewToCellLayout(View child, int index, int childId,
96            PagedViewCellLayout.LayoutParams params) {
97        final PagedViewCellLayout.LayoutParams lp = params;
98
99        // Generate an id for each view, this assumes we have at most 256x256 cells
100        // per workspace screen
101        if (lp.cellX >= 0 && lp.cellX <= (mCellCountX - 1) &&
102                lp.cellY >= 0 && (lp.cellY <= mCellCountY - 1)) {
103            // If the horizontal or vertical span is set to -1, it is taken to
104            // mean that it spans the extent of the CellLayout
105            if (lp.cellHSpan < 0) lp.cellHSpan = mCellCountX;
106            if (lp.cellVSpan < 0) lp.cellVSpan = mCellCountY;
107
108            child.setId(childId);
109            mChildren.addView(child, index, lp);
110
111            return true;
112        }
113        return false;
114    }
115
116    @Override
117    public void removeAllViewsOnPage() {
118        mChildren.removeAllViews();
119        setLayerType(LAYER_TYPE_NONE, null);
120    }
121
122    @Override
123    public void removeViewOnPageAt(int index) {
124        mChildren.removeViewAt(index);
125    }
126
127    /**
128     * Clears all the key listeners for the individual icons.
129     */
130    public void resetChildrenOnKeyListeners() {
131        int childCount = mChildren.getChildCount();
132        for (int j = 0; j < childCount; ++j) {
133            mChildren.getChildAt(j).setOnKeyListener(null);
134        }
135    }
136
137    @Override
138    public int getPageChildCount() {
139        return mChildren.getChildCount();
140    }
141
142    public PagedViewCellLayoutChildren getChildrenLayout() {
143        return mChildren;
144    }
145
146    @Override
147    public View getChildOnPageAt(int i) {
148        return mChildren.getChildAt(i);
149    }
150
151    @Override
152    public int indexOfChildOnPage(View v) {
153        return mChildren.indexOfChild(v);
154    }
155
156    public int getCellCountX() {
157        return mCellCountX;
158    }
159
160    public int getCellCountY() {
161        return mCellCountY;
162    }
163
164    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
165        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
166        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
167
168        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
169        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
170
171        if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
172            throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions");
173        }
174
175        int numWidthGaps = mCellCountX - 1;
176        int numHeightGaps = mCellCountY - 1;
177
178        if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) {
179            int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight();
180            int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom();
181            int hFreeSpace = hSpace - (mCellCountX * mOriginalCellWidth);
182            int vFreeSpace = vSpace - (mCellCountY * mOriginalCellHeight);
183            mWidthGap = numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0;
184            mHeightGap = numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0;
185
186            mChildren.setGap(mWidthGap, mHeightGap);
187        } else {
188            mWidthGap = mOriginalWidthGap;
189            mHeightGap = mOriginalHeightGap;
190        }
191
192        // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY
193        int newWidth = widthSpecSize;
194        int newHeight = heightSpecSize;
195        if (widthSpecMode == MeasureSpec.AT_MOST) {
196            newWidth = getPaddingLeft() + getPaddingRight() + (mCellCountX * mCellWidth) +
197                ((mCellCountX - 1) * mWidthGap);
198            newHeight = getPaddingTop() + getPaddingBottom() + (mCellCountY * mCellHeight) +
199                ((mCellCountY - 1) * mHeightGap);
200            setMeasuredDimension(newWidth, newHeight);
201        }
202
203        final int count = getChildCount();
204        for (int i = 0; i < count; i++) {
205            View child = getChildAt(i);
206            int childWidthMeasureSpec =
207                MeasureSpec.makeMeasureSpec(newWidth - getPaddingLeft() -
208                        getPaddingRight(), MeasureSpec.EXACTLY);
209            int childheightMeasureSpec =
210                MeasureSpec.makeMeasureSpec(newHeight - getPaddingTop() -
211                        getPaddingBottom(), MeasureSpec.EXACTLY);
212            child.measure(childWidthMeasureSpec, childheightMeasureSpec);
213        }
214
215        setMeasuredDimension(newWidth, newHeight);
216    }
217
218    int getContentWidth() {
219        return getWidthBeforeFirstLayout() + getPaddingLeft() + getPaddingRight();
220    }
221
222    int getContentHeight() {
223        if (mCellCountY > 0) {
224            return mCellCountY * mCellHeight + (mCellCountY - 1) * Math.max(0, mHeightGap);
225        }
226        return 0;
227    }
228
229    int getWidthBeforeFirstLayout() {
230        if (mCellCountX > 0) {
231            return mCellCountX * mCellWidth + (mCellCountX - 1) * Math.max(0, mWidthGap);
232        }
233        return 0;
234    }
235
236    @Override
237    protected void onLayout(boolean changed, int l, int t, int r, int b) {
238        int count = getChildCount();
239        for (int i = 0; i < count; i++) {
240            View child = getChildAt(i);
241            child.layout(getPaddingLeft(), getPaddingTop(),
242                r - l - getPaddingRight(), b - t - getPaddingBottom());
243        }
244    }
245
246    @Override
247    public boolean onTouchEvent(MotionEvent event) {
248        boolean result = super.onTouchEvent(event);
249        int count = getPageChildCount();
250        if (count > 0) {
251            // We only intercept the touch if we are tapping in empty space after the final row
252            View child = getChildOnPageAt(count - 1);
253            int bottom = child.getBottom();
254            int numRows = (int) Math.ceil((float) getPageChildCount() / getCellCountX());
255            if (numRows < getCellCountY()) {
256                // Add a little bit of buffer if there is room for another row
257                bottom += mCellHeight / 2;
258            }
259            result = result || (event.getY() < bottom);
260        }
261        return result;
262    }
263
264    public void enableCenteredContent(boolean enabled) {
265        mChildren.enableCenteredContent(enabled);
266    }
267
268    @Override
269    protected void setChildrenDrawingCacheEnabled(boolean enabled) {
270        mChildren.setChildrenDrawingCacheEnabled(enabled);
271    }
272
273    public void setCellCount(int xCount, int yCount) {
274        mCellCountX = xCount;
275        mCellCountY = yCount;
276        requestLayout();
277    }
278
279    public void setGap(int widthGap, int heightGap) {
280        mOriginalWidthGap = mWidthGap = widthGap;
281        mOriginalHeightGap = mHeightGap = heightGap;
282        mChildren.setGap(widthGap, heightGap);
283    }
284
285    public int[] getCellCountForDimensions(int width, int height) {
286        // Always assume we're working with the smallest span to make sure we
287        // reserve enough space in both orientations
288        int smallerSize = Math.min(mCellWidth, mCellHeight);
289
290        // Always round up to next largest cell
291        int spanX = (width + smallerSize) / smallerSize;
292        int spanY = (height + smallerSize) / smallerSize;
293
294        return new int[] { spanX, spanY };
295    }
296
297    /**
298     * Start dragging the specified child
299     *
300     * @param child The child that is being dragged
301     */
302    void onDragChild(View child) {
303        PagedViewCellLayout.LayoutParams lp = (PagedViewCellLayout.LayoutParams) child.getLayoutParams();
304        lp.isDragging = true;
305    }
306
307    /**
308     * Estimates the number of cells that the specified width would take up.
309     */
310    public int estimateCellHSpan(int width) {
311        // We don't show the next/previous pages any more, so we use the full width, minus the
312        // padding
313        int availWidth = width - (getPaddingLeft() + getPaddingRight());
314
315        // We know that we have to fit N cells with N-1 width gaps, so we just juggle to solve for N
316        int n = Math.max(1, (availWidth + mWidthGap) / (mCellWidth + mWidthGap));
317
318        // We don't do anything fancy to determine if we squeeze another row in.
319        return n;
320    }
321
322    /**
323     * Estimates the number of cells that the specified height would take up.
324     */
325    public int estimateCellVSpan(int height) {
326        // The space for a page is the height - top padding (current page) - bottom padding (current
327        // page)
328        int availHeight = height - (getPaddingTop() + getPaddingBottom());
329
330        // We know that we have to fit N cells with N-1 height gaps, so we juggle to solve for N
331        int n = Math.max(1, (availHeight + mHeightGap) / (mCellHeight + mHeightGap));
332
333        // We don't do anything fancy to determine if we squeeze another row in.
334        return n;
335    }
336
337    /** Returns an estimated center position of the cell at the specified index */
338    public int[] estimateCellPosition(int x, int y) {
339        return new int[] {
340                getPaddingLeft() + (x * mCellWidth) + (x * mWidthGap) + (mCellWidth / 2),
341                getPaddingTop() + (y * mCellHeight) + (y * mHeightGap) + (mCellHeight / 2)
342        };
343    }
344
345    public void calculateCellCount(int width, int height, int maxCellCountX, int maxCellCountY) {
346        mCellCountX = Math.min(maxCellCountX, estimateCellHSpan(width));
347        mCellCountY = Math.min(maxCellCountY, estimateCellVSpan(height));
348        requestLayout();
349    }
350
351    /**
352     * Estimates the width that the number of hSpan cells will take up.
353     */
354    public int estimateCellWidth(int hSpan) {
355        // TODO: we need to take widthGap into effect
356        return hSpan * mCellWidth;
357    }
358
359    /**
360     * Estimates the height that the number of vSpan cells will take up.
361     */
362    public int estimateCellHeight(int vSpan) {
363        // TODO: we need to take heightGap into effect
364        return vSpan * mCellHeight;
365    }
366
367    @Override
368    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
369        return new PagedViewCellLayout.LayoutParams(getContext(), attrs);
370    }
371
372    @Override
373    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
374        return p instanceof PagedViewCellLayout.LayoutParams;
375    }
376
377    @Override
378    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
379        return new PagedViewCellLayout.LayoutParams(p);
380    }
381
382    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
383        /**
384         * Horizontal location of the item in the grid.
385         */
386        @ViewDebug.ExportedProperty
387        public int cellX;
388
389        /**
390         * Vertical location of the item in the grid.
391         */
392        @ViewDebug.ExportedProperty
393        public int cellY;
394
395        /**
396         * Number of cells spanned horizontally by the item.
397         */
398        @ViewDebug.ExportedProperty
399        public int cellHSpan;
400
401        /**
402         * Number of cells spanned vertically by the item.
403         */
404        @ViewDebug.ExportedProperty
405        public int cellVSpan;
406
407        /**
408         * Is this item currently being dragged
409         */
410        public boolean isDragging;
411
412        // a data object that you can bind to this layout params
413        private Object mTag;
414
415        // X coordinate of the view in the layout.
416        @ViewDebug.ExportedProperty
417        int x;
418        // Y coordinate of the view in the layout.
419        @ViewDebug.ExportedProperty
420        int y;
421
422        public LayoutParams() {
423            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
424            cellHSpan = 1;
425            cellVSpan = 1;
426        }
427
428        public LayoutParams(Context c, AttributeSet attrs) {
429            super(c, attrs);
430            cellHSpan = 1;
431            cellVSpan = 1;
432        }
433
434        public LayoutParams(ViewGroup.LayoutParams source) {
435            super(source);
436            cellHSpan = 1;
437            cellVSpan = 1;
438        }
439
440        public LayoutParams(LayoutParams source) {
441            super(source);
442            this.cellX = source.cellX;
443            this.cellY = source.cellY;
444            this.cellHSpan = source.cellHSpan;
445            this.cellVSpan = source.cellVSpan;
446        }
447
448        public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) {
449            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
450            this.cellX = cellX;
451            this.cellY = cellY;
452            this.cellHSpan = cellHSpan;
453            this.cellVSpan = cellVSpan;
454        }
455
456        public void setup(Context context,
457                          int cellWidth, int cellHeight, int widthGap, int heightGap,
458                          int hStartPadding, int vStartPadding) {
459
460            final int myCellHSpan = cellHSpan;
461            final int myCellVSpan = cellVSpan;
462            final int myCellX = cellX;
463            final int myCellY = cellY;
464
465            width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) -
466                    leftMargin - rightMargin;
467            height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) -
468                    topMargin - bottomMargin;
469
470            if (LauncherAppState.getInstance().isScreenLarge()) {
471                x = hStartPadding + myCellX * (cellWidth + widthGap) + leftMargin;
472                y = vStartPadding + myCellY * (cellHeight + heightGap) + topMargin;
473            } else {
474                x = myCellX * (cellWidth + widthGap) + leftMargin;
475                y = myCellY * (cellHeight + heightGap) + topMargin;
476            }
477        }
478
479        public Object getTag() {
480            return mTag;
481        }
482
483        public void setTag(Object tag) {
484            mTag = tag;
485        }
486
487        public String toString() {
488            return "(" + this.cellX + ", " + this.cellY + ", " +
489                this.cellHSpan + ", " + this.cellVSpan + ")";
490        }
491    }
492}