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.graphics.Color;
21import android.util.AttributeSet;
22import android.view.Gravity;
23import android.view.View;
24import android.view.ViewGroup;
25import android.widget.AbsListView;
26import android.widget.AbsListView.OnScrollListener;
27import android.widget.Adapter;
28import android.widget.FrameLayout;
29import 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 */
50public 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 mDummyHeader = 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
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         */
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        */
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
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
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
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     */
174    public void setOnScrollListener(ListView.OnScrollListener listener) {
175        mListener = listener;
176    }
177
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     */
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
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
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     */
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
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 dummy invisible one, hide the separator
273                if (sectionPos == -1) {
274                    sectionSize = 0;
275                    this.removeView(mStickyHeader);
276                    mStickyHeader = mDummyHeader;
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(mDummyHeader)) {
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(mDummyHeader)){
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
345    protected void onFinishInflate() {
346        super.onFinishInflate();
347        if (!mChildViewsCreated) {
348            setChildViews();
349        }
350        mDoHeaderReset = true;
351    }
352
353    @Override
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
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 dummy view , it will be used in case a section has no header
386        mDummyHeader = new View (mContext);
387        ViewGroup.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
388                1, Gravity.TOP);
389        mDummyHeader.setLayoutParams(params);
390        mDummyHeader.setBackgroundColor(Color.TRANSPARENT);
391
392        mChildViewsCreated = true;
393    }
394
395}
396