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