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