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