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 
17 package com.android.photos.views;
18 
19 import android.content.Context;
20 import android.database.DataSetObservable;
21 import android.database.DataSetObserver;
22 import android.util.AttributeSet;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.widget.AdapterView;
26 import android.widget.Filter;
27 import android.widget.Filterable;
28 import android.widget.FrameLayout;
29 import android.widget.GridView;
30 import android.widget.ListAdapter;
31 import android.widget.WrapperListAdapter;
32 
33 import 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  */
40 public 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 
initHeaderGridView()59     private void initHeaderGridView() {
60         super.setClipChildren(false);
61     }
62 
HeaderGridView(Context context)63     public HeaderGridView(Context context) {
64         super(context);
65         initHeaderGridView();
66     }
67 
HeaderGridView(Context context, AttributeSet attrs)68     public HeaderGridView(Context context, AttributeSet attrs) {
69         super(context, attrs);
70         initHeaderGridView();
71     }
72 
HeaderGridView(Context context, AttributeSet attrs, int defStyle)73     public HeaderGridView(Context context, AttributeSet attrs, int defStyle) {
74         super(context, attrs, defStyle);
75         initHeaderGridView();
76     }
77 
78     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
setClipChildren(boolean clipChildren)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      */
addHeaderView(View v, Object data, boolean isSelectable)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      */
addHeaderView(View v)138     public void addHeaderView(View v) {
139         addHeaderView(v, null, true);
140     }
141 
getHeaderViewCount()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      */
removeHeaderView(View v)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 
removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where)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
setAdapter(ListAdapter adapter)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 {
FullWidthFixedViewLayout(Context context)192         public FullWidthFixedViewLayout(Context context) {
193             super(context);
194         }
195 
196         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 
HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ListAdapter adapter)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 
getHeadersCount()242         public int getHeadersCount() {
243             return mHeaderViewInfos.size();
244         }
245 
246         @Override
isEmpty()247         public boolean isEmpty() {
248             return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0;
249         }
250 
setNumColumns(int numColumns)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 
areAllListInfosSelectable(ArrayList<FixedViewInfo> infos)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 
removeHeader(View v)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
getCount()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
areAllItemsEnabled()298         public boolean areAllItemsEnabled() {
299             if (mAdapter != null) {
300                 return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
301             } else {
302                 return true;
303             }
304         }
305 
306         @Override
isEnabled(int position)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
getItem(int position)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
getItemId(int position)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
hasStableIds()366         public boolean hasStableIds() {
367             if (mAdapter != null) {
368                 return mAdapter.hasStableIds();
369             }
370             return false;
371         }
372 
373         @Override
getView(int position, View convertView, ViewGroup parent)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
getItemViewType(int position)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
getViewTypeCount()426         public int getViewTypeCount() {
427             if (mAdapter != null) {
428                 return mAdapter.getViewTypeCount() + 1;
429             }
430             return 2;
431         }
432 
433         @Override
registerDataSetObserver(DataSetObserver observer)434         public void registerDataSetObserver(DataSetObserver observer) {
435             mDataSetObservable.registerObserver(observer);
436             if (mAdapter != null) {
437                 mAdapter.registerDataSetObserver(observer);
438             }
439         }
440 
441         @Override
unregisterDataSetObserver(DataSetObserver observer)442         public void unregisterDataSetObserver(DataSetObserver observer) {
443             mDataSetObservable.unregisterObserver(observer);
444             if (mAdapter != null) {
445                 mAdapter.unregisterDataSetObserver(observer);
446             }
447         }
448 
449         @Override
getFilter()450         public Filter getFilter() {
451             if (mIsFilterable) {
452                 return ((Filterable) mAdapter).getFilter();
453             }
454             return null;
455         }
456 
457         @Override
getWrappedAdapter()458         public ListAdapter getWrappedAdapter() {
459             return mAdapter;
460         }
461 
notifyDataSetChanged()462         public void notifyDataSetChanged() {
463             mDataSetObservable.notifyChanged();
464         }
465     }
466 }
467