1/*
2 * Copyright (C) 2013 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.photos.views;
18
19import android.content.Context;
20import android.database.DataSetObservable;
21import android.database.DataSetObserver;
22import android.util.AttributeSet;
23import android.view.View;
24import android.view.ViewGroup;
25import android.widget.AdapterView;
26import android.widget.Filter;
27import android.widget.Filterable;
28import android.widget.FrameLayout;
29import android.widget.GridView;
30import android.widget.ListAdapter;
31import android.widget.WrapperListAdapter;
32
33import java.util.ArrayList;
34
35/**
36 * A {@link GridView} that supports adding header rows in a
37 * very similar way to {@link ListView}.
38 * See {@link HeaderGridView#addHeaderView(View, Object, boolean)}
39 */
40public class HeaderGridView extends GridView {
41    private static final String TAG = "HeaderGridView";
42
43    /**
44     * A class that represents a fixed view in a list, for example a header at the top
45     * or a footer at the bottom.
46     */
47    private static class FixedViewInfo {
48        /** The view to add to the grid */
49        public View view;
50        public ViewGroup viewContainer;
51        /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
52        public Object data;
53        /** <code>true</code> if the fixed view should be selectable in the grid */
54        public boolean isSelectable;
55    }
56
57    private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>();
58
59    private void initHeaderGridView() {
60        super.setClipChildren(false);
61    }
62
63    public HeaderGridView(Context context) {
64        super(context);
65        initHeaderGridView();
66    }
67
68    public HeaderGridView(Context context, AttributeSet attrs) {
69        super(context, attrs);
70        initHeaderGridView();
71    }
72
73    public HeaderGridView(Context context, AttributeSet attrs, int defStyle) {
74        super(context, attrs, defStyle);
75        initHeaderGridView();
76    }
77
78    @Override
79    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
80        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
81        ListAdapter adapter = getAdapter();
82        if (adapter != null && adapter instanceof HeaderViewGridAdapter) {
83            ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumns());
84        }
85    }
86
87    @Override
88    public void setClipChildren(boolean clipChildren) {
89       // Ignore, since the header rows depend on not being clipped
90    }
91
92    /**
93     * Add a fixed view to appear at the top of the grid. If addHeaderView is
94     * called more than once, the views will appear in the order they were
95     * added. Views added using this call can take focus if they want.
96     * <p>
97     * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
98     * the supplied cursor with one that will also account for header views.
99     *
100     * @param v The view to add.
101     * @param data Data to associate with this view
102     * @param isSelectable whether the item is selectable
103     */
104    public void addHeaderView(View v, Object data, boolean isSelectable) {
105        ListAdapter adapter = getAdapter();
106
107        if (adapter != null && ! (adapter instanceof HeaderViewGridAdapter)) {
108            throw new IllegalStateException(
109                    "Cannot add header view to grid -- setAdapter has already been called.");
110        }
111
112        FixedViewInfo info = new FixedViewInfo();
113        FrameLayout fl = new FullWidthFixedViewLayout(getContext());
114        fl.addView(v);
115        info.view = v;
116        info.viewContainer = fl;
117        info.data = data;
118        info.isSelectable = isSelectable;
119        mHeaderViewInfos.add(info);
120
121        // in the case of re-adding a header view, or adding one later on,
122        // we need to notify the observer
123        if (adapter != null) {
124            ((HeaderViewGridAdapter) adapter).notifyDataSetChanged();
125        }
126    }
127
128    /**
129     * Add a fixed view to appear at the top of the grid. If addHeaderView is
130     * called more than once, the views will appear in the order they were
131     * added. Views added using this call can take focus if they want.
132     * <p>
133     * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
134     * the supplied cursor with one that will also account for header views.
135     *
136     * @param v The view to add.
137     */
138    public void addHeaderView(View v) {
139        addHeaderView(v, null, true);
140    }
141
142    public int getHeaderViewCount() {
143        return mHeaderViewInfos.size();
144    }
145
146    /**
147     * Removes a previously-added header view.
148     *
149     * @param v The view to remove
150     * @return true if the view was removed, false if the view was not a header
151     *         view
152     */
153    public boolean removeHeaderView(View v) {
154        if (mHeaderViewInfos.size() > 0) {
155            boolean result = false;
156            ListAdapter adapter = getAdapter();
157            if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) {
158                result = true;
159            }
160            removeFixedViewInfo(v, mHeaderViewInfos);
161            return result;
162        }
163        return false;
164    }
165
166    private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
167        int len = where.size();
168        for (int i = 0; i < len; ++i) {
169            FixedViewInfo info = where.get(i);
170            if (info.view == v) {
171                where.remove(i);
172                break;
173            }
174        }
175    }
176
177    @Override
178    public void setAdapter(ListAdapter adapter) {
179        if (mHeaderViewInfos.size() > 0) {
180            HeaderViewGridAdapter hadapter = new HeaderViewGridAdapter(mHeaderViewInfos, adapter);
181            int numColumns = getNumColumns();
182            if (numColumns > 1) {
183                hadapter.setNumColumns(numColumns);
184            }
185            super.setAdapter(hadapter);
186        } else {
187            super.setAdapter(adapter);
188        }
189    }
190
191    private class FullWidthFixedViewLayout extends FrameLayout {
192        public FullWidthFixedViewLayout(Context context) {
193            super(context);
194        }
195
196        @Override
197        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
198            int targetWidth = HeaderGridView.this.getMeasuredWidth()
199                    - HeaderGridView.this.getPaddingLeft()
200                    - HeaderGridView.this.getPaddingRight();
201            widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth,
202                    MeasureSpec.getMode(widthMeasureSpec));
203            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
204        }
205    }
206
207    /**
208     * ListAdapter used when a HeaderGridView has header views. This ListAdapter
209     * wraps another one and also keeps track of the header views and their
210     * associated data objects.
211     *<p>This is intended as a base class; you will probably not need to
212     * use this class directly in your own code.
213     */
214    private static class HeaderViewGridAdapter implements WrapperListAdapter, Filterable {
215
216        // This is used to notify the container of updates relating to number of columns
217        // or headers changing, which changes the number of placeholders needed
218        private final DataSetObservable mDataSetObservable = new DataSetObservable();
219
220        private final ListAdapter mAdapter;
221        private int mNumColumns = 1;
222
223        // This ArrayList is assumed to NOT be null.
224        ArrayList<FixedViewInfo> mHeaderViewInfos;
225
226        boolean mAreAllFixedViewsSelectable;
227
228        private final boolean mIsFilterable;
229
230        public HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ListAdapter adapter) {
231            mAdapter = adapter;
232            mIsFilterable = adapter instanceof Filterable;
233
234            if (headerViewInfos == null) {
235                throw new IllegalArgumentException("headerViewInfos cannot be null");
236            }
237            mHeaderViewInfos = headerViewInfos;
238
239            mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
240        }
241
242        public int getHeadersCount() {
243            return mHeaderViewInfos.size();
244        }
245
246        @Override
247        public boolean isEmpty() {
248            return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0;
249        }
250
251        public void setNumColumns(int numColumns) {
252            if (numColumns < 1) {
253                throw new IllegalArgumentException("Number of columns must be 1 or more");
254            }
255            if (mNumColumns != numColumns) {
256                mNumColumns = numColumns;
257                notifyDataSetChanged();
258            }
259        }
260
261        private boolean areAllListInfosSelectable(ArrayList<FixedViewInfo> infos) {
262            if (infos != null) {
263                for (FixedViewInfo info : infos) {
264                    if (!info.isSelectable) {
265                        return false;
266                    }
267                }
268            }
269            return true;
270        }
271
272        public boolean removeHeader(View v) {
273            for (int i = 0; i < mHeaderViewInfos.size(); i++) {
274                FixedViewInfo info = mHeaderViewInfos.get(i);
275                if (info.view == v) {
276                    mHeaderViewInfos.remove(i);
277
278                    mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
279
280                    mDataSetObservable.notifyChanged();
281                    return true;
282                }
283            }
284
285            return false;
286        }
287
288        @Override
289        public int getCount() {
290            if (mAdapter != null) {
291                return getHeadersCount() * mNumColumns + mAdapter.getCount();
292            } else {
293                return getHeadersCount() * mNumColumns;
294            }
295        }
296
297        @Override
298        public boolean areAllItemsEnabled() {
299            if (mAdapter != null) {
300                return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
301            } else {
302                return true;
303            }
304        }
305
306        @Override
307        public boolean isEnabled(int position) {
308            // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
309            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
310            if (position < numHeadersAndPlaceholders) {
311                return (position % mNumColumns == 0)
312                        && mHeaderViewInfos.get(position / mNumColumns).isSelectable;
313            }
314
315            // Adapter
316            final int adjPosition = position - numHeadersAndPlaceholders;
317            int adapterCount = 0;
318            if (mAdapter != null) {
319                adapterCount = mAdapter.getCount();
320                if (adjPosition < adapterCount) {
321                    return mAdapter.isEnabled(adjPosition);
322                }
323            }
324
325            throw new ArrayIndexOutOfBoundsException(position);
326        }
327
328        @Override
329        public Object getItem(int position) {
330            // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
331            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
332            if (position < numHeadersAndPlaceholders) {
333                if (position % mNumColumns == 0) {
334                    return mHeaderViewInfos.get(position / mNumColumns).data;
335                }
336                return null;
337            }
338
339            // Adapter
340            final int adjPosition = position - numHeadersAndPlaceholders;
341            int adapterCount = 0;
342            if (mAdapter != null) {
343                adapterCount = mAdapter.getCount();
344                if (adjPosition < adapterCount) {
345                    return mAdapter.getItem(adjPosition);
346                }
347            }
348
349            throw new ArrayIndexOutOfBoundsException(position);
350        }
351
352        @Override
353        public long getItemId(int position) {
354            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
355            if (mAdapter != null && position >= numHeadersAndPlaceholders) {
356                int adjPosition = position - numHeadersAndPlaceholders;
357                int adapterCount = mAdapter.getCount();
358                if (adjPosition < adapterCount) {
359                    return mAdapter.getItemId(adjPosition);
360                }
361            }
362            return -1;
363        }
364
365        @Override
366        public boolean hasStableIds() {
367            if (mAdapter != null) {
368                return mAdapter.hasStableIds();
369            }
370            return false;
371        }
372
373        @Override
374        public View getView(int position, View convertView, ViewGroup parent) {
375            // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
376            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns ;
377            if (position < numHeadersAndPlaceholders) {
378                View headerViewContainer = mHeaderViewInfos
379                        .get(position / mNumColumns).viewContainer;
380                if (position % mNumColumns == 0) {
381                    return headerViewContainer;
382                } else {
383                    if (convertView == null) {
384                        convertView = new View(parent.getContext());
385                    }
386                    // We need to do this because GridView uses the height of the last item
387                    // in a row to determine the height for the entire row.
388                    convertView.setVisibility(View.INVISIBLE);
389                    convertView.setMinimumHeight(headerViewContainer.getHeight());
390                    return convertView;
391                }
392            }
393
394            // Adapter
395            final int adjPosition = position - numHeadersAndPlaceholders;
396            int adapterCount = 0;
397            if (mAdapter != null) {
398                adapterCount = mAdapter.getCount();
399                if (adjPosition < adapterCount) {
400                    return mAdapter.getView(adjPosition, convertView, parent);
401                }
402            }
403
404            throw new ArrayIndexOutOfBoundsException(position);
405        }
406
407        @Override
408        public int getItemViewType(int position) {
409            int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
410            if (position < numHeadersAndPlaceholders && (position % mNumColumns != 0)) {
411                // Placeholders get the last view type number
412                return mAdapter != null ? mAdapter.getViewTypeCount() : 1;
413            }
414            if (mAdapter != null && position >= numHeadersAndPlaceholders) {
415                int adjPosition = position - numHeadersAndPlaceholders;
416                int adapterCount = mAdapter.getCount();
417                if (adjPosition < adapterCount) {
418                    return mAdapter.getItemViewType(adjPosition);
419                }
420            }
421
422            return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
423        }
424
425        @Override
426        public int getViewTypeCount() {
427            if (mAdapter != null) {
428                return mAdapter.getViewTypeCount() + 1;
429            }
430            return 2;
431        }
432
433        @Override
434        public void registerDataSetObserver(DataSetObserver observer) {
435            mDataSetObservable.registerObserver(observer);
436            if (mAdapter != null) {
437                mAdapter.registerDataSetObserver(observer);
438            }
439        }
440
441        @Override
442        public void unregisterDataSetObserver(DataSetObserver observer) {
443            mDataSetObservable.unregisterObserver(observer);
444            if (mAdapter != null) {
445                mAdapter.unregisterDataSetObserver(observer);
446            }
447        }
448
449        @Override
450        public Filter getFilter() {
451            if (mIsFilterable) {
452                return ((Filterable) mAdapter).getFilter();
453            }
454            return null;
455        }
456
457        @Override
458        public ListAdapter getWrappedAdapter() {
459            return mAdapter;
460        }
461
462        public void notifyDataSetChanged() {
463            mDataSetObservable.notifyChanged();
464        }
465    }
466}
467