StickyHeaderListView.java revision 37f12e5cee7ed2d354e9366bd6d8e15d1a934f2a
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 public void setAdapter(Adapter a) { 110 111 // This code is needed only if dataset changes do not force a call to OnScroll 112 // if (mAdapter != null && mListDataObserver != null) { 113 // mAdapter.unregisterDataSetObserver(mListDataObserver); 114 // } 115 116 if (a != null) { 117 mAdapter = a; 118 // This code is needed only if dataset changes do not force a call to OnScroll 119 //mAdapter.registerDataSetObserver(mListDataObserver); 120 } 121 } 122 123 public void setIndexer(HeaderIndexer i) { 124 mIndexer = i; 125 } 126 127 public void setListView(ListView lv) { 128 mListView = lv; 129 mListView.setOnScrollListener(this); 130 mListViewHeadersCount = mListView.getHeaderViewsCount(); 131 } 132 133 public void setOnScrollListener(ListView.OnScrollListener l) { 134 mListener = l; 135 } 136 137 // This code is needed only if dataset changes do not force a call to OnScroll 138 // protected void createDataListener() { 139 // mListDataObserver = new DataSetObserver() { 140 // @Override 141 // public void onChanged() { 142 // onDataChanged(); 143 // } 144 // }; 145 // } 146 147 public StickyHeaderListView(Context context, AttributeSet attrs) { 148 super(context, attrs); 149 mContext = context; 150 // This code is needed only if dataset changes do not force a call to OnScroll 151 // createDataListener(); 152 } 153 154 public void onScrollStateChanged(AbsListView view, int scrollState) { 155 if (mListener != null) { 156 mListener.onScrollStateChanged(view, scrollState); 157 } 158 } 159 160 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 161 int totalItemCount) { 162 163 updateStickyHeader(firstVisibleItem); 164 165 if (mListener != null) { 166 mListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); 167 } 168 } 169 170 protected void updateStickyHeader(int firstVisibleItem) { 171 172 // Try to make sure we have an adapter to work with (may not succeed). 173 if (mAdapter == null && mListView != null) { 174 setAdapter(mListView.getAdapter()); 175 } 176 177 firstVisibleItem -= mListViewHeadersCount; 178 if (mAdapter != null && mIndexer != null && mDoHeaderReset) { 179 180 // Get the section header position 181 int sectionSize = 0; 182 int sectionPos = mIndexer.getHeaderPositionFromItemPosition(firstVisibleItem); 183 184 // New section - set it in the header view 185 boolean newView = false; 186 if (sectionPos != mCurrentSectionPos) { 187 188 // No header for current position , use the dummy invisible one 189 if (sectionPos == -1) { 190 sectionSize = 0; 191 this.removeView(mStickyHeader); 192 mStickyHeader = mDummyHeader; 193 newView = true; 194 } else { 195 // Create a copy of the header view to show on top 196 sectionSize = mIndexer.getHeaderItemsNumber(sectionPos); 197 View v = mAdapter.getView(sectionPos + mListViewHeadersCount, null, mListView); 198 v.measure(MeasureSpec.makeMeasureSpec(mListView.getWidth(), 199 MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mListView.getHeight(), 200 MeasureSpec.AT_MOST)); 201 this.removeView(mStickyHeader); 202 mStickyHeader = v; 203 newView = true; 204 } 205 mCurrentSectionPos = sectionPos; 206 mNextSectionPosition = sectionSize + sectionPos + 1; 207 } 208 209 210 // Do transitions 211 // If position of bottom of last item in a section is smaller than the height of the 212 // sticky header - shift drawable of header. 213 if (mStickyHeader != null) { 214 int sectionLastItemPosition = mNextSectionPosition - firstVisibleItem - 1; 215 int stickyHeaderHeight = mStickyHeader.getHeight(); 216 if (stickyHeaderHeight == 0) { 217 stickyHeaderHeight = mStickyHeader.getMeasuredHeight(); 218 } 219 View SectionLastView = mListView.getChildAt(sectionLastItemPosition); 220 if (SectionLastView != null && SectionLastView.getBottom() <= stickyHeaderHeight) { 221 int lastViewBottom = SectionLastView.getBottom(); 222 mStickyHeader.setTranslationY(lastViewBottom - stickyHeaderHeight); 223 } else if (stickyHeaderHeight != 0) { 224 mStickyHeader.setTranslationY(0); 225 } 226 if (newView) { 227 mStickyHeader.setVisibility(View.INVISIBLE); 228 this.addView(mStickyHeader); 229 mStickyHeader.setVisibility(View.VISIBLE); 230 } 231 } 232 } 233 } 234 235 @Override 236 protected void onFinishInflate() { 237 super.onFinishInflate(); 238 if (!mChildViewsCreated) { 239 setChildViews(); 240 } 241 mDoHeaderReset = true; 242 } 243 244 @Override 245 protected void onAttachedToWindow() { 246 super.onAttachedToWindow(); 247 if (!mChildViewsCreated) { 248 setChildViews(); 249 } 250 mDoHeaderReset = true; 251 } 252 253 254 // Resets the sticky header when the adapter data set was changed 255 // This code is needed only if dataset changes do not force a call to OnScroll 256 // protected void onDataChanged() { 257 // Should do a call to updateStickyHeader if needed 258 // } 259 260 private void setChildViews() { 261 262 // Find a child ListView (if any) 263 int iChildNum = getChildCount(); 264 for (int i = 0; i < iChildNum; i++) { 265 Object v = getChildAt(i); 266 if (v instanceof ListView) { 267 setListView((ListView) v); 268 } 269 } 270 271 // No child ListView - add one 272 if (mListView == null) { 273 setListView(new ListView(mContext)); 274 } 275 276 // Create a dummy view , it will be used in case a section has no header 277 mDummyHeader = new View (mContext); 278 ViewGroup.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, 279 1, Gravity.TOP); 280 mDummyHeader.setLayoutParams(params); 281 mDummyHeader.setBackgroundColor(Color.TRANSPARENT); 282 283 mChildViewsCreated = true; 284 } 285 286} 287