1 /* 2 * Copyright (C) 2011 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.calendar; 18 19 import android.content.Context; 20 import android.graphics.Color; 21 import android.util.AttributeSet; 22 import android.view.Gravity; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.widget.AbsListView; 26 import android.widget.AbsListView.OnScrollListener; 27 import android.widget.Adapter; 28 import android.widget.FrameLayout; 29 import android.widget.ListView; 30 31 /** 32 * Implements a ListView class with a sticky header at the top. The header is 33 * per section and it is pinned to the top as long as its section is at the top 34 * of the view. If it is not, the header slides up or down (depending on the 35 * scroll movement) and the header of the current section slides to the top. 36 * Notes: 37 * 1. The class uses the first available child ListView as the working 38 * ListView. If no ListView child exists, the class will create a default one. 39 * 2. The ListView's adapter must be passed to this class using the 'setAdapter' 40 * method. The adapter must implement the HeaderIndexer interface. If no adapter 41 * is specified, the class will try to extract it from the ListView 42 * 3. The class registers itself as a listener to scroll events (OnScrollListener), if the 43 * ListView needs to receive scroll events, it must register its listener using 44 * this class' setOnScrollListener method. 45 * 4. Headers for the list view must be added before using the StickyHeaderListView 46 * 5. The implementation should register to listen to dataset changes. Right now this is not done 47 * since a change the dataset in a listview forces a call to OnScroll. The needed code is 48 * commented out. 49 */ 50 public class StickyHeaderListView extends FrameLayout implements OnScrollListener { 51 52 private static final String TAG = "StickyHeaderListView"; 53 protected boolean mChildViewsCreated = false; 54 protected boolean mDoHeaderReset = false; 55 56 protected Context mContext = null; 57 protected Adapter mAdapter = null; 58 protected HeaderIndexer mIndexer = null; 59 protected HeaderHeightListener mHeaderHeightListener = null; 60 protected View mStickyHeader = null; 61 protected View mNonessentialHeader = null; // A invisible header used when a section has no header 62 protected ListView mListView = null; 63 protected ListView.OnScrollListener mListener = null; 64 65 private int mSeparatorWidth; 66 private View mSeparatorView; 67 private int mLastStickyHeaderHeight = 0; 68 69 // This code is needed only if dataset changes do not force a call to OnScroll 70 // protected DataSetObserver mListDataObserver = null; 71 72 73 protected int mCurrentSectionPos = -1; // Position of section that has its header on the 74 // top of the view 75 protected int mNextSectionPosition = -1; // Position of next section's header 76 protected int mListViewHeadersCount = 0; 77 78 /** 79 * Interface that must be implemented by the ListView adapter to provide headers locations 80 * and number of items under each header. 81 * 82 */ 83 public interface HeaderIndexer { 84 /** 85 * Calculates the position of the header of a specific item in the adapter's data set. 86 * For example: Assuming you have a list with albums and songs names: 87 * Album A, song 1, song 2, ...., song 10, Album B, song 1, ..., song 7. A call to 88 * this method with the position of song 5 in Album B, should return the position 89 * of Album B. 90 * @param position - Position of the item in the ListView dataset 91 * @return Position of header. -1 if the is no header 92 */ 93 getHeaderPositionFromItemPosition(int position)94 int getHeaderPositionFromItemPosition(int position); 95 96 /** 97 * Calculates the number of items in the section defined by the header (not including 98 * the header). 99 * For example: A list with albums and songs, the method should return 100 * the number of songs names (without the album name). 101 * 102 * @param headerPosition - the value returned by 'getHeaderPositionFromItemPosition' 103 * @return Number of items. -1 on error. 104 */ getHeaderItemsNumber(int headerPosition)105 int getHeaderItemsNumber(int headerPosition); 106 } 107 108 /*** 109 * 110 * Interface that is used to update the sticky header's height 111 * 112 */ 113 public interface HeaderHeightListener { 114 115 /*** 116 * Updated a change in the sticky header's size 117 * 118 * @param height - new height of sticky header 119 */ OnHeaderHeightChanged(int height)120 void OnHeaderHeightChanged(int height); 121 } 122 123 /** 124 * Sets the adapter to be used by the class to get views of headers 125 * 126 * @param adapter - The adapter. 127 */ 128 setAdapter(Adapter adapter)129 public void setAdapter(Adapter adapter) { 130 131 // This code is needed only if dataset changes do not force a call to 132 // OnScroll 133 // if (mAdapter != null && mListDataObserver != null) { 134 // mAdapter.unregisterDataSetObserver(mListDataObserver); 135 // } 136 137 if (adapter != null) { 138 mAdapter = adapter; 139 // This code is needed only if dataset changes do not force a call 140 // to OnScroll 141 // mAdapter.registerDataSetObserver(mListDataObserver); 142 } 143 } 144 145 /** 146 * Sets the indexer object (that implements the HeaderIndexer interface). 147 * 148 * @param indexer - The indexer. 149 */ 150 setIndexer(HeaderIndexer indexer)151 public void setIndexer(HeaderIndexer indexer) { 152 mIndexer = indexer; 153 } 154 155 /** 156 * Sets the list view that is displayed 157 * @param lv - The list view. 158 */ 159 setListView(ListView lv)160 public void setListView(ListView lv) { 161 mListView = lv; 162 mListView.setOnScrollListener(this); 163 mListViewHeadersCount = mListView.getHeaderViewsCount(); 164 } 165 166 /** 167 * Sets an external OnScroll listener. Since the StickyHeaderListView sets 168 * itself as the scroll events listener of the listview, this method allows 169 * the user to register another listener that will be called after this 170 * class listener is called. 171 * 172 * @param listener - The external listener. 173 */ setOnScrollListener(ListView.OnScrollListener listener)174 public void setOnScrollListener(ListView.OnScrollListener listener) { 175 mListener = listener; 176 } 177 setHeaderHeightListener(HeaderHeightListener listener)178 public void setHeaderHeightListener(HeaderHeightListener listener) { 179 mHeaderHeightListener = listener; 180 } 181 182 // This code is needed only if dataset changes do not force a call to OnScroll 183 // protected void createDataListener() { 184 // mListDataObserver = new DataSetObserver() { 185 // @Override 186 // public void onChanged() { 187 // onDataChanged(); 188 // } 189 // }; 190 // } 191 192 /** 193 * Constructor 194 * 195 * @param context - application context. 196 * @param attrs - layout attributes. 197 */ StickyHeaderListView(Context context, AttributeSet attrs)198 public StickyHeaderListView(Context context, AttributeSet attrs) { 199 super(context, attrs); 200 mContext = context; 201 // This code is needed only if dataset changes do not force a call to OnScroll 202 // createDataListener(); 203 } 204 205 /** 206 * Scroll status changes listener 207 * 208 * @param view - the scrolled view 209 * @param scrollState - new scroll state. 210 */ 211 @Override onScrollStateChanged(AbsListView view, int scrollState)212 public void onScrollStateChanged(AbsListView view, int scrollState) { 213 if (mListener != null) { 214 mListener.onScrollStateChanged(view, scrollState); 215 } 216 } 217 218 /** 219 * Scroll events listener 220 * 221 * @param view - the scrolled view 222 * @param firstVisibleItem - the index (in the list's adapter) of the top 223 * visible item. 224 * @param visibleItemCount - the number of visible items in the list 225 * @param totalItemCount - the total number items in the list 226 */ 227 @Override onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)228 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 229 int totalItemCount) { 230 231 updateStickyHeader(firstVisibleItem); 232 233 if (mListener != null) { 234 mListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); 235 } 236 } 237 238 /** 239 * Sets a separator below the sticky header, which will be visible while the sticky header 240 * is not scrolling up. 241 * @param color - color of separator 242 * @param width - width in pixels of separator 243 */ setHeaderSeparator(int color, int width)244 public void setHeaderSeparator(int color, int width) { 245 mSeparatorView = new View(mContext); 246 ViewGroup.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, 247 width, Gravity.TOP); 248 mSeparatorView.setLayoutParams(params); 249 mSeparatorView.setBackgroundColor(color); 250 mSeparatorWidth = width; 251 this.addView(mSeparatorView); 252 } 253 updateStickyHeader(int firstVisibleItem)254 protected void updateStickyHeader(int firstVisibleItem) { 255 256 // Try to make sure we have an adapter to work with (may not succeed). 257 if (mAdapter == null && mListView != null) { 258 setAdapter(mListView.getAdapter()); 259 } 260 261 firstVisibleItem -= mListViewHeadersCount; 262 if (mAdapter != null && mIndexer != null && mDoHeaderReset) { 263 264 // Get the section header position 265 int sectionSize = 0; 266 int sectionPos = mIndexer.getHeaderPositionFromItemPosition(firstVisibleItem); 267 268 // New section - set it in the header view 269 boolean newView = false; 270 if (sectionPos != mCurrentSectionPos) { 271 272 // No header for current position , use the nonessential invisible one, hide the separator 273 if (sectionPos == -1) { 274 sectionSize = 0; 275 this.removeView(mStickyHeader); 276 mStickyHeader = mNonessentialHeader; 277 if (mSeparatorView != null) { 278 mSeparatorView.setVisibility(View.GONE); 279 } 280 newView = true; 281 } else { 282 // Create a copy of the header view to show on top 283 sectionSize = mIndexer.getHeaderItemsNumber(sectionPos); 284 View v = mAdapter.getView(sectionPos + mListViewHeadersCount, null, mListView); 285 v.measure(MeasureSpec.makeMeasureSpec(mListView.getWidth(), 286 MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mListView.getHeight(), 287 MeasureSpec.AT_MOST)); 288 this.removeView(mStickyHeader); 289 mStickyHeader = v; 290 newView = true; 291 } 292 mCurrentSectionPos = sectionPos; 293 mNextSectionPosition = sectionSize + sectionPos + 1; 294 } 295 296 297 // Do transitions 298 // If position of bottom of last item in a section is smaller than the height of the 299 // sticky header - shift drawable of header. 300 if (mStickyHeader != null) { 301 int sectionLastItemPosition = mNextSectionPosition - firstVisibleItem - 1; 302 int stickyHeaderHeight = mStickyHeader.getHeight(); 303 if (stickyHeaderHeight == 0) { 304 stickyHeaderHeight = mStickyHeader.getMeasuredHeight(); 305 } 306 307 // Update new header height 308 if (mHeaderHeightListener != null && 309 mLastStickyHeaderHeight != stickyHeaderHeight) { 310 mLastStickyHeaderHeight = stickyHeaderHeight; 311 mHeaderHeightListener.OnHeaderHeightChanged(stickyHeaderHeight); 312 } 313 314 View SectionLastView = mListView.getChildAt(sectionLastItemPosition); 315 if (SectionLastView != null && SectionLastView.getBottom() <= stickyHeaderHeight) { 316 int lastViewBottom = SectionLastView.getBottom(); 317 mStickyHeader.setTranslationY(lastViewBottom - stickyHeaderHeight); 318 if (mSeparatorView != null) { 319 mSeparatorView.setVisibility(View.GONE); 320 } 321 } else if (stickyHeaderHeight != 0) { 322 mStickyHeader.setTranslationY(0); 323 if (mSeparatorView != null && !mStickyHeader.equals(mNonessentialHeader)) { 324 mSeparatorView.setVisibility(View.VISIBLE); 325 } 326 } 327 if (newView) { 328 mStickyHeader.setVisibility(View.INVISIBLE); 329 this.addView(mStickyHeader); 330 if (mSeparatorView != null && !mStickyHeader.equals(mNonessentialHeader)){ 331 FrameLayout.LayoutParams params = 332 new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, 333 mSeparatorWidth); 334 params.setMargins(0, mStickyHeader.getMeasuredHeight(), 0, 0); 335 mSeparatorView.setLayoutParams(params); 336 mSeparatorView.setVisibility(View.VISIBLE); 337 } 338 mStickyHeader.setVisibility(View.VISIBLE); 339 } 340 } 341 } 342 } 343 344 @Override onFinishInflate()345 protected void onFinishInflate() { 346 super.onFinishInflate(); 347 if (!mChildViewsCreated) { 348 setChildViews(); 349 } 350 mDoHeaderReset = true; 351 } 352 353 @Override onAttachedToWindow()354 protected void onAttachedToWindow() { 355 super.onAttachedToWindow(); 356 if (!mChildViewsCreated) { 357 setChildViews(); 358 } 359 mDoHeaderReset = true; 360 } 361 362 363 // Resets the sticky header when the adapter data set was changed 364 // This code is needed only if dataset changes do not force a call to OnScroll 365 // protected void onDataChanged() { 366 // Should do a call to updateStickyHeader if needed 367 // } 368 setChildViews()369 private void setChildViews() { 370 371 // Find a child ListView (if any) 372 int iChildNum = getChildCount(); 373 for (int i = 0; i < iChildNum; i++) { 374 Object v = getChildAt(i); 375 if (v instanceof ListView) { 376 setListView((ListView) v); 377 } 378 } 379 380 // No child ListView - add one 381 if (mListView == null) { 382 setListView(new ListView(mContext)); 383 } 384 385 // Create a nonessential view , it will be used in case a section has no header 386 mNonessentialHeader = new View (mContext); 387 ViewGroup.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, 388 1, Gravity.TOP); 389 mNonessentialHeader.setLayoutParams(params); 390 mNonessentialHeader.setBackgroundColor(Color.TRANSPARENT); 391 392 mChildViewsCreated = true; 393 } 394 395 } 396