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