| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.calendar; |
| |
| import android.content.Context; |
| import android.graphics.Color; |
| import android.util.AttributeSet; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.AbsListView; |
| import android.widget.AbsListView.OnScrollListener; |
| import android.widget.Adapter; |
| import android.widget.FrameLayout; |
| import android.widget.ListView; |
| |
| /** |
| * Implements a ListView class with a sticky header at the top. The header is |
| * per section and it is pinned to the top as long as its section is at the top |
| * of the view. If it is not, the header slides up or down (depending on the |
| * scroll movement) and the header of the current section slides to the top. |
| * Notes: |
| * 1. The class uses the first available child ListView as the working |
| * ListView. If no ListView child exists, the class will create a default one. |
| * 2. The ListView's adapter must be passed to this class using the 'setAdapter' |
| * method. The adapter must implement the HeaderIndexer interface. If no adapter |
| * is specified, the class will try to extract it from the ListView |
| * 3. The class registers itself as a listener to scroll events (OnScrollListener), if the |
| * ListView needs to receive scroll events, it must register its listener using |
| * this class' setOnScrollListener method. |
| * 4. Headers for the list view must be added before using the StickyHeaderListView |
| * 5. The implementation should register to listen to dataset changes. Right now this is not done |
| * since a change the dataset in a listview forces a call to OnScroll. The needed code is |
| * commented out. |
| */ |
| public class StickyHeaderListView extends FrameLayout implements OnScrollListener { |
| |
| private static final String TAG = "StickyHeaderListView"; |
| protected boolean mChildViewsCreated = false; |
| protected boolean mDoHeaderReset = false; |
| |
| protected Context mContext = null; |
| protected Adapter mAdapter = null; |
| protected HeaderIndexer mIndexer = null; |
| protected HeaderHeightListener mHeaderHeightListener = null; |
| protected View mStickyHeader = null; |
| protected View mDummyHeader = null; // A invisible header used when a section has no header |
| protected ListView mListView = null; |
| protected ListView.OnScrollListener mListener = null; |
| |
| private int mSeparatorWidth; |
| private View mSeparatorView; |
| private int mLastStickyHeaderHeight = 0; |
| |
| // This code is needed only if dataset changes do not force a call to OnScroll |
| // protected DataSetObserver mListDataObserver = null; |
| |
| |
| protected int mCurrentSectionPos = -1; // Position of section that has its header on the |
| // top of the view |
| protected int mNextSectionPosition = -1; // Position of next section's header |
| protected int mListViewHeadersCount = 0; |
| |
| /** |
| * Interface that must be implemented by the ListView adapter to provide headers locations |
| * and number of items under each header. |
| * |
| */ |
| public interface HeaderIndexer { |
| /** |
| * Calculates the position of the header of a specific item in the adapter's data set. |
| * For example: Assuming you have a list with albums and songs names: |
| * Album A, song 1, song 2, ...., song 10, Album B, song 1, ..., song 7. A call to |
| * this method with the position of song 5 in Album B, should return the position |
| * of Album B. |
| * @param position - Position of the item in the ListView dataset |
| * @return Position of header. -1 if the is no header |
| */ |
| |
| int getHeaderPositionFromItemPosition(int position); |
| |
| /** |
| * Calculates the number of items in the section defined by the header (not including |
| * the header). |
| * For example: A list with albums and songs, the method should return |
| * the number of songs names (without the album name). |
| * |
| * @param headerPosition - the value returned by 'getHeaderPositionFromItemPosition' |
| * @return Number of items. -1 on error. |
| */ |
| int getHeaderItemsNumber(int headerPosition); |
| } |
| |
| /*** |
| * |
| * Interface that is used to update the sticky header's height |
| * |
| */ |
| public interface HeaderHeightListener { |
| |
| /*** |
| * Updated a change in the sticky header's size |
| * |
| * @param height - new height of sticky header |
| */ |
| void OnHeaderHeightChanged(int height); |
| } |
| |
| /** |
| * Sets the adapter to be used by the class to get views of headers |
| * |
| * @param adapter - The adapter. |
| */ |
| |
| public void setAdapter(Adapter adapter) { |
| |
| // This code is needed only if dataset changes do not force a call to |
| // OnScroll |
| // if (mAdapter != null && mListDataObserver != null) { |
| // mAdapter.unregisterDataSetObserver(mListDataObserver); |
| // } |
| |
| if (adapter != null) { |
| mAdapter = adapter; |
| // This code is needed only if dataset changes do not force a call |
| // to OnScroll |
| // mAdapter.registerDataSetObserver(mListDataObserver); |
| } |
| } |
| |
| /** |
| * Sets the indexer object (that implements the HeaderIndexer interface). |
| * |
| * @param indexer - The indexer. |
| */ |
| |
| public void setIndexer(HeaderIndexer indexer) { |
| mIndexer = indexer; |
| } |
| |
| /** |
| * Sets the list view that is displayed |
| * @param lv - The list view. |
| */ |
| |
| public void setListView(ListView lv) { |
| mListView = lv; |
| mListView.setOnScrollListener(this); |
| mListViewHeadersCount = mListView.getHeaderViewsCount(); |
| } |
| |
| /** |
| * Sets an external OnScroll listener. Since the StickyHeaderListView sets |
| * itself as the scroll events listener of the listview, this method allows |
| * the user to register another listener that will be called after this |
| * class listener is called. |
| * |
| * @param listener - The external listener. |
| */ |
| public void setOnScrollListener(ListView.OnScrollListener listener) { |
| mListener = listener; |
| } |
| |
| public void setHeaderHeightListener(HeaderHeightListener listener) { |
| mHeaderHeightListener = listener; |
| } |
| |
| // This code is needed only if dataset changes do not force a call to OnScroll |
| // protected void createDataListener() { |
| // mListDataObserver = new DataSetObserver() { |
| // @Override |
| // public void onChanged() { |
| // onDataChanged(); |
| // } |
| // }; |
| // } |
| |
| /** |
| * Constructor |
| * |
| * @param context - application context. |
| * @param attrs - layout attributes. |
| */ |
| public StickyHeaderListView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| mContext = context; |
| // This code is needed only if dataset changes do not force a call to OnScroll |
| // createDataListener(); |
| } |
| |
| /** |
| * Scroll status changes listener |
| * |
| * @param view - the scrolled view |
| * @param scrollState - new scroll state. |
| */ |
| @Override |
| public void onScrollStateChanged(AbsListView view, int scrollState) { |
| if (mListener != null) { |
| mListener.onScrollStateChanged(view, scrollState); |
| } |
| } |
| |
| /** |
| * Scroll events listener |
| * |
| * @param view - the scrolled view |
| * @param firstVisibleItem - the index (in the list's adapter) of the top |
| * visible item. |
| * @param visibleItemCount - the number of visible items in the list |
| * @param totalItemCount - the total number items in the list |
| */ |
| @Override |
| public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, |
| int totalItemCount) { |
| |
| updateStickyHeader(firstVisibleItem); |
| |
| if (mListener != null) { |
| mListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); |
| } |
| } |
| |
| /** |
| * Sets a separator below the sticky header, which will be visible while the sticky header |
| * is not scrolling up. |
| * @param color - color of separator |
| * @param width - width in pixels of separator |
| */ |
| public void setHeaderSeparator(int color, int width) { |
| mSeparatorView = new View(mContext); |
| ViewGroup.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, |
| width, Gravity.TOP); |
| mSeparatorView.setLayoutParams(params); |
| mSeparatorView.setBackgroundColor(color); |
| mSeparatorWidth = width; |
| this.addView(mSeparatorView); |
| } |
| |
| protected void updateStickyHeader(int firstVisibleItem) { |
| |
| // Try to make sure we have an adapter to work with (may not succeed). |
| if (mAdapter == null && mListView != null) { |
| setAdapter(mListView.getAdapter()); |
| } |
| |
| firstVisibleItem -= mListViewHeadersCount; |
| if (mAdapter != null && mIndexer != null && mDoHeaderReset) { |
| |
| // Get the section header position |
| int sectionSize = 0; |
| int sectionPos = mIndexer.getHeaderPositionFromItemPosition(firstVisibleItem); |
| |
| // New section - set it in the header view |
| boolean newView = false; |
| if (sectionPos != mCurrentSectionPos) { |
| |
| // No header for current position , use the dummy invisible one, hide the separator |
| if (sectionPos == -1) { |
| sectionSize = 0; |
| this.removeView(mStickyHeader); |
| mStickyHeader = mDummyHeader; |
| if (mSeparatorView != null) { |
| mSeparatorView.setVisibility(View.GONE); |
| } |
| newView = true; |
| } else { |
| // Create a copy of the header view to show on top |
| sectionSize = mIndexer.getHeaderItemsNumber(sectionPos); |
| View v = mAdapter.getView(sectionPos + mListViewHeadersCount, null, mListView); |
| v.measure(MeasureSpec.makeMeasureSpec(mListView.getWidth(), |
| MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mListView.getHeight(), |
| MeasureSpec.AT_MOST)); |
| this.removeView(mStickyHeader); |
| mStickyHeader = v; |
| newView = true; |
| } |
| mCurrentSectionPos = sectionPos; |
| mNextSectionPosition = sectionSize + sectionPos + 1; |
| } |
| |
| |
| // Do transitions |
| // If position of bottom of last item in a section is smaller than the height of the |
| // sticky header - shift drawable of header. |
| if (mStickyHeader != null) { |
| int sectionLastItemPosition = mNextSectionPosition - firstVisibleItem - 1; |
| int stickyHeaderHeight = mStickyHeader.getHeight(); |
| if (stickyHeaderHeight == 0) { |
| stickyHeaderHeight = mStickyHeader.getMeasuredHeight(); |
| } |
| |
| // Update new header height |
| if (mHeaderHeightListener != null && |
| mLastStickyHeaderHeight != stickyHeaderHeight) { |
| mLastStickyHeaderHeight = stickyHeaderHeight; |
| mHeaderHeightListener.OnHeaderHeightChanged(stickyHeaderHeight); |
| } |
| |
| View SectionLastView = mListView.getChildAt(sectionLastItemPosition); |
| if (SectionLastView != null && SectionLastView.getBottom() <= stickyHeaderHeight) { |
| int lastViewBottom = SectionLastView.getBottom(); |
| mStickyHeader.setTranslationY(lastViewBottom - stickyHeaderHeight); |
| if (mSeparatorView != null) { |
| mSeparatorView.setVisibility(View.GONE); |
| } |
| } else if (stickyHeaderHeight != 0) { |
| mStickyHeader.setTranslationY(0); |
| if (mSeparatorView != null && !mStickyHeader.equals(mDummyHeader)) { |
| mSeparatorView.setVisibility(View.VISIBLE); |
| } |
| } |
| if (newView) { |
| mStickyHeader.setVisibility(View.INVISIBLE); |
| this.addView(mStickyHeader); |
| if (mSeparatorView != null && !mStickyHeader.equals(mDummyHeader)){ |
| FrameLayout.LayoutParams params = |
| new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, |
| mSeparatorWidth); |
| params.setMargins(0, mStickyHeader.getMeasuredHeight(), 0, 0); |
| mSeparatorView.setLayoutParams(params); |
| mSeparatorView.setVisibility(View.VISIBLE); |
| } |
| mStickyHeader.setVisibility(View.VISIBLE); |
| } |
| } |
| } |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| if (!mChildViewsCreated) { |
| setChildViews(); |
| } |
| mDoHeaderReset = true; |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| if (!mChildViewsCreated) { |
| setChildViews(); |
| } |
| mDoHeaderReset = true; |
| } |
| |
| |
| // Resets the sticky header when the adapter data set was changed |
| // This code is needed only if dataset changes do not force a call to OnScroll |
| // protected void onDataChanged() { |
| // Should do a call to updateStickyHeader if needed |
| // } |
| |
| private void setChildViews() { |
| |
| // Find a child ListView (if any) |
| int iChildNum = getChildCount(); |
| for (int i = 0; i < iChildNum; i++) { |
| Object v = getChildAt(i); |
| if (v instanceof ListView) { |
| setListView((ListView) v); |
| } |
| } |
| |
| // No child ListView - add one |
| if (mListView == null) { |
| setListView(new ListView(mContext)); |
| } |
| |
| // Create a dummy view , it will be used in case a section has no header |
| mDummyHeader = new View (mContext); |
| ViewGroup.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, |
| 1, Gravity.TOP); |
| mDummyHeader.setLayoutParams(params); |
| mDummyHeader.setBackgroundColor(Color.TRANSPARENT); |
| |
| mChildViewsCreated = true; |
| } |
| |
| } |