package com.devsmart.android.ui; import java.util.LinkedList; import java.util.ListIterator; import java.util.Queue; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.RectF; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Scroller; @SuppressLint("DrawAllocation") public class IteratorListView extends ViewGroup { public static abstract class ViewAdapter<T extends Object> { abstract public View getView(T obj, View poll, IteratorListView iteratorListView); public void onTap(View view, int x, int y) {} public void onLongPress(View view, int x, int y) {} public void onDoubleTap(View view, int x, int y) {} } private Queue<View> mRemovedViewQueue = new LinkedList<View>(); private Scroller mScroller; private GestureDetector mScrollGestureDetector; //number of pixes to scroll on the next onLayout private int mdY = 0; private int mLastY = 0; private int mYOffset = 0; private boolean mIsFingerDown; private int mTopItem = 0; private int mBottomItem = 0; private ListIterator<?> mIterator; private ViewAdapter<Object> mAdapter; public IteratorListView(Context context) { super(context); init(); } public IteratorListView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { mScroller = new Scroller(getContext()); mScrollGestureDetector = new GestureDetector(getContext(), mOnGesture); } @SuppressWarnings("unchecked") public <T> void setIteratorAdapter(ListIterator<T> iterator, ViewAdapter<T> adapter) { mIterator = iterator; mAdapter = (ViewAdapter<Object>) adapter; //seek to the top of the list while(mIterator.hasPrevious()){ mIterator.previous(); } mTopItem = 0; mBottomItem = -1; removeAllViews(); } private GestureDetector.SimpleOnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { mIsFingerDown = true; mScroller.forceFinished(true); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { final int maxScrollDistance = getMeasuredHeight()*1; mLastY = 0; mScroller.fling(0, mdY, 0, Math.round(velocityY), 0, 0, -maxScrollDistance, maxScrollDistance); requestLayout(); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { mdY -= Math.round(distanceY); requestLayout(); return true; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { if(mAdapter != null){ RectF r = new RectF(); for(int i=0;i<getChildCount();i++){ View child = getChildAt(i); r.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); if(r.contains(e.getX(), e.getY())){ mAdapter.onTap(child, Math.round(e.getX() - child.getLeft()), Math.round(e.getY() - child.getTop())); } } } return super.onSingleTapConfirmed(e); } @Override public boolean onSingleTapUp(MotionEvent e) { return super.onSingleTapUp(e); } @Override public boolean onDoubleTap(MotionEvent e) { if(mAdapter != null){ RectF r = new RectF(); for(int i=0;i<getChildCount();i++){ View child = getChildAt(i); r.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); if(r.contains(e.getX(), e.getY())){ mAdapter.onDoubleTap(child, Math.round(e.getX() - child.getLeft()), Math.round(e.getY() - child.getTop())); } } } return super.onDoubleTap(e); } @Override public void onLongPress(MotionEvent e) { if(mAdapter != null){ RectF r = new RectF(); for(int i=0;i<getChildCount();i++){ View child = getChildAt(i); r.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); if(r.contains(e.getX(), e.getY())){ mAdapter.onLongPress(child, Math.round(e.getX() - child.getLeft()), Math.round(e.getY() - child.getTop())); } } } super.onLongPress(e); } }; @Override public boolean onTouchEvent(MotionEvent event) { boolean retval = mScrollGestureDetector.onTouchEvent(event); if(event.getAction() == MotionEvent.ACTION_UP){ mIsFingerDown = false; if(getChildCount() > 0){ int topOfFirstChild = getChildAt(0).getTop(); if(topOfFirstChild + mdY > 0){ mScroller.forceFinished(true); mLastY = 0; mScroller.startScroll(0, 0, mdY, -mYOffset); requestLayout(); } } } return retval; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); for(int i = 0;i<getChildCount();i++){ View child = getChildAt(i); int oldHeight = child.getMeasuredHeight(); child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED)); int childDiff = child.getMeasuredHeight() - oldHeight; if(childDiff > 0 && child.getBottom() < getHeight()/2){ mYOffset -= childDiff; } } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { Log.d(IteratorListView.class.getName(), String.format("t:%d b:%d", mTopItem, mBottomItem)); if(mAdapter == null){ return; } if(mScroller.computeScrollOffset()){ int y = mScroller.getCurrY(); mdY += y - mLastY; mLastY = y; } mYOffset += mdY; fillDown(); fillUp(); removeNonVisibleItems(); positionItems(); mdY = 0; if(!mScroller.isFinished()){ post(new Runnable(){ @Override public void run() { requestLayout(); } }); } else if(mYOffset > 0 && !mIsFingerDown){ mLastY = 0; mScroller.startScroll(0, 0, mdY, -mYOffset); post(new Runnable(){ @Override public void run() { requestLayout(); } }); } } private void removeNonVisibleItems(){ final int height = getHeight(); View child = getChildAt(0); //remove from top while(child != null && child.getBottom() + mdY < 0){ removeViewsInLayout(0, 1); mRemovedViewQueue.offer(child); mYOffset += child.getMeasuredHeight(); child = getChildAt(0); mTopItem++; } //remove from bottom child = getChildAt(getChildCount()-1); while(child != null && child.getTop() + mdY > height){ removeViewsInLayout(getChildCount()-1, 1); mRemovedViewQueue.offer(child); child = getChildAt(getChildCount()-1); mBottomItem--; } } @SuppressWarnings("deprecation") private void addAndMeasureChild(View child, int index){ LayoutParams layoutparams = child.getLayoutParams(); if(layoutparams == null){ layoutparams = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT); } addViewInLayout(child, index, layoutparams, true); child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED)); } private View getView(Object obj) { View child = mAdapter.getView(obj, mRemovedViewQueue.poll(), this); return child; } private void seekTo(int index){ while(mIterator.nextIndex() <= index && mIterator.hasNext()){ mIterator.next(); } while(mIterator.previousIndex() >= index && mIterator.hasPrevious()){ mIterator.previous(); } } private void fillDown() { if(mAdapter == null){ return; } final int windowHeight = getHeight(); int bottomOfLastChild = 0; if(getChildCount() > 0){ bottomOfLastChild = getChildAt(getChildCount()-1).getBottom(); } while(bottomOfLastChild + mdY < windowHeight) { seekTo(mBottomItem); if(mIterator.hasNext()){ View child = getView(mIterator.next()); addAndMeasureChild(child, -1); bottomOfLastChild += child.getMeasuredHeight(); mBottomItem++; } else { break; } } } private void fillUp() { if(mAdapter == null){ return; } int topOfFirstChild = 0; if(getChildCount() > 0){ topOfFirstChild = getChildAt(0).getTop(); } while(topOfFirstChild + mdY > 0) { seekTo(mTopItem); if(mIterator.hasPrevious()){ View child = getView(mIterator.previous()); addAndMeasureChild(child, 0); topOfFirstChild -= child.getMeasuredHeight(); mYOffset -= child.getMeasuredHeight(); mTopItem--; } else { break; } } } private void positionItems() { int top = mYOffset; for(int i=0;i<getChildCount();i++){ View child = getChildAt(i); int childHeight = child.getMeasuredHeight(); child.layout(0, top, child.getMeasuredWidth(), top + childHeight); top += childHeight; } } }