Home | History | Annotate | Download | only in calendar
      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 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