blob: 981e7af7577e96e1f01770f506033475c3ab68ef [file] [log] [blame]
/*
* 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 mNonessentialHeader = 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 nonessential invisible one, hide the separator
if (sectionPos == -1) {
sectionSize = 0;
this.removeView(mStickyHeader);
mStickyHeader = mNonessentialHeader;
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(mNonessentialHeader)) {
mSeparatorView.setVisibility(View.VISIBLE);
}
}
if (newView) {
mStickyHeader.setVisibility(View.INVISIBLE);
this.addView(mStickyHeader);
if (mSeparatorView != null && !mStickyHeader.equals(mNonessentialHeader)){
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 nonessential view , it will be used in case a section has no header
mNonessentialHeader = new View (mContext);
ViewGroup.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
1, Gravity.TOP);
mNonessentialHeader.setLayoutParams(params);
mNonessentialHeader.setBackgroundColor(Color.TRANSPARENT);
mChildViewsCreated = true;
}
}