();
73 |
74 | private int mRequestedNumColumns;
75 |
76 | private int mNumColmuns = 1;
77 |
78 | private void initHeaderGridView() {
79 | super.setClipChildren(false);
80 | }
81 |
82 | public HeaderFooterGridView(Context context) {
83 | super(context);
84 | initHeaderGridView();
85 | }
86 |
87 | public HeaderFooterGridView(Context context, AttributeSet attrs) {
88 | super(context, attrs);
89 | initHeaderGridView();
90 | }
91 |
92 | public HeaderFooterGridView(Context context, AttributeSet attrs, int defStyle) {
93 | super(context, attrs, defStyle);
94 | initHeaderGridView();
95 | }
96 |
97 | @Override
98 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
99 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
100 |
101 | if (mRequestedNumColumns != AUTO_FIT) {
102 | mNumColmuns = mRequestedNumColumns;
103 | }
104 | if (mNumColmuns <= 0) {
105 | mNumColmuns = 1;
106 | }
107 |
108 | ListAdapter adapter = getAdapter();
109 | if (adapter != null && adapter instanceof HeaderFooterViewGridAdapter) {
110 | ((HeaderFooterViewGridAdapter) adapter).setNumColumns(getNumColumns());
111 | }
112 | }
113 |
114 | @Override
115 | public void setClipChildren(boolean clipChildren) {
116 | // Ignore, since the header rows depend on not being clipped
117 | }
118 |
119 | /**
120 | * Add a fixed view to appear at the top of the grid. If addHeaderView is
121 | * called more than once, the views will appear in the order they were
122 | * added. Views added using this call can take focus if they want.
123 | *
124 | * NOTE: Call this before calling setAdapter. This is so HeaderFooterGridView can wrap
125 | * the supplied cursor with one that will also account for header views.
126 | *
127 | * @param v The view to add.
128 | * @param data Data to associate with this view
129 | * @param isSelectable whether the item is selectable
130 | */
131 | public void addHeaderView(View v, Object data, boolean isSelectable) {
132 | ListAdapter adapter = getAdapter();
133 |
134 | if (adapter != null && !(adapter instanceof HeaderFooterViewGridAdapter)) {
135 | throw new IllegalStateException(
136 | "Cannot add header view to grid -- setAdapter has already been called.");
137 | }
138 |
139 | FixedViewInfo info = new FixedViewInfo();
140 | FrameLayout fl = new FullWidthFixedViewLayout(getContext());
141 | fl.addView(v);
142 | info.view = v;
143 | info.viewContainer = fl;
144 | info.data = data;
145 | info.isSelectable = isSelectable;
146 | mHeaderViewInfos.add(info);
147 |
148 | // in the case of re-adding a header view, or adding one later on,
149 | // we need to notify the observer
150 | if (adapter != null) {
151 | ((HeaderFooterViewGridAdapter) adapter).notifyDataSetChanged();
152 | }
153 | }
154 |
155 | /**
156 | * Add a fixed view to appear at the top of the grid. If addHeaderView is
157 | * called more than once, the views will appear in the order they were
158 | * added. Views added using this call can take focus if they want.
159 | *
160 | * NOTE: Call this before calling setAdapter. This is so HeaderFooterGridView can wrap
161 | * the supplied cursor with one that will also account for header views.
162 | *
163 | * @param v The view to add.
164 | */
165 | public void addHeaderView(View v) {
166 | addHeaderView(v, null, true);
167 | }
168 |
169 | /**
170 | * Add a fixed view to appear at the bottom of the grid. If addFooterView is
171 | * called more than once, the views will appear in the order they were
172 | * added. Views added using this call can take focus if they want.
173 | *
174 | * NOTE: Call this before calling setAdapter. This is so HeaderFooterGridView can wrap
175 | * the supplied cursor with one that will also account for header views.
176 | *
177 | * @param v The view to add.
178 | * @param data Data to associate with this view
179 | * @param isSelectable whether the item is selectable
180 | */
181 | public void addFooterView(View v, Object data, boolean isSelectable) {
182 | ListAdapter adapter = getAdapter();
183 |
184 | if (adapter != null && !(adapter instanceof HeaderFooterViewGridAdapter)) {
185 | throw new IllegalStateException(
186 | "Cannot add footer view to grid -- setAdapter has already been called.");
187 | }
188 |
189 | FixedViewInfo info = new FixedViewInfo();
190 | FrameLayout fl = new FullWidthFixedViewLayout(getContext());
191 | fl.addView(v);
192 | info.view = v;
193 | info.viewContainer = fl;
194 | info.data = data;
195 | info.isSelectable = isSelectable;
196 | mFooterViewInfos.add(info);
197 |
198 | // in the case of re-adding a header view, or adding one later on,
199 | // we need to notify the observer
200 | if (adapter != null) {
201 | ((HeaderFooterViewGridAdapter) adapter).notifyDataSetChanged();
202 | }
203 | }
204 |
205 | /**
206 | * Add a fixed view to appear at the bottom of the grid. If addFooterView is
207 | * called more than once, the views will appear in the order they were
208 | * added. Views added using this call can take focus if they want.
209 | *
210 | * NOTE: Call this before calling setAdapter. This is so HeaderFooterGridView can wrap
211 | * the supplied cursor with one that will also account for header views.
212 | *
213 | * @param v The view to add.
214 | */
215 | public void addFooterView(View v) {
216 | addFooterView(v, null, true);
217 | }
218 |
219 | public int getHeaderViewCount() {
220 | return mHeaderViewInfos.size();
221 | }
222 |
223 | public int getFooterViewCount() {
224 | return mFooterViewInfos.size();
225 | }
226 |
227 | /**
228 | * Removes a previously-added header view.
229 | *
230 | * @param v The view to remove
231 | * @return true if the view was removed, false if the view was not a header view
232 | */
233 | public boolean removeHeaderView(View v) {
234 | if (mHeaderViewInfos.size() > 0) {
235 | boolean result = false;
236 | ListAdapter adapter = getAdapter();
237 | if (adapter != null && ((HeaderFooterViewGridAdapter) adapter).removeHeader(v)) {
238 | result = true;
239 | }
240 | removeFixedViewInfo(v, mHeaderViewInfos);
241 | return result;
242 | }
243 | return false;
244 | }
245 |
246 | /**
247 | * Removes a previously-added footer view.
248 | *
249 | * @param v The view to remove
250 | * @return true if the view was removed, false if the view was not a footer view
251 | */
252 | public boolean removeFooterView(View v) {
253 | if (mFooterViewInfos.size() > 0) {
254 | boolean result = false;
255 | ListAdapter adapter = getAdapter();
256 | if (adapter != null && ((HeaderFooterViewGridAdapter) adapter).removeFooter(v)) {
257 | result = true;
258 | }
259 | removeFixedViewInfo(v, mFooterViewInfos);
260 | return result;
261 | }
262 | return false;
263 | }
264 |
265 | private void removeFixedViewInfo(View v, ArrayList where) {
266 | int len = where.size();
267 | for (int i = 0; i < len; ++i) {
268 | FixedViewInfo info = where.get(i);
269 | if (info.view == v) {
270 | where.remove(i);
271 | break;
272 | }
273 | }
274 | }
275 |
276 | @Override
277 | public void setAdapter(ListAdapter adapter) {
278 | if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) {
279 | HeaderFooterViewGridAdapter hadapter = new HeaderFooterViewGridAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
280 | int numColumns = getNumColumns();
281 | if (numColumns > 1) {
282 | hadapter.setNumColumns(numColumns);
283 | }
284 | super.setAdapter(hadapter);
285 | } else {
286 | super.setAdapter(adapter);
287 | }
288 | }
289 |
290 | private class FullWidthFixedViewLayout extends FrameLayout {
291 | public FullWidthFixedViewLayout(Context context) {
292 | super(context);
293 | }
294 |
295 | @Override
296 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
297 | int targetWidth = HeaderFooterGridView.this.getMeasuredWidth()
298 | - HeaderFooterGridView.this.getPaddingLeft()
299 | - HeaderFooterGridView.this.getPaddingRight();
300 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth,
301 | MeasureSpec.getMode(widthMeasureSpec));
302 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
303 | }
304 | }
305 |
306 | @Override
307 | public void setNumColumns(int numColumns) {
308 | super.setNumColumns(numColumns);
309 | // Store specified value for less than Honeycomb.
310 | mRequestedNumColumns = numColumns;
311 | }
312 |
313 | @Override
314 | @SuppressLint("NewApi")
315 | public int getNumColumns() {
316 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
317 | return super.getNumColumns();
318 | }
319 |
320 | // Return value for less than Honeycomb.
321 | return mNumColmuns;
322 | }
323 |
324 | /**
325 | * ListAdapter used when a HeaderFooterGridView has header views. This ListAdapter
326 | * wraps another one and also keeps track of the header views and their
327 | * associated data objects.
328 | * This is intended as a base class; you will probably not need to
329 | * use this class directly in your own code.
330 | */
331 | private static class HeaderFooterViewGridAdapter implements WrapperListAdapter, Filterable {
332 |
333 | // This is used to notify the container of updates relating to number of columns
334 | // or headers changing, which changes the number of placeholders needed
335 | private final DataSetObservable mDataSetObservable = new DataSetObservable();
336 |
337 | private final ListAdapter mAdapter;
338 | private int mNumColumns = 1;
339 |
340 | // This ArrayList is assumed to NOT be null.
341 | ArrayList mHeaderViewInfos;
342 |
343 | ArrayList mFooterViewInfos;
344 |
345 | boolean mAreAllFixedViewsSelectable;
346 |
347 | private final boolean mIsFilterable;
348 |
349 | public HeaderFooterViewGridAdapter(ArrayList headerViewInfos, ArrayList footerViewInfos, ListAdapter adapter) {
350 | mAdapter = adapter;
351 | mIsFilterable = adapter instanceof Filterable;
352 |
353 | if (headerViewInfos == null) {
354 | throw new IllegalArgumentException("headerViewInfos cannot be null");
355 | }
356 | if (footerViewInfos == null) {
357 | throw new IllegalArgumentException("footerViewInfos cannot be null");
358 | }
359 | mHeaderViewInfos = headerViewInfos;
360 | mFooterViewInfos = footerViewInfos;
361 |
362 | mAreAllFixedViewsSelectable = (areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos));
363 | }
364 |
365 | public int getHeadersCount() {
366 | return mHeaderViewInfos.size();
367 | }
368 |
369 | public int getFootersCount() {
370 | return mFooterViewInfos.size();
371 | }
372 |
373 | @Override
374 | public boolean isEmpty() {
375 | return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0 && getFootersCount() == 0;
376 | }
377 |
378 | public void setNumColumns(int numColumns) {
379 | if (numColumns < 1) {
380 | throw new IllegalArgumentException("Number of columns must be 1 or more");
381 | }
382 | if (mNumColumns != numColumns) {
383 | mNumColumns = numColumns;
384 | notifyDataSetChanged();
385 | }
386 | }
387 |
388 | private boolean areAllListInfosSelectable(ArrayList infos) {
389 | if (infos != null) {
390 | for (FixedViewInfo info : infos) {
391 | if (!info.isSelectable) {
392 | return false;
393 | }
394 | }
395 | }
396 | return true;
397 | }
398 |
399 | public boolean removeHeader(View v) {
400 | for (int i = 0; i < mHeaderViewInfos.size(); i++) {
401 | FixedViewInfo info = mHeaderViewInfos.get(i);
402 | if (info.view == v) {
403 | mHeaderViewInfos.remove(i);
404 |
405 | mAreAllFixedViewsSelectable = (areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos));
406 |
407 | mDataSetObservable.notifyChanged();
408 | return true;
409 | }
410 | }
411 |
412 | return false;
413 | }
414 |
415 | public boolean removeFooter(View v) {
416 | for (int i = 0; i < mFooterViewInfos.size(); i++) {
417 | FixedViewInfo info = mFooterViewInfos.get(i);
418 | if (info.view == v) {
419 | mFooterViewInfos.remove(i);
420 |
421 | mAreAllFixedViewsSelectable = (areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos));
422 |
423 | mDataSetObservable.notifyChanged();
424 | return true;
425 | }
426 | }
427 |
428 | return false;
429 | }
430 |
431 | @Override
432 | public int getCount() {
433 | if (mAdapter != null) {
434 | final int lastRowItemCount = (mAdapter.getCount() % mNumColumns);
435 | final int emptyItemCount = ((lastRowItemCount == 0) ? 0 : mNumColumns - lastRowItemCount);
436 | return (getHeadersCount() * mNumColumns) + mAdapter.getCount() + emptyItemCount + (getFootersCount() * mNumColumns);
437 | } else {
438 | return (getHeadersCount() * mNumColumns) + (getFootersCount() * mNumColumns);
439 | }
440 | }
441 |
442 | @Override
443 | public boolean areAllItemsEnabled() {
444 | if (mAdapter != null) {
445 | return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
446 | } else {
447 | return true;
448 | }
449 | }
450 |
451 | @Override
452 | public boolean isEnabled(int position) {
453 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
454 | int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
455 | if (position < numHeadersAndPlaceholders) {
456 | return (position % mNumColumns == 0)
457 | && mHeaderViewInfos.get(position / mNumColumns).isSelectable;
458 | }
459 |
460 | // Adapter
461 | if (position < numHeadersAndPlaceholders + mAdapter.getCount()) {
462 | final int adjPosition = position - numHeadersAndPlaceholders;
463 | int adapterCount = 0;
464 | if (mAdapter != null) {
465 | adapterCount = mAdapter.getCount();
466 | if (adjPosition < adapterCount) {
467 | return mAdapter.isEnabled(adjPosition);
468 | }
469 | }
470 | }
471 |
472 | // Empty item
473 | final int lastRowItemCount = (mAdapter.getCount() % mNumColumns);
474 | final int emptyItemCount = ((lastRowItemCount == 0) ? 0 : mNumColumns - lastRowItemCount);
475 | if (position < numHeadersAndPlaceholders + mAdapter.getCount() + emptyItemCount) {
476 | return false;
477 | }
478 |
479 | // Footer
480 | int numFootersAndPlaceholders = getFootersCount() * mNumColumns;
481 | if (position < numHeadersAndPlaceholders + mAdapter.getCount() + emptyItemCount + numFootersAndPlaceholders) {
482 | return (position % mNumColumns == 0)
483 | && mFooterViewInfos.get((position - numHeadersAndPlaceholders - mAdapter.getCount() - emptyItemCount) / mNumColumns).isSelectable;
484 | }
485 |
486 | throw new ArrayIndexOutOfBoundsException(position);
487 | }
488 |
489 | @Override
490 | public Object getItem(int position) {
491 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
492 | int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
493 | if (position < numHeadersAndPlaceholders) {
494 | if (position % mNumColumns == 0) {
495 | return mHeaderViewInfos.get(position / mNumColumns).data;
496 | }
497 | return null;
498 | }
499 |
500 | // Adapter
501 | if (position < numHeadersAndPlaceholders + mAdapter.getCount()) {
502 | final int adjPosition = position - numHeadersAndPlaceholders;
503 | int adapterCount = 0;
504 | if (mAdapter != null) {
505 | adapterCount = mAdapter.getCount();
506 | if (adjPosition < adapterCount) {
507 | return mAdapter.getItem(adjPosition);
508 | }
509 | }
510 | }
511 |
512 | // Empty item
513 | final int lastRowItemCount = (mAdapter.getCount() % mNumColumns);
514 | final int emptyItemCount = ((lastRowItemCount == 0) ? 0 : mNumColumns - lastRowItemCount);
515 | if (position < numHeadersAndPlaceholders + mAdapter.getCount() + emptyItemCount) {
516 | return null;
517 | }
518 |
519 | // Footer
520 | int numFootersAndPlaceholders = getFootersCount() * mNumColumns;
521 | if (position < numHeadersAndPlaceholders + mAdapter.getCount() + emptyItemCount + numFootersAndPlaceholders) {
522 | if (position % mNumColumns == 0) {
523 | return mFooterViewInfos.get((position - numHeadersAndPlaceholders - mAdapter.getCount() - emptyItemCount) / mNumColumns).data;
524 | }
525 | }
526 |
527 | throw new ArrayIndexOutOfBoundsException(position);
528 | }
529 |
530 | @Override
531 | public long getItemId(int position) {
532 | int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
533 | if (mAdapter != null) {
534 | if (position >= numHeadersAndPlaceholders && position < numHeadersAndPlaceholders + mAdapter.getCount()) {
535 | int adjPosition = position - numHeadersAndPlaceholders;
536 | int adapterCount = mAdapter.getCount();
537 | if (adjPosition < adapterCount) {
538 | return mAdapter.getItemId(adjPosition);
539 | }
540 | }
541 | }
542 | return -1;
543 | }
544 |
545 | @Override
546 | public boolean hasStableIds() {
547 | if (mAdapter != null) {
548 | return mAdapter.hasStableIds();
549 | }
550 | return false;
551 | }
552 |
553 | @Override
554 | public View getView(int position, View convertView, ViewGroup parent) {
555 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
556 | int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
557 | if (position < numHeadersAndPlaceholders) {
558 | View headerViewContainer = mHeaderViewInfos
559 | .get(position / mNumColumns).viewContainer;
560 | if (position % mNumColumns == 0) {
561 | return headerViewContainer;
562 | } else {
563 | convertView = new View(parent.getContext());
564 | // We need to do this because GridView uses the height of the last item
565 | // in a row to determine the height for the entire row.
566 | convertView.setVisibility(View.INVISIBLE);
567 | convertView.setMinimumHeight(headerViewContainer.getHeight());
568 | return convertView;
569 | }
570 | }
571 |
572 | // Adapter
573 | if (position < numHeadersAndPlaceholders + mAdapter.getCount()) {
574 | final int adjPosition = position - numHeadersAndPlaceholders;
575 | int adapterCount = 0;
576 | if (mAdapter != null) {
577 | adapterCount = mAdapter.getCount();
578 | if (adjPosition < adapterCount) {
579 | convertView = mAdapter.getView(adjPosition, convertView, parent);
580 | convertView.setVisibility(View.VISIBLE);
581 | return convertView;
582 | }
583 | }
584 | }
585 |
586 | // Empty item
587 | final int lastRowItemCount = (mAdapter.getCount() % mNumColumns);
588 | final int emptyItemCount = ((lastRowItemCount == 0) ? 0 : mNumColumns - lastRowItemCount);
589 | if (position < numHeadersAndPlaceholders + mAdapter.getCount() + emptyItemCount) {
590 | // We need to do this because GridView uses the height of the last item
591 | // in a row to determine the height for the entire row.
592 | // TODO Current implementation may not be enough in the case of 3 or more column. May need to be careful on the INVISIBLE View height.
593 | convertView = mAdapter.getView(mAdapter.getCount() - 1, convertView, parent);
594 | convertView.setVisibility(View.INVISIBLE);
595 | return convertView;
596 | }
597 |
598 | // Footer
599 | int numFootersAndPlaceholders = getFootersCount() * mNumColumns;
600 | if (position < numHeadersAndPlaceholders + mAdapter.getCount() + emptyItemCount + numFootersAndPlaceholders) {
601 | View footerViewContainer = mFooterViewInfos
602 | .get((position - numHeadersAndPlaceholders - mAdapter.getCount() - emptyItemCount) / mNumColumns).viewContainer;
603 | if (position % mNumColumns == 0) {
604 | return footerViewContainer;
605 | } else {
606 | convertView = new View(parent.getContext());
607 | // We need to do this because GridView uses the height of the last item
608 | // in a row to determine the height for the entire row.
609 | convertView.setVisibility(View.INVISIBLE);
610 | convertView.setMinimumHeight(footerViewContainer.getHeight());
611 | return convertView;
612 | }
613 | }
614 |
615 | throw new ArrayIndexOutOfBoundsException(position);
616 | }
617 |
618 | @Override
619 | public int getItemViewType(int position) {
620 | int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
621 | if (position < numHeadersAndPlaceholders && (position % mNumColumns != 0)) {
622 | // Placeholders get the last view type number
623 | return mAdapter != null ? mAdapter.getViewTypeCount() : 1;
624 | }
625 | if (mAdapter != null && position >= numHeadersAndPlaceholders && position < numHeadersAndPlaceholders + mAdapter.getCount() + (mNumColumns - (mAdapter.getCount() % mNumColumns))) {
626 | int adjPosition = position - numHeadersAndPlaceholders;
627 | int adapterCount = mAdapter.getCount();
628 | if (adjPosition < adapterCount) {
629 | return mAdapter.getItemViewType(adjPosition);
630 | } else if (adapterCount != 0 && mNumColumns != 1) {
631 | return mAdapter.getItemViewType(adapterCount - 1);
632 | }
633 | }
634 | int numFootersAndPlaceholders = getFootersCount() * mNumColumns;
635 | if (mAdapter != null && position < numHeadersAndPlaceholders + mAdapter.getCount() + numFootersAndPlaceholders) {
636 | return mAdapter != null ? mAdapter.getViewTypeCount() : 1;
637 | }
638 |
639 | return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
640 | }
641 |
642 | @Override
643 | public int getViewTypeCount() {
644 | if (mAdapter != null) {
645 | return mAdapter.getViewTypeCount() + 1;
646 | }
647 | return 2;
648 | }
649 |
650 | @Override
651 | public void registerDataSetObserver(DataSetObserver observer) {
652 | mDataSetObservable.registerObserver(observer);
653 | if (mAdapter != null) {
654 | mAdapter.registerDataSetObserver(observer);
655 | }
656 | }
657 |
658 | @Override
659 | public void unregisterDataSetObserver(DataSetObserver observer) {
660 | mDataSetObservable.unregisterObserver(observer);
661 | if (mAdapter != null) {
662 | mAdapter.unregisterDataSetObserver(observer);
663 | }
664 | }
665 |
666 | @Override
667 | public Filter getFilter() {
668 | if (mIsFilterable) {
669 | return ((Filterable) mAdapter).getFilter();
670 | }
671 | return null;
672 | }
673 |
674 | @Override
675 | public ListAdapter getWrappedAdapter() {
676 | return mAdapter;
677 | }
678 |
679 | public void notifyDataSetChanged() {
680 | mDataSetObservable.notifyChanged();
681 | }
682 | }
683 |
684 | }
685 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':library', ':demo'
2 |
--------------------------------------------------------------------------------