diff --git a/library/src/com/hb/views/PinnedSectionListView.java b/library/src/com/hb/views/PinnedSectionListView.java index eeb41dc..9416a87 100644 --- a/library/src/com/hb/views/PinnedSectionListView.java +++ b/library/src/com/hb/views/PinnedSectionListView.java @@ -34,38 +34,48 @@ import android.widget.ListView; import android.widget.SectionIndexer; -import com.hb.views.pinnedsection.BuildConfig; +//import com.hb.views.pinnedsection.BuildConfig; /** * ListView capable to pin views at its top while the rest is still scrolled. + * Pinned view's height should large than Body's height. */ public class PinnedSectionListView extends ListView { - //-- inner classes - - /** List adapter to be implemented for being used with PinnedSectionListView adapter. */ - public static interface PinnedSectionListAdapter extends ListAdapter { - /** This method shall return 'true' if views of given type has to be pinned. */ - boolean isItemViewTypePinned(int viewType); - } - - /** Wrapper class for pinned section view and its position in the list. */ - static class PinnedViewShadow { - public View view; - public int position; - public long id; - } - - //-- class fields - - /** Default change observer. */ - private final DataSetObserver mDataSetObserver = new DataSetObserver() { - @Override public void onChanged() { - destroyPinnedShadow(); - }; - @Override public void onInvalidated() { - destroyPinnedShadow(); - } + // -- inner classes + + /** + * List adapter to be implemented for being used with PinnedSectionListView + * adapter. + */ + public static interface PinnedSectionListAdapter extends ListAdapter { + /** + * This method shall return 'true' if views of given type has to be + * pinned. + */ + boolean isItemViewTypePinned(int viewType); + } + + /** Wrapper class for pinned section view and its position in the list. */ + static class PinnedViewShadow { + public View view; + public int position; + public long id; + } + + // -- class fields + + /** Default change observer. */ + private final DataSetObserver mDataSetObserver = new DataSetObserver() { + @Override + public void onChanged() { + destroyPinnedShadow(); + }; + + @Override + public void onInvalidated() { + destroyPinnedShadow(); + } }; // fields used for handling touch events @@ -84,95 +94,164 @@ static class PinnedViewShadow { /** shadow instance with a pinned view, can be null. */ PinnedViewShadow mPinnedShadow; - /** Pinned view Y-translation. We use it to stick pinned view to the next section. */ + /** + * Pinned view Y-translation. We use it to stick pinned view to the next + * section. + */ int mTranslateY; - /** Scroll listener which does the magic */ - private final OnScrollListener mOnScrollListener = new OnScrollListener() { - @Override public void onScrollStateChanged(AbsListView view, int scrollState) { - if (mDelegateOnScrollListener != null) { // delegate - mDelegateOnScrollListener.onScrollStateChanged(view, scrollState); - } - } + /** Scroll listener which does the magic */ + private final OnScrollListener mOnScrollListener = new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (mDelegateOnScrollListener != null) { // delegate + mDelegateOnScrollListener.onScrollStateChanged(view, scrollState); + } + } @SuppressLint("NewApi") @Override - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - - if (mDelegateOnScrollListener != null) { // delegate - mDelegateOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); - } - - // get expected adapter or fail - PinnedSectionListAdapter adapter = (PinnedSectionListAdapter) view.getAdapter(); - if (adapter == null || visibleItemCount == 0) return; // nothing to do - - int visibleSectionPosition = findFirstVisibleSectionPosition(firstVisibleItem, visibleItemCount); - if (visibleSectionPosition == -1) { // there is no visible sections - - // try to find invisible view - int currentSectionPosition = findCurrentSectionPosition(firstVisibleItem); - if (currentSectionPosition == -1) return; // exit here, we have no sections - // else, we have a section to pin - - if (mPinnedShadow != null) { - if (mPinnedShadow.position == currentSectionPosition) { - // this section is already pinned - mTranslateY = 0; - return; // exit, as pinned section is the current one - } else { - // we have a pinned section, which differs from the current - destroyPinnedShadow(); // destroy old pinned view - } - } - - // create new pinned view for candidate - createPinnedShadow(currentSectionPosition); - return; // exit, as we have created a pinned candidate already - } - - int visibleSectionTop = view.getChildAt(visibleSectionPosition - firstVisibleItem).getTop(); - int topBorder = getListPaddingTop(); - - if (mPinnedShadow == null) { - if (visibleSectionTop < topBorder) { - createPinnedShadow(visibleSectionPosition); - } - - } else { - - if (visibleSectionPosition == mPinnedShadow.position) { - if (visibleSectionTop > topBorder) { - destroyPinnedShadow(); - visibleSectionPosition = findCurrentSectionPosition(visibleSectionPosition - 1); - if (visibleSectionPosition > -1) { - createPinnedShadow(visibleSectionPosition); - int translateY = visibleSectionTop - topBorder - mPinnedShadow.view.getHeight(); - if (translateY > 0) translateY = 0; - mTranslateY = translateY; - } - } - - } else { - - int pinnedSectionBottom = topBorder + mPinnedShadow.view.getHeight(); - if (visibleSectionTop < pinnedSectionBottom) { - if (visibleSectionTop < topBorder) { - destroyPinnedShadow(); - createPinnedShadow(visibleSectionPosition); - } else { - mTranslateY = visibleSectionTop - pinnedSectionBottom; - } - } else { - mTranslateY = 0; - } - } - } - } - }; - - //-- class methods + public void onScroll(AbsListView view, int firstVisibleItemPosition, int visibleItemCount, + int totalItemCount) { + + if (mDelegateOnScrollListener != null) { // delegate + mDelegateOnScrollListener.onScroll(view, firstVisibleItemPosition, + visibleItemCount, totalItemCount); + } + + // get expected adapter or fail + PinnedSectionListAdapter adapter = (PinnedSectionListAdapter) view.getAdapter(); + if (adapter == null || visibleItemCount == 0) + return; // nothing to do + + int firstSectionPosition = findFirstSectionPositionInScreen(firstVisibleItemPosition, + visibleItemCount); + + if (firstSectionPosition == -1) { // there is no visible sections + + // try to find invisible view + int currentSectionPosition = findCurrentSectionPosition(firstVisibleItemPosition); + if (currentSectionPosition == -1) + return; // exit here, we have no sections + // else, we have a section to pin + + if (mPinnedShadow != null) { + if (mPinnedShadow.position == currentSectionPosition) { + // this section is already pinned + mTranslateY = 0; + return; // exit, as pinned section is the current one + } else { + // we have a pinned section, which differs from the + // current + destroyPinnedShadow(); // destroy old pinned view + } + } + + // create new pinned view for candidate + createPinnedShadow(currentSectionPosition); + return; // exit, as we have created a pinned candidate already + } + View firstSectionView = view + .getChildAt(firstSectionPosition - firstVisibleItemPosition); + int firstSectionTop = firstSectionView.getTop(); + int topBorder = getListPaddingTop(); + + if (mPinnedShadow == null) { + + if (firstSectionTop < topBorder) { + // only two sections instance > one's height we need + // create pinned shadow + int nextSectionPosition = findSectionPositionInScreen(firstVisibleItemPosition, + visibleItemCount, firstSectionPosition + 1); + boolean isCreat = true; + if (nextSectionPosition != -1) { + int nextSectionTop = view.getChildAt( + nextSectionPosition - firstVisibleItemPosition).getTop(); + if (nextSectionTop - firstSectionView.getTop() <= firstSectionView + .getHeight()) + isCreat = false; + } + if (isCreat) + createPinnedShadow(firstSectionPosition); + } else if (firstSectionTop > topBorder) { + int pinnedSectionBottom = topBorder + firstSectionView.getHeight(); + if (firstSectionTop < pinnedSectionBottom) { + View firstChild = getChildAt(0); + if (!adapter.isItemViewTypePinned(adapter + .getItemViewType(firstVisibleItemPosition)) + && firstChild.getHeight() > 0) { + int currentSectionPosition = findCurrentSectionPosition(firstVisibleItemPosition); + if (currentSectionPosition > -1) { + createPinnedShadow(currentSectionPosition, + firstSectionView.getTop() - pinnedSectionBottom); + } + } + } + } + + } else { + + if (firstSectionPosition == mPinnedShadow.position) { + if (firstSectionTop > topBorder) { + destroyPinnedShadow(); + int prevSectionPosition = findCurrentSectionPosition(firstSectionPosition - 1); + if (prevSectionPosition > -1) { + createPinnedShadow(prevSectionPosition); + int translateY = firstSectionTop - topBorder + - mPinnedShadow.view.getHeight(); + if (translateY > 0) + translateY = 0; + mTranslateY = translateY; + } + } else if (firstSectionTop <= topBorder) { + // in fast scroll mode to the top, the first section top + // is uncollect. + mTranslateY = 0; + } + + } else { + + int pinnedSectionBottom = topBorder + firstSectionView.getHeight(); + if (firstSectionTop < pinnedSectionBottom) { + if (firstSectionTop < topBorder) { + // only two sections instance > one's height we need + // create pinned shadow + int nextSectionPosition = findSectionPositionInScreen( + firstVisibleItemPosition, visibleItemCount, + firstSectionPosition + 1); + boolean reCreat = true; + if (nextSectionPosition != -1) { + int nextSectionTop = view.getChildAt( + nextSectionPosition - firstVisibleItemPosition).getTop(); + if (nextSectionTop - firstSectionView.getTop() <= firstSectionView + .getHeight()) + reCreat = false; + } + destroyPinnedShadow(); + if (reCreat) { + createPinnedShadow(firstSectionPosition); + } + } else { + mTranslateY = firstSectionTop - pinnedSectionBottom; + // in fast scroll mode to the bottom, the first + // section top is uncollect. + int currentSectionPosition = findCurrentSectionPosition(firstSectionPosition - 1); + if (mPinnedShadow.position != currentSectionPosition) { + destroyPinnedShadow(); + createPinnedShadow(currentSectionPosition, mTranslateY); + } + } + } else { + mTranslateY = 0; + } + } + } + } + }; + + // -- class methods public PinnedSectionListView(Context context, AttributeSet attrs) { super(context, attrs); @@ -188,116 +267,147 @@ private void initView() { setOnScrollListener(mOnScrollListener); } - /** Create shadow wrapper with a pinned view for a view at given position */ - private void createPinnedShadow(int position) { - - // try to recycle shadow - PinnedViewShadow pinnedShadow = mRecycleShadow; - mRecycleShadow = null; - - // create new shadow, if needed - if (pinnedShadow == null) pinnedShadow = new PinnedViewShadow(); - // request new view using recycled view, if such - View pinnedView = getAdapter().getView(position, pinnedShadow.view, PinnedSectionListView.this); - - // read layout parameters - LayoutParams layoutParams = (LayoutParams) pinnedView.getLayoutParams(); - if (layoutParams == null) { // create default layout params - layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - } - - int heightMode = MeasureSpec.getMode(layoutParams.height); - int heightSize = MeasureSpec.getSize(layoutParams.height); - - if (heightMode == MeasureSpec.UNSPECIFIED) heightMode = MeasureSpec.EXACTLY; - - int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom(); - if (heightSize > maxHeight) heightSize = maxHeight; - - // measure & layout - int ws = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() - getListPaddingRight(), MeasureSpec.EXACTLY); - int hs = MeasureSpec.makeMeasureSpec(heightSize, heightMode); - pinnedView.measure(ws, hs); - pinnedView.layout(0, 0, pinnedView.getMeasuredWidth(), pinnedView.getMeasuredHeight()); - mTranslateY = 0; - - // initialize pinned shadow - pinnedShadow.view = pinnedView; - pinnedShadow.position = position; - pinnedShadow.id = getAdapter().getItemId(position); - - // store pinned shadow - mPinnedShadow = pinnedShadow; - } - - /** Destroy shadow wrapper for currently pinned view */ - private void destroyPinnedShadow() { - // keep shadow for being recycled later - mRecycleShadow = mPinnedShadow; - mPinnedShadow = null; - } - - private int findFirstVisibleSectionPosition(int firstVisibleItem, int visibleItemCount) { - PinnedSectionListAdapter adapter = (PinnedSectionListAdapter) getAdapter(); - for (int childIndex = 0; childIndex < visibleItemCount; childIndex++) { - int position = firstVisibleItem + childIndex; - int viewType = adapter.getItemViewType(position); - if (adapter.isItemViewTypePinned(viewType)) return position; - } - return -1; - } - - private int findCurrentSectionPosition(int fromPosition) { - PinnedSectionListAdapter adapter = (PinnedSectionListAdapter) getAdapter(); - - if (adapter instanceof SectionIndexer) { - // try fast way by asking section indexer - SectionIndexer indexer = (SectionIndexer) adapter; - int sectionPosition = indexer.getSectionForPosition(fromPosition); - int itemPosition = indexer.getPositionForSection(sectionPosition); - int typeView = adapter.getItemViewType(itemPosition); - if (adapter.isItemViewTypePinned(typeView)) { - return itemPosition; - } // else, no luck - } - - // try slow way by looking through to the next section item above - for (int position=fromPosition; position>=0; position--) { - int viewType = adapter.getItemViewType(position); - if (adapter.isItemViewTypePinned(viewType)) return position; - } - return -1; // no candidate found - } - - @Override - public void setOnScrollListener(OnScrollListener listener) { - if (listener == mOnScrollListener) { - super.setOnScrollListener(listener); - } else { - mDelegateOnScrollListener = listener; - } - } - - private boolean isPinnedViewTouched(View view, float x, float y) { - view.getHitRect(mTouchRect); - mTouchRect.top += mTranslateY; - mTouchRect.bottom += mTranslateY; - return mTouchRect.contains((int)x, (int)y); - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - super.onRestoreInstanceState(state); - - // restore pinned view after configuration change - post(new Runnable() { - @Override public void run() { - ListAdapter adapter = getAdapter(); + /** Create shadow wrapper with a pinned view for a view at given position */ + private void createPinnedShadow(int position) { + createPinnedShadow(position, 0); + } + + private void createPinnedShadow(int position, int translateY) { + if (position < 0 || mPinnedShadow != null) { + return; + } + + // try to recycle shadow + PinnedViewShadow pinnedShadow = mRecycleShadow; + mRecycleShadow = null; + + // create new shadow, if needed + if (pinnedShadow == null) + pinnedShadow = new PinnedViewShadow(); + // request new view using recycled view, if such + View pinnedView = getAdapter().getView(position, pinnedShadow.view, + PinnedSectionListView.this); + + // read layout parameters + LayoutParams layoutParams = (LayoutParams) pinnedView.getLayoutParams(); + if (layoutParams == null) { // create default layout params + layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + pinnedView.setLayoutParams(layoutParams); + } + + int heightMode = MeasureSpec.getMode(layoutParams.height); + int heightSize = MeasureSpec.getSize(layoutParams.height); + + if (heightMode == MeasureSpec.UNSPECIFIED) + heightMode = MeasureSpec.EXACTLY; + + int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom(); + if (heightSize > maxHeight) + heightSize = maxHeight; + + // measure & layout + int ws = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() + - getListPaddingRight(), MeasureSpec.EXACTLY); + int hs = MeasureSpec.makeMeasureSpec(heightSize, heightMode); + pinnedView.measure(ws, hs); + pinnedView.layout(0, 0, pinnedView.getMeasuredWidth(), pinnedView.getMeasuredHeight()); + + // initialize pinned shadow + pinnedShadow.view = pinnedView; + pinnedShadow.position = position; + pinnedShadow.id = getAdapter().getItemId(position); + + // store pinned shadow + mPinnedShadow = pinnedShadow; + + mTranslateY = translateY; + } + + /** Destroy shadow wrapper for currently pinned view */ + private void destroyPinnedShadow() { + // keep shadow for being recycled later + // if (mPinnedShadow != null) + // Log.d("show", "destroyPinnedShadow = " + mPinnedShadow.position); + mRecycleShadow = mPinnedShadow; + mPinnedShadow = null; + } + + /** find first section in screen */ + private int findFirstSectionPositionInScreen(int firstVisibleItemPosition, int visibleItemCount) { + return findSectionPositionInScreen(firstVisibleItemPosition, visibleItemCount, + firstVisibleItemPosition); + } + + /** find section from fromPosition in screen */ + private int findSectionPositionInScreen(int firstVisibleItemPosition, int visibleItemCount, + int fromPosition) { + PinnedSectionListAdapter adapter = (PinnedSectionListAdapter) getAdapter(); + if (fromPosition < firstVisibleItemPosition + || fromPosition >= firstVisibleItemPosition + visibleItemCount) + return -1; + for (int position = fromPosition; position < firstVisibleItemPosition + visibleItemCount; position++) { + int viewType = adapter.getItemViewType(position); + if (adapter.isItemViewTypePinned(viewType)) + return position; + } + return -1; + } + + /** find current position's section position */ + private int findCurrentSectionPosition(int fromPosition) { + PinnedSectionListAdapter adapter = (PinnedSectionListAdapter) getAdapter(); + + if (adapter instanceof SectionIndexer) { + // try fast way by asking section indexer + SectionIndexer indexer = (SectionIndexer) adapter; + int sectionPosition = indexer.getSectionForPosition(fromPosition); + int itemPosition = indexer.getPositionForSection(sectionPosition); + int typeView = adapter.getItemViewType(itemPosition); + if (adapter.isItemViewTypePinned(typeView)) { + return itemPosition; + } // else, no luck + } + + // try slow way by looking through to the next section item above + for (int position = fromPosition; position >= 0; position--) { + int viewType = adapter.getItemViewType(position); + if (adapter.isItemViewTypePinned(viewType)) + return position; + } + return -1; // no candidate found + } + + @Override + public void setOnScrollListener(OnScrollListener listener) { + if (listener == mOnScrollListener) { + super.setOnScrollListener(listener); + } else { + mDelegateOnScrollListener = listener; + } + } + + private boolean isPinnedViewTouched(View view, float x, float y) { + view.getHitRect(mTouchRect); + mTouchRect.top += mTranslateY; + mTouchRect.bottom += mTranslateY; + return mTouchRect.contains((int) x, (int) y); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + super.onRestoreInstanceState(state); + + // restore pinned view after configuration change + post(new Runnable() { + @Override + public void run() { + ListAdapter adapter = getAdapter(); if (adapter != null && adapter.getCount() > 0) { // detect pinned position int firstVisiblePosition = getFirstVisiblePosition(); int position = findCurrentSectionPosition(firstVisiblePosition); - if (position == -1) return; // no views to pin, exit + if (position == -1) + return; // no views to pin, exit if (firstVisiblePosition == position) { // create pinned shadow for position @@ -309,51 +419,56 @@ public void onRestoreInstanceState(Parcelable state) { createPinnedShadow(position); } } - } - }); - } - - @Override - public void setAdapter(ListAdapter adapter) { - - // assert adapter in debug mode - if (BuildConfig.DEBUG && adapter != null) { - if (!(adapter instanceof PinnedSectionListAdapter)) - throw new IllegalArgumentException("Does your adapter implement PinnedSectionListAdapter?"); - if (adapter.getViewTypeCount() < 2) - throw new IllegalArgumentException("Does your adapter handle at least two types of views - items and sections?"); - } - - // unregister observer at old adapter and register on new one - ListAdapter currentAdapter = getAdapter(); - if (currentAdapter != null) currentAdapter.unregisterDataSetObserver(mDataSetObserver); - if (adapter != null) adapter.registerDataSetObserver(mDataSetObserver); - - // destroy pinned shadow, if new adapter is not same as old one - if (currentAdapter != adapter) destroyPinnedShadow(); - - super.setAdapter(adapter); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - - if (mPinnedShadow != null) { - - // prepare variables - int pLeft = getListPaddingLeft(); - int pTop = getListPaddingTop(); - View view = mPinnedShadow.view; - - // draw child - canvas.save(); - canvas.clipRect(pLeft, pTop, pLeft + view.getWidth(), pTop + view.getHeight()); - canvas.translate(pLeft, pTop + mTranslateY); - drawChild(canvas, mPinnedShadow.view, getDrawingTime()); - canvas.restore(); - } - } + } + }); + } + + @Override + public void setAdapter(ListAdapter adapter) { + + // assert adapter in debug mode + if (/* BuildConfig.DEBUG && */adapter != null) { + if (!(adapter instanceof PinnedSectionListAdapter)) + throw new IllegalArgumentException( + "Does your adapter implement PinnedSectionListAdapter?"); + if (adapter.getViewTypeCount() < 2) + throw new IllegalArgumentException( + "Does your adapter handle at least two types of views - items and sections?"); + } + + // unregister observer at old adapter and register on new one + ListAdapter currentAdapter = getAdapter(); + if (currentAdapter != null) + currentAdapter.unregisterDataSetObserver(mDataSetObserver); + if (adapter != null) + adapter.registerDataSetObserver(mDataSetObserver); + + // destroy pinned shadow, if new adapter is not same as old one + if (currentAdapter != adapter) + destroyPinnedShadow(); + + super.setAdapter(adapter); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mPinnedShadow != null) { + + // prepare variables + int pLeft = getListPaddingLeft(); + int pTop = getListPaddingTop(); + View view = mPinnedShadow.view; + + // draw child + canvas.save(); + canvas.clipRect(pLeft, pTop, pLeft + view.getWidth(), pTop + view.getHeight()); + canvas.translate(pLeft, pTop + mTranslateY); + drawChild(canvas, mPinnedShadow.view, getDrawingTime()); + canvas.restore(); + } + } @Override public boolean dispatchTouchEvent(MotionEvent ev) { @@ -376,11 +491,13 @@ && isPinnedViewTouched(mPinnedShadow.view, x, y)) { } if (mTouchTarget != null) { - if (isPinnedViewTouched(mTouchTarget, x, y)) { // forward event to pinned view + if (isPinnedViewTouched(mTouchTarget, x, y)) { // forward event to + // pinned view mTouchTarget.dispatchTouchEvent(ev); } - if (action == MotionEvent.ACTION_UP) { // perform onClick on pinned view + if (action == MotionEvent.ACTION_UP) { // perform onClick on pinned + // view super.dispatchTouchEvent(ev); performPinnedItemClick(); clearTouchTarget(); @@ -397,7 +514,8 @@ && isPinnedViewTouched(mPinnedShadow.view, x, y)) { mTouchTarget.dispatchTouchEvent(event); event.recycle(); - // provide correct sequence to super class for further handling + // provide correct sequence to super class for further + // handling super.dispatchTouchEvent(mDownEvent); super.dispatchTouchEvent(ev); clearTouchTarget(); @@ -421,11 +539,12 @@ private void clearTouchTarget() { } private boolean performPinnedItemClick() { - if (mPinnedShadow == null) return false; + if (mPinnedShadow == null) + return false; OnItemClickListener listener = getOnItemClickListener(); if (listener != null) { - View view = mPinnedShadow.view; + View view = mPinnedShadow.view; playSoundEffect(SoundEffectConstants.CLICK); if (view != null) { view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);