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