From 90fda85a25bb7d8cf9cb6f39d4f4f0a77d4ddf06 Mon Sep 17 00:00:00 2001 From: Chansuk Yang Date: Thu, 8 Nov 2012 16:14:03 +0900 Subject: [PATCH] v0.5 based on android framework listview --- lint.xml | 3 + res/values/pla__attrs.xml | 840 ++++ .../huewu/pla/lib/MultiColumnAdapterView.java | 696 ++++ .../huewu/pla/lib/MultiColumnListView.java | 265 ++ .../pla/lib/internal/PLA_AbsListView.java | 3693 +++++++++++++++++ .../pla/lib/internal/PLA_AdapterView.java | 1136 +++++ .../internal/PLA_HeaderViewListAdapter.java | 274 ++ .../huewu/pla/lib/internal/PLA_ListView.java | 2619 ++++++++++++ src/com/huewu/pla/sample/SampleActivity.java | 62 + 9 files changed, 9588 insertions(+) create mode 100644 lint.xml create mode 100644 res/values/pla__attrs.xml create mode 100644 src/com/huewu/pla/lib/MultiColumnAdapterView.java create mode 100644 src/com/huewu/pla/lib/MultiColumnListView.java create mode 100644 src/com/huewu/pla/lib/internal/PLA_AbsListView.java create mode 100644 src/com/huewu/pla/lib/internal/PLA_AdapterView.java create mode 100644 src/com/huewu/pla/lib/internal/PLA_HeaderViewListAdapter.java create mode 100644 src/com/huewu/pla/lib/internal/PLA_ListView.java create mode 100644 src/com/huewu/pla/sample/SampleActivity.java diff --git a/lint.xml b/lint.xml new file mode 100644 index 0000000..ee0eead --- /dev/null +++ b/lint.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/res/values/pla__attrs.xml b/res/values/pla__attrs.xml new file mode 100644 index 0000000..dced2ae --- /dev/null +++ b/res/values/pla__attrs.xml @@ -0,0 +1,840 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/huewu/pla/lib/MultiColumnAdapterView.java b/src/com/huewu/pla/lib/MultiColumnAdapterView.java new file mode 100644 index 0000000..dedf67c --- /dev/null +++ b/src/com/huewu/pla/lib/MultiColumnAdapterView.java @@ -0,0 +1,696 @@ +package com.huewu.pla.lib; +/* + * Copyright (c) 2010, Sony Ericsson Mobile Communication AB. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of the Sony Ericsson Mobile Communication AB nor the names + * of its contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.TreeSet; + +import com.huewu.pla.lib.internal.AbsListView; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Adapter; +import android.widget.AdapterView; +import android.widget.ListAdapter; + +/** + * @author huewu.ynag + * @date 2012-11-06 + */ +public class MultiColumnAdapterView extends AbsListView { + + private static final String TAG = "TwoColumnAdapterView"; //constructor. + + /** Distance to drag before we intercept touch events */ + private static final int TOUCH_SCROLL_THRESHOLD = 10; + + /** Children added with this layout mode will be added below the last child */ + private static final int LAYOUT_MODE_BELOW = 0; + + /** Children added with this layout mode will be added above the first child */ + private static final int LAYOUT_MODE_ABOVE = 1; + + /** User is not touching the list */ + private static final int TOUCH_STATE_RESTING = 0; + + /** User is touching the list and right now it's still a "click" */ + private static final int TOUCH_STATE_CLICK = 1; + + /** User is scrolling the list */ + private static final int TOUCH_STATE_SCROLL = 2; + + /** The adapter with all the data */ + //private ListAdapter mAdapter; + + /** Current touch state */ + private int mTouchState = TOUCH_STATE_RESTING; + + /** X-coordinate of the down event */ + private int mTouchStartX; + + /** Y-coordinate of the down event */ + private int mTouchStartY; + + ///////////////////////////////////////////////////// + // Column realted fields... + ///////////////////////////////////////////////////// + + private Column[] mColumns = null; + + /** with of one column **/ + private int mColumnWidth = 0; + + /** number of columns **/ + private int mColumnCount = 1; + + private boolean[] isScrap = new boolean[1]; + + /** + * Constructor + * @param context + */ + public MultiColumnAdapterView(Context context) { + super(context); + init(); + } + + /** + * Constructor + * @param context + */ + public MultiColumnAdapterView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + /** + * Constructor + * @param context + */ + public MultiColumnAdapterView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + //TODO read attributes. + //# of columns... + mColumnCount = 2; + mColumns = new Column[mColumnCount]; + + for( int index = 0; index < mColumnCount; ++index ){ + mColumns[index] = new Column(index); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + //calculate column width. + mColumnWidth = getMeasuredWidth() / mColumnCount; + } + +// @Override +// public void setAdapter(ListAdapter adapter) { +// mAdapter = adapter; +// removeAllViewsInLayout(); +// requestLayout(); +// } +// +// @Override +// public Adapter getAdapter() { +// return mAdapter; +// } + + @Override + public void setSelection(final int position) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public View getSelectedView() { + throw new UnsupportedOperationException("Not supported"); + } + +// @Override +// public boolean onInterceptTouchEvent(final MotionEvent event) { +// switch (event.getAction()) { +// case MotionEvent.ACTION_DOWN: +// startTouch(event); +// return false; +// +// case MotionEvent.ACTION_MOVE: +// return startScrollIfNeeded(event); +// +// default: +// endTouch(); +// return false; +// } +// } + +// @Override +// public boolean onTouchEvent(final MotionEvent event) { +// if (getChildCount() == 0) { +// return false; +// } +// switch (event.getAction()) { +// case MotionEvent.ACTION_DOWN: +// startTouch(event); +// break; +// +// case MotionEvent.ACTION_MOVE: +// if (mTouchState == TOUCH_STATE_CLICK) { +// startScrollIfNeeded(event); +// } +// if (mTouchState == TOUCH_STATE_SCROLL) { +// scrollList((int)event.getY() - mTouchStartY); +// } +// break; +// +// case MotionEvent.ACTION_UP: +// if (mTouchState == TOUCH_STATE_CLICK) { +// // clickChildAt((int)event.getX(), (int)event.getY()); +// } +// if (mTouchState == TOUCH_STATE_SCROLL) { +// //TODO some fling effect here... +// } +// +// endTouch(); +// break; +// +// default: +// endTouch(); +// break; +// } +// return true; +// } + + @Override + protected void onLayout(final boolean changed, final int left, final int top, final int right, + final int bottom) { + super.onLayout(changed, left, top, right, bottom); + +// // if we don't have an adapter, we don't need to do anything +// if (getAdapter() == null) { +// return; +// } +// +// removeNonVisibleViews(); +// fillList(); +// +// positionItems(); +// invalidate(); +// +// printLog(); + + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + //removeNonVisibleViews(); + fillList(); + positionItems(); + } + + ///////////////////////////////////////////////////////////// + //Fill ListView. + ///////////////////////////////////////////////////////////// + + private void fillList() { + + //add child view to viewgroup. + //question is "how many view should be added..." + //get last item postiion. + + fillDown(); + fillUp(); + } + + private void fillUp() { + + int firstItemPos = getFirstVisiblePosition() - 1; + for( int itemPos = firstItemPos; itemPos >= 0; --itemPos){ + + Column col = getUpEmptyColumn(); + if( col == null ) //all column is full. + break; + + View child = obtainView(itemPos, isScrap ); + //getAdapter().getView(itemPos, getCachedView(), this); + addAndMeasureChild(child, LAYOUT_MODE_ABOVE); + col.addFirst(child, itemPos); + } + } + + private void fillDown() { + + int lastItemPos = getLastItemPosition() + 1; + for( int itemPos = lastItemPos; itemPos < getAdapter().getCount(); ++itemPos){ + + Column col = getDownEmptyColumn(); + if( col == null ) //all column is full. + break; + + View child = obtainView(itemPos, isScrap); + addAndMeasureChild(child, LAYOUT_MODE_BELOW); + col.addLast(child, itemPos); + } + } + + /** + * get not full column to insert a child view. + * @return not full column or null if all columns are full. + */ + private Column getDownEmptyColumn() { + Column[] cols = sortColumns(); + for( Column c : cols ){ + if( c.isDownFull() == false ) + return c; + } + return null; + } + + private Column getUpEmptyColumn() { + Column[] cols = sortColumns(); + for( Column c : cols ){ + if( c.isUpFull() == false ) + return c; + } + return null; + } + + private Comparator mColHeightComparator = new Comparator() { + + @Override + public int compare(Column a, Column b) { + return a.mHeight - b.mHeight; + } + }; + + private Column[] sortColumns() { + //FIXME this method is called too frequently...don't need to call every time. + Column[] cols = mColumns.clone(); + Arrays.sort(cols, mColHeightComparator); + return cols; + } + + private int getLastItemPosition() { + //find largest position between columns... + int lastPos = Integer.MIN_VALUE; + for( Column c : mColumns ){ + lastPos = Math.max(lastPos, c.getLastItemPosition()); + } + + return lastPos; + } + + /** + * Adds a view as a child view and takes care of measuring it + * + * @param child The view to add + * @param layoutMode Either LAYOUT_MODE_ABOVE or LAYOUT_MODE_BELOW + */ + private void addAndMeasureChild(final View child, final int layoutMode) { + LayoutParams params = (LayoutParams) child.getLayoutParams(); + if (params == null) { + params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + final int index = layoutMode == LAYOUT_MODE_ABOVE ? 0 : -1; + addViewInLayout(child, index, params, true); + + final int itemWidth = mColumnWidth; + child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED); + } + + ///////////////////////////////////////////////////////////// + //Remove Non-visible Views. + ///////////////////////////////////////////////////////////// + + /** + * Removes view that are outside of the visible part of the list. Will not + * remove all views. + * + * @param offset Offset of the visible area + */ + private void removeNonVisibleViews() { + for( Column c : mColumns ) + removeNonVisibleViews(c); + } + + private Rect mParentRect = new Rect(); + private Rect mChildRect = new Rect(); + + private void removeNonVisibleViews(Column col) { + + View[] views = col.getViews(); + getDrawingRect(mParentRect); + + for( View v : views){ + if( v == null ) + continue; + + v.getHitRect(mChildRect); + //find hidden view. + if( Rect.intersects(mParentRect, mChildRect) == false){ + //addToCache(v); //do not destry view. we can reuse it!. + col.removeView(v); + removeViewInLayout(v); + } + } + } + + ///////////////////////////////////////////////////////////// + //ListView Scroll + ///////////////////////////////////////////////////////////// + + /** + * Sets and initializes all things that need to when we start a touch + * gesture. + * + * @param event The down event + */ + private void startTouch(final MotionEvent event) { + // save the start place + mTouchStartX = (int)event.getX(); + mTouchStartY = (int)event.getY(); + + for( Column c : mColumns ) + c.beginScroll(mTouchStartY); + + // we don't know if it's a click or a scroll yet, but until we know + // assume it's a click + mTouchState = TOUCH_STATE_CLICK; + } + + /** + * Resets and recycles all things that need to when we end a touch gesture + */ + private void endTouch() { + + // reset touch state + mTouchState = TOUCH_STATE_RESTING; + } + + /** + * Scrolls the list. Takes care of updating rotation (if enabled) and + * snapping + * + * @param scrolledDistance The distance to scroll + */ + private void scrollList(final int scrolledDistance) { + + for( Column c : mColumns ) + c.scroll(scrolledDistance); + + requestLayout(); + } + + /** + * Checks if the user has moved far enough for this to be a scroll and if + * so, sets the list in scroll mode + * + * @param event The (move) event + * @return true if scroll was started, false otherwise + */ + private boolean startScrollIfNeeded(final MotionEvent event) { + final int xPos = (int)event.getX(); + final int yPos = (int)event.getY(); + if (xPos < mTouchStartX - TOUCH_SCROLL_THRESHOLD + || xPos > mTouchStartX + TOUCH_SCROLL_THRESHOLD + || yPos < mTouchStartY - TOUCH_SCROLL_THRESHOLD + || yPos > mTouchStartY + TOUCH_SCROLL_THRESHOLD) { + // we've moved far enough for this to be a scroll + mTouchState = TOUCH_STATE_SCROLL; + return true; + } + return false; + } + + ///////////////////////////////////////////////////////////// + //Column Positioning + ///////////////////////////////////////////////////////////// + + private void positionItems() { + for(Column c : mColumns) + positionItems(c); + } + + /** + * Positions the children of each column at the "correct" positions + */ + private void positionItems(Column col) { + + View[] views = col.getViews(); + //get col position + + + int top = col.getOffset(); //apply scrolled position + int left = col.getIndex() * mColumnWidth; + + for( View v : views ){ + if( v == null) + continue; + + int width = v.getMeasuredWidth(); + int height = v.getMeasuredHeight(); + + v.layout(left, top, left + width, top + height); + top += height; + } + } + + ///////////////////////////////////////////////////////////// + //DEBUG INFOs. + ///////////////////////////////////////////////////////////// + + private void printLog() { + Log.v(TAG, "Current Children Count: " + getChildCount()); + + for( Column c : mColumns ) + printLog( c ); + } + + private void printLog(Column col) { + //DEBUG purpose.. + //TODO comment out below code before release. + + Log.v(TAG, "Column Index: " + col.mColumnIndex); + Log.v(TAG, "Column List Items: " + col.mItemPositions.toString()); + Log.v(TAG, "Column List Top: " + col.getDrawingTop()); + Log.v(TAG, "Column List Offset: " + col.mListOffset); + } + + ///////////////////////////////////////////////////////////// + //Inner Class Column. + ///////////////////////////////////////////////////////////// + + private class Column { + //TODO is it ok to use a some magic number here? (it should not be duplicated...) + private final static int TAG_KEY = -9812323; + + private int mColumnIndex; + + /** The current top of the first item */ + private int mHeight = 0; + private int mLastItemPosition = -1; + + private ArrayList mViews = new ArrayList(); + + //TODO is it ok to use item position info to identify item?? + private TreeSet mItemPositions = new TreeSet(); + + /** the y starting point of this column **/ + private int mListOffset; + + /** scrolling is started at this y position. **/ + private int mListTopStart; + + public Column(int index) { + mColumnIndex = index; + } + + public int getOffset(){ + return mListOffset; + } + + public int getIndex() { + return mColumnIndex; + } + + public int getFirstItemPosition(){ + if(mItemPositions.isEmpty()) + return mLastItemPosition; + return mItemPositions.first(); + } + + public int getLastItemPosition(){ + if(mItemPositions.isEmpty()) + return mLastItemPosition; + return mItemPositions.last(); + } + + public void addLast(View child, int itemPosition) { + mHeight += child.getMeasuredHeight(); + child.setTag(TAG_KEY, itemPosition); + mItemPositions.add(itemPosition); + mViews.add(child); + } + + public void addFirst(View child, int itemPosition) { + View first = mViews.get(0); + int height = child.getMeasuredHeight(); + + mHeight += height; + child.setTag(TAG_KEY, itemPosition); + + //before do layout... add temp layout. (in order to correct top value...) + child.layout( + first.getLeft(), first.getTop() - height, first.getRight(), first.getTop() ); + mItemPositions.add(itemPosition); + mViews.add(0, child); + + mListOffset -= height; + mListTopStart -= height; + } + + public void removeView(View child) { + int height = child.getMeasuredHeight(); + + //recalculate height. + mHeight -= height; + + //remove view's item position. + int itemPos = (Integer) child.getTag(TAG_KEY); + mItemPositions.remove(itemPos); + + //check index of view. + int viewPos = mViews.indexOf(child); + + if( viewPos == 0 ){ + //up view is removed.. + mListOffset += height; + mListTopStart += height; + }else if(viewPos == mViews.size() - 1){ + //down view is removed.. + } + + mViews.remove(child); + + if( mItemPositions.isEmpty() ){ + //if this is the last view... let's keep last item position of this column. + //if not, we will lost the track of columns... + mLastItemPosition = itemPos; + } + } + + public View[] getViews(){ + return mViews.toArray(new View[]{}); + } + + public int getDrawingTop(){ + if(mViews == null || mViews.size() == 0) + return 0; + + return mViews.get(0).getTop(); + } + + public boolean isDownFull(){ + //check bottom of col. + int top = getDrawingTop(); + return mHeight + top > getHeight(); + } + + public boolean isUpFull(){ + //if child view is inserted before doing layout process, + //there is no proper way to get correct the first child's top position. + //so, we need offset info. + return getDrawingTop() < 0; + } + + public void scroll(int scrolledDistance) { + mListOffset = mListTopStart + scrolledDistance; + } + + public void beginScroll(int mTouchStartY) { + mListTopStart = mListOffset; + } + }//end of inner class. + + @Override + public ListAdapter getAdapter() { + return mAdapter; + } + + @Override + public void onGlobalLayout() { + } + + @Override + public void fillGap(boolean down) { + + if (down) { + fillDown(); + } else { + fillUp(); + } + } + + @Override + public int findMotionRow(int y) { + return 0; + } + + @Override + public void setSelectionInt(int position) { + } + + @Override + public void setAdapter(ListAdapter adapter) { + mAdapter = adapter; + } + + @Override + public int getFirstVisiblePosition() { + //find smallest position between columns... + int firstPos = Integer.MAX_VALUE; + for( Column c : mColumns ){ + firstPos = Math.min(firstPos, c.getFirstItemPosition()); + } + return firstPos; + } + +}//end of class diff --git a/src/com/huewu/pla/lib/MultiColumnListView.java b/src/com/huewu/pla/lib/MultiColumnListView.java new file mode 100644 index 0000000..f00b777 --- /dev/null +++ b/src/com/huewu/pla/lib/MultiColumnListView.java @@ -0,0 +1,265 @@ +package com.huewu.pla.lib; +/* + * Copyright (c) 2010, Sony Ericsson Mobile Communication AB. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of the Sony Ericsson Mobile Communication AB nor the names + * of its contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseIntArray; +import android.view.View; + +import com.huewu.pla.lib.internal.PLA_ListView; + +/** + * @author huewu.ynag + * @date 2012-11-06 + */ +public class MultiColumnListView extends PLA_ListView { + + @SuppressWarnings("unused") + private static final String TAG = "MultiColumnListView"; + + private int mColumnCount = 2; + private Column[] mColumns = null; + private SparseIntArray mItems = new SparseIntArray(); + + private class Column { + + private int mIndex; + private int mColumnWidth; + private int mColumnLeft; + + //TODO is it ok to use item position info to identify item?? + + public Column(int index) { + mIndex = index; + } + + public int getColumnLeft() { + return mColumnLeft; + } + + public int getColumnWidth() { + return mColumnWidth; + } + + public int getIndex() { + return mIndex; + } + + public int getBottom() { + //find biggest value. + int bottom = Integer.MIN_VALUE; + int childCount = getChildCount(); + for( int index = 0; index < childCount; ++index ){ + View v = getChildAt(index); + if(v.getLeft() != mColumnLeft) + continue; + bottom = bottom < v.getBottom() ? v.getBottom() : bottom; + } + return bottom; + } + + public int getTop() { + //find smallest value. + int top = Integer.MAX_VALUE; + int childCount = getChildCount(); + for( int index = 0; index < childCount; ++index ){ + View v = getChildAt(index); + if(v.getLeft() != mColumnLeft) + continue; + top = top > v.getTop() ? v.getTop() : top; + } + return top; + } + } + + public MultiColumnListView(Context context) { + super(context); + init(); + } + + public MultiColumnListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public MultiColumnListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + //TODO read from attribute. + mColumnCount = 3; + mColumns = new Column[mColumnCount]; + + for( int i = 0; i < mColumnCount; ++i ) + mColumns[i] = new Column(i); + } + + /////////////////////////////////////////////////////////////////////// + //Override Methods... + /////////////////////////////////////////////////////////////////////// + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int width = getMeasuredWidth() / mColumnCount; + + for( int index = 0; index < mColumnCount; ++ index ){ + mColumns[index].mColumnWidth = width; + mColumns[index].mColumnLeft = mListPadding.left + width * index; + } + } + + @Override + protected void onMeasureChild(View child, int position, int widthMeasureSpec, + int heightMeasureSpec) { + //super.onMeasureChild(child, widthMeasureSpec, heightMeasureSpec); + child.measure(MeasureSpec.EXACTLY | getColumnWidth(position), heightMeasureSpec); + } + + @Override + protected void onItemAddedToList(int position, boolean flow ) { + super.onItemAddedToList(position, flow); + + //Column col = getNextColumn(flow); + Column col = getNextColumn(flow, position ); + mItems.append(position, col.getIndex()); + Log.v("PLA_ListView", String.format("Item [%d] dAdded to Column [%d].", position, col.getIndex()) ); + } + + @Override + protected int getCurrentChildBottom() { + int result = Integer.MAX_VALUE; + for(Column c : mColumns){ + int bottom = c.getBottom(); + result = result > bottom ? bottom : result; + } + return result; + } + + @Override + protected int getCurrentChildTop() { + int result = Integer.MIN_VALUE; + for(Column c : mColumns){ + int top = c.getTop(); + result = result < top ? top : result; + } + return result; + } + + @Override + protected int getPreservedChildTop( int pos ){ + int colIndex = mItems.get(pos, -1); + if(colIndex == -1) + return getCurrentChildBottom(); + + return mColumns[colIndex].getBottom(); + } + + @Override + protected int getPreservedChildBottom( int pos ){ + int colIndex = mItems.get(pos, -1); + if(colIndex == -1) + return getCurrentChildTop(); + + return mColumns[colIndex].getTop(); + } + + @Override + protected int getChildLeft(int pos) { + return getColumnLeft(pos); + } + + ////////////////////////////////////////////////////////////////////////////// + //Private Methods... + ////////////////////////////////////////////////////////////////////////////// + + //flow If flow is true, align top edge to y. If false, align bottom edge to y. + private Column getNextColumn(boolean flow, int position) { + + if( flow ){ + //find column which has the smallest bottom value. + return getShortestBottomColumn(); + }else{ + //find column which has the biggest top value. + //we already have this item... + //return mColumns[mItems.get(position)]; + return getLongestTopColumn(); + } + } + + private Column getLongestTopColumn() { + int childCount = getChildCount(); + if( childCount < mColumnCount ) + return mColumns[childCount]; + + Column result = mColumns[0]; + for( Column c : mColumns ){ + result = result.getTop() < c.getTop() ? c : result; + } + return result; + } + + private Column getShortestBottomColumn() { + int childCount = getChildCount(); + if( childCount < mColumnCount ) + return mColumns[childCount]; + + Column result = mColumns[0]; + for( Column c : mColumns ){ + result = result.getBottom() > c.getBottom() ? c : result; + } + + Log.v("Column", "get Shortest Bottom Column: " + result.getIndex()); + return result; + } + + private int getColumnLeft(int pos) { + int colIndex = mItems.get(pos, -1); + + if( colIndex == -1 ) + return 0; + + return mColumns[colIndex].getColumnLeft(); + } + + private int getColumnWidth(int pos) { + int colIndex = mItems.get(pos, -1 ); + + if( colIndex == -1 ) + return 0; + + return mColumns[colIndex].getColumnWidth(); + } + +}//end of class diff --git a/src/com/huewu/pla/lib/internal/PLA_AbsListView.java b/src/com/huewu/pla/lib/internal/PLA_AbsListView.java new file mode 100644 index 0000000..d7d308e --- /dev/null +++ b/src/com/huewu/pla/lib/internal/PLA_AbsListView.java @@ -0,0 +1,3693 @@ +/* + * Copyright (C) 2006 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.huewu.pla.lib.internal; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.Debug; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.Adapter; +import android.widget.ListAdapter; +import android.widget.Scroller; + +import com.huewu.lib.pla.R; + +/** + * Base class that can be used to implement virtualized lists of items. A list does + * not have a spatial definition here. For instance, subclases of this class can + * display the content of the list in a grid, in a carousel, as stack, etc. + * + * @attr ref android.R.styleable#AbsListView_listSelector + * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop + * @attr ref android.R.styleable#AbsListView_stackFromBottom + * @attr ref android.R.styleable#AbsListView_scrollingCache + * @attr ref android.R.styleable#AbsListView_textFilterEnabled + * @attr ref android.R.styleable#AbsListView_transcriptMode + * @attr ref android.R.styleable#AbsListView_cacheColorHint + * @attr ref android.R.styleable#AbsListView_fastScrollEnabled + * @attr ref android.R.styleable#AbsListView_smoothScrollbar + */ +public abstract class PLA_AbsListView extends PLA_AdapterView implements +ViewTreeObserver.OnGlobalLayoutListener, ViewTreeObserver.OnTouchModeChangeListener { + + //FIXME not supported features... (removed from original AbsListView)... + //Filter + //Fast Scroll + //Clipping Padding Region + + /** + * Disables the transcript mode. + * + * @see #setTranscriptMode(int) + */ + public static final int TRANSCRIPT_MODE_DISABLED = 0; + /** + * The list will automatically scroll to the bottom when a data set change + * notification is received and only if the last item is already visible + * on screen. + * + * @see #setTranscriptMode(int) + */ + public static final int TRANSCRIPT_MODE_NORMAL = 1; + /** + * The list will automatically scroll to the bottom, no matter what items + * are currently visible. + * + * @see #setTranscriptMode(int) + */ + public static final int TRANSCRIPT_MODE_ALWAYS_SCROLL = 2; + + /** + * Indicates that we are not in the middle of a touch gesture + */ + static final int TOUCH_MODE_REST = -1; + + /** + * Indicates we just received the touch event and we are waiting to see if the it is a tap or a + * scroll gesture. + */ + protected static final int TOUCH_MODE_DOWN = 0; + + /** + * Indicates the touch has been recognized as a tap and we are now waiting to see if the touch + * is a longpress + */ + protected static final int TOUCH_MODE_TAP = 1; + + /** + * Indicates we have waited for everything we can wait for, but the user's finger is still down + */ + protected static final int TOUCH_MODE_DONE_WAITING = 2; + + /** + * Indicates the touch gesture is a scroll + */ + protected static final int TOUCH_MODE_SCROLL = 3; + + /** + * Indicates the view is in the process of being flung + */ + protected static final int TOUCH_MODE_FLING = 4; + + /** + * Regular layout - usually an unsolicited layout from the view system + */ + static final int LAYOUT_NORMAL = 0; + + /** + * Show the first item + */ + static final int LAYOUT_FORCE_TOP = 1; + + /** + * Force the selected item to be on somewhere on the screen + */ + static final int LAYOUT_SET_SELECTION = 2; + + /** + * Show the last item + */ + static final int LAYOUT_FORCE_BOTTOM = 3; + + /** + * Make a mSelectedItem appear in a specific location and build the rest of + * the views from there. The top is specified by mSpecificTop. + */ + static final int LAYOUT_SPECIFIC = 4; + + /** + * Layout to sync as a result of a data change. Restore mSyncPosition to have its top + * at mSpecificTop + */ + static final int LAYOUT_SYNC = 5; + + /** + * Layout as a result of using the navigation keys + */ + static final int LAYOUT_MOVE_SELECTION = 6; + + /** + * Controls how the next layout will happen + */ + int mLayoutMode = LAYOUT_NORMAL; + + /** + * Should be used by subclasses to listen to changes in the dataset + */ + AdapterDataSetObserver mDataSetObserver; + + /** + * The adapter containing the data to be displayed by this view + */ + protected ListAdapter mAdapter; + + /** + * Indicates whether the list selector should be drawn on top of the children or behind + */ + boolean mDrawSelectorOnTop = false; + + /** + * The drawable used to draw the selector + */ + Drawable mSelector; + + /** + * Defines the selector's location and dimension at drawing time + */ + Rect mSelectorRect = new Rect(); + + /** + * The data set used to store unused views that should be reused during the next layout + * to avoid creating new ones + */ + final RecycleBin mRecycler = new RecycleBin(); + + /** + * The selection's left padding + */ + int mSelectionLeftPadding = 0; + + /** + * The selection's top padding + */ + int mSelectionTopPadding = 0; + + /** + * The selection's right padding + */ + int mSelectionRightPadding = 0; + + /** + * The selection's bottom padding + */ + int mSelectionBottomPadding = 0; + + /** + * This view's padding + */ + protected Rect mListPadding = new Rect(); + + /** + * Subclasses must retain their measure spec from onMeasure() into this member + */ + protected int mWidthMeasureSpec = 0; + + /** + * The top scroll indicator + */ + View mScrollUp; + + /** + * The down scroll indicator + */ + View mScrollDown; + + /** + * When the view is scrolling, this flag is set to true to indicate subclasses that + * the drawing cache was enabled on the children + */ + protected boolean mCachingStarted; + + /** + * The position of the view that received the down motion event + */ + protected int mMotionPosition; + + /** + * The offset to the top of the mMotionPosition view when the down motion event was received + */ + int mMotionViewOriginalTop; + + /** + * The desired offset to the top of the mMotionPosition view after a scroll + */ + int mMotionViewNewTop; + + /** + * The X value associated with the the down motion event + */ + int mMotionX; + + /** + * The Y value associated with the the down motion event + */ + int mMotionY; + + /** + * One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP, TOUCH_MODE_SCROLL, or + * TOUCH_MODE_DONE_WAITING + */ + protected int mTouchMode = TOUCH_MODE_REST; + + /** + * Y value from on the previous motion event (if any) + */ + int mLastY; + + /** + * How far the finger moved before we started scrolling + */ + int mMotionCorrection; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * Handles one frame of a fling + */ + private FlingRunnable mFlingRunnable; + + /** + * Handles scrolling between positions within the list. + */ + private PositionScroller mPositionScroller; + + /** + * The offset in pixels form the top of the AdapterView to the top + * of the currently selected view. Used to save and restore state. + */ + int mSelectedTop = 0; + + /** + * Indicates whether the list is stacked from the bottom edge or + * the top edge. + */ + boolean mStackFromBottom; + + /** + * When set to true, the list automatically discards the children's + * bitmap cache after scrolling. + */ + boolean mScrollingCacheEnabled; + + /** + * Optional callback to notify client when scroll position has changed + */ + private OnScrollListener mOnScrollListener; + + /** + * Indicates whether to use pixels-based or position-based scrollbar + * properties. + */ + private boolean mSmoothScrollbarEnabled = true; + + /** + * Rectangle used for hit testing children + */ + private Rect mTouchFrame; + + /** + * The position to resurrect the selected position to. + */ + int mResurrectToPosition = INVALID_POSITION; + + private ContextMenuInfo mContextMenuInfo = null; + + /** + * Used to request a layout when we changed touch mode + */ + private static final int TOUCH_MODE_UNKNOWN = -1; + private static final int TOUCH_MODE_ON = 0; + private static final int TOUCH_MODE_OFF = 1; + + private int mLastTouchMode = TOUCH_MODE_UNKNOWN; + + private static final boolean PROFILE_SCROLLING = false; + private boolean mScrollProfilingStarted = false; + + private static final boolean PROFILE_FLINGING = false; + private boolean mFlingProfilingStarted = false; + + /** + * The last CheckForLongPress runnable we posted, if any + */ + private CheckForLongPress mPendingCheckForLongPress; + + /** + * The last CheckForTap runnable we posted, if any + */ + private Runnable mPendingCheckForTap; + + /** + * The last CheckForKeyLongPress runnable we posted, if any + */ + private CheckForKeyLongPress mPendingCheckForKeyLongPress; + + /** + * Acts upon click + */ + private PLA_AbsListView.PerformClick mPerformClick; + + /** + * This view is in transcript mode -- it shows the bottom of the list when the data + * changes + */ + private int mTranscriptMode; + + /** + * Indicates that this list is always drawn on top of a solid, single-color, opaque + * background + */ + private int mCacheColorHint; + + /** + * The select child's view (from the adapter's getView) is enabled. + */ + private boolean mIsChildViewEnabled; + + /** + * The last scroll state reported to clients through {@link OnScrollListener}. + */ + private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + private int mTouchSlop; + private Runnable mClearScrollingCache; + private int mMinimumVelocity; + private int mMaximumVelocity; + + final boolean[] mIsScrap = new boolean[1]; + + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + /** + * Interface definition for a callback to be invoked when the list or grid + * has been scrolled. + */ + public interface OnScrollListener { + + /** + * The view is not scrolling. Note navigating the list using the trackball counts as + * being in the idle state since these transitions are not animated. + */ + public static int SCROLL_STATE_IDLE = 0; + + /** + * The user is scrolling using touch, and their finger is still on the screen + */ + public static int SCROLL_STATE_TOUCH_SCROLL = 1; + + /** + * The user had previously been scrolling using touch and had performed a fling. The + * animation is now coasting to a stop + */ + public static int SCROLL_STATE_FLING = 2; + + /** + * Callback method to be invoked while the list view or grid view is being scrolled. If the + * view is being scrolled, this method will be called before the next frame of the scroll is + * rendered. In particular, it will be called before any calls to + * {@link Adapter#getView(int, View, ViewGroup)}. + * + * @param view The view whose scroll state is being reported + * + * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE}, + * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. + */ + public void onScrollStateChanged(PLA_AbsListView view, int scrollState); + + /** + * Callback method to be invoked when the list or grid has been scrolled. This will be + * called after the scroll has completed + * @param view The view whose scroll state is being reported + * @param firstVisibleItem the index of the first visible cell (ignore if + * visibleItemCount == 0) + * @param visibleItemCount the number of visible cells + * @param totalItemCount the number of items in the list adaptor + */ + public void onScroll(PLA_AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount); + } + + public PLA_AbsListView(Context context) { + super(context); + initAbsListView(); + + setVerticalScrollBarEnabled(true); + TypedArray a = context.obtainStyledAttributes(R.styleable.View); + initializeScrollbars(a); + a.recycle(); + } + + public PLA_AbsListView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.absListViewStyle); + } + + public PLA_AbsListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initAbsListView(); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.AbsListView, defStyle, 0); + + Drawable d = a.getDrawable(R.styleable.AbsListView_listSelector); + if (d != null) { + setSelector(d); + } + + mDrawSelectorOnTop = a.getBoolean( + R.styleable.AbsListView_drawSelectorOnTop, false); + + boolean stackFromBottom = a.getBoolean(R.styleable.AbsListView_stackFromBottom, false); + setStackFromBottom(stackFromBottom); + + boolean scrollingCacheEnabled = a.getBoolean(R.styleable.AbsListView_scrollingCache, true); + setScrollingCacheEnabled(scrollingCacheEnabled); + + int transcriptMode = a.getInt(R.styleable.AbsListView_transcriptMode, + TRANSCRIPT_MODE_DISABLED); + setTranscriptMode(transcriptMode); + + int color = a.getColor(R.styleable.AbsListView_cacheColorHint, 0); + setCacheColorHint(color); + + boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true); + setSmoothScrollbarEnabled(smoothScrollbar); + + a.recycle(); + } + + private void initAbsListView() { + // Setting focusable in touch mode will set the focusable property to true + setClickable(true); + setFocusableInTouchMode(true); + setWillNotDraw(false); + setAlwaysDrawnWithCacheEnabled(false); + setScrollingCacheEnabled(true); + + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity() / 2; + } + + /** + * When smooth scrollbar is enabled, the position and size of the scrollbar thumb + * is computed based on the number of visible pixels in the visible items. This + * however assumes that all list items have the same height. If you use a list in + * which items have different heights, the scrollbar will change appearance as the + * user scrolls through the list. To avoid this issue, you need to disable this + * property. + * + * When smooth scrollbar is disabled, the position and size of the scrollbar thumb + * is based solely on the number of items in the adapter and the position of the + * visible items inside the adapter. This provides a stable scrollbar as the user + * navigates through a list of items with varying heights. + * + * @param enabled Whether or not to enable smooth scrollbar. + * + * @see #setSmoothScrollbarEnabled(boolean) + * @attr ref android.R.styleable#AbsListView_smoothScrollbar + */ + public void setSmoothScrollbarEnabled(boolean enabled) { + mSmoothScrollbarEnabled = enabled; + } + + /** + * Returns the current state of the fast scroll feature. + * + * @return True if smooth scrollbar is enabled is enabled, false otherwise. + * + * @see #setSmoothScrollbarEnabled(boolean) + */ + @ViewDebug.ExportedProperty + public boolean isSmoothScrollbarEnabled() { + return mSmoothScrollbarEnabled; + } + + /** + * Set the listener that will receive notifications every time the list scrolls. + * + * @param l the scroll listener + */ + public void setOnScrollListener(OnScrollListener l) { + mOnScrollListener = l; + invokeOnItemScrollListener(); + } + + /** + * Notify our scroll listener (if there is one) of a change in scroll state + */ + void invokeOnItemScrollListener() { + if (mOnScrollListener != null) { + mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); + } + } + + /** + * Indicates whether the children's drawing cache is used during a scroll. + * By default, the drawing cache is enabled but this will consume more memory. + * + * @return true if the scrolling cache is enabled, false otherwise + * + * @see #setScrollingCacheEnabled(boolean) + * @see View#setDrawingCacheEnabled(boolean) + */ + @ViewDebug.ExportedProperty + public boolean isScrollingCacheEnabled() { + return mScrollingCacheEnabled; + } + + /** + * Enables or disables the children's drawing cache during a scroll. + * By default, the drawing cache is enabled but this will use more memory. + * + * When the scrolling cache is enabled, the caches are kept after the + * first scrolling. You can manually clear the cache by calling + * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}. + * + * @param enabled true to enable the scroll cache, false otherwise + * + * @see #isScrollingCacheEnabled() + * @see View#setDrawingCacheEnabled(boolean) + */ + public void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled && !enabled) { + clearScrollingCache(); + } + mScrollingCacheEnabled = enabled; + } + + @Override + public void getFocusedRect(Rect r) { + View view = getSelectedView(); + if (view != null && view.getParent() == this) { + // the focused rectangle of the selected view offset into the + // coordinate space of this view. + view.getFocusedRect(r); + offsetDescendantRectToMyCoords(view, r); + } else { + // otherwise, just the norm + super.getFocusedRect(r); + } + } + + private void useDefaultSelector() { + setSelector(getResources().getDrawable( + android.R.drawable.list_selector_background)); + } + + /** + * Indicates whether the content of this view is pinned to, or stacked from, + * the bottom edge. + * + * @return true if the content is stacked from the bottom edge, false otherwise + */ + @ViewDebug.ExportedProperty + public boolean isStackFromBottom() { + return mStackFromBottom; + } + + /** + * When stack from bottom is set to true, the list fills its content starting from + * the bottom of the view. + * + * @param stackFromBottom true to pin the view's content to the bottom edge, + * false to pin the view's content to the top edge + */ + public void setStackFromBottom(boolean stackFromBottom) { + if (mStackFromBottom != stackFromBottom) { + mStackFromBottom = stackFromBottom; + requestLayoutIfNecessary(); + } + } + + void requestLayoutIfNecessary() { + if (getChildCount() > 0) { + resetList(); + requestLayout(); + invalidate(); + } + } + + static class SavedState extends BaseSavedState { + long selectedId; + long firstId; + int viewTop; + int position; + int height; + + /** + * Constructor called from {@link PLA_AbsListView#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + selectedId = in.readLong(); + firstId = in.readLong(); + viewTop = in.readInt(); + position = in.readInt(); + height = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeLong(selectedId); + out.writeLong(firstId); + out.writeInt(viewTop); + out.writeInt(position); + out.writeInt(height); + } + + @Override + public String toString() { + return "AbsListView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " selectedId=" + selectedId + + " firstId=" + firstId + + " viewTop=" + viewTop + + " position=" + position + + " height=" + height + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + /* + * This doesn't really make sense as the place to dismiss the + * popups, but there don't seem to be any other useful hooks + * that happen early enough to keep from getting complaints + * about having leaked the window. + */ + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + + boolean haveChildren = getChildCount() > 0; + long selectedId = getSelectedItemId(); + ss.selectedId = selectedId; + ss.height = getHeight(); + + if (selectedId >= 0) { + // Remember the selection + ss.viewTop = mSelectedTop; + ss.position = getSelectedItemPosition(); + ss.firstId = INVALID_POSITION; + } else { + if (haveChildren) { + // Remember the position of the first child + View v = getChildAt(0); + ss.viewTop = v.getTop(); + ss.position = mFirstPosition; + ss.firstId = mAdapter.getItemId(mFirstPosition); + } else { + ss.viewTop = 0; + ss.firstId = INVALID_POSITION; + ss.position = 0; + } + } + + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + mDataChanged = true; + + mSyncHeight = ss.height; + + if (ss.selectedId >= 0) { + mNeedSync = true; + mSyncRowId = ss.selectedId; + mSyncPosition = ss.position; + mSpecificTop = ss.viewTop; + mSyncMode = SYNC_SELECTED_POSITION; + } else if (ss.firstId >= 0) { + setSelectedPositionInt(INVALID_POSITION); + // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync + setNextSelectedPositionInt(INVALID_POSITION); + mNeedSync = true; + mSyncRowId = ss.firstId; + mSyncPosition = ss.position; + mSpecificTop = ss.viewTop; + mSyncMode = SYNC_FIRST_POSITION; + } + + requestLayout(); + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) { + resurrectSelection(); + } + } + + @Override + public void requestLayout() { + if (!mBlockLayoutRequests && !mInLayout) { + super.requestLayout(); + } + } + + /** + * The list is empty. Clear everything out. + */ + void resetList() { + removeAllViewsInLayout(); + mFirstPosition = 0; + mDataChanged = false; + mNeedSync = false; + mOldSelectedPosition = INVALID_POSITION; + mOldSelectedRowId = INVALID_ROW_ID; + setSelectedPositionInt(INVALID_POSITION); + setNextSelectedPositionInt(INVALID_POSITION); + mSelectedTop = 0; + mSelectorRect.setEmpty(); + invalidate(); + } + + @Override + protected int computeVerticalScrollExtent() { + final int count = getChildCount(); + if (count > 0) { + if (mSmoothScrollbarEnabled) { + int extent = count * 100; + + View view = getChildAt(0); + //final int top = view.getTop(); + final int top = getCurrentChildTop(); + + int height = view.getHeight(); + if (height > 0) { + extent += (top * 100) / height; + } + + view = getChildAt(count - 1); + //final int bottom = view.getBottom(); + final int bottom = getCurrentChildBottom(); + + height = view.getHeight(); + if (height > 0) { + extent -= ((bottom - getHeight()) * 100) / height; + } + + return extent; + } else { + return 1; + } + } + return 0; + } + + @Override + protected int computeVerticalScrollOffset() { + final int firstPosition = mFirstPosition; + final int childCount = getChildCount(); + if (firstPosition >= 0 && childCount > 0) { + if (mSmoothScrollbarEnabled) { + final View view = getChildAt(0); +// final int top = view.getTop(); + final int top = getCurrentChildTop(); + int height = view.getHeight(); + if (height > 0) { + return Math.max(firstPosition * 100 - (top * 100) / height + + (int)((float)getScrollY() / getHeight() * mItemCount * 100), 0); + } + } else { + int index; + final int count = mItemCount; + if (firstPosition == 0) { + index = 0; + } else if (firstPosition + childCount == count) { + index = count; + } else { + index = firstPosition + childCount / 2; + } + return (int) (firstPosition + childCount * (index / (float) count)); + } + } + return 0; + } + + @Override + protected int computeVerticalScrollRange() { + int result; + if (mSmoothScrollbarEnabled) { + result = Math.max(mItemCount * 100, 0); + } else { + result = mItemCount; + } + return result; + } + + @Override + protected float getTopFadingEdgeStrength() { + final int count = getChildCount(); + final float fadeEdge = super.getTopFadingEdgeStrength(); + if (count == 0) { + return fadeEdge; + } else { + if (mFirstPosition > 0) { + return 1.0f; + } + + final int top = getChildAt(0).getTop(); + final float fadeLength = (float) getVerticalFadingEdgeLength(); + // return top < mPaddingTop ? (float) -(top - mPaddingTop) / fadeLength : fadeEdge; + return top < getPaddingTop() ? (float) -(top - getPaddingTop()) / fadeLength : fadeEdge; + } + } + + @Override + protected float getBottomFadingEdgeStrength() { + final int count = getChildCount(); + final float fadeEdge = super.getBottomFadingEdgeStrength(); + if (count == 0) { + return fadeEdge; + } else { + if (mFirstPosition + count - 1 < mItemCount - 1) { + return 1.0f; + } + + final int bottom = getChildAt(count - 1).getBottom(); + final int height = getHeight(); + final float fadeLength = (float) getVerticalFadingEdgeLength(); + // return bottom > height - mPaddingBottom ? (float) (bottom - height + mPaddingBottom) / fadeLength : fadeEdge; + return bottom > height - getPaddingBottom() ? (float) (bottom - height + getPaddingBottom()) / fadeLength : fadeEdge; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mSelector == null) { + useDefaultSelector(); + } + final Rect listPadding = mListPadding; + // listPadding.left = mSelectionLeftPadding + mPaddingLeft; + // listPadding.top = mSelectionTopPadding + mPaddingTop; + // listPadding.right = mSelectionRightPadding + mPaddingRight; + // listPadding.bottom = mSelectionBottomPadding + mPaddingBottom; + listPadding.left = mSelectionLeftPadding + getPaddingLeft(); + listPadding.top = mSelectionTopPadding + getPaddingTop(); + listPadding.right = mSelectionRightPadding + getPaddingRight(); + listPadding.bottom = mSelectionBottomPadding + getPaddingBottom(); + } + + /** + * Subclasses should NOT override this method but + * {@link #layoutChildren()} instead. + */ + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mInLayout = true; + if (changed) { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).forceLayout(); + } + mRecycler.markChildrenDirty(); + } + + layoutChildren(); + mInLayout = false; + } + + /** + * Subclasses must override this method to layout their children. + */ + protected void layoutChildren() { + } + + void updateScrollIndicators() { + if (mScrollUp != null) { + boolean canScrollUp; + // 0th element is not visible + canScrollUp = mFirstPosition > 0; + + // ... Or top of 0th element is not visible + if (!canScrollUp) { + if (getChildCount() > 0) { + View child = getChildAt(0); + canScrollUp = child.getTop() < mListPadding.top; + } + } + + mScrollUp.setVisibility(canScrollUp ? View.VISIBLE : View.INVISIBLE); + } + + if (mScrollDown != null) { + boolean canScrollDown; + int count = getChildCount(); + + // Last item is not visible + canScrollDown = (mFirstPosition + count) < mItemCount; + + // ... Or bottom of the last element is not visible + if (!canScrollDown && count > 0) { + View child = getChildAt(count - 1); + // canScrollDown = child.getBottom() > mBottom - mListPadding.bottom; + canScrollDown = child.getBottom() > getBottom() - mListPadding.bottom; + } + + mScrollDown.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE); + } + } + + @Override + @ViewDebug.ExportedProperty + public View getSelectedView() { + if (mItemCount > 0 && mSelectedPosition >= 0) { + return getChildAt(mSelectedPosition - mFirstPosition); + } else { + return null; + } + } + + /** + * List padding is the maximum of the normal view's padding and the padding of the selector. + * + * @see android.view.View#getPaddingTop() + * @see #getSelector() + * + * @return The top list padding. + */ + public int getListPaddingTop() { + return mListPadding.top; + } + + /** + * List padding is the maximum of the normal view's padding and the padding of the selector. + * + * @see android.view.View#getPaddingBottom() + * @see #getSelector() + * + * @return The bottom list padding. + */ + public int getListPaddingBottom() { + return mListPadding.bottom; + } + + /** + * List padding is the maximum of the normal view's padding and the padding of the selector. + * + * @see android.view.View#getPaddingLeft() + * @see #getSelector() + * + * @return The left list padding. + */ + public int getListPaddingLeft() { + return mListPadding.left; + } + + /** + * List padding is the maximum of the normal view's padding and the padding of the selector. + * + * @see android.view.View#getPaddingRight() + * @see #getSelector() + * + * @return The right list padding. + */ + public int getListPaddingRight() { + return mListPadding.right; + } + + /** + * Get a view and have it show the data associated with the specified + * position. This is called when we have already discovered that the view is + * not available for reuse in the recycle bin. The only choices left are + * converting an old view or making a new one. + * + * @param position The position to display + * @param isScrap Array of at least 1 boolean, the first entry will become true if + * the returned view was taken from the scrap heap, false if otherwise. + * + * @return A view displaying the data associated with the specified position + */ + @SuppressWarnings("deprecation") + View obtainView(int position, boolean[] isScrap) { + isScrap[0] = false; + View scrapView; + + scrapView = mRecycler.getScrapView(position); + + View child; + if (scrapView != null) { + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.RECYCLE_FROM_SCRAP_HEAP, + position, -1); + } + + child = mAdapter.getView(position, scrapView, this); + + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(child, ViewDebug.RecyclerTraceType.BIND_VIEW, + position, getChildCount()); + } + + if (child != scrapView) { + mRecycler.addScrapView(scrapView); + if (mCacheColorHint != 0) { + child.setDrawingCacheBackgroundColor(mCacheColorHint); + } + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, + position, -1); + } + } else { + isScrap[0] = true; + //child.dispatchFinishTemporaryDetach(); + dispatchFinishTemporaryDetach(child); + } + } else { + child = mAdapter.getView(position, null, this); + if (mCacheColorHint != 0) { + child.setDrawingCacheBackgroundColor(mCacheColorHint); + } + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(child, ViewDebug.RecyclerTraceType.NEW_VIEW, + position, getChildCount()); + } + } + + return child; + } + + void positionSelector(View sel) { + final Rect selectorRect = mSelectorRect; + selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom()); + positionSelector(selectorRect.left, selectorRect.top, selectorRect.right, + selectorRect.bottom); + + final boolean isChildViewEnabled = mIsChildViewEnabled; + if (sel.isEnabled() != isChildViewEnabled) { + mIsChildViewEnabled = !isChildViewEnabled; + refreshDrawableState(); + } + } + + private void positionSelector(int l, int t, int r, int b) { + mSelectorRect.set(l - mSelectionLeftPadding, t - mSelectionTopPadding, r + + mSelectionRightPadding, b + mSelectionBottomPadding); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + + final boolean drawSelectorOnTop = mDrawSelectorOnTop; + if (!drawSelectorOnTop) { + drawSelector(canvas); + } + + super.dispatchDraw(canvas); + + if (drawSelectorOnTop) { + drawSelector(canvas); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (getChildCount() > 0) { + mDataChanged = true; + rememberSyncState(); + } + } + + /** + * @return True if the current touch mode requires that we draw the selector in the pressed + * state. + */ + boolean touchModeDrawsInPressedState() { + // FIXME use isPressed for this + switch (mTouchMode) { + case TOUCH_MODE_TAP: + case TOUCH_MODE_DONE_WAITING: + return true; + default: + return false; + } + } + + /** + * Indicates whether this view is in a state where the selector should be drawn. This will + * happen if we have focus but are not in touch mode, or we are in the middle of displaying + * the pressed state for an item. + * + * @return True if the selector should be shown + */ + protected boolean shouldShowSelector() { + return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState(); + } + + private void drawSelector(Canvas canvas) { + if (shouldShowSelector() && mSelectorRect != null && !mSelectorRect.isEmpty()) { + final Drawable selector = mSelector; + selector.setBounds(mSelectorRect); + selector.draw(canvas); + } + } + + /** + * Controls whether the selection highlight drawable should be drawn on top of the item or + * behind it. + * + * @param onTop If true, the selector will be drawn on the item it is highlighting. The default + * is false. + * + * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop + */ + public void setDrawSelectorOnTop(boolean onTop) { + mDrawSelectorOnTop = onTop; + } + + /** + * Set a Drawable that should be used to highlight the currently selected item. + * + * @param resID A Drawable resource to use as the selection highlight. + * + * @attr ref android.R.styleable#AbsListView_listSelector + */ + public void setSelector(int resID) { + setSelector(getResources().getDrawable(resID)); + } + + public void setSelector(Drawable sel) { + if (mSelector != null) { + mSelector.setCallback(null); + unscheduleDrawable(mSelector); + } + mSelector = sel; + Rect padding = new Rect(); + sel.getPadding(padding); + mSelectionLeftPadding = padding.left; + mSelectionTopPadding = padding.top; + mSelectionRightPadding = padding.right; + mSelectionBottomPadding = padding.bottom; + sel.setCallback(this); + sel.setState(getDrawableState()); + } + + /** + * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the + * selection in the list. + * + * @return the drawable used to display the selector + */ + public Drawable getSelector() { + return mSelector; + } + + /** + * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if + * this is a long press. + */ + void keyPressed() { + if (!isEnabled() || !isClickable()) { + return; + } + + Drawable selector = mSelector; + Rect selectorRect = mSelectorRect; + if (selector != null && (isFocused() || touchModeDrawsInPressedState()) + && selectorRect != null && !selectorRect.isEmpty()) { + + final View v = getChildAt(mSelectedPosition - mFirstPosition); + + if (v != null) { + if (v.hasFocusable()) return; + v.setPressed(true); + } + setPressed(true); + + final boolean longClickable = isLongClickable(); + Drawable d = selector.getCurrent(); + if (d != null && d instanceof TransitionDrawable) { + if (longClickable) { + ((TransitionDrawable) d).startTransition( + ViewConfiguration.getLongPressTimeout()); + } else { + ((TransitionDrawable) d).resetTransition(); + } + } + if (longClickable && !mDataChanged) { + if (mPendingCheckForKeyLongPress == null) { + mPendingCheckForKeyLongPress = new CheckForKeyLongPress(); + } + mPendingCheckForKeyLongPress.rememberWindowAttachCount(); + postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout()); + } + } + } + + public void setScrollIndicators(View up, View down) { + mScrollUp = up; + mScrollDown = down; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mSelector != null) { + mSelector.setState(getDrawableState()); + } + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + // If the child view is enabled then do the default behavior. + if (mIsChildViewEnabled) { + // Common case + return super.onCreateDrawableState(extraSpace); + } + + // The selector uses this View's drawable state. The selected child view + // is disabled, so we need to remove the enabled state from the drawable + // states. + final int enabledState = ENABLED_STATE_SET[0]; + + // If we don't have any extra space, it will return one of the static state arrays, + // and clearing the enabled state on those arrays is a bad thing! If we specify + // we need extra space, it will create+copy into a new array that safely mutable. + int[] state = super.onCreateDrawableState(extraSpace + 1); + int enabledPos = -1; + for (int i = state.length - 1; i >= 0; i--) { + if (state[i] == enabledState) { + enabledPos = i; + break; + } + } + + // Remove the enabled state + if (enabledPos >= 0) { + System.arraycopy(state, enabledPos + 1, state, enabledPos, + state.length - enabledPos - 1); + } + + return state; + } + + @Override + public boolean verifyDrawable(Drawable dr) { + return mSelector == dr || super.verifyDrawable(dr); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + final ViewTreeObserver treeObserver = getViewTreeObserver(); + if (treeObserver != null) { + treeObserver.addOnTouchModeChangeListener(this); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + // Detach any view left in the scrap heap + mRecycler.clear(); + + final ViewTreeObserver treeObserver = getViewTreeObserver(); + if (treeObserver != null) { + treeObserver.removeOnTouchModeChangeListener(this); + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + + final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF; + + if (!hasWindowFocus) { + setChildrenDrawingCacheEnabled(false); + if (mFlingRunnable != null) { + removeCallbacks(mFlingRunnable); + // let the fling runnable report it's new state which + // should be idle + mFlingRunnable.endFling(); + + if (getScrollY() != 0) { + //mScrollY = 0; + scrollTo(getScrollX(), 0); + invalidate(); + } + } + + if (touchMode == TOUCH_MODE_OFF) { + // Remember the last selected element + mResurrectToPosition = mSelectedPosition; + } + } else { + + // If we changed touch mode since the last time we had focus + if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) { + // If we come back in trackball mode, we bring the selection back + if (touchMode == TOUCH_MODE_OFF) { + // This will trigger a layout + resurrectSelection(); + + // If we come back in touch mode, then we want to hide the selector + } else { + hideSelector(); + mLayoutMode = LAYOUT_NORMAL; + layoutChildren(); + } + } + } + + mLastTouchMode = touchMode; + } + + /** + * Creates the ContextMenuInfo returned from {@link #getContextMenuInfo()}. This + * methods knows the view, position and ID of the item that received the + * long press. + * + * @param view The view that received the long press. + * @param position The position of the item that received the long press. + * @param id The ID of the item that received the long press. + * @return The extra information that should be returned by + * {@link #getContextMenuInfo()}. + */ + ContextMenuInfo createContextMenuInfo(View view, int position, long id) { + return new AdapterContextMenuInfo(view, position, id); + } + + /** + * A base class for Runnables that will check that their view is still attached to + * the original window as when the Runnable was created. + * + */ + private class WindowRunnnable { + private int mOriginalAttachCount; + + public void rememberWindowAttachCount() { + mOriginalAttachCount = getWindowAttachCount(); + } + + public boolean sameWindow() { + return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; + } + } + + private class PerformClick extends WindowRunnnable implements Runnable { + View mChild; + int mClickMotionPosition; + + public void run() { + // The data has changed since we posted this action in the event queue, + // bail out before bad things happen + if (mDataChanged) return; + + final ListAdapter adapter = mAdapter; + final int motionPosition = mClickMotionPosition; + if (adapter != null && mItemCount > 0 && + motionPosition != INVALID_POSITION && + motionPosition < adapter.getCount() && sameWindow()) { + performItemClick(mChild, motionPosition, adapter.getItemId(motionPosition)); + } + } + } + + private class CheckForLongPress extends WindowRunnnable implements Runnable { + public void run() { + final int motionPosition = mMotionPosition; + final View child = getChildAt(motionPosition - mFirstPosition); + if (child != null) { + final int longPressPosition = mMotionPosition; + final long longPressId = mAdapter.getItemId(mMotionPosition); + + boolean handled = false; + if (sameWindow() && !mDataChanged) { + handled = performLongPress(child, longPressPosition, longPressId); + } + if (handled) { + mTouchMode = TOUCH_MODE_REST; + setPressed(false); + child.setPressed(false); + } else { + mTouchMode = TOUCH_MODE_DONE_WAITING; + } + + } + } + } + + private class CheckForKeyLongPress extends WindowRunnnable implements Runnable { + public void run() { + if (isPressed() && mSelectedPosition >= 0) { + int index = mSelectedPosition - mFirstPosition; + View v = getChildAt(index); + + if (!mDataChanged) { + boolean handled = false; + if (sameWindow()) { + handled = performLongPress(v, mSelectedPosition, mSelectedRowId); + } + if (handled) { + setPressed(false); + v.setPressed(false); + } + } else { + setPressed(false); + if (v != null) v.setPressed(false); + } + } + } + } + + private boolean performLongPress(final View child, + final int longPressPosition, final long longPressId) { + boolean handled = false; + + if (mOnItemLongClickListener != null) { + handled = mOnItemLongClickListener.onItemLongClick(PLA_AbsListView.this, child, + longPressPosition, longPressId); + } + if (!handled) { + mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); + handled = super.showContextMenuForChild(PLA_AbsListView.this); + } + if (handled) { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + return handled; + } + + @Override + protected ContextMenuInfo getContextMenuInfo() { + return mContextMenuInfo; + } + + @Override + public boolean showContextMenuForChild(View originalView) { + final int longPressPosition = getPositionForView(originalView); + if (longPressPosition >= 0) { + final long longPressId = mAdapter.getItemId(longPressPosition); + boolean handled = false; + + if (mOnItemLongClickListener != null) { + handled = mOnItemLongClickListener.onItemLongClick(PLA_AbsListView.this, originalView, + longPressPosition, longPressId); + } + if (!handled) { + mContextMenuInfo = createContextMenuInfo( + getChildAt(longPressPosition - mFirstPosition), + longPressPosition, longPressId); + handled = super.showContextMenuForChild(originalView); + } + + return handled; + } + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (!isEnabled()) { + return true; + } + if (isClickable() && isPressed() && + mSelectedPosition >= 0 && mAdapter != null && + mSelectedPosition < mAdapter.getCount()) { + + final View view = getChildAt(mSelectedPosition - mFirstPosition); + if (view != null) { + performItemClick(view, mSelectedPosition, mSelectedRowId); + view.setPressed(false); + } + setPressed(false); + return true; + } + break; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void dispatchSetPressed(boolean pressed) { + // Don't dispatch setPressed to our children. We call setPressed on ourselves to + // get the selector in the right state, but we don't want to press each child. + } + + /** + * Maps a point to a position in the list. + * + * @param x X in local coordinate + * @param y Y in local coordinate + * @return The position of the item which contains the specified point, or + * {@link #INVALID_POSITION} if the point does not intersect an item. + */ + public int pointToPosition(int x, int y) { + Rect frame = mTouchFrame; + if (frame == null) { + mTouchFrame = new Rect(); + frame = mTouchFrame; + } + + final int count = getChildCount(); + for (int i = count - 1; i >= 0; i--) { + final View child = getChildAt(i); + if (child.getVisibility() == View.VISIBLE) { + child.getHitRect(frame); + if (frame.contains(x, y)) { + return mFirstPosition + i; + } + } + } + return INVALID_POSITION; + } + + + /** + * Maps a point to a the rowId of the item which intersects that point. + * + * @param x X in local coordinate + * @param y Y in local coordinate + * @return The rowId of the item which contains the specified point, or {@link #INVALID_ROW_ID} + * if the point does not intersect an item. + */ + public long pointToRowId(int x, int y) { + int position = pointToPosition(x, y); + if (position >= 0) { + return mAdapter.getItemId(position); + } + return INVALID_ROW_ID; + } + + final class CheckForTap implements Runnable { + public void run() { + if (mTouchMode == TOUCH_MODE_DOWN) { + mTouchMode = TOUCH_MODE_TAP; + final View child = getChildAt(mMotionPosition - mFirstPosition); + if (child != null && !child.hasFocusable()) { + mLayoutMode = LAYOUT_NORMAL; + + if (!mDataChanged) { + layoutChildren(); + child.setPressed(true); + positionSelector(child); + setPressed(true); + + final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); + final boolean longClickable = isLongClickable(); + + if (mSelector != null) { + Drawable d = mSelector.getCurrent(); + if (d != null && d instanceof TransitionDrawable) { + if (longClickable) { + ((TransitionDrawable) d).startTransition(longPressTimeout); + } else { + ((TransitionDrawable) d).resetTransition(); + } + } + } + + if (longClickable) { + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + mPendingCheckForLongPress.rememberWindowAttachCount(); + postDelayed(mPendingCheckForLongPress, longPressTimeout); + } else { + mTouchMode = TOUCH_MODE_DONE_WAITING; + } + } else { + mTouchMode = TOUCH_MODE_DONE_WAITING; + } + } + } + } + } + + private boolean startScrollIfNeeded(int deltaY) { + // Check if we have moved far enough that it looks more like a + // scroll than a tap + final int distance = Math.abs(deltaY); + if (distance > mTouchSlop) { + createScrollingCache(); + mTouchMode = TOUCH_MODE_SCROLL; + mMotionCorrection = deltaY; + final Handler handler = getHandler(); + // Handler should not be null unless the AbsListView is not attached to a + // window, which would make it very hard to scroll it... but the monkeys + // say it's possible. + if (handler != null) { + handler.removeCallbacks(mPendingCheckForLongPress); + } + setPressed(false); + View motionView = getChildAt(mMotionPosition - mFirstPosition); + if (motionView != null) { + motionView.setPressed(false); + } + reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); + // Time to start stealing events! Once we've stolen them, don't let anyone + // steal from us + requestDisallowInterceptTouchEvent(true); + return true; + } + + return false; + } + + public void onTouchModeChanged(boolean isInTouchMode) { + if (isInTouchMode) { + // Get rid of the selection when we enter touch mode + hideSelector(); + // Layout, but only if we already have done so previously. + // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore + // state.) + if (getHeight() > 0 && getChildCount() > 0) { + // We do not lose focus initiating a touch (since AbsListView is focusable in + // touch mode). Force an initial layout to get rid of the selection. + layoutChildren(); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!isEnabled()) { + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return isClickable() || isLongClickable(); + } + + final int action = ev.getAction(); + + View v; + int deltaY; + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + mActivePointerId = ev.getPointerId(0); + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + int motionPosition = pointToPosition(x, y); + if (!mDataChanged) { + if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0) + && (getAdapter().isEnabled(motionPosition))) { + // User clicked on an actual view (and was not stopping a fling). It might be a + // click or a scroll. Assume it is a click until proven otherwise + mTouchMode = TOUCH_MODE_DOWN; + // FIXME Debounce + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + } else { + if (ev.getEdgeFlags() != 0 && motionPosition < 0) { + // If we couldn't find a view to click on, but the down event was touching + // the edge, we will bail out and try again. This allows the edge correcting + // code in ViewRoot to try to find a nearby view to select + return false; + } + + if (mTouchMode == TOUCH_MODE_FLING) { + // Stopped a fling. It is a scroll. + createScrollingCache(); + mTouchMode = TOUCH_MODE_SCROLL; + mMotionCorrection = 0; + motionPosition = findMotionRow(y); + reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); + } + } + } + + if (motionPosition >= 0) { + // Remember where the motion event started + v = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = v.getTop(); + } + mMotionX = x; + mMotionY = y; + mMotionPosition = motionPosition; + mLastY = Integer.MIN_VALUE; + break; + } + + case MotionEvent.ACTION_MOVE: { + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final int y = (int) ev.getY(pointerIndex); + deltaY = y - mMotionY; + switch (mTouchMode) { + case TOUCH_MODE_DOWN: + case TOUCH_MODE_TAP: + case TOUCH_MODE_DONE_WAITING: + // Check if we have moved far enough that it looks more like a + // scroll than a tap + startScrollIfNeeded(deltaY); + break; + case TOUCH_MODE_SCROLL: + if (PROFILE_SCROLLING) { + if (!mScrollProfilingStarted) { + Debug.startMethodTracing("AbsListViewScroll"); + mScrollProfilingStarted = true; + } + } + + if (y != mLastY) { + deltaY -= mMotionCorrection; + int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY; + + // No need to do all this work if we're not going to move anyway + boolean atEdge = false; + if (incrementalDeltaY != 0) { + atEdge = trackMotionScroll(deltaY, incrementalDeltaY); + } + + // Check to see if we have bumped into the scroll limit + if (atEdge && getChildCount() > 0) { + // Treat this like we're starting a new scroll from the current + // position. This will let the user start scrolling back into + // content immediately rather than needing to scroll back to the + // point where they hit the limit first. + int motionPosition = findMotionRow(y); + if (motionPosition >= 0) { + final View motionView = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = motionView.getTop(); + } + mMotionY = y; + mMotionPosition = motionPosition; + invalidate(); + } + mLastY = y; + } + break; + } + + break; + } + + case MotionEvent.ACTION_UP: { + switch (mTouchMode) { + case TOUCH_MODE_DOWN: + case TOUCH_MODE_TAP: + case TOUCH_MODE_DONE_WAITING: + final int motionPosition = mMotionPosition; + final View child = getChildAt(motionPosition - mFirstPosition); + if (child != null && !child.hasFocusable()) { + if (mTouchMode != TOUCH_MODE_DOWN) { + child.setPressed(false); + } + + if (mPerformClick == null) { + mPerformClick = new PerformClick(); + } + + final PLA_AbsListView.PerformClick performClick = mPerformClick; + performClick.mChild = child; + performClick.mClickMotionPosition = motionPosition; + performClick.rememberWindowAttachCount(); + + mResurrectToPosition = motionPosition; + + if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { + final Handler handler = getHandler(); + if (handler != null) { + handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? + mPendingCheckForTap : mPendingCheckForLongPress); + } + mLayoutMode = LAYOUT_NORMAL; + if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { + mTouchMode = TOUCH_MODE_TAP; + setSelectedPositionInt(mMotionPosition); + layoutChildren(); + child.setPressed(true); + positionSelector(child); + setPressed(true); + if (mSelector != null) { + Drawable d = mSelector.getCurrent(); + if (d != null && d instanceof TransitionDrawable) { + ((TransitionDrawable) d).resetTransition(); + } + } + postDelayed(new Runnable() { + public void run() { + child.setPressed(false); + setPressed(false); + if (!mDataChanged) { + post(performClick); + } + mTouchMode = TOUCH_MODE_REST; + } + }, ViewConfiguration.getPressedStateDuration()); + } else { + mTouchMode = TOUCH_MODE_REST; + } + return true; + } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { + post(performClick); + } + } + mTouchMode = TOUCH_MODE_REST; + break; + case TOUCH_MODE_SCROLL: + final int childCount = getChildCount(); + if (childCount > 0) { + int top = getCurrentChildTop(); + int bottom = getCurrentChildBottom(); + if (mFirstPosition == 0 && top >= mListPadding.top && + mFirstPosition + childCount < mItemCount && + bottom <= getHeight() - mListPadding.bottom) { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } else { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); + + if (Math.abs(initialVelocity) > mMinimumVelocity) { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + + mFlingRunnable.start(-initialVelocity); + } else { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } + } + } else { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } + break; + } + + setPressed(false); + + // Need to redraw since we probably aren't drawing the selector anymore + invalidate(); + + final Handler handler = getHandler(); + if (handler != null) { + handler.removeCallbacks(mPendingCheckForLongPress); + } + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + mActivePointerId = INVALID_POINTER; + + if (PROFILE_SCROLLING) { + if (mScrollProfilingStarted) { + Debug.stopMethodTracing(); + mScrollProfilingStarted = false; + } + } + break; + } + + case MotionEvent.ACTION_CANCEL: { + mTouchMode = TOUCH_MODE_REST; + setPressed(false); + View motionView = this.getChildAt(mMotionPosition - mFirstPosition); + if (motionView != null) { + motionView.setPressed(false); + } + clearScrollingCache(); + + final Handler handler = getHandler(); + if (handler != null) { + handler.removeCallbacks(mPendingCheckForLongPress); + } + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + mActivePointerId = INVALID_POINTER; + break; + } + + case MotionEvent.ACTION_POINTER_UP: { + onSecondaryPointerUp(ev); + final int x = mMotionX; + final int y = mMotionY; + final int motionPosition = pointToPosition(x, y); + if (motionPosition >= 0) { + // Remember where the motion event started + v = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = v.getTop(); + mMotionPosition = motionPosition; + } + mLastY = y; + break; + } + } + + return true; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + int action = ev.getAction(); + View v; + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + int touchMode = mTouchMode; + + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + + int motionPosition = findMotionRow(y); + if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) { + // User clicked on an actual view (and was not stopping a fling). + // Remember where the motion event started + v = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = v.getTop(); + mMotionX = x; + mMotionY = y; + mMotionPosition = motionPosition; + mTouchMode = TOUCH_MODE_DOWN; + clearScrollingCache(); + } + mLastY = Integer.MIN_VALUE; + if (touchMode == TOUCH_MODE_FLING) { + return true; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + switch (mTouchMode) { + case TOUCH_MODE_DOWN: + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final int y = (int) ev.getY(pointerIndex); + if (startScrollIfNeeded(y - mMotionY)) { + return true; + } + break; + } + break; + } + + case MotionEvent.ACTION_UP: { + mTouchMode = TOUCH_MODE_REST; + mActivePointerId = INVALID_POINTER; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + break; + } + + case MotionEvent.ACTION_POINTER_UP: { + onSecondaryPointerUp(ev); + break; + } + } + + return false; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mMotionX = (int) ev.getX(newPointerIndex); + mMotionY = (int) ev.getY(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addTouchables(ArrayList views) { + final int count = getChildCount(); + final int firstPosition = mFirstPosition; + final ListAdapter adapter = mAdapter; + + if (adapter == null) { + return; + } + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (adapter.isEnabled(firstPosition + i)) { + views.add(child); + } + child.addTouchables(views); + } + } + + /** + * Fires an "on scroll state changed" event to the registered + * {@link android.widget.AbsListView.OnScrollListener}, if any. The state change + * is fired only if the specified state is different from the previously known state. + * + * @param newState The new scroll state. + */ + void reportScrollStateChange(int newState) { + if (newState != mLastScrollState) { + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChanged(this, newState); + mLastScrollState = newState; + } + } + } + + /** + * Responsible for fling behavior. Use {@link #start(int)} to + * initiate a fling. Each frame of the fling is handled in {@link #run()}. + * A FlingRunnable will keep re-posting itself until the fling is done. + * + */ + private class FlingRunnable implements Runnable { + /** + * Tracks the decay of a fling scroll + */ + private final Scroller mScroller; + + /** + * Y value reported by mScroller on the previous fling + */ + private int mLastFlingY; + + FlingRunnable() { + mScroller = new Scroller(getContext()); + } + + void start(int initialVelocity) { + int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0; + mLastFlingY = initialY; + mScroller.fling(0, initialY, 0, initialVelocity, + 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); + mTouchMode = TOUCH_MODE_FLING; + post(this); + + if (PROFILE_FLINGING) { + if (!mFlingProfilingStarted) { + Debug.startMethodTracing("AbsListViewFling"); + mFlingProfilingStarted = true; + } + } + } + + void startScroll(int distance, int duration) { + int initialY = distance < 0 ? Integer.MAX_VALUE : 0; + mLastFlingY = initialY; + mScroller.startScroll(0, initialY, 0, distance, duration); + mTouchMode = TOUCH_MODE_FLING; + post(this); + } + + private void endFling() { + mTouchMode = TOUCH_MODE_REST; + + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + clearScrollingCache(); + + removeCallbacks(this); + + if (mPositionScroller != null) { + removeCallbacks(mPositionScroller); + } + } + + public void run() { + switch (mTouchMode) { + default: + return; + + case TOUCH_MODE_FLING: { + if (mItemCount == 0 || getChildCount() == 0) { + endFling(); + return; + } + + final Scroller scroller = mScroller; + boolean more = scroller.computeScrollOffset(); + final int y = scroller.getCurrY(); + + // Flip sign to convert finger direction to list items direction + // (e.g. finger moving down means list is moving towards the top) + int delta = mLastFlingY - y; + + // Pretend that each frame of a fling scroll is a touch scroll + if (delta > 0) { + // List is moving towards the top. Use first view as mMotionPosition + mMotionPosition = mFirstPosition; + final View firstView = getChildAt(0); + mMotionViewOriginalTop = firstView.getTop(); + + // Don't fling more than 1 screen + // delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta); + delta = Math.min(getHeight() - getPaddingBottom() - getPaddingTop() - 1, delta); + } else { + // List is moving towards the bottom. Use last view as mMotionPosition + int offsetToLast = getChildCount() - 1; + mMotionPosition = mFirstPosition + offsetToLast; + + final View lastView = getChildAt(offsetToLast); + mMotionViewOriginalTop = lastView.getTop(); + + // Don't fling more than 1 screen + // delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta); + delta = Math.max(-(getHeight() - getPaddingBottom() - getPaddingTop() - 1), delta); + } + + final boolean atEnd = trackMotionScroll(delta, delta); + + if (more && !atEnd) { + invalidate(); + mLastFlingY = y; + post(this); + } else { + endFling(); + + if (PROFILE_FLINGING) { + if (mFlingProfilingStarted) { + Debug.stopMethodTracing(); + mFlingProfilingStarted = false; + } + } + } + break; + } + } + + } + } + + + class PositionScroller implements Runnable { + private static final int SCROLL_DURATION = 400; + + private static final int MOVE_DOWN_POS = 1; + private static final int MOVE_UP_POS = 2; + private static final int MOVE_DOWN_BOUND = 3; + private static final int MOVE_UP_BOUND = 4; + + private int mMode; + private int mTargetPos; + private int mBoundPos; + private int mLastSeenPos; + private int mScrollDuration; + private final int mExtraScroll; + + PositionScroller() { + mExtraScroll = ViewConfiguration.get(getContext()).getScaledFadingEdgeLength(); + } + + void start(int position) { + final int firstPos = mFirstPosition; + final int lastPos = firstPos + getChildCount() - 1; + + int viewTravelCount = 0; + if (position <= firstPos) { + viewTravelCount = firstPos - position + 1; + mMode = MOVE_UP_POS; + } else if (position >= lastPos) { + viewTravelCount = position - lastPos + 1; + mMode = MOVE_DOWN_POS; + } else { + // Already on screen, nothing to do + return; + } + + if (viewTravelCount > 0) { + mScrollDuration = SCROLL_DURATION / viewTravelCount; + } else { + mScrollDuration = SCROLL_DURATION; + } + mTargetPos = position; + mBoundPos = INVALID_POSITION; + mLastSeenPos = INVALID_POSITION; + + post(this); + } + + void start(int position, int boundPosition) { + if (boundPosition == INVALID_POSITION) { + start(position); + return; + } + + final int firstPos = mFirstPosition; + final int lastPos = firstPos + getChildCount() - 1; + + int viewTravelCount = 0; + if (position <= firstPos) { + final int boundPosFromLast = lastPos - boundPosition; + if (boundPosFromLast < 1) { + // Moving would shift our bound position off the screen. Abort. + return; + } + + final int posTravel = firstPos - position + 1; + final int boundTravel = boundPosFromLast - 1; + if (boundTravel < posTravel) { + viewTravelCount = boundTravel; + mMode = MOVE_UP_BOUND; + } else { + viewTravelCount = posTravel; + mMode = MOVE_UP_POS; + } + } else if (position >= lastPos) { + final int boundPosFromFirst = boundPosition - firstPos; + if (boundPosFromFirst < 1) { + // Moving would shift our bound position off the screen. Abort. + return; + } + + final int posTravel = position - lastPos + 1; + final int boundTravel = boundPosFromFirst - 1; + if (boundTravel < posTravel) { + viewTravelCount = boundTravel; + mMode = MOVE_DOWN_BOUND; + } else { + viewTravelCount = posTravel; + mMode = MOVE_DOWN_POS; + } + } else { + // Already on screen, nothing to do + return; + } + + if (viewTravelCount > 0) { + mScrollDuration = SCROLL_DURATION / viewTravelCount; + } else { + mScrollDuration = SCROLL_DURATION; + } + mTargetPos = position; + mBoundPos = boundPosition; + mLastSeenPos = INVALID_POSITION; + + post(this); + } + + void stop() { + removeCallbacks(this); + } + + public void run() { + final int listHeight = getHeight(); + final int firstPos = mFirstPosition; + + switch (mMode) { + case MOVE_DOWN_POS: { + final int lastViewIndex = getChildCount() - 1; + final int lastPos = firstPos + lastViewIndex; + + if (lastViewIndex < 0) { + return; + } + + if (lastPos == mLastSeenPos) { + // No new views, let things keep going. + post(this); + return; + } + + final View lastView = getChildAt(lastViewIndex); + final int lastViewHeight = lastView.getHeight(); + final int lastViewTop = lastView.getTop(); + final int lastViewPixelsShowing = listHeight - lastViewTop; + final int extraScroll = lastPos < mItemCount - 1 ? mExtraScroll : mListPadding.bottom; + + smoothScrollBy(lastViewHeight - lastViewPixelsShowing + extraScroll, + mScrollDuration); + + mLastSeenPos = lastPos; + if (lastPos < mTargetPos) { + post(this); + } + break; + } + + case MOVE_DOWN_BOUND: { + final int nextViewIndex = 1; + final int childCount = getChildCount(); + + if (firstPos == mBoundPos || childCount <= nextViewIndex + || firstPos + childCount >= mItemCount) { + return; + } + final int nextPos = firstPos + nextViewIndex; + + if (nextPos == mLastSeenPos) { + // No new views, let things keep going. + post(this); + return; + } + + final View nextView = getChildAt(nextViewIndex); + final int nextViewHeight = nextView.getHeight(); + final int nextViewTop = nextView.getTop(); + final int extraScroll = mExtraScroll; + if (nextPos < mBoundPos) { + smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll), + mScrollDuration); + + mLastSeenPos = nextPos; + + post(this); + } else { + if (nextViewTop > extraScroll) { + smoothScrollBy(nextViewTop - extraScroll, mScrollDuration); + } + } + break; + } + + case MOVE_UP_POS: { + if (firstPos == mLastSeenPos) { + // No new views, let things keep going. + post(this); + return; + } + + final View firstView = getChildAt(0); + if (firstView == null) { + return; + } + final int firstViewTop = firstView.getTop(); + final int extraScroll = firstPos > 0 ? mExtraScroll : mListPadding.top; + + smoothScrollBy(firstViewTop - extraScroll, mScrollDuration); + + mLastSeenPos = firstPos; + + if (firstPos > mTargetPos) { + post(this); + } + break; + } + + case MOVE_UP_BOUND: { + final int lastViewIndex = getChildCount() - 2; + if (lastViewIndex < 0) { + return; + } + final int lastPos = firstPos + lastViewIndex; + + if (lastPos == mLastSeenPos) { + // No new views, let things keep going. + post(this); + return; + } + + final View lastView = getChildAt(lastViewIndex); + final int lastViewHeight = lastView.getHeight(); + final int lastViewTop = lastView.getTop(); + final int lastViewPixelsShowing = listHeight - lastViewTop; + mLastSeenPos = lastPos; + if (lastPos > mBoundPos) { + smoothScrollBy(-(lastViewPixelsShowing - mExtraScroll), mScrollDuration); + post(this); + } else { + final int bottom = listHeight - mExtraScroll; + final int lastViewBottom = lastViewTop + lastViewHeight; + if (bottom > lastViewBottom) { + smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration); + } + } + break; + } + + default: + break; + } + } + } + + /** + * Smoothly scroll to the specified adapter position. The view will + * scroll such that the indicated position is displayed. + * @param position Scroll to this adapter position. + */ + public void smoothScrollToPosition(int position) { + if (mPositionScroller == null) { + mPositionScroller = new PositionScroller(); + } + mPositionScroller.start(position); + } + + /** + * Smoothly scroll to the specified adapter position. The view will + * scroll such that the indicated position is displayed, but it will + * stop early if scrolling further would scroll boundPosition out of + * view. + * @param position Scroll to this adapter position. + * @param boundPosition Do not scroll if it would move this adapter + * position out of view. + */ + public void smoothScrollToPosition(int position, int boundPosition) { + if (mPositionScroller == null) { + mPositionScroller = new PositionScroller(); + } + mPositionScroller.start(position, boundPosition); + } + + /** + * Smoothly scroll by distance pixels over duration milliseconds. + * @param distance Distance to scroll in pixels. + * @param duration Duration of the scroll animation in milliseconds. + */ + public void smoothScrollBy(int distance, int duration) { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } else { + mFlingRunnable.endFling(); + } + mFlingRunnable.startScroll(distance, duration); + } + + private void createScrollingCache() { + if (mScrollingCacheEnabled && !mCachingStarted) { + setChildrenDrawnWithCacheEnabled(true); + setChildrenDrawingCacheEnabled(true); + mCachingStarted = true; + } + } + + private void clearScrollingCache() { + if (mClearScrollingCache == null) { + mClearScrollingCache = new Runnable() { + public void run() { + if (mCachingStarted) { + mCachingStarted = false; + setChildrenDrawnWithCacheEnabled(false); + final int mPersistentDrawingCache = getPersistentDrawingCache(); + if ((mPersistentDrawingCache & PERSISTENT_SCROLLING_CACHE) == 0) { + setChildrenDrawingCacheEnabled(false); + } + if (!isAlwaysDrawnWithCacheEnabled()) { + invalidate(); + } + } + } + }; + } + post(mClearScrollingCache); + } + + /** + * Track a motion scroll + * + * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion + * began. Positive numbers mean the user's finger is moving down the screen. + * @param incrementalDeltaY Change in deltaY from the previous event. + * @return true if we're already at the beginning/end of the list and have nothing to do. + */ + @SuppressWarnings("deprecation") + boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { + final int childCount = getChildCount(); + if (childCount == 0) { + return true; + } + + final int firstTop = getCurrentChildTop(); + //final int lastBottom = getChildAt(childCount - 1).getBottom(); + final int lastBottom = getCurrentChildBottom(); + + final Rect listPadding = mListPadding; + + // FIXME account for grid vertical spacing too? + final int spaceAbove = listPadding.top - firstTop; + final int end = getHeight() - listPadding.bottom; + final int spaceBelow = lastBottom - end; + + // final int height = getHeight() - mPaddingBottom - mPaddingTop; + final int height = getHeight() - getPaddingBottom() - getPaddingTop(); + if (deltaY < 0) { + deltaY = Math.max(-(height - 1), deltaY); + } else { + deltaY = Math.min(height - 1, deltaY); + } + + if (incrementalDeltaY < 0) { + incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); + } else { + incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); + } + + final int firstPosition = mFirstPosition; + + if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) { + // Don't need to move views down if the top of the first position + // is already visible + return true; + } + + if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) { + // Don't need to move views up if the bottom of the last position + // is already visible + return true; + } + + final boolean down = incrementalDeltaY < 0; + + final boolean inTouchMode = isInTouchMode(); + if (inTouchMode) { + hideSelector(); + } + + final int headerViewsCount = getHeaderViewsCount(); + final int footerViewsStart = mItemCount - getFooterViewsCount(); + + int start = 0; + int count = 0; + + if (down) { + final int top = listPadding.top - incrementalDeltaY; + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getBottom() >= top) { + break; + } else { + count++; + int position = firstPosition + i; + if (position >= headerViewsCount && position < footerViewsStart) { + mRecycler.addScrapView(child); + + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(child, + ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, + firstPosition + i, -1); + } + } + } + } + } else { + final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY; + for (int i = childCount - 1; i >= 0; i--) { + final View child = getChildAt(i); + if (child.getTop() <= bottom) { + break; + } else { + start = i; + count++; + int position = firstPosition + i; + if (position >= headerViewsCount && position < footerViewsStart) { + mRecycler.addScrapView(child); + + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(child, + ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, + firstPosition + i, -1); + } + } + } + } + } + + mMotionViewNewTop = mMotionViewOriginalTop + deltaY; + + mBlockLayoutRequests = true; + + if (count > 0) { + detachViewsFromParent(start, count); + } + + //offsetChildrenTopAndBottom(incrementalDeltaY); + tryOffsetChildrenTopAndBottom(incrementalDeltaY); + + if (down) { + mFirstPosition += count; + } + + invalidate(); + + final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); + if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { + fillGap(down); + } + + if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { + final int childIndex = mSelectedPosition - mFirstPosition; + if (childIndex >= 0 && childIndex < getChildCount()) { + positionSelector(getChildAt(childIndex)); + } + } + + mBlockLayoutRequests = false; + invokeOnItemScrollListener(); + awakenScrollBars(); + + return false; + } + + protected int getCurrentChildBottom() { + return getChildAt(getChildCount() - 1).getBottom(); + } + + protected int getCurrentChildTop() { + return getChildAt(0).getTop(); + } + + protected void tryOffsetChildrenTopAndBottom(int offset) { + + Method method; + try { + method = getClass().getMethod("offsetChildrenTopAndBottom", Integer.class); + method.invoke(this, offset); + } catch (Exception e) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View v = getChildAt(i); + v.layout(v.getLeft(), v.getTop() + offset, v.getRight(), v.getBottom() + offset); + } + } + } + + /** + * Returns the number of header views in the list. Header views are special views + * at the top of the list that should not be recycled during a layout. + * + * @return The number of header views, 0 in the default implementation. + */ + int getHeaderViewsCount() { + return 0; + } + + /** + * Returns the number of footer views in the list. Footer views are special views + * at the bottom of the list that should not be recycled during a layout. + * + * @return The number of footer views, 0 in the default implementation. + */ + int getFooterViewsCount() { + return 0; + } + + /** + * Fills the gap left open by a touch-scroll. During a touch scroll, children that + * remain on screen are shifted and the other ones are discarded. The role of this + * method is to fill the gap thus created by performing a partial layout in the + * empty space. + * + * @param down true if the scroll is going down, false if it is going up + */ + abstract void fillGap(boolean down); + + void hideSelector() { + if (mSelectedPosition != INVALID_POSITION) { + if (mLayoutMode != LAYOUT_SPECIFIC) { + mResurrectToPosition = mSelectedPosition; + } + if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) { + mResurrectToPosition = mNextSelectedPosition; + } + setSelectedPositionInt(INVALID_POSITION); + setNextSelectedPositionInt(INVALID_POSITION); + mSelectedTop = 0; + mSelectorRect.setEmpty(); + } + } + + /** + * @return A position to select. First we try mSelectedPosition. If that has been clobbered by + * entering touch mode, we then try mResurrectToPosition. Values are pinned to the range + * of items available in the adapter + */ + int reconcileSelectedPosition() { + int position = mSelectedPosition; + if (position < 0) { + position = mResurrectToPosition; + } + position = Math.max(0, position); + position = Math.min(position, mItemCount - 1); + return position; + } + + /** + * Find the row closest to y. This row will be used as the motion row when scrolling + * + * @param y Where the user touched + * @return The position of the first (or only) item in the row containing y + */ + abstract int findMotionRow(int y); + + /** + * Find the row closest to y. This row will be used as the motion row when scrolling. + * + * @param y Where the user touched + * @return The position of the first (or only) item in the row closest to y + */ + int findClosestMotionRow(int y) { + final int childCount = getChildCount(); + if (childCount == 0) { + return INVALID_POSITION; + } + + final int motionRow = findMotionRow(y); + return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1; + } + + /** + * Causes all the views to be rebuilt and redrawn. + */ + public void invalidateViews() { + mDataChanged = true; + rememberSyncState(); + requestLayout(); + invalidate(); + } + + /** + * Makes the item at the supplied position selected. + * + * @param position the position of the new selection + */ + abstract void setSelectionInt(int position); + + /** + * Attempt to bring the selection back if the user is switching from touch + * to trackball mode + * @return Whether selection was set to something. + */ + boolean resurrectSelection() { + final int childCount = getChildCount(); + + if (childCount <= 0) { + return false; + } + + int selectedTop = 0; + int selectedPos; + int childrenTop = mListPadding.top; + // int childrenBottom = mBottom - mTop - mListPadding.bottom; + int childrenBottom = getBottom() - getTop() - mListPadding.bottom; + final int firstPosition = mFirstPosition; + final int toPosition = mResurrectToPosition; + boolean down = true; + + if (toPosition >= firstPosition && toPosition < firstPosition + childCount) { + selectedPos = toPosition; + + final View selected = getChildAt(selectedPos - mFirstPosition); + selectedTop = selected.getTop(); + int selectedBottom = selected.getBottom(); + + // We are scrolled, don't get in the fade + if (selectedTop < childrenTop) { + selectedTop = childrenTop + getVerticalFadingEdgeLength(); + } else if (selectedBottom > childrenBottom) { + selectedTop = childrenBottom - selected.getMeasuredHeight() + - getVerticalFadingEdgeLength(); + } + } else { + if (toPosition < firstPosition) { + // Default to selecting whatever is first + selectedPos = firstPosition; + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + final int top = v.getTop(); + + if (i == 0) { + // Remember the position of the first item + selectedTop = top; + // See if we are scrolled at all + if (firstPosition > 0 || top < childrenTop) { + // If we are scrolled, don't select anything that is + // in the fade region + childrenTop += getVerticalFadingEdgeLength(); + } + } + if (top >= childrenTop) { + // Found a view whose top is fully visisble + selectedPos = firstPosition + i; + selectedTop = top; + break; + } + } + } else { + final int itemCount = mItemCount; + down = false; + selectedPos = firstPosition + childCount - 1; + + for (int i = childCount - 1; i >= 0; i--) { + final View v = getChildAt(i); + final int top = v.getTop(); + final int bottom = v.getBottom(); + + if (i == childCount - 1) { + selectedTop = top; + if (firstPosition + childCount < itemCount || bottom > childrenBottom) { + childrenBottom -= getVerticalFadingEdgeLength(); + } + } + + if (bottom <= childrenBottom) { + selectedPos = firstPosition + i; + selectedTop = top; + break; + } + } + } + } + + mResurrectToPosition = INVALID_POSITION; + removeCallbacks(mFlingRunnable); + mTouchMode = TOUCH_MODE_REST; + clearScrollingCache(); + mSpecificTop = selectedTop; + selectedPos = lookForSelectablePosition(selectedPos, down); + if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) { + mLayoutMode = LAYOUT_SPECIFIC; + setSelectionInt(selectedPos); + invokeOnItemScrollListener(); + } else { + selectedPos = INVALID_POSITION; + } + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + + return selectedPos >= 0; + } + + @Override + protected void handleDataChanged() { + int count = mItemCount; + if (count > 0) { + + int newPos; + + int selectablePos; + + // Find the row we are supposed to sync to + if (mNeedSync) { + // Update this first, since setNextSelectedPositionInt inspects it + mNeedSync = false; + + if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL || + (mTranscriptMode == TRANSCRIPT_MODE_NORMAL && + mFirstPosition + getChildCount() >= mOldItemCount)) { + mLayoutMode = LAYOUT_FORCE_BOTTOM; + return; + } + + switch (mSyncMode) { + case SYNC_SELECTED_POSITION: + if (isInTouchMode()) { + // We saved our state when not in touch mode. (We know this because + // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to + // restore in touch mode. Just leave mSyncPosition as it is (possibly + // adjusting if the available range changed) and return. + mLayoutMode = LAYOUT_SYNC; + mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); + + return; + } else { + // See if we can find a position in the new data with the same + // id as the old selection. This will change mSyncPosition. + newPos = findSyncPosition(); + if (newPos >= 0) { + // Found it. Now verify that new selection is still selectable + selectablePos = lookForSelectablePosition(newPos, true); + if (selectablePos == newPos) { + // Same row id is selected + mSyncPosition = newPos; + + if (mSyncHeight == getHeight()) { + // If we are at the same height as when we saved state, try + // to restore the scroll position too. + mLayoutMode = LAYOUT_SYNC; + } else { + // We are not the same height as when the selection was saved, so + // don't try to restore the exact position + mLayoutMode = LAYOUT_SET_SELECTION; + } + + // Restore selection + setNextSelectedPositionInt(newPos); + return; + } + } + } + break; + case SYNC_FIRST_POSITION: + // Leave mSyncPosition as it is -- just pin to available range + mLayoutMode = LAYOUT_SYNC; + mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); + + return; + } + } + + if (!isInTouchMode()) { + // We couldn't find matching data -- try to use the same position + newPos = getSelectedItemPosition(); + + // Pin position to the available range + if (newPos >= count) { + newPos = count - 1; + } + if (newPos < 0) { + newPos = 0; + } + + // Make sure we select something selectable -- first look down + selectablePos = lookForSelectablePosition(newPos, true); + + if (selectablePos >= 0) { + setNextSelectedPositionInt(selectablePos); + return; + } else { + // Looking down didn't work -- try looking up + selectablePos = lookForSelectablePosition(newPos, false); + if (selectablePos >= 0) { + setNextSelectedPositionInt(selectablePos); + return; + } + } + } else { + + // We already know where we want to resurrect the selection + if (mResurrectToPosition >= 0) { + return; + } + } + + } + + // Nothing is selected. Give up and reset everything. + mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP; + mSelectedPosition = INVALID_POSITION; + mSelectedRowId = INVALID_ROW_ID; + mNextSelectedPosition = INVALID_POSITION; + mNextSelectedRowId = INVALID_ROW_ID; + mNeedSync = false; + checkSelectionChanged(); + } + + /** + * What is the distance between the source and destination rectangles given the direction of + * focus navigation between them? The direction basically helps figure out more quickly what is + * self evident by the relationship between the rects... + * + * @param source the source rectangle + * @param dest the destination rectangle + * @param direction the direction + * @return the distance between the rectangles + */ + static int getDistance(Rect source, Rect dest, int direction) { + int sX, sY; // source x, y + int dX, dY; // dest x, y + switch (direction) { + case View.FOCUS_RIGHT: + sX = source.right; + sY = source.top + source.height() / 2; + dX = dest.left; + dY = dest.top + dest.height() / 2; + break; + case View.FOCUS_DOWN: + sX = source.left + source.width() / 2; + sY = source.bottom; + dX = dest.left + dest.width() / 2; + dY = dest.top; + break; + case View.FOCUS_LEFT: + sX = source.left; + sY = source.top + source.height() / 2; + dX = dest.right; + dY = dest.top + dest.height() / 2; + break; + case View.FOCUS_UP: + sX = source.left + source.width() / 2; + sY = source.top; + dX = dest.left + dest.width() / 2; + dY = dest.bottom; + break; + default: + throw new IllegalArgumentException("direction must be one of " + + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); + } + int deltaX = dX - sX; + int deltaY = dY - sY; + return deltaY * deltaY + deltaX * deltaX; + } + + + @Override + public void onGlobalLayout() { + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new PLA_AbsListView.LayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof PLA_AbsListView.LayoutParams; + } + + /** + * Puts the list or grid into transcript mode. In this mode the list or grid will always scroll + * to the bottom to show new items. + * + * @param mode the transcript mode to set + * + * @see #TRANSCRIPT_MODE_DISABLED + * @see #TRANSCRIPT_MODE_NORMAL + * @see #TRANSCRIPT_MODE_ALWAYS_SCROLL + */ + public void setTranscriptMode(int mode) { + mTranscriptMode = mode; + } + + /** + * Returns the current transcript mode. + * + * @return {@link #TRANSCRIPT_MODE_DISABLED}, {@link #TRANSCRIPT_MODE_NORMAL} or + * {@link #TRANSCRIPT_MODE_ALWAYS_SCROLL} + */ + public int getTranscriptMode() { + return mTranscriptMode; + } + + @Override + public int getSolidColor() { + return mCacheColorHint; + } + + /** + * When set to a non-zero value, the cache color hint indicates that this list is always drawn + * on top of a solid, single-color, opaque background + * + * @param color The background color + */ + public void setCacheColorHint(int color) { + if (color != mCacheColorHint) { + mCacheColorHint = color; + int count = getChildCount(); + for (int i = 0; i < count; i++) { + getChildAt(i).setDrawingCacheBackgroundColor(color); + } + mRecycler.setCacheColorHint(color); + } + } + + /** + * When set to a non-zero value, the cache color hint indicates that this list is always drawn + * on top of a solid, single-color, opaque background + * + * @return The cache color hint + */ + public int getCacheColorHint() { + return mCacheColorHint; + } + + /** + * Move all views (excluding headers and footers) held by this AbsListView into the supplied + * List. This includes views displayed on the screen as well as views stored in AbsListView's + * internal view recycler. + * + * @param views A list into which to put the reclaimed views + */ + public void reclaimViews(List views) { + int childCount = getChildCount(); + RecyclerListener listener = mRecycler.mRecyclerListener; + + // Reclaim views on screen + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + PLA_AbsListView.LayoutParams lp = (PLA_AbsListView.LayoutParams) child.getLayoutParams(); + // Don't reclaim header or footer views, or views that should be ignored + if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) { + views.add(child); + if (listener != null) { + // Pretend they went through the scrap heap + listener.onMovedToScrapHeap(child); + } + } + } + mRecycler.reclaimScrapViews(views); + removeAllViewsInLayout(); + } + + //TODO I'm not sure the purpose of onConsistencyCheck mehtod. it looks like debug related.. so just comment out. + // /** + // * @hide + // */ + // @Override + // protected boolean onConsistencyCheck(int consistency) { + // boolean result = super.onConsistencyCheck(consistency); + // + // final boolean checkLayout = (consistency & ViewDebug.CONSISTENCY_LAYOUT) != 0; + // + // if (checkLayout) { + // // The active recycler must be empty + // final View[] activeViews = mRecycler.mActiveViews; + // int count = activeViews.length; + // for (int i = 0; i < count; i++) { + // if (activeViews[i] != null) { + // result = false; + // Log.d("ViewDebug", + // "AbsListView " + this + " has a view in its active recycler: " + + // activeViews[i]); + // } + // } + // + // // All views in the recycler must NOT be on screen and must NOT have a parent + // final ArrayList scrap = mRecycler.mCurrentScrap; + // if (!checkScrap(scrap)) result = false; + // final ArrayList[] scraps = mRecycler.mScrapViews; + // count = scraps.length; + // for (int i = 0; i < count; i++) { + // if (!checkScrap(scraps[i])) result = false; + // } + // } + // + // return result; + // } + + // private boolean checkScrap(ArrayList scrap) { + // if (scrap == null) return true; + // boolean result = true; + // + // final int count = scrap.size(); + // for (int i = 0; i < count; i++) { + // final View view = scrap.get(i); + // if (view.getParent() != null) { + // result = false; + // Log.d("ViewDebug", "AbsListView " + this + + // " has a view in its scrap heap still attached to a parent: " + view); + // } + // if (indexOfChild(view) >= 0) { + // result = false; + // Log.d("ViewDebug", "AbsListView " + this + + // " has a view in its scrap heap that is also a direct child: " + view); + // } + // } + // + // return result; + // } + + /** + * Sets the recycler listener to be notified whenever a View is set aside in + * the recycler for later reuse. This listener can be used to free resources + * associated to the View. + * + * @param listener The recycler listener to be notified of views set aside + * in the recycler. + * + * @see android.widget.PLA_AbsListView.RecycleBin + * @see android.widget.AbsListView.RecyclerListener + */ + public void setRecyclerListener(RecyclerListener listener) { + mRecycler.mRecyclerListener = listener; + } + + /** + * AbsListView extends LayoutParams to provide a place to hold the view type. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * View type for this view, as returned by + * {@link android.widget.Adapter#getItemViewType(int) } + */ + @ViewDebug.ExportedProperty(mapping = { + @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_IGNORE, to = "ITEM_VIEW_TYPE_IGNORE"), + @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_HEADER_OR_FOOTER, to = "ITEM_VIEW_TYPE_HEADER_OR_FOOTER") + }) + public int viewType; + + /** + * When this boolean is set, the view has been added to the AbsListView + * at least once. It is used to know whether headers/footers have already + * been added to the list view and whether they should be treated as + * recycled views or not. + */ + @ViewDebug.ExportedProperty + public boolean recycledHeaderFooter; + + /** + * When an AbsListView is measured with an AT_MOST measure spec, it needs + * to obtain children views to measure itself. When doing so, the children + * are not attached to the window, but put in the recycler which assumes + * they've been attached before. Setting this flag will force the reused + * view to be attached to the window rather than just attached to the + * parent. + */ + @ViewDebug.ExportedProperty + public boolean forceAdd; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int w, int h) { + super(w, h); + } + + public LayoutParams(int w, int h, int viewType) { + super(w, h); + this.viewType = viewType; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + } + + /** + * A RecyclerListener is used to receive a notification whenever a View is placed + * inside the RecycleBin's scrap heap. This listener is used to free resources + * associated to Views placed in the RecycleBin. + * + * @see android.widget.PLA_AbsListView.RecycleBin + * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) + */ + public static interface RecyclerListener { + /** + * Indicates that the specified View was moved into the recycler's scrap heap. + * The view is not displayed on screen any more and any expensive resource + * associated with the view should be discarded. + * + * @param view + */ + void onMovedToScrapHeap(View view); + } + + /** + * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of + * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the + * start of a layout. By construction, they are displaying current information. At the end of + * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that + * could potentially be used by the adapter to avoid allocating views unnecessarily. + * + * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) + * @see android.widget.AbsListView.RecyclerListener + */ + class RecycleBin { + private RecyclerListener mRecyclerListener; + + /** + * The position of the first view stored in mActiveViews. + */ + private int mFirstActivePosition; + + /** + * Views that were on screen at the start of layout. This array is populated at the start of + * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. + * Views in mActiveViews represent a contiguous range of Views, with position of the first + * view store in mFirstActivePosition. + */ + private View[] mActiveViews = new View[0]; + + /** + * Unsorted views that can be used by the adapter as a convert view. + */ + private ArrayList[] mScrapViews; + + private int mViewTypeCount; + + private ArrayList mCurrentScrap; + + public void setViewTypeCount(int viewTypeCount) { + if (viewTypeCount < 1) { + throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); + } + //noinspection unchecked + ArrayList[] scrapViews = new ArrayList[viewTypeCount]; + for (int i = 0; i < viewTypeCount; i++) { + scrapViews[i] = new ArrayList(); + } + mViewTypeCount = viewTypeCount; + mCurrentScrap = scrapViews[0]; + mScrapViews = scrapViews; + } + + public void markChildrenDirty() { + if (mViewTypeCount == 1) { + final ArrayList scrap = mCurrentScrap; + final int scrapCount = scrap.size(); + for (int i = 0; i < scrapCount; i++) { + scrap.get(i).forceLayout(); + } + } else { + final int typeCount = mViewTypeCount; + for (int i = 0; i < typeCount; i++) { + final ArrayList scrap = mScrapViews[i]; + final int scrapCount = scrap.size(); + for (int j = 0; j < scrapCount; j++) { + scrap.get(j).forceLayout(); + } + } + } + } + + public boolean shouldRecycleViewType(int viewType) { + return viewType >= 0; + } + + /** + * Clears the scrap heap. + */ + void clear() { + if (mViewTypeCount == 1) { + final ArrayList scrap = mCurrentScrap; + final int scrapCount = scrap.size(); + for (int i = 0; i < scrapCount; i++) { + removeDetachedView(scrap.remove(scrapCount - 1 - i), false); + } + } else { + final int typeCount = mViewTypeCount; + for (int i = 0; i < typeCount; i++) { + final ArrayList scrap = mScrapViews[i]; + final int scrapCount = scrap.size(); + for (int j = 0; j < scrapCount; j++) { + removeDetachedView(scrap.remove(scrapCount - 1 - j), false); + } + } + } + } + + /** + * Fill ActiveViews with all of the children of the AbsListView. + * + * @param childCount The minimum number of views mActiveViews should hold + * @param firstActivePosition The position of the first view that will be stored in + * mActiveViews + */ + void fillActiveViews(int childCount, int firstActivePosition) { + if (mActiveViews.length < childCount) { + mActiveViews = new View[childCount]; + } + mFirstActivePosition = firstActivePosition; + + final View[] activeViews = mActiveViews; + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + PLA_AbsListView.LayoutParams lp = (PLA_AbsListView.LayoutParams) child.getLayoutParams(); + // Don't put header or footer views into the scrap heap + if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. + // However, we will NOT place them into scrap views. + activeViews[i] = child; + } + } + } + + /** + * Get the view corresponding to the specified position. The view will be removed from + * mActiveViews if it is found. + * + * @param position The position to look up in mActiveViews + * @return The view if it is found, null otherwise + */ + View getActiveView(int position) { + int index = position - mFirstActivePosition; + final View[] activeViews = mActiveViews; + if (index >=0 && index < activeViews.length) { + final View match = activeViews[index]; + activeViews[index] = null; + return match; + } + return null; + } + + /** + * @return A view from the ScrapViews collection. These are unordered. + */ + View getScrapView(int position) { + ArrayList scrapViews; + if (mViewTypeCount == 1) { + scrapViews = mCurrentScrap; + int size = scrapViews.size(); + if (size > 0) { + return scrapViews.remove(size - 1); + } else { + return null; + } + } else { + int whichScrap = mAdapter.getItemViewType(position); + if (whichScrap >= 0 && whichScrap < mScrapViews.length) { + scrapViews = mScrapViews[whichScrap]; + int size = scrapViews.size(); + if (size > 0) { + return scrapViews.remove(size - 1); + } + } + } + return null; + } + + /** + * Put a view into the ScapViews list. These views are unordered. + * + * @param scrap The view to add + */ + void addScrapView(View scrap) { + PLA_AbsListView.LayoutParams lp = (PLA_AbsListView.LayoutParams) scrap.getLayoutParams(); + if (lp == null) { + return; + } + + // Don't put header or footer views or views that should be ignored + // into the scrap heap + int viewType = lp.viewType; + if (!shouldRecycleViewType(viewType)) { + if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + removeDetachedView(scrap, false); + } + return; + } + + if (mViewTypeCount == 1) { + // scrap.dispatchStartTemporaryDetach(); + dispatchFinishTemporaryDetach(scrap); + mCurrentScrap.add(scrap); + } else { + // scrap.dispatchStartTemporaryDetach(); + dispatchFinishTemporaryDetach(scrap); + mScrapViews[viewType].add(scrap); + } + + if (mRecyclerListener != null) { + mRecyclerListener.onMovedToScrapHeap(scrap); + } + } + + /** + * Move all views remaining in mActiveViews to mScrapViews. + */ + @SuppressWarnings("deprecation") + void scrapActiveViews() { + final View[] activeViews = mActiveViews; + final boolean hasListener = mRecyclerListener != null; + final boolean multipleScraps = mViewTypeCount > 1; + + ArrayList scrapViews = mCurrentScrap; + final int count = activeViews.length; + for (int i = count - 1; i >= 0; i--) { + final View victim = activeViews[i]; + if (victim != null) { + int whichScrap = ((PLA_AbsListView.LayoutParams) victim.getLayoutParams()).viewType; + + activeViews[i] = null; + + if (!shouldRecycleViewType(whichScrap)) { + // Do not move views that should be ignored + if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + removeDetachedView(victim, false); + } + continue; + } + + if (multipleScraps) { + scrapViews = mScrapViews[whichScrap]; + } + //victim.dispatchStartTemporaryDetach(); + dispatchFinishTemporaryDetach(victim); + scrapViews.add(victim); + + if (hasListener) { + mRecyclerListener.onMovedToScrapHeap(victim); + } + + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(victim, + ViewDebug.RecyclerTraceType.MOVE_FROM_ACTIVE_TO_SCRAP_HEAP, + mFirstActivePosition + i, -1); + } + } + } + + pruneScrapViews(); + } + + /** + * Makes sure that the size of mScrapViews does not exceed the size of mActiveViews. + * (This can happen if an adapter does not recycle its views). + */ + private void pruneScrapViews() { + final int maxViews = mActiveViews.length; + final int viewTypeCount = mViewTypeCount; + final ArrayList[] scrapViews = mScrapViews; + for (int i = 0; i < viewTypeCount; ++i) { + final ArrayList scrapPile = scrapViews[i]; + int size = scrapPile.size(); + final int extras = size - maxViews; + size--; + for (int j = 0; j < extras; j++) { + removeDetachedView(scrapPile.remove(size--), false); + } + } + } + + /** + * Puts all views in the scrap heap into the supplied list. + */ + void reclaimScrapViews(List views) { + if (mViewTypeCount == 1) { + views.addAll(mCurrentScrap); + } else { + final int viewTypeCount = mViewTypeCount; + final ArrayList[] scrapViews = mScrapViews; + for (int i = 0; i < viewTypeCount; ++i) { + final ArrayList scrapPile = scrapViews[i]; + views.addAll(scrapPile); + } + } + } + + /** + * Updates the cache color hint of all known views. + * + * @param color The new cache color hint. + */ + void setCacheColorHint(int color) { + if (mViewTypeCount == 1) { + final ArrayList scrap = mCurrentScrap; + final int scrapCount = scrap.size(); + for (int i = 0; i < scrapCount; i++) { + scrap.get(i).setDrawingCacheBackgroundColor(color); + } + } else { + final int typeCount = mViewTypeCount; + for (int i = 0; i < typeCount; i++) { + final ArrayList scrap = mScrapViews[i]; + final int scrapCount = scrap.size(); + for (int j = 0; j < scrapCount; j++) { + scrap.get(i).setDrawingCacheBackgroundColor(color); + } + } + } + // Just in case this is called during a layout pass + final View[] activeViews = mActiveViews; + final int count = activeViews.length; + for (int i = 0; i < count; ++i) { + final View victim = activeViews[i]; + if (victim != null) { + victim.setDrawingCacheBackgroundColor(color); + } + } + } + } + + ///////////////////////////////////////////////////// + //Newly Added Methods. + ///////////////////////////////////////////////////// + + private void dispatchFinishTemporaryDetach(View v) { + if( v == null ) + return; + + v.onFinishTemporaryDetach(); + if( v instanceof ViewGroup){ + ViewGroup group = (ViewGroup) v; + final int count = group.getChildCount(); + for (int i = 0; i < count; i++) { + dispatchFinishTemporaryDetach(group.getChildAt(i)); + } + } + } +}//end of class diff --git a/src/com/huewu/pla/lib/internal/PLA_AdapterView.java b/src/com/huewu/pla/lib/internal/PLA_AdapterView.java new file mode 100644 index 0000000..844a873 --- /dev/null +++ b/src/com/huewu/pla/lib/internal/PLA_AdapterView.java @@ -0,0 +1,1136 @@ +/* + * Copyright (C) 2006 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.huewu.pla.lib.internal; + +import android.content.Context; +import android.database.DataSetObserver; +import android.os.Handler; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.ContextMenu; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.accessibility.AccessibilityEvent; +import android.widget.Adapter; +import android.widget.ListView; + + +/** + * An AdapterView is a view whose children are determined by an {@link Adapter}. + * + *

+ * See {@link ListView}, {@link GridView}, {@link Spinner} and + * {@link Gallery} for commonly used subclasses of AdapterView. + */ +public abstract class PLA_AdapterView extends ViewGroup { + + /** + * The item view type returned by {@link Adapter#getItemViewType(int)} when + * the adapter does not want the item's view recycled. + */ + public static final int ITEM_VIEW_TYPE_IGNORE = -1; + + /** + * The item view type returned by {@link Adapter#getItemViewType(int)} when + * the item is a header or footer. + */ + public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2; + + /** + * The position of the first child displayed + */ + @ViewDebug.ExportedProperty + int mFirstPosition = 0; + + /** + * The offset in pixels from the top of the AdapterView to the top + * of the view to select during the next layout. + */ + int mSpecificTop; + + /** + * Position from which to start looking for mSyncRowId + */ + int mSyncPosition; + + /** + * Row id to look for when data has changed + */ + long mSyncRowId = INVALID_ROW_ID; + + /** + * Height of the view when mSyncPosition and mSyncRowId where set + */ + long mSyncHeight; + + /** + * True if we need to sync to mSyncRowId + */ + boolean mNeedSync = false; + + /** + * Indicates whether to sync based on the selection or position. Possible + * values are {@link #SYNC_SELECTED_POSITION} or + * {@link #SYNC_FIRST_POSITION}. + */ + int mSyncMode; + + /** + * Our height after the last layout + */ + private int mLayoutHeight; + + /** + * Sync based on the selected child + */ + static final int SYNC_SELECTED_POSITION = 0; + + /** + * Sync based on the first child displayed + */ + static final int SYNC_FIRST_POSITION = 1; + + /** + * Maximum amount of time to spend in {@link #findSyncPosition()} + */ + static final int SYNC_MAX_DURATION_MILLIS = 100; + + /** + * Indicates that this view is currently being laid out. + */ + boolean mInLayout = false; + + /** + * The listener that receives notifications when an item is selected. + */ + OnItemSelectedListener mOnItemSelectedListener; + + /** + * The listener that receives notifications when an item is clicked. + */ + OnItemClickListener mOnItemClickListener; + + /** + * The listener that receives notifications when an item is long clicked. + */ + OnItemLongClickListener mOnItemLongClickListener; + + /** + * True if the data has changed since the last layout + */ + boolean mDataChanged; + + /** + * The position within the adapter's data set of the item to select + * during the next layout. + */ + @ViewDebug.ExportedProperty + int mNextSelectedPosition = INVALID_POSITION; + + /** + * The item id of the item to select during the next layout. + */ + long mNextSelectedRowId = INVALID_ROW_ID; + + /** + * The position within the adapter's data set of the currently selected item. + */ + @ViewDebug.ExportedProperty + int mSelectedPosition = INVALID_POSITION; + + /** + * The item id of the currently selected item. + */ + long mSelectedRowId = INVALID_ROW_ID; + + /** + * View to show if there are no items to show. + */ + private View mEmptyView; + + /** + * The number of items in the current adapter. + */ + @ViewDebug.ExportedProperty + int mItemCount; + + /** + * The number of items in the adapter before a data changed event occured. + */ + int mOldItemCount; + + /** + * Represents an invalid position. All valid positions are in the range 0 to 1 less than the + * number of items in the current adapter. + */ + public static final int INVALID_POSITION = -1; + + /** + * Represents an empty or invalid row id + */ + public static final long INVALID_ROW_ID = Long.MIN_VALUE; + + /** + * The last selected position we used when notifying + */ + int mOldSelectedPosition = INVALID_POSITION; + + /** + * The id of the last selected position we used when notifying + */ + long mOldSelectedRowId = INVALID_ROW_ID; + + /** + * Indicates what focusable state is requested when calling setFocusable(). + * In addition to this, this view has other criteria for actually + * determining the focusable state (such as whether its empty or the text + * filter is shown). + * + * @see #setFocusable(boolean) + * @see #checkFocus() + */ + private boolean mDesiredFocusableState; + private boolean mDesiredFocusableInTouchModeState; + + private SelectionNotifier mSelectionNotifier; + /** + * When set to true, calls to requestLayout() will not propagate up the parent hierarchy. + * This is used to layout the children during a layout pass. + */ + boolean mBlockLayoutRequests = false; + + public PLA_AdapterView(Context context) { + super(context); + } + + public PLA_AdapterView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PLA_AdapterView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + + /** + * Interface definition for a callback to be invoked when an item in this + * AdapterView has been clicked. + */ + public interface OnItemClickListener { + + /** + * Callback method to be invoked when an item in this AdapterView has + * been clicked. + *

+ * Implementers can call getItemAtPosition(position) if they need + * to access the data associated with the selected item. + * + * @param parent The AdapterView where the click happened. + * @param view The view within the AdapterView that was clicked (this + * will be a view provided by the adapter) + * @param position The position of the view in the adapter. + * @param id The row id of the item that was clicked. + */ + void onItemClick(PLA_AdapterView parent, View view, int position, long id); + } + + /** + * Register a callback to be invoked when an item in this AdapterView has + * been clicked. + * + * @param listener The callback that will be invoked. + */ + public void setOnItemClickListener(OnItemClickListener listener) { + mOnItemClickListener = listener; + } + + /** + * @return The callback to be invoked with an item in this AdapterView has + * been clicked, or null id no callback has been set. + */ + public final OnItemClickListener getOnItemClickListener() { + return mOnItemClickListener; + } + + /** + * Call the OnItemClickListener, if it is defined. + * + * @param view The view within the AdapterView that was clicked. + * @param position The position of the view in the adapter. + * @param id The row id of the item that was clicked. + * @return True if there was an assigned OnItemClickListener that was + * called, false otherwise is returned. + */ + public boolean performItemClick(View view, int position, long id) { + if (mOnItemClickListener != null) { + playSoundEffect(SoundEffectConstants.CLICK); + mOnItemClickListener.onItemClick(this, view, position, id); + return true; + } + + return false; + } + + /** + * Interface definition for a callback to be invoked when an item in this + * view has been clicked and held. + */ + public interface OnItemLongClickListener { + /** + * Callback method to be invoked when an item in this view has been + * clicked and held. + * + * Implementers can call getItemAtPosition(position) if they need to access + * the data associated with the selected item. + * + * @param parent The AbsListView where the click happened + * @param view The view within the AbsListView that was clicked + * @param position The position of the view in the list + * @param id The row id of the item that was clicked + * + * @return true if the callback consumed the long click, false otherwise + */ + boolean onItemLongClick(PLA_AdapterView parent, View view, int position, long id); + } + + + /** + * Register a callback to be invoked when an item in this AdapterView has + * been clicked and held + * + * @param listener The callback that will run + */ + public void setOnItemLongClickListener(OnItemLongClickListener listener) { + if (!isLongClickable()) { + setLongClickable(true); + } + mOnItemLongClickListener = listener; + } + + /** + * @return The callback to be invoked with an item in this AdapterView has + * been clicked and held, or null id no callback as been set. + */ + public final OnItemLongClickListener getOnItemLongClickListener() { + return mOnItemLongClickListener; + } + + /** + * Interface definition for a callback to be invoked when + * an item in this view has been selected. + */ + public interface OnItemSelectedListener { + /** + * Callback method to be invoked when an item in this view has been + * selected. + * + * Impelmenters can call getItemAtPosition(position) if they need to access the + * data associated with the selected item. + * + * @param parent The AdapterView where the selection happened + * @param view The view within the AdapterView that was clicked + * @param position The position of the view in the adapter + * @param id The row id of the item that is selected + */ + void onItemSelected(PLA_AdapterView parent, View view, int position, long id); + + /** + * Callback method to be invoked when the selection disappears from this + * view. The selection can disappear for instance when touch is activated + * or when the adapter becomes empty. + * + * @param parent The AdapterView that now contains no selected item. + */ + void onNothingSelected(PLA_AdapterView parent); + } + + + /** + * Register a callback to be invoked when an item in this AdapterView has + * been selected. + * + * @param listener The callback that will run + */ + public void setOnItemSelectedListener(OnItemSelectedListener listener) { + mOnItemSelectedListener = listener; + } + + public final OnItemSelectedListener getOnItemSelectedListener() { + return mOnItemSelectedListener; + } + + /** + * Extra menu information provided to the + * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) } + * callback when a context menu is brought up for this AdapterView. + * + */ + public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo { + + public AdapterContextMenuInfo(View targetView, int position, long id) { + this.targetView = targetView; + this.position = position; + this.id = id; + } + + /** + * The child view for which the context menu is being displayed. This + * will be one of the children of this AdapterView. + */ + public View targetView; + + /** + * The position in the adapter for which the context menu is being + * displayed. + */ + public int position; + + /** + * The row id of the item for which the context menu is being displayed. + */ + public long id; + } + + /** + * Returns the adapter currently associated with this widget. + * + * @return The adapter used to provide this view's content. + */ + public abstract T getAdapter(); + + /** + * Sets the adapter that provides the data and the views to represent the data + * in this widget. + * + * @param adapter The adapter to use to create this view's content. + */ + public abstract void setAdapter(T adapter); + + /** + * This method is not supported and throws an UnsupportedOperationException when called. + * + * @param child Ignored. + * + * @throws UnsupportedOperationException Every time this method is invoked. + */ + @Override + public void addView(View child) { + throw new UnsupportedOperationException("addView(View) is not supported in AdapterView"); + } + + /** + * This method is not supported and throws an UnsupportedOperationException when called. + * + * @param child Ignored. + * @param index Ignored. + * + * @throws UnsupportedOperationException Every time this method is invoked. + */ + @Override + public void addView(View child, int index) { + throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView"); + } + + /** + * This method is not supported and throws an UnsupportedOperationException when called. + * + * @param child Ignored. + * @param params Ignored. + * + * @throws UnsupportedOperationException Every time this method is invoked. + */ + @Override + public void addView(View child, LayoutParams params) { + throw new UnsupportedOperationException("addView(View, LayoutParams) " + + "is not supported in AdapterView"); + } + + /** + * This method is not supported and throws an UnsupportedOperationException when called. + * + * @param child Ignored. + * @param index Ignored. + * @param params Ignored. + * + * @throws UnsupportedOperationException Every time this method is invoked. + */ + @Override + public void addView(View child, int index, LayoutParams params) { + throw new UnsupportedOperationException("addView(View, int, LayoutParams) " + + "is not supported in AdapterView"); + } + + /** + * This method is not supported and throws an UnsupportedOperationException when called. + * + * @param child Ignored. + * + * @throws UnsupportedOperationException Every time this method is invoked. + */ + @Override + public void removeView(View child) { + throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView"); + } + + /** + * This method is not supported and throws an UnsupportedOperationException when called. + * + * @param index Ignored. + * + * @throws UnsupportedOperationException Every time this method is invoked. + */ + @Override + public void removeViewAt(int index) { + throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView"); + } + + /** + * This method is not supported and throws an UnsupportedOperationException when called. + * + * @throws UnsupportedOperationException Every time this method is invoked. + */ + @Override + public void removeAllViews() { + throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView"); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + mLayoutHeight = getHeight(); + } + + /** + * Return the position of the currently selected item within the adapter's data set + * + * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected. + */ + @ViewDebug.CapturedViewProperty + public int getSelectedItemPosition() { + return mNextSelectedPosition; + } + + /** + * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID} + * if nothing is selected. + */ + @ViewDebug.CapturedViewProperty + public long getSelectedItemId() { + return mNextSelectedRowId; + } + + /** + * @return The view corresponding to the currently selected item, or null + * if nothing is selected + */ + public abstract View getSelectedView(); + + /** + * @return The data corresponding to the currently selected item, or + * null if there is nothing selected. + */ + public Object getSelectedItem() { + T adapter = getAdapter(); + int selection = getSelectedItemPosition(); + if (adapter != null && adapter.getCount() > 0 && selection >= 0) { + return adapter.getItem(selection); + } else { + return null; + } + } + + /** + * @return The number of items owned by the Adapter associated with this + * AdapterView. (This is the number of data items, which may be + * larger than the number of visible view.) + */ + @ViewDebug.CapturedViewProperty + public int getCount() { + return mItemCount; + } + + /** + * Get the position within the adapter's data set for the view, where view is a an adapter item + * or a descendant of an adapter item. + * + * @param view an adapter item, or a descendant of an adapter item. This must be visible in this + * AdapterView at the time of the call. + * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION} + * if the view does not correspond to a list item (or it is not currently visible). + */ + public int getPositionForView(View view) { + View listItem = view; + try { + View v; + while (!(v = (View) listItem.getParent()).equals(this)) { + listItem = v; + } + } catch (ClassCastException e) { + // We made it up to the window without find this list view + return INVALID_POSITION; + } + + // Search the children for the list item + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + if (getChildAt(i).equals(listItem)) { + return mFirstPosition + i; + } + } + + // Child not found! + return INVALID_POSITION; + } + + /** + * Returns the position within the adapter's data set for the first item + * displayed on screen. + * + * @return The position within the adapter's data set + */ + public int getFirstVisiblePosition() { + return mFirstPosition; + } + + /** + * Returns the position within the adapter's data set for the last item + * displayed on screen. + * + * @return The position within the adapter's data set + */ + public int getLastVisiblePosition() { + return mFirstPosition + getChildCount() - 1; + } + + /** + * Sets the currently selected item. To support accessibility subclasses that + * override this method must invoke the overriden super method first. + * + * @param position Index (starting at 0) of the data item to be selected. + */ + public abstract void setSelection(int position); + + /** + * Sets the view to show if the adapter is empty + */ + public void setEmptyView(View emptyView) { + mEmptyView = emptyView; + + final T adapter = getAdapter(); + final boolean empty = ((adapter == null) || adapter.isEmpty()); + updateEmptyStatus(empty); + } + + /** + * When the current adapter is empty, the AdapterView can display a special view + * call the empty view. The empty view is used to provide feedback to the user + * that no data is available in this AdapterView. + * + * @return The view to show if the adapter is empty. + */ + public View getEmptyView() { + return mEmptyView; + } + + /** + * Indicates whether this view is in filter mode. Filter mode can for instance + * be enabled by a user when typing on the keyboard. + * + * @return True if the view is in filter mode, false otherwise. + */ + boolean isInFilterMode() { + return false; + } + + @Override + public void setFocusable(boolean focusable) { + final T adapter = getAdapter(); + final boolean empty = adapter == null || adapter.getCount() == 0; + + mDesiredFocusableState = focusable; + if (!focusable) { + mDesiredFocusableInTouchModeState = false; + } + + super.setFocusable(focusable && (!empty || isInFilterMode())); + } + + @Override + public void setFocusableInTouchMode(boolean focusable) { + final T adapter = getAdapter(); + final boolean empty = adapter == null || adapter.getCount() == 0; + + mDesiredFocusableInTouchModeState = focusable; + if (focusable) { + mDesiredFocusableState = true; + } + + super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode())); + } + + void checkFocus() { + final T adapter = getAdapter(); + final boolean empty = adapter == null || adapter.getCount() == 0; + final boolean focusable = !empty || isInFilterMode(); + // The order in which we set focusable in touch mode/focusable may matter + // for the client, see View.setFocusableInTouchMode() comments for more + // details + super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); + super.setFocusable(focusable && mDesiredFocusableState); + if (mEmptyView != null) { + updateEmptyStatus((adapter == null) || adapter.isEmpty()); + } + } + + /** + * Update the status of the list based on the empty parameter. If empty is true and + * we have an empty view, display it. In all the other cases, make sure that the listview + * is VISIBLE and that the empty view is GONE (if it's not null). + */ + private void updateEmptyStatus(boolean empty) { + if (isInFilterMode()) { + empty = false; + } + + if (empty) { + if (mEmptyView != null) { + mEmptyView.setVisibility(View.VISIBLE); + setVisibility(View.GONE); + } else { + // If the caller just removed our empty view, make sure the list view is visible + setVisibility(View.VISIBLE); + } + + // We are now GONE, so pending layouts will not be dispatched. + // Force one here to make sure that the state of the list matches + // the state of the adapter. + if (mDataChanged) { + this.onLayout(false, getLeft(), getTop(), getRight(), getBottom()); + } + } else { + if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); + setVisibility(View.VISIBLE); + } + } + + /** + * Gets the data associated with the specified position in the list. + * + * @param position Which data to get + * @return The data associated with the specified position in the list + */ + public Object getItemAtPosition(int position) { + T adapter = getAdapter(); + return (adapter == null || position < 0) ? null : adapter.getItem(position); + } + + public long getItemIdAtPosition(int position) { + T adapter = getAdapter(); + return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position); + } + + @Override + public void setOnClickListener(OnClickListener l) { + throw new RuntimeException("Don't call setOnClickListener for an AdapterView. " + + "You probably want setOnItemClickListener instead"); + } + + /** + * Override to prevent freezing of any views created by the adapter. + */ + @Override + protected void dispatchSaveInstanceState(SparseArray container) { + dispatchFreezeSelfOnly(container); + } + + /** + * Override to prevent thawing of any views created by the adapter. + */ + @Override + protected void dispatchRestoreInstanceState(SparseArray container) { + dispatchThawSelfOnly(container); + } + + class AdapterDataSetObserver extends DataSetObserver { + + private Parcelable mInstanceState = null; + + @Override + public void onChanged() { + mDataChanged = true; + mOldItemCount = mItemCount; + mItemCount = getAdapter().getCount(); + + // Detect the case where a cursor that was previously invalidated has + // been repopulated with new data. + if (PLA_AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null + && mOldItemCount == 0 && mItemCount > 0) { + PLA_AdapterView.this.onRestoreInstanceState(mInstanceState); + mInstanceState = null; + } else { + rememberSyncState(); + } + checkFocus(); + requestLayout(); + } + + @Override + public void onInvalidated() { + mDataChanged = true; + + if (PLA_AdapterView.this.getAdapter().hasStableIds()) { + // Remember the current state for the case where our hosting activity is being + // stopped and later restarted + mInstanceState = PLA_AdapterView.this.onSaveInstanceState(); + } + + // Data is invalid so we should reset our state + mOldItemCount = mItemCount; + mItemCount = 0; + mSelectedPosition = INVALID_POSITION; + mSelectedRowId = INVALID_ROW_ID; + mNextSelectedPosition = INVALID_POSITION; + mNextSelectedRowId = INVALID_ROW_ID; + mNeedSync = false; + checkSelectionChanged(); + + checkFocus(); + requestLayout(); + } + + public void clearSavedState() { + mInstanceState = null; + } + } + + private class SelectionNotifier extends Handler implements Runnable { + public void run() { + if (mDataChanged) { + // Data has changed between when this SelectionNotifier + // was posted and now. We need to wait until the AdapterView + // has been synched to the new data. + post(this); + } else { + fireOnSelected(); + } + } + } + + void selectionChanged() { + if (mOnItemSelectedListener != null) { + if (mInLayout || mBlockLayoutRequests) { + // If we are in a layout traversal, defer notification + // by posting. This ensures that the view tree is + // in a consistent state and is able to accomodate + // new layout or invalidate requests. + if (mSelectionNotifier == null) { + mSelectionNotifier = new SelectionNotifier(); + } + mSelectionNotifier.post(mSelectionNotifier); + } else { + fireOnSelected(); + } + } + + // we fire selection events here not in View + if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + } + + private void fireOnSelected() { + if (mOnItemSelectedListener == null) + return; + + int selection = this.getSelectedItemPosition(); + if (selection >= 0) { + View v = getSelectedView(); + mOnItemSelectedListener.onItemSelected(this, v, selection, + getAdapter().getItemId(selection)); + } else { + mOnItemSelectedListener.onNothingSelected(this); + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + boolean populated = false; + // This is an exceptional case which occurs when a window gets the + // focus and sends a focus event via its focused child to announce + // current focus/selection. AdapterView fires selection but not focus + // events so we change the event type here. + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) { + event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + + // we send selection events only from AdapterView to avoid + // generation of such event for each child + View selectedView = getSelectedView(); + if (selectedView != null) { + populated = selectedView.dispatchPopulateAccessibilityEvent(event); + } + + if (!populated) { + if (selectedView != null) { + event.setEnabled(selectedView.isEnabled()); + } + event.setItemCount(getCount()); + event.setCurrentItemIndex(getSelectedItemPosition()); + } + + return populated; + } + + @Override + protected boolean canAnimate() { + return super.canAnimate() && mItemCount > 0; + } + + void handleDataChanged() { + final int count = mItemCount; + boolean found = false; + + if (count > 0) { + + int newPos; + + // Find the row we are supposed to sync to + if (mNeedSync) { + // Update this first, since setNextSelectedPositionInt inspects + // it + mNeedSync = false; + + // See if we can find a position in the new data with the same + // id as the old selection + newPos = findSyncPosition(); + if (newPos >= 0) { + // Verify that new selection is selectable + int selectablePos = lookForSelectablePosition(newPos, true); + if (selectablePos == newPos) { + // Same row id is selected + setNextSelectedPositionInt(newPos); + found = true; + } + } + } + if (!found) { + // Try to use the same position if we can't find matching data + newPos = getSelectedItemPosition(); + + // Pin position to the available range + if (newPos >= count) { + newPos = count - 1; + } + if (newPos < 0) { + newPos = 0; + } + + // Make sure we select something selectable -- first look down + int selectablePos = lookForSelectablePosition(newPos, true); + if (selectablePos < 0) { + // Looking down didn't work -- try looking up + selectablePos = lookForSelectablePosition(newPos, false); + } + if (selectablePos >= 0) { + setNextSelectedPositionInt(selectablePos); + checkSelectionChanged(); + found = true; + } + } + } + if (!found) { + // Nothing is selected + mSelectedPosition = INVALID_POSITION; + mSelectedRowId = INVALID_ROW_ID; + mNextSelectedPosition = INVALID_POSITION; + mNextSelectedRowId = INVALID_ROW_ID; + mNeedSync = false; + checkSelectionChanged(); + } + } + + void checkSelectionChanged() { + if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { + selectionChanged(); + mOldSelectedPosition = mSelectedPosition; + mOldSelectedRowId = mSelectedRowId; + } + } + + /** + * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition + * and then alternates between moving up and moving down until 1) we find the right position, or + * 2) we run out of time, or 3) we have looked at every position + * + * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't + * be found + */ + int findSyncPosition() { + int count = mItemCount; + + if (count == 0) { + return INVALID_POSITION; + } + + long idToMatch = mSyncRowId; + int seed = mSyncPosition; + + // If there isn't a selection don't hunt for it + if (idToMatch == INVALID_ROW_ID) { + return INVALID_POSITION; + } + + // Pin seed to reasonable values + seed = Math.max(0, seed); + seed = Math.min(count - 1, seed); + + long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; + + long rowId; + + // first position scanned so far + int first = seed; + + // last position scanned so far + int last = seed; + + // True if we should move down on the next iteration + boolean next = false; + + // True when we have looked at the first item in the data + boolean hitFirst; + + // True when we have looked at the last item in the data + boolean hitLast; + + // Get the item ID locally (instead of getItemIdAtPosition), so + // we need the adapter + T adapter = getAdapter(); + if (adapter == null) { + return INVALID_POSITION; + } + + while (SystemClock.uptimeMillis() <= endTime) { + rowId = adapter.getItemId(seed); + if (rowId == idToMatch) { + // Found it! + return seed; + } + + hitLast = last == count - 1; + hitFirst = first == 0; + + if (hitLast && hitFirst) { + // Looked at everything + break; + } + + if (hitFirst || (next && !hitLast)) { + // Either we hit the top, or we are trying to move down + last++; + seed = last; + // Try going up next time + next = false; + } else if (hitLast || (!next && !hitFirst)) { + // Either we hit the bottom, or we are trying to move up + first--; + seed = first; + // Try going down next time + next = true; + } + + } + + return INVALID_POSITION; + } + + /** + * Find a position that can be selected (i.e., is not a separator). + * + * @param position The starting position to look at. + * @param lookDown Whether to look down for other positions. + * @return The next selectable position starting at position and then searching either up or + * down. Returns {@link #INVALID_POSITION} if nothing can be found. + */ + int lookForSelectablePosition(int position, boolean lookDown) { + return position; + } + + /** + * Utility to keep mSelectedPosition and mSelectedRowId in sync + * @param position Our current position + */ + void setSelectedPositionInt(int position) { + mSelectedPosition = position; + mSelectedRowId = getItemIdAtPosition(position); + } + + /** + * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync + * @param position Intended value for mSelectedPosition the next time we go + * through layout + */ + void setNextSelectedPositionInt(int position) { + mNextSelectedPosition = position; + mNextSelectedRowId = getItemIdAtPosition(position); + // If we are trying to sync to the selection, update that too + if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { + mSyncPosition = position; + mSyncRowId = mNextSelectedRowId; + } + } + + /** + * Remember enough information to restore the screen state when the data has + * changed. + * + */ + void rememberSyncState() { + if (getChildCount() > 0) { + mNeedSync = true; + mSyncHeight = mLayoutHeight; + if (mSelectedPosition >= 0) { + // Sync the selection state + View v = getChildAt(mSelectedPosition - mFirstPosition); + mSyncRowId = mNextSelectedRowId; + mSyncPosition = mNextSelectedPosition; + if (v != null) { + mSpecificTop = v.getTop(); + } + mSyncMode = SYNC_SELECTED_POSITION; + } else { + // Sync the based on the offset of the first view + View v = getChildAt(0); + T adapter = getAdapter(); + if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { + mSyncRowId = adapter.getItemId(mFirstPosition); + } else { + mSyncRowId = NO_ID; + } + mSyncPosition = mFirstPosition; + if (v != null) { + mSpecificTop = v.getTop(); + } + mSyncMode = SYNC_FIRST_POSITION; + } + } + } +} diff --git a/src/com/huewu/pla/lib/internal/PLA_HeaderViewListAdapter.java b/src/com/huewu/pla/lib/internal/PLA_HeaderViewListAdapter.java new file mode 100644 index 0000000..392da84 --- /dev/null +++ b/src/com/huewu/pla/lib/internal/PLA_HeaderViewListAdapter.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2006 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.huewu.pla.lib.internal; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ListAdapter; +import android.widget.WrapperListAdapter; + +import java.util.ArrayList; + +/** + * ListAdapter used when a ListView has header views. This ListAdapter + * wraps another one and also keeps track of the header views and their + * associated data objects. + *

This is intended as a base class; you will probably not need to + * use this class directly in your own code. + */ +public class PLA_HeaderViewListAdapter implements WrapperListAdapter, Filterable { + + private final ListAdapter mAdapter; + + // These two ArrayList are assumed to NOT be null. + // They are indeed created when declared in ListView and then shared. + ArrayList mHeaderViewInfos; + ArrayList mFooterViewInfos; + + // Used as a placeholder in case the provided info views are indeed null. + // Currently only used by some CTS tests, which may be removed. + static final ArrayList EMPTY_INFO_LIST = + new ArrayList(); + + boolean mAreAllFixedViewsSelectable; + + private final boolean mIsFilterable; + + public PLA_HeaderViewListAdapter(ArrayList headerViewInfos, + ArrayList footerViewInfos, + ListAdapter adapter) { + mAdapter = adapter; + mIsFilterable = adapter instanceof Filterable; + + if (headerViewInfos == null) { + mHeaderViewInfos = EMPTY_INFO_LIST; + } else { + mHeaderViewInfos = headerViewInfos; + } + + if (footerViewInfos == null) { + mFooterViewInfos = EMPTY_INFO_LIST; + } else { + mFooterViewInfos = footerViewInfos; + } + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + } + + public int getHeadersCount() { + return mHeaderViewInfos.size(); + } + + public int getFootersCount() { + return mFooterViewInfos.size(); + } + + public boolean isEmpty() { + return mAdapter == null || mAdapter.isEmpty(); + } + + private boolean areAllListInfosSelectable(ArrayList infos) { + if (infos != null) { + for (PLA_ListView.FixedViewInfo info : infos) { + if (!info.isSelectable) { + return false; + } + } + } + return true; + } + + public boolean removeHeader(View v) { + for (int i = 0; i < mHeaderViewInfos.size(); i++) { + PLA_ListView.FixedViewInfo info = mHeaderViewInfos.get(i); + if (info.view == v) { + mHeaderViewInfos.remove(i); + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + + return true; + } + } + + return false; + } + + public boolean removeFooter(View v) { + for (int i = 0; i < mFooterViewInfos.size(); i++) { + PLA_ListView.FixedViewInfo info = mFooterViewInfos.get(i); + if (info.view == v) { + mFooterViewInfos.remove(i); + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + + return true; + } + } + + return false; + } + + public int getCount() { + if (mAdapter != null) { + return getFootersCount() + getHeadersCount() + mAdapter.getCount(); + } else { + return getFootersCount() + getHeadersCount(); + } + } + + public boolean areAllItemsEnabled() { + if (mAdapter != null) { + return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled(); + } else { + return true; + } + } + + public boolean isEnabled(int position) { + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeaders = getHeadersCount(); + if (position < numHeaders) { + return mHeaderViewInfos.get(position).isSelectable; + } + + // Adapter + final int adjPosition = position - numHeaders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.isEnabled(adjPosition); + } + } + + // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) + return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable; + } + + public Object getItem(int position) { + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeaders = getHeadersCount(); + if (position < numHeaders) { + return mHeaderViewInfos.get(position).data; + } + + // Adapter + final int adjPosition = position - numHeaders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItem(adjPosition); + } + } + + // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) + return mFooterViewInfos.get(adjPosition - adapterCount).data; + } + + public long getItemId(int position) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItemId(adjPosition); + } + } + return -1; + } + + public boolean hasStableIds() { + if (mAdapter != null) { + return mAdapter.hasStableIds(); + } + return false; + } + + public View getView(int position, View convertView, ViewGroup parent) { + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeaders = getHeadersCount(); + if (position < numHeaders) { + return mHeaderViewInfos.get(position).view; + } + + // Adapter + final int adjPosition = position - numHeaders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getView(adjPosition, convertView, parent); + } + } + + // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) + return mFooterViewInfos.get(adjPosition - adapterCount).view; + } + + public int getItemViewType(int position) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItemViewType(adjPosition); + } + } + + return PLA_AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; + } + + public int getViewTypeCount() { + if (mAdapter != null) { + return mAdapter.getViewTypeCount(); + } + return 1; + } + + public void registerDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) { + mAdapter.registerDataSetObserver(observer); + } + } + + public void unregisterDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(observer); + } + } + + public Filter getFilter() { + if (mIsFilterable) { + return ((Filterable) mAdapter).getFilter(); + } + return null; + } + + public ListAdapter getWrappedAdapter() { + return mAdapter; + } +} diff --git a/src/com/huewu/pla/lib/internal/PLA_ListView.java b/src/com/huewu/pla/lib/internal/PLA_ListView.java new file mode 100644 index 0000000..a36be0f --- /dev/null +++ b/src/com/huewu/pla/lib/internal/PLA_ListView.java @@ -0,0 +1,2619 @@ +/* + * Copyright (C) 2006 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.huewu.pla.lib.internal; + +import java.util.ArrayList; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ListAdapter; +import android.widget.WrapperListAdapter; + +import com.huewu.lib.pla.R; + +/* + * Implementation Notes: + * + * Some terminology: + * + * index - index of the items that are currently visible + * position - index of the items in the cursor + */ + + +/** + * A view that shows items in a vertically scrolling list. The items + * come from the {@link ListAdapter} associated with this view. + * + *

See the List View + * tutorial.

+ * + * @attr ref android.R.styleable#ListView_entries + * @attr ref android.R.styleable#ListView_divider + * @attr ref android.R.styleable#ListView_dividerHeight + * @attr ref android.R.styleable#ListView_choiceMode + * @attr ref android.R.styleable#ListView_headerDividersEnabled + * @attr ref android.R.styleable#ListView_footerDividersEnabled + */ +public class PLA_ListView extends PLA_AbsListView { + + //TODO Not Supproted Features + //Entry from XML. + //Choice Mode & Item Selection. + //Filter + //Handle Key Event & Arrow Scrolling.. + //Can't find Footer & Header findBy methods... + + /** + * Used to indicate a no preference for a position type. + */ + static final int NO_POSITION = -1; + + /** + * When arrow scrolling, ListView will never scroll more than this factor + * times the height of the list. + */ + private static final float MAX_SCROLL_FACTOR = 0.33f; + + /** + * A class that represents a fixed view in a list, for example a header at the top + * or a footer at the bottom. + */ + public class FixedViewInfo { + /** The view to add to the list */ + public View view; + /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */ + public Object data; + /** true if the fixed view should be selectable in the list */ + public boolean isSelectable; + } + + private ArrayList mHeaderViewInfos = new ArrayList(); + private ArrayList mFooterViewInfos = new ArrayList(); + + Drawable mDivider; + int mDividerHeight; + + Drawable mOverScrollHeader; + Drawable mOverScrollFooter; + + private boolean mIsCacheColorOpaque; + private boolean mDividerIsOpaque; + private boolean mClipDivider; + + private boolean mHeaderDividersEnabled; + private boolean mFooterDividersEnabled; + + private boolean mAreAllItemsSelectable = true; + + private boolean mItemsCanFocus = false; + + // used for temporary calculations. + private final Rect mTempRect = new Rect(); + private Paint mDividerPaint; + + // Keeps focused children visible through resizes + private FocusSelector mFocusSelector; + + public PLA_ListView(Context context) { + this(context, null); + } + + public PLA_ListView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.listViewStyle); + } + + public PLA_ListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.ListView, defStyle, 0); + + // final Drawable d = a.getDrawable(android.R.drawable.divider_horizontal_bright); + // if (d != null) { + // // If a divider is specified use its intrinsic height for divider height + // setDivider(d); + // } + + final Drawable osHeader = a.getDrawable( + R.styleable.ListView_overScrollHeader); + if (osHeader != null) { + setOverscrollHeader(osHeader); + } + + final Drawable osFooter = a.getDrawable( + R.styleable.ListView_overScrollFooter); + if (osFooter != null) { + setOverscrollFooter(osFooter); + } + + // Use the height specified, zero being the default + final int dividerHeight = a.getDimensionPixelSize( + R.styleable.ListView_dividerHeight, 0); + if (dividerHeight != 0) { + setDividerHeight(dividerHeight); + } + + mHeaderDividersEnabled = a.getBoolean(R.styleable.ListView_headerDividersEnabled, true); + mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true); + + a.recycle(); + } + + /** + * @return The maximum amount a list view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmount() { + // return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop)); + return (int) (MAX_SCROLL_FACTOR * (getBottom() - getTop())); + } + + /** + * Make sure views are touching the top or bottom edge, as appropriate for + * our gravity + */ + private void adjustViewsUpOrDown() { + final int childCount = getChildCount(); + int delta; + + if (childCount > 0) { + View child; + + if (!mStackFromBottom) { + // Uh-oh -- we came up short. Slide all views up to make them + // align with the top + child = getChildAt(0); + delta = child.getTop() - mListPadding.top; + if (mFirstPosition != 0) { + // It's OK to have some space above the first item if it is + // part of the vertical spacing + delta -= mDividerHeight; + } + if (delta < 0) { + // We only are looking to see if we are too low, not too high + delta = 0; + } + } else { + // we are too high, slide all views down to align with bottom + child = getChildAt(childCount - 1); + delta = child.getBottom() - (getHeight() - mListPadding.bottom); + + if (mFirstPosition + childCount < mItemCount) { + // It's OK to have some space below the last item if it is + // part of the vertical spacing + delta += mDividerHeight; + } + + if (delta > 0) { + delta = 0; + } + } + + if (delta != 0) { + // offsetChildrenTopAndBottom(-delta); + tryOffsetChildrenTopAndBottom(-delta); + } + } + } + + /** + * Add a fixed view to appear at the top of the list. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap + * the supplied cursor with one that will also account for header and footer + * views. + * + * @param v The view to add. + * @param data Data to associate with this view + * @param isSelectable whether the item is selectable + */ + public void addHeaderView(View v, Object data, boolean isSelectable) { + + if (mAdapter != null) { + throw new IllegalStateException( + "Cannot add header view to list -- setAdapter has already been called."); + } + + FixedViewInfo info = new FixedViewInfo(); + info.view = v; + info.data = data; + info.isSelectable = isSelectable; + mHeaderViewInfos.add(info); + } + + /** + * Add a fixed view to appear at the top of the list. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap + * the supplied cursor with one that will also account for header and footer + * views. + * + * @param v The view to add. + */ + public void addHeaderView(View v) { + addHeaderView(v, null, true); + } + + @Override + public int getHeaderViewsCount() { + return mHeaderViewInfos.size(); + } + + /** + * Removes a previously-added header view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a header + * view + */ + public boolean removeHeaderView(View v) { + if (mHeaderViewInfos.size() > 0) { + boolean result = false; + if (((PLA_HeaderViewListAdapter) mAdapter).removeHeader(v)) { + mDataSetObserver.onChanged(); + result = true; + } + removeFixedViewInfo(v, mHeaderViewInfos); + return result; + } + return false; + } + + private void removeFixedViewInfo(View v, ArrayList where) { + int len = where.size(); + for (int i = 0; i < len; ++i) { + FixedViewInfo info = where.get(i); + if (info.view == v) { + where.remove(i); + break; + } + } + } + + /** + * Add a fixed view to appear at the bottom of the list. If addFooterView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap + * the supplied cursor with one that will also account for header and footer + * views. + * + * @param v The view to add. + * @param data Data to associate with this view + * @param isSelectable true if the footer view can be selected + */ + public void addFooterView(View v, Object data, boolean isSelectable) { + FixedViewInfo info = new FixedViewInfo(); + info.view = v; + info.data = data; + info.isSelectable = isSelectable; + mFooterViewInfos.add(info); + + // in the case of re-adding a footer view, or adding one later on, + // we need to notify the observer + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } + } + + /** + * Add a fixed view to appear at the bottom of the list. If addFooterView is called more + * than once, the views will appear in the order they were added. Views added using + * this call can take focus if they want. + *

NOTE: Call this before calling setAdapter. This is so ListView can wrap the supplied + * cursor with one that will also account for header and footer views. + * + * + * @param v The view to add. + */ + public void addFooterView(View v) { + addFooterView(v, null, true); + } + + @Override + public int getFooterViewsCount() { + return mFooterViewInfos.size(); + } + + /** + * Removes a previously-added footer view. + * + * @param v The view to remove + * @return + * true if the view was removed, false if the view was not a footer view + */ + public boolean removeFooterView(View v) { + if (mFooterViewInfos.size() > 0) { + boolean result = false; + if (((PLA_HeaderViewListAdapter) mAdapter).removeFooter(v)) { + mDataSetObserver.onChanged(); + result = true; + } + removeFixedViewInfo(v, mFooterViewInfos); + return result; + } + return false; + } + + /** + * Returns the adapter currently in use in this ListView. The returned adapter + * might not be the same adapter passed to {@link #setAdapter(ListAdapter)} but + * might be a {@link WrapperListAdapter}. + * + * @return The adapter currently used to display data in this ListView. + * + * @see #setAdapter(ListAdapter) + */ + @Override + public ListAdapter getAdapter() { + return mAdapter; + } + + /** + * Sets the data behind this ListView. + * + * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter}, + * depending on the ListView features currently in use. For instance, adding + * headers and/or footers will cause the adapter to be wrapped. + * + * @param adapter The ListAdapter which is responsible for maintaining the + * data backing this list and for producing a view to represent an + * item in that data set. + * + * @see #getAdapter() + */ + @Override + public void setAdapter(ListAdapter adapter) { + if (null != mAdapter) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + } + + resetList(); + mRecycler.clear(); + + if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { + mAdapter = new PLA_HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); + } else { + mAdapter = adapter; + } + + mOldSelectedPosition = INVALID_POSITION; + mOldSelectedRowId = INVALID_ROW_ID; + if (mAdapter != null) { + mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); + mOldItemCount = mItemCount; + mItemCount = mAdapter.getCount(); + checkFocus(); + + mDataSetObserver = new AdapterDataSetObserver(); + mAdapter.registerDataSetObserver(mDataSetObserver); + + mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); + + int position; + if (mStackFromBottom) { + position = lookForSelectablePosition(mItemCount - 1, false); + } else { + position = lookForSelectablePosition(0, true); + } + setSelectedPositionInt(position); + setNextSelectedPositionInt(position); + + if (mItemCount == 0) { + // Nothing selected + checkSelectionChanged(); + } + + } else { + mAreAllItemsSelectable = true; + checkFocus(); + // Nothing selected + checkSelectionChanged(); + } + + requestLayout(); + } + + + /** + * The list is empty. Clear everything out. + */ + @Override + void resetList() { + // The parent's resetList() will remove all views from the layout so we need to + // cleanup the state of our footers and headers + clearRecycledState(mHeaderViewInfos); + clearRecycledState(mFooterViewInfos); + + super.resetList(); + + mLayoutMode = LAYOUT_NORMAL; + } + + private void clearRecycledState(ArrayList infos) { + if (infos != null) { + final int count = infos.size(); + + for (int i = 0; i < count; i++) { + final View child = infos.get(i).view; + final LayoutParams p = (LayoutParams) child.getLayoutParams(); + if (p != null) { + p.recycledHeaderFooter = false; + } + } + } + } + + /** + * @return Whether the list needs to show the top fading edge + */ + private boolean showingTopFadingEdge() { + // final int listTop = mScrollY + mListPadding.top; + final int listTop = getScrollY() + mListPadding.top; + return (mFirstPosition > 0) || (getChildAt(0).getTop() > listTop); + } + + /** + * @return Whether the list needs to show the bottom fading edge + */ + private boolean showingBottomFadingEdge() { + final int childCount = getChildCount(); + final int bottomOfBottomChild = getChildAt(childCount - 1).getBottom(); + final int lastVisiblePosition = mFirstPosition + childCount - 1; + + // final int listBottom = mScrollY + getHeight() - mListPadding.bottom; + final int listBottom = getScrollY() + getHeight() - mListPadding.bottom; + + return (lastVisiblePosition < mItemCount - 1) + || (bottomOfBottomChild < listBottom); + } + + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { + + int rectTopWithinChild = rect.top; + + // offset so rect is in coordinates of the this view + rect.offset(child.getLeft(), child.getTop()); + rect.offset(-child.getScrollX(), -child.getScrollY()); + + final int height = getHeight(); + int listUnfadedTop = getScrollY(); + int listUnfadedBottom = listUnfadedTop + height; + final int fadingEdge = getVerticalFadingEdgeLength(); + + if (showingTopFadingEdge()) { + // leave room for top fading edge as long as rect isn't at very top + if ((mSelectedPosition > 0) || (rectTopWithinChild > fadingEdge)) { + listUnfadedTop += fadingEdge; + } + } + + int childCount = getChildCount(); + int bottomOfBottomChild = getChildAt(childCount - 1).getBottom(); + + if (showingBottomFadingEdge()) { + // leave room for bottom fading edge as long as rect isn't at very bottom + if ((mSelectedPosition < mItemCount - 1) + || (rect.bottom < (bottomOfBottomChild - fadingEdge))) { + listUnfadedBottom -= fadingEdge; + } + } + + int scrollYDelta = 0; + + if (rect.bottom > listUnfadedBottom && rect.top > listUnfadedTop) { + // need to MOVE DOWN to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - listUnfadedTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - listUnfadedBottom); + } + + // make sure we aren't scrolling beyond the end of our children + int distanceToBottom = bottomOfBottomChild - listUnfadedBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + } else if (rect.top < listUnfadedTop && rect.bottom < listUnfadedBottom) { + // need to MOVE UP to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (listUnfadedBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (listUnfadedTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our children + int top = getChildAt(0).getTop(); + int deltaToTop = top - listUnfadedTop; + scrollYDelta = Math.max(scrollYDelta, deltaToTop); + } + + final boolean scroll = scrollYDelta != 0; + if (scroll) { + scrollListItemsBy(-scrollYDelta); + positionSelector(child); + mSelectedTop = child.getTop(); + invalidate(); + } + return scroll; + } + + protected int getChildLeft(int pos) { + return mListPadding.left; + } + + + protected int getPreservedChildTop(int pos) { + // TODO Auto-generated method stub + return 0; + } + + protected int getPreservedChildBottom(int pos) { + // TODO Auto-generated method stub + return 0; + } + + + /** + * {@inheritDoc} + */ + @Override + protected void fillGap(boolean down) { + final int count = getChildCount(); + if (down) { + final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : + getListPaddingTop(); + fillDown(mFirstPosition + count, startOffset); + correctTooHigh(getChildCount()); + } else { + final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : + getHeight() - getListPaddingBottom(); + fillUp(mFirstPosition - 1, startOffset); + correctTooLow(getChildCount()); + } + } + + /** + * Fills the list from pos down to the end of the list view. + * + * @param pos The first position to put in the list + * + * @param nextTop The location where the top of the item associated with pos + * should be drawn + * + * @return The view that is currently selected, if it happens to be in the + * range that we draw. + */ + private View fillDown(int pos, int top) { + View selectedView = null; + + //int end = (mBottom - mTop) - mListPadding.bottom; + int end = (getBottom() - getTop()) - mListPadding.bottom; + int nextTop = getCurrentChildBottom(); + + while (nextTop < end && pos < mItemCount) { + // is this the selected item? + boolean selected = pos == mSelectedPosition; + View child = makeAndAddView(pos, nextTop, true, selected); + // nextTop = child.getBottom() + mDividerHeight; + nextTop = getCurrentChildBottom() + mDividerHeight; + if (selected) { + selectedView = child; + } + pos++; + } + + return selectedView; + } + + /** + * Fills the list from pos up to the top of the list view. + * + * @param pos The first position to put in the list + * + * @param nextBottom The location where the bottom of the item associated + * with pos should be drawn + * + * @return The view that is currently selected + */ + private View fillUp(int pos, int bottom) { + View selectedView = null; + int end = mListPadding.top; + int nextBottom = getCurrentChildTop(); + + while (nextBottom > end && pos >= 0) { + // is this the selected item? + boolean selected = pos == mSelectedPosition; + View child = makeAndAddView(pos, nextBottom, false, selected); + // nextBottom = child.getTop() - mDividerHeight; + nextBottom = getCurrentChildTop(); + if (selected) { + selectedView = child; + } + pos--; + } + + mFirstPosition = pos + 1; + + return selectedView; + } + + /** + * Fills the list from top to bottom, starting with mFirstPosition + * + * @param nextTop The location where the top of the first item should be + * drawn + * + * @return The view that is currently selected + */ + private View fillFromTop(int nextTop) { + mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); + mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); + if (mFirstPosition < 0) { + mFirstPosition = 0; + } + return fillDown(mFirstPosition, nextTop); + } + + + /** + * Put mSelectedPosition in the middle of the screen and then build up and + * down from there. This method forces mSelectedPosition to the center. + * + * @param childrenTop Top of the area in which children can be drawn, as + * measured in pixels + * @param childrenBottom Bottom of the area in which children can be drawn, + * as measured in pixels + * @return Currently selected view + */ + private View fillFromMiddle(int childrenTop, int childrenBottom) { + int height = childrenBottom - childrenTop; + + int position = reconcileSelectedPosition(); + + View sel = makeAndAddView(position, childrenTop, true, true); + mFirstPosition = position; + + int selHeight = sel.getMeasuredHeight(); + if (selHeight <= height) { + sel.offsetTopAndBottom((height - selHeight) / 2); + } + + fillAboveAndBelow(sel, position); + + if (!mStackFromBottom) { + correctTooHigh(getChildCount()); + } else { + correctTooLow(getChildCount()); + } + + return sel; + } + + /** + * Once the selected view as been placed, fill up the visible area above and + * below it. + * + * @param sel The selected view + * @param position The position corresponding to sel + */ + private void fillAboveAndBelow(View sel, int position) { + final int dividerHeight = mDividerHeight; + if (!mStackFromBottom) { + fillUp(position - 1, sel.getTop() - dividerHeight); + adjustViewsUpOrDown(); + fillDown(position + 1, sel.getBottom() + dividerHeight); + } else { + fillDown(position + 1, sel.getBottom() + dividerHeight); + adjustViewsUpOrDown(); + fillUp(position - 1, sel.getTop() - dividerHeight); + } + } + + + /** + * Fills the grid based on positioning the new selection at a specific + * location. The selection may be moved so that it does not intersect the + * faded edges. The grid is then filled upwards and downwards from there. + * + * @param selectedTop Where the selected item should be + * @param childrenTop Where to start drawing children + * @param childrenBottom Last pixel where children can be drawn + * @return The view that currently has selection + */ + private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) { + int fadingEdgeLength = getVerticalFadingEdgeLength(); + final int selectedPosition = mSelectedPosition; + + View sel; + + final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, + selectedPosition); + final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, + selectedPosition); + + sel = makeAndAddView(selectedPosition, selectedTop, true, true); + + + // Some of the newly selected item extends below the bottom of the list + if (sel.getBottom() > bottomSelectionPixel) { + // Find space available above the selection into which we can scroll + // upwards + final int spaceAbove = sel.getTop() - topSelectionPixel; + + // Find space required to bring the bottom of the selected item + // fully into view + final int spaceBelow = sel.getBottom() - bottomSelectionPixel; + final int offset = Math.min(spaceAbove, spaceBelow); + + // Now offset the selected item to get it into view + sel.offsetTopAndBottom(-offset); + } else if (sel.getTop() < topSelectionPixel) { + // Find space required to bring the top of the selected item fully + // into view + final int spaceAbove = topSelectionPixel - sel.getTop(); + + // Find space available below the selection into which we can scroll + // downwards + final int spaceBelow = bottomSelectionPixel - sel.getBottom(); + final int offset = Math.min(spaceAbove, spaceBelow); + + // Offset the selected item to get it into view + sel.offsetTopAndBottom(offset); + } + + // Fill in views above and below + fillAboveAndBelow(sel, selectedPosition); + + if (!mStackFromBottom) { + correctTooHigh(getChildCount()); + } else { + correctTooLow(getChildCount()); + } + + return sel; + } + + /** + * Calculate the bottom-most pixel we can draw the selection into + * + * @param childrenBottom Bottom pixel were children can be drawn + * @param fadingEdgeLength Length of the fading edge in pixels, if present + * @param selectedPosition The position that will be selected + * @return The bottom-most pixel we can draw the selection into + */ + private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, + int selectedPosition) { + int bottomSelectionPixel = childrenBottom; + if (selectedPosition != mItemCount - 1) { + bottomSelectionPixel -= fadingEdgeLength; + } + return bottomSelectionPixel; + } + + /** + * Calculate the top-most pixel we can draw the selection into + * + * @param childrenTop Top pixel were children can be drawn + * @param fadingEdgeLength Length of the fading edge in pixels, if present + * @param selectedPosition The position that will be selected + * @return The top-most pixel we can draw the selection into + */ + private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int selectedPosition) { + // first pixel we can draw the selection into + int topSelectionPixel = childrenTop; + if (selectedPosition > 0) { + topSelectionPixel += fadingEdgeLength; + } + return topSelectionPixel; + } + + + private class FocusSelector implements Runnable { + private int mPosition; + private int mPositionTop; + + public FocusSelector setup(int position, int top) { + mPosition = position; + mPositionTop = top; + return this; + } + + public void run() { + setSelectionFromTop(mPosition, mPositionTop); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (getChildCount() > 0) { + View focusedChild = getFocusedChild(); + if (focusedChild != null) { + final int childPosition = mFirstPosition + indexOfChild(focusedChild); + final int childBottom = focusedChild.getBottom(); + // final int offset = Math.max(0, childBottom - (h - mPaddingTop)); + final int offset = Math.max(0, childBottom - (h - getPaddingTop())); + final int top = focusedChild.getTop() - offset; + if (mFocusSelector == null) { + mFocusSelector = new FocusSelector(); + } + post(mFocusSelector.setup(childPosition, top)); + } + } + super.onSizeChanged(w, h, oldw, oldh); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Sets up mListPadding + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + int childWidth = 0; + int childHeight = 0; + + mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); + if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || + heightMode == MeasureSpec.UNSPECIFIED)) { + final View child = obtainView(0, mIsScrap); + + measureScrapChild(child, 0, widthMeasureSpec); + + childWidth = child.getMeasuredWidth(); + childHeight = child.getMeasuredHeight(); + + if (recycleOnMeasure() && mRecycler.shouldRecycleViewType( + ((LayoutParams) child.getLayoutParams()).viewType)) { + mRecycler.addScrapView(child); + } + } + + if (widthMode == MeasureSpec.UNSPECIFIED) { + widthSize = mListPadding.left + mListPadding.right + childWidth + + getVerticalScrollbarWidth(); + } + + if (heightMode == MeasureSpec.UNSPECIFIED) { + heightSize = mListPadding.top + mListPadding.bottom + childHeight + + getVerticalFadingEdgeLength() * 2; + } + + if (heightMode == MeasureSpec.AT_MOST) { + // TODO: after first layout we should maybe start at the first visible position, not 0 + heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); + } + + setMeasuredDimension(widthSize, heightSize); + mWidthMeasureSpec = widthMeasureSpec; + } + + private void measureScrapChild(View child, int position, int widthMeasureSpec) { + LayoutParams p = (LayoutParams) child.getLayoutParams(); + if (p == null) { + p = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + child.setLayoutParams(p); + } + p.viewType = mAdapter.getItemViewType(position); + p.forceAdd = true; + + int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, + mListPadding.left + mListPadding.right, p.width); + int lpHeight = p.height; + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + child.measure(childWidthSpec, childHeightSpec); + } + + /** + * @return True to recycle the views used to measure this ListView in + * UNSPECIFIED/AT_MOST modes, false otherwise. + * @hide + */ + @ViewDebug.ExportedProperty(category = "list") + protected boolean recycleOnMeasure() { + return true; + } + + /** + * Measures the height of the given range of children (inclusive) and + * returns the height with this ListView's padding and divider heights + * included. If maxHeight is provided, the measuring will stop when the + * current height reaches maxHeight. + * + * @param widthMeasureSpec The width measure spec to be given to a child's + * {@link View#measure(int, int)}. + * @param startPosition The position of the first child to be shown. + * @param endPosition The (inclusive) position of the last child to be + * shown. Specify {@link #NO_POSITION} if the last child should be + * the last available child from the adapter. + * @param maxHeight The maximum height that will be returned (if all the + * children don't fit in this value, this value will be + * returned). + * @param disallowPartialChildPosition In general, whether the returned + * height should only contain entire children. This is more + * powerful--it is the first inclusive position at which partial + * children will not be allowed. Example: it looks nice to have + * at least 3 completely visible children, and in portrait this + * will most likely fit; but in landscape there could be times + * when even 2 children can not be completely shown, so a value + * of 2 (remember, inclusive) would be good (assuming + * startPosition is 0). + * @return The height of this ListView with the given children. + */ + final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, + final int maxHeight, int disallowPartialChildPosition) { + + final ListAdapter adapter = mAdapter; + if (adapter == null) { + return mListPadding.top + mListPadding.bottom; + } + + // Include the padding of the list + int returnedHeight = mListPadding.top + mListPadding.bottom; + final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0; + // The previous height value that was less than maxHeight and contained + // no partial children + int prevHeightWithoutPartialChild = 0; + int i; + View child; + + // mItemCount - 1 since endPosition parameter is inclusive + endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; + final PLA_AbsListView.RecycleBin recycleBin = mRecycler; + final boolean recyle = recycleOnMeasure(); + final boolean[] isScrap = mIsScrap; + + for (i = startPosition; i <= endPosition; ++i) { + child = obtainView(i, isScrap); + + measureScrapChild(child, i, widthMeasureSpec); + + if (i > 0) { + // Count the divider for all but one child + returnedHeight += dividerHeight; + } + + // Recycle the view before we possibly return from the method + if (recyle && recycleBin.shouldRecycleViewType( + ((LayoutParams) child.getLayoutParams()).viewType)) { + recycleBin.addScrapView(child); + } + + returnedHeight += child.getMeasuredHeight(); + + if (returnedHeight >= maxHeight) { + // We went over, figure out which height to return. If returnedHeight > maxHeight, + // then the i'th position did not fit completely. + return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) + && (i > disallowPartialChildPosition) // We've past the min pos + && (prevHeightWithoutPartialChild > 0) // We have a prev height + && (returnedHeight != maxHeight) // i'th child did not fit completely + ? prevHeightWithoutPartialChild + : maxHeight; + } + + if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { + prevHeightWithoutPartialChild = returnedHeight; + } + } + + // At this point, we went through the range of children, and they each + // completely fit, so return the returnedHeight + return returnedHeight; + } + + @Override + int findMotionRow(int y) { + int childCount = getChildCount(); + if (childCount > 0) { + if (!mStackFromBottom) { + for (int i = 0; i < childCount; i++) { + View v = getChildAt(i); + if (y <= v.getBottom()) { + return mFirstPosition + i; + } + } + } else { + for (int i = childCount - 1; i >= 0; i--) { + View v = getChildAt(i); + if (y >= v.getTop()) { + return mFirstPosition + i; + } + } + } + } + return INVALID_POSITION; + } + + /** + * Put a specific item at a specific location on the screen and then build + * up and down from there. + * + * @param position The reference view to use as the starting point + * @param top Pixel offset from the top of this view to the top of the + * reference view. + * + * @return The selected view, or null if the selected view is outside the + * visible area. + */ + private View fillSpecific(int position, int top) { + boolean tempIsSelected = position == mSelectedPosition; + View temp = makeAndAddView(position, top, true, tempIsSelected); + // Possibly changed again in fillUp if we add rows above this one. + mFirstPosition = position; + + View above; + View below; + + final int dividerHeight = mDividerHeight; + if (!mStackFromBottom) { + above = fillUp(position - 1, temp.getTop() - dividerHeight); + // This will correct for the top of the first view not touching the top of the list + adjustViewsUpOrDown(); + below = fillDown(position + 1, temp.getBottom() + dividerHeight); + int childCount = getChildCount(); + if (childCount > 0) { + correctTooHigh(childCount); + } + } else { + below = fillDown(position + 1, temp.getBottom() + dividerHeight); + // This will correct for the bottom of the last view not touching the bottom of the list + adjustViewsUpOrDown(); + above = fillUp(position - 1, temp.getTop() - dividerHeight); + int childCount = getChildCount(); + if (childCount > 0) { + correctTooLow(childCount); + } + } + + if (tempIsSelected) { + return temp; + } else if (above != null) { + return above; + } else { + return below; + } + } + + /** + * Check if we have dragged the bottom of the list too high (we have pushed the + * top element off the top of the screen when we did not need to). Correct by sliding + * everything back down. + * + * @param childCount Number of children + */ + private void correctTooHigh(int childCount) { + // First see if the last item is visible. If it is not, it is OK for the + // top of the list to be pushed up. + int lastPosition = mFirstPosition + childCount - 1; + if (lastPosition == mItemCount - 1 && childCount > 0) { + + // Get the last child ... + final View lastChild = getChildAt(childCount - 1); + + // ... and its bottom edge + final int lastBottom = lastChild.getBottom(); + + // This is bottom of our drawable area + // final int end = (mBottom - mTop) - mListPadding.bottom; + final int end = (getBottom() - getTop()) - mListPadding.bottom; + + // This is how far the bottom edge of the last view is from the bottom of the + // drawable area + int bottomOffset = end - lastBottom; + View firstChild = getChildAt(0); + final int firstTop = firstChild.getTop(); + + // Make sure we are 1) Too high, and 2) Either there are more rows above the + // first row or the first row is scrolled off the top of the drawable area + if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) { + if (mFirstPosition == 0) { + // Don't pull the top too far down + bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop); + } + // Move everything down + // offsetChildrenTopAndBottom(bottomOffset); + tryOffsetChildrenTopAndBottom(bottomOffset); + if (mFirstPosition > 0) { + // Fill the gap that was opened above mFirstPosition with more rows, if + // possible + fillUp(mFirstPosition - 1, firstChild.getTop() - mDividerHeight); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + + } + } + } + + /** + * Check if we have dragged the bottom of the list too low (we have pushed the + * bottom element off the bottom of the screen when we did not need to). Correct by sliding + * everything back up. + * + * @param childCount Number of children + */ + private void correctTooLow(int childCount) { + // First see if the first item is visible. If it is not, it is OK for the + // bottom of the list to be pushed down. + if (mFirstPosition == 0 && childCount > 0) { + + // Get the first child ... + final View firstChild = getChildAt(0); + + // ... and its top edge + final int firstTop = firstChild.getTop(); + + // This is top of our drawable area + final int start = mListPadding.top; + + // This is bottom of our drawable area + // final int end = (mBottom - mTop) - mListPadding.bottom; + final int end = (getBottom() -getTop()) - mListPadding.bottom; + + // This is how far the top edge of the first view is from the top of the + // drawable area + int topOffset = firstTop - start; + View lastChild = getChildAt(childCount - 1); + final int lastBottom = lastChild.getBottom(); + int lastPosition = mFirstPosition + childCount - 1; + + // Make sure we are 1) Too low, and 2) Either there are more rows below the + // last row or the last row is scrolled off the bottom of the drawable area + if (topOffset > 0) { + if (lastPosition < mItemCount - 1 || lastBottom > end) { + if (lastPosition == mItemCount - 1) { + // Don't pull the bottom too far up + topOffset = Math.min(topOffset, lastBottom - end); + } + // Move everything up + // offsetChildrenTopAndBottom(-topOffset); + tryOffsetChildrenTopAndBottom(-topOffset); + if (lastPosition < mItemCount - 1) { + // Fill the gap that was opened below the last position with more rows, if + // possible + fillDown(lastPosition + 1, lastChild.getBottom() + mDividerHeight); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + } else if (lastPosition == mItemCount - 1) { + adjustViewsUpOrDown(); + } + } + } + } + + @SuppressWarnings("deprecation") + @Override + protected void layoutChildren() { + final boolean blockLayoutRequests = mBlockLayoutRequests; + if (!blockLayoutRequests) { + mBlockLayoutRequests = true; + } else { + return; + } + + try { + super.layoutChildren(); + + invalidate(); + + if (mAdapter == null) { + resetList(); + invokeOnItemScrollListener(); + return; + } + + int childrenTop = mListPadding.top; + //int childrenBottom = mBottom - mTop - mListPadding.bottom; + int childrenBottom = getBottom() - getTop() - mListPadding.bottom; + + int childCount = getChildCount(); + int index = 0; + int delta = 0; + + View sel; + View oldSel = null; + View oldFirst = null; + View newSel = null; + + View focusLayoutRestoreView = null; + + // Remember stuff we will need down below + switch (mLayoutMode) { + case LAYOUT_SET_SELECTION: + index = mNextSelectedPosition - mFirstPosition; + if (index >= 0 && index < childCount) { + newSel = getChildAt(index); + } + break; + case LAYOUT_FORCE_TOP: + case LAYOUT_FORCE_BOTTOM: + case LAYOUT_SPECIFIC: + case LAYOUT_SYNC: + break; + case LAYOUT_MOVE_SELECTION: + default: + // Remember the previously selected view + index = mSelectedPosition - mFirstPosition; + if (index >= 0 && index < childCount) { + oldSel = getChildAt(index); + } + + // Remember the previous first child + oldFirst = getChildAt(0); + + if (mNextSelectedPosition >= 0) { + delta = mNextSelectedPosition - mSelectedPosition; + } + + // Caution: newSel might be null + newSel = getChildAt(index + delta); + } + + + boolean dataChanged = mDataChanged; + if (dataChanged) { + handleDataChanged(); + } + + // Handle the empty set by removing all views that are visible + // and calling it a day + if (mItemCount == 0) { + resetList(); + invokeOnItemScrollListener(); + return; + } else if (mItemCount != mAdapter.getCount()) { + throw new IllegalStateException("The content of the adapter has changed but " + + "ListView did not receive a notification. Make sure the content of " + + "your adapter is not modified from a background thread, but only " + + "from the UI thread. [in ListView(" + getId() + ", " + getClass() + + ") with Adapter(" + mAdapter.getClass() + ")]"); + } + + setSelectedPositionInt(mNextSelectedPosition); + + // Pull all children into the RecycleBin. + // These views will be reused if possible + final int firstPosition = mFirstPosition; + final RecycleBin recycleBin = mRecycler; + + // reset the focus restoration + View focusLayoutRestoreDirectChild = null; + + + // Don't put header or footer views into the Recycler. Those are + // already cached in mHeaderViews; + if (dataChanged) { + for (int i = 0; i < childCount; i++) { + recycleBin.addScrapView(getChildAt(i)); + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(getChildAt(i), + ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i); + } + } + } else { + recycleBin.fillActiveViews(childCount, firstPosition); + } + + // take focus back to us temporarily to avoid the eventual + // call to clear focus when removing the focused child below + // from messing things up when ViewRoot assigns focus back + // to someone else + final View focusedChild = getFocusedChild(); + if (focusedChild != null) { + // TODO: in some cases focusedChild.getParent() == null + + // we can remember the focused view to restore after relayout if the + // data hasn't changed, or if the focused position is a header or footer + if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) { + focusLayoutRestoreDirectChild = focusedChild; + // remember the specific view that had focus + focusLayoutRestoreView = findFocus(); + if (focusLayoutRestoreView != null) { + // tell it we are going to mess with it + focusLayoutRestoreView.onStartTemporaryDetach(); + } + } + requestFocus(); + } + + // Clear out old views + detachAllViewsFromParent(); + + switch (mLayoutMode) { + case LAYOUT_SET_SELECTION: + if (newSel != null) { + sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); + } else { + sel = fillFromMiddle(childrenTop, childrenBottom); + } + break; + case LAYOUT_SYNC: + sel = fillSpecific(mSyncPosition, mSpecificTop); + break; + case LAYOUT_FORCE_BOTTOM: + sel = fillUp(mItemCount - 1, childrenBottom); + adjustViewsUpOrDown(); + break; + case LAYOUT_FORCE_TOP: + mFirstPosition = 0; + sel = fillFromTop(childrenTop); + adjustViewsUpOrDown(); + break; + case LAYOUT_SPECIFIC: + sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); + break; + default: + if (childCount == 0) { + if (!mStackFromBottom) { + final int position = lookForSelectablePosition(0, true); + setSelectedPositionInt(position); + sel = fillFromTop(childrenTop); + } else { + final int position = lookForSelectablePosition(mItemCount - 1, false); + setSelectedPositionInt(position); + sel = fillUp(mItemCount - 1, childrenBottom); + } + } else { + if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { + sel = fillSpecific(mSelectedPosition, + oldSel == null ? childrenTop : oldSel.getTop()); + } else if (mFirstPosition < mItemCount) { + sel = fillSpecific(mFirstPosition, + oldFirst == null ? childrenTop : oldFirst.getTop()); + } else { + sel = fillSpecific(0, childrenTop); + } + } + break; + } + + // Flush any cached views that did not get reused above + recycleBin.scrapActiveViews(); + + if (sel != null) { + // the current selected item should get focus if items + // are focusable + if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) { + final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild && + focusLayoutRestoreView.requestFocus()) || sel.requestFocus(); + if (!focusWasTaken) { + // selected item didn't take focus, fine, but still want + // to make sure something else outside of the selected view + // has focus + final View focused = getFocusedChild(); + if (focused != null) { + focused.clearFocus(); + } + positionSelector(sel); + } else { + sel.setSelected(false); + mSelectorRect.setEmpty(); + } + } else { + positionSelector(sel); + } + mSelectedTop = sel.getTop(); + } else { + if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) { + View child = getChildAt(mMotionPosition - mFirstPosition); + if (child != null) positionSelector(child); + } else { + mSelectedTop = 0; + mSelectorRect.setEmpty(); + } + + // even if there is not selected position, we may need to restore + // focus (i.e. something focusable in touch mode) + if (hasFocus() && focusLayoutRestoreView != null) { + focusLayoutRestoreView.requestFocus(); + } + } + + // tell focus view we are done mucking with it, if it is still in + // our view hierarchy. + if (focusLayoutRestoreView != null + && focusLayoutRestoreView.getWindowToken() != null) { + focusLayoutRestoreView.onFinishTemporaryDetach(); + } + + mLayoutMode = LAYOUT_NORMAL; + mDataChanged = false; + mNeedSync = false; + setNextSelectedPositionInt(mSelectedPosition); + + updateScrollIndicators(); + + if (mItemCount > 0) { + checkSelectionChanged(); + } + + invokeOnItemScrollListener(); + } finally { + if (!blockLayoutRequests) { + mBlockLayoutRequests = false; + } + } + } + + /** + * @param child a direct child of this list. + * @return Whether child is a header or footer view. + */ + private boolean isDirectChildHeaderOrFooter(View child) { + + final ArrayList headers = mHeaderViewInfos; + final int numHeaders = headers.size(); + for (int i = 0; i < numHeaders; i++) { + if (child == headers.get(i).view) { + return true; + } + } + final ArrayList footers = mFooterViewInfos; + final int numFooters = footers.size(); + for (int i = 0; i < numFooters; i++) { + if (child == footers.get(i).view) { + return true; + } + } + return false; + } + + /** + * Obtain the view and add it to our list of children. The view can be made + * fresh, converted from an unused view, or used as is if it was in the + * recycle bin. + * + * @param position Logical position in the list + * @param childrenBottomOrTop Top or bottom edge of the view to add + * @param flow If flow is true, align top edge to y. If false, align bottom + * edge to y. + * @param childrenLeft Left edge where children should be positioned + * @param selected Is this position selected? + * @return View that was added + */ + @SuppressWarnings("deprecation") + private View makeAndAddView(int position, int childrenBottomOrTop, boolean flow, + boolean selected) { + View child; + + int childrenLeft; + if (!mDataChanged) { + // Try to use an exsiting view for this position + child = mRecycler.getActiveView(position); + if (child != null) { + + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP, + position, getChildCount()); + } + + // Found it -- we're using an existing child + // This just needs to be positioned + childrenLeft = getChildLeft(position); + setupChild(child, position, childrenBottomOrTop, flow, childrenLeft, selected, true); + return child; + } + } + + //Notify new item is added to view. + onItemAddedToList( position, flow ); + childrenLeft = getChildLeft( position ); + + // Make a new view for this position, or convert an unused view if possible + child = obtainView(position, mIsScrap); + + // This needs to be positioned and measured + setupChild(child, position, childrenBottomOrTop, flow, childrenLeft, selected, mIsScrap[0]); + + return child; + } + + /** + * @param position position of newly adde ditem. + * @param flow If flow is true, align top edge to y. If false, align bottom edge to y. + */ + protected void onItemAddedToList(int position, boolean flow) { + } + + /** + * Add a view as a child and make sure it is measured (if necessary) and + * positioned properly. + * + * @param child The view to add + * @param position The position of this child + * @param y The y position relative to which this view will be positioned + * @param flowDown If true, align top edge to y. If false, align bottom + * edge to y. + * @param childrenLeft Left edge where children should be positioned + * @param selected Is this position selected? + * @param recycled Has this view been pulled from the recycle bin? If so it + * does not need to be remeasured. + */ + private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, + boolean selected, boolean recycled) { + + Log.v("PLA_ListView", "setupChild: " + position); + + final boolean isSelected = selected && shouldShowSelector(); + final boolean updateChildSelected = isSelected != child.isSelected(); + final int mode = mTouchMode; + final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && + mMotionPosition == position; + final boolean updateChildPressed = isPressed != child.isPressed(); + final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); + + // Respect layout params that are already in the view. Otherwise make some up... + // noinspection unchecked + PLA_AbsListView.LayoutParams p = (PLA_AbsListView.LayoutParams) child.getLayoutParams(); + if (p == null) { + p = new PLA_AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + p.viewType = mAdapter.getItemViewType(position); + + if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && + p.viewType == PLA_AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { + attachViewToParent(child, flowDown ? -1 : 0, p); + } else { + p.forceAdd = false; + if (p.viewType == PLA_AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + p.recycledHeaderFooter = true; + } + addViewInLayout(child, flowDown ? -1 : 0, p, true); + } + + if (updateChildSelected) { + child.setSelected(isSelected); + } + + if (updateChildPressed) { + child.setPressed(isPressed); + } + + if (needToMeasure) { + int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, + mListPadding.left + mListPadding.right, p.width); + int lpHeight = p.height; + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + + onMeasureChild( child, position, childWidthSpec, childHeightSpec ); + //child.measure(childWidthSpec, childHeightSpec); + } else { + cleanupLayoutState(child); + } + + final int w = child.getMeasuredWidth(); + final int h = child.getMeasuredHeight(); + final int childTop = flowDown ? y : y - h; + + if (needToMeasure) { + final int childRight = childrenLeft + w; + final int childBottom = childTop + h; + //child.layout(childrenLeft, childTop, childRight, childBottom); + onLayoutChild(child, position, childrenLeft, childTop, childRight, childBottom); + } else { + final int offsetLeft = childrenLeft - child.getLeft(); + final int offsetTop = childTop - child.getTop(); + onOffsetChild(child, position, offsetLeft, offsetTop); + } + + if (mCachingStarted && !child.isDrawingCacheEnabled()) { + child.setDrawingCacheEnabled(true); + } + } + + protected void onOffsetChild(View child, int position, int offsetLeft, int offsetTop) { + child.offsetLeftAndRight(offsetLeft); + child.offsetTopAndBottom(offsetTop); + } + + protected void onLayoutChild(View child, int position, int l, int t, int r, int b) { + child.layout(l, t, r, b); + } + + /** + * this method is called every time a new child is mesaure. + * @param child + * @param widthMeasureSpec + * @param heightMeasureSpec + */ + protected void onMeasureChild(View child, int position, int widthMeasureSpec, int heightMeasureSpec) { + child.measure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected boolean canAnimate() { + return super.canAnimate() && mItemCount > 0; + } + + /** + * Sets the currently selected item. If in touch mode, the item will not be selected + * but it will still be positioned appropriately. If the specified selection position + * is less than 0, then the item at position 0 will be selected. + * + * @param position Index (starting at 0) of the data item to be selected. + */ + @Override + public void setSelection(int position) { + setSelectionFromTop(position, 0); + } + + /** + * Sets the selected item and positions the selection y pixels from the top edge + * of the ListView. (If in touch mode, the item will not be selected but it will + * still be positioned appropriately.) + * + * @param position Index (starting at 0) of the data item to be selected. + * @param y The distance from the top edge of the ListView (plus padding) that the + * item will be positioned. + */ + public void setSelectionFromTop(int position, int y) { + if (mAdapter == null) { + return; + } + + if (!isInTouchMode()) { + position = lookForSelectablePosition(position, true); + if (position >= 0) { + setNextSelectedPositionInt(position); + } + } else { + mResurrectToPosition = position; + } + + if (position >= 0) { + mLayoutMode = LAYOUT_SPECIFIC; + mSpecificTop = mListPadding.top + y; + + if (mNeedSync) { + mSyncPosition = position; + mSyncRowId = mAdapter.getItemId(position); + } + + requestLayout(); + } + } + + /** + * Makes the item at the supplied position selected. + * + * @param position the position of the item to select + */ + @Override + void setSelectionInt(int position) { + setNextSelectedPositionInt(position); + boolean awakeScrollbars = false; + + final int selectedPosition = mSelectedPosition; + + if (selectedPosition >= 0) { + if (position == selectedPosition - 1) { + awakeScrollbars = true; + } else if (position == selectedPosition + 1) { + awakeScrollbars = true; + } + } + + layoutChildren(); + + if (awakeScrollbars) { + awakenScrollBars(); + } + } + + /** + * Find a position that can be selected (i.e., is not a separator). + * + * @param position The starting position to look at. + * @param lookDown Whether to look down for other positions. + * @return The next selectable position starting at position and then searching either up or + * down. Returns {@link #INVALID_POSITION} if nothing can be found. + */ + @Override + int lookForSelectablePosition(int position, boolean lookDown) { + final ListAdapter adapter = mAdapter; + if (adapter == null || isInTouchMode()) { + return INVALID_POSITION; + } + + final int count = adapter.getCount(); + if (!mAreAllItemsSelectable) { + if (lookDown) { + position = Math.max(0, position); + while (position < count && !adapter.isEnabled(position)) { + position++; + } + } else { + position = Math.min(position, count - 1); + while (position >= 0 && !adapter.isEnabled(position)) { + position--; + } + } + + if (position < 0 || position >= count) { + return INVALID_POSITION; + } + return position; + } else { + if (position < 0 || position >= count) { + return INVALID_POSITION; + } + return position; + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + boolean populated = super.dispatchPopulateAccessibilityEvent(event); + + // If the item count is less than 15 then subtract disabled items from the count and + // position. Otherwise ignore disabled items. + if (!populated) { + int itemCount = 0; + int currentItemIndex = getSelectedItemPosition(); + + ListAdapter adapter = getAdapter(); + if (adapter != null) { + final int count = adapter.getCount(); + if (count < 15) { + for (int i = 0; i < count; i++) { + if (adapter.isEnabled(i)) { + itemCount++; + } else if (i <= currentItemIndex) { + currentItemIndex--; + } + } + } else { + itemCount = count; + } + } + + event.setItemCount(itemCount); + event.setCurrentItemIndex(currentItemIndex); + } + + return populated; + } + + /** + * setSelectionAfterHeaderView set the selection to be the first list item + * after the header views. + */ + public void setSelectionAfterHeaderView() { + final int count = mHeaderViewInfos.size(); + if (count > 0) { + mNextSelectedPosition = 0; + return; + } + + if (mAdapter != null) { + setSelection(count); + } else { + mNextSelectedPosition = count; + mLayoutMode = LAYOUT_SET_SELECTION; + } + + } + + /** + * Scrolls up or down by the number of items currently present on screen. + * + * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} + * @return whether selection was moved + */ + boolean pageScroll(int direction) { + int nextPage = -1; + boolean down = false; + + if (direction == FOCUS_UP) { + nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); + } else if (direction == FOCUS_DOWN) { + nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); + down = true; + } + + if (nextPage >= 0) { + int position = lookForSelectablePosition(nextPage, down); + if (position >= 0) { + mLayoutMode = LAYOUT_SPECIFIC; + // mSpecificTop = mPaddingTop + getVerticalFadingEdgeLength(); + mSpecificTop = getPaddingTop() + getVerticalFadingEdgeLength(); + + if (down && position > mItemCount - getChildCount()) { + mLayoutMode = LAYOUT_FORCE_BOTTOM; + } + + if (!down && position < getChildCount()) { + mLayoutMode = LAYOUT_FORCE_TOP; + } + + setSelectionInt(position); + invokeOnItemScrollListener(); + if (!awakenScrollBars()) { + invalidate(); + } + + return true; + } + } + + return false; + } + + /** + * Go to the last or first item if possible (not worrying about panning across or navigating + * within the internal focus of the currently selected item.) + * + * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} + * + * @return whether selection was moved + */ + public boolean fullScroll(int direction) { + boolean moved = false; + if (direction == FOCUS_UP) { + if (mSelectedPosition != 0) { + int position = lookForSelectablePosition(0, true); + if (position >= 0) { + mLayoutMode = LAYOUT_FORCE_TOP; + setSelectionInt(position); + invokeOnItemScrollListener(); + } + moved = true; + } + } else if (direction == FOCUS_DOWN) { + if (mSelectedPosition < mItemCount - 1) { + int position = lookForSelectablePosition(mItemCount - 1, true); + if (position >= 0) { + mLayoutMode = LAYOUT_FORCE_BOTTOM; + setSelectionInt(position); + invokeOnItemScrollListener(); + } + moved = true; + } + } + + if (moved && !awakenScrollBars()) { + awakenScrollBars(); + invalidate(); + } + + return moved; + } + + /** + * Scroll the children by amount, adding a view at the end and removing + * views that fall off as necessary. + * + * @param amount The amount (positive or negative) to scroll. + */ + private void scrollListItemsBy(int amount) { + // offsetChildrenTopAndBottom(amount); + tryOffsetChildrenTopAndBottom(amount); + + final int listBottom = getHeight() - mListPadding.bottom; + final int listTop = mListPadding.top; + final PLA_AbsListView.RecycleBin recycleBin = mRecycler; + + if (amount < 0) { + // shifted items up + + // may need to pan views into the bottom space + View last = getLastChild(); + int numChildren = getChildCount(); + // View last = getChildAt(numChildren - 1); + + while (last.getBottom() < listBottom) { + final int lastVisiblePosition = mFirstPosition + numChildren - 1; + if (lastVisiblePosition < mItemCount - 1) { + addViewBelow(last, lastVisiblePosition); + last = getLastChild(); + numChildren++; + } else { + break; + } + } + + // may have brought in the last child of the list that is skinnier + // than the fading edge, thereby leaving space at the end. need + // to shift back + if (last.getBottom() < listBottom) { + // offsetChildrenTopAndBottom(listBottom - last.getBottom()); + tryOffsetChildrenTopAndBottom(listBottom - last.getBottom()); + } + + // top views may be panned off screen + View first = getChildAt(0); + while (first.getBottom() < listTop) { + PLA_AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); + if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { + detachViewFromParent(first); + recycleBin.addScrapView(first); + } else { + removeViewInLayout(first); + } + first = getChildAt(0); + mFirstPosition++; + } + } else { + // shifted items down + View first = getChildAt(0); + + // may need to pan views into top + while ((first.getTop() > listTop) && (mFirstPosition > 0)) { + first = addViewAbove(first, mFirstPosition); + mFirstPosition--; + } + + // may have brought the very first child of the list in too far and + // need to shift it back + if (first.getTop() > listTop) { + // offsetChildrenTopAndBottom(listTop - first.getTop()); + tryOffsetChildrenTopAndBottom(listTop - first.getTop()); + } + + int lastIndex = getChildCount() - 1; + View last = getChildAt(lastIndex); + + // bottom view may be panned off screen + while (last.getTop() > listBottom) { + PLA_AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams(); + if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { + detachViewFromParent(last); + recycleBin.addScrapView(last); + } else { + removeViewInLayout(last); + } + last = getChildAt(--lastIndex); + } + } + } + + protected View getLastChild() { + int numChildren = getChildCount(); + return getChildAt(numChildren - 1); + } + + private View addViewAbove(View theView, int position) { + int abovePosition = position - 1; + View view = obtainView(abovePosition, mIsScrap); + int edgeOfNewChild = theView.getTop() - mDividerHeight; + setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left, + false, mIsScrap[0]); + return view; + } + + private View addViewBelow(View theView, int position) { + int belowPosition = position + 1; + View view = obtainView(belowPosition, mIsScrap); + int edgeOfNewChild = theView.getBottom() + mDividerHeight; + setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left, + false, mIsScrap[0]); + return view; + } + + /** + * Indicates that the views created by the ListAdapter can contain focusable + * items. + * + * @param itemsCanFocus true if items can get focus, false otherwise + */ + public void setItemsCanFocus(boolean itemsCanFocus) { + mItemsCanFocus = itemsCanFocus; + if (!itemsCanFocus) { + setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + } + } + + /** + * @return Whether the views created by the ListAdapter can contain focusable + * items. + */ + public boolean getItemsCanFocus() { + return mItemsCanFocus; + } + + /** + * @hide Pending API council approval. + */ + @Override + public boolean isOpaque() { + // return (mCachingStarted && mIsCacheColorOpaque && mDividerIsOpaque && + // hasOpaqueScrollbars()) || super.isOpaque(); + //we can ignore scrollbar... + return (mCachingStarted && mIsCacheColorOpaque && mDividerIsOpaque) || super.isOpaque(); + } + + @Override + public void setCacheColorHint(int color) { + final boolean opaque = (color >>> 24) == 0xFF; + mIsCacheColorOpaque = opaque; + if (opaque) { + if (mDividerPaint == null) { + mDividerPaint = new Paint(); + } + mDividerPaint.setColor(color); + } + super.setCacheColorHint(color); + } + + void drawOverscrollHeader(Canvas canvas, Drawable drawable, Rect bounds) { + final int height = drawable.getMinimumHeight(); + + canvas.save(); + canvas.clipRect(bounds); + + final int span = bounds.bottom - bounds.top; + if (span < height) { + bounds.top = bounds.bottom - height; + } + + drawable.setBounds(bounds); + drawable.draw(canvas); + + canvas.restore(); + } + + void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) { + final int height = drawable.getMinimumHeight(); + + canvas.save(); + canvas.clipRect(bounds); + + final int span = bounds.bottom - bounds.top; + if (span < height) { + bounds.bottom = bounds.top + height; + } + + drawable.setBounds(bounds); + drawable.draw(canvas); + + canvas.restore(); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + // Draw the dividers + final int dividerHeight = mDividerHeight; + final Drawable overscrollHeader = mOverScrollHeader; + final Drawable overscrollFooter = mOverScrollFooter; + final boolean drawOverscrollHeader = overscrollHeader != null; + final boolean drawOverscrollFooter = overscrollFooter != null; + final boolean drawDividers = dividerHeight > 0 && mDivider != null; + + if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) { + // Only modify the top and bottom in the loop, we set the left and right here + final Rect bounds = mTempRect; + // bounds.left = mPaddingLeft; + // bounds.right = mRight - mLeft - mPaddingRight; + bounds.left = getPaddingLeft(); + bounds.right = getRight() - getLeft() - getPaddingRight(); + + final int count = getChildCount(); + final int headerCount = mHeaderViewInfos.size(); + final int itemCount = mItemCount; + final int footerLimit = itemCount - mFooterViewInfos.size() - 1; + final boolean headerDividers = mHeaderDividersEnabled; + final boolean footerDividers = mFooterDividersEnabled; + final int first = mFirstPosition; + final boolean areAllItemsSelectable = mAreAllItemsSelectable; + final ListAdapter adapter = mAdapter; + // If the list is opaque *and* the background is not, we want to + // fill a rect where the dividers would be for non-selectable items + // If the list is opaque and the background is also opaque, we don't + // need to draw anything since the background will do it for us + final boolean fillForMissingDividers = drawDividers && isOpaque() && !super.isOpaque(); + + if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) { + mDividerPaint = new Paint(); + mDividerPaint.setColor(getCacheColorHint()); + } + final Paint paint = mDividerPaint; + + // final int listBottom = mBottom - mTop - mListPadding.bottom + mScrollY; + final int listBottom = getBottom() - getTop() - mListPadding.bottom + getScrollY(); + if (!mStackFromBottom) { + int bottom = 0; + + // Draw top divider or header for overscroll + // final int scrollY = mScrollY; + final int scrollY = getScrollY(); + if (count > 0 && scrollY < 0) { + if (drawOverscrollHeader) { + bounds.bottom = 0; + bounds.top = scrollY; + drawOverscrollHeader(canvas, overscrollHeader, bounds); + } else if (drawDividers) { + bounds.bottom = 0; + bounds.top = -dividerHeight; + drawDivider(canvas, bounds, -1); + } + } + + for (int i = 0; i < count; i++) { + if ((headerDividers || first + i >= headerCount) && + (footerDividers || first + i < footerLimit)) { + View child = getChildAt(i); + bottom = child.getBottom(); + // Don't draw dividers next to items that are not enabled + if (drawDividers && + (bottom < listBottom && !(drawOverscrollFooter && i == count - 1))) { + if ((areAllItemsSelectable || + (adapter.isEnabled(first + i) && (i == count - 1 || + adapter.isEnabled(first + i + 1))))) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + drawDivider(canvas, bounds, i); + } else if (fillForMissingDividers) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + canvas.drawRect(bounds, paint); + } + } + } + } + + // final int overFooterBottom = mBottom + mScrollY; + final int overFooterBottom = getBottom() + getScrollY(); + if (drawOverscrollFooter && first + count == itemCount && + overFooterBottom > bottom) { + bounds.top = bottom; + bounds.bottom = overFooterBottom; + drawOverscrollFooter(canvas, overscrollFooter, bounds); + } + } else { + int top; + int listTop = mListPadding.top; + + // final int scrollY = mScrollY; + final int scrollY = getScrollY(); + + if (count > 0 && drawOverscrollHeader) { + bounds.top = scrollY; + bounds.bottom = getChildAt(0).getTop(); + drawOverscrollHeader(canvas, overscrollHeader, bounds); + } + + final int start = drawOverscrollHeader ? 1 : 0; + for (int i = start; i < count; i++) { + if ((headerDividers || first + i >= headerCount) && + (footerDividers || first + i < footerLimit)) { + View child = getChildAt(i); + top = child.getTop(); + // Don't draw dividers next to items that are not enabled + if (drawDividers && top > listTop) { + if ((areAllItemsSelectable || + (adapter.isEnabled(first + i) && (i == count - 1 || + adapter.isEnabled(first + i + 1))))) { + bounds.top = top - dividerHeight; + bounds.bottom = top; + // Give the method the child ABOVE the divider, so we + // subtract one from our child + // position. Give -1 when there is no child above the + // divider. + drawDivider(canvas, bounds, i - 1); + } else if (fillForMissingDividers) { + bounds.top = top - dividerHeight; + bounds.bottom = top; + canvas.drawRect(bounds, paint); + } + } + } + } + + if (count > 0 && scrollY > 0) { + if (drawOverscrollFooter) { + // final int absListBottom = mBottom; + final int absListBottom = getBottom(); + bounds.top = absListBottom; + bounds.bottom = absListBottom + scrollY; + drawOverscrollFooter(canvas, overscrollFooter, bounds); + } else if (drawDividers) { + bounds.top = listBottom; + bounds.bottom = listBottom + dividerHeight; + drawDivider(canvas, bounds, -1); + } + } + } + } + + // Draw the indicators (these should be drawn above the dividers) and children + super.dispatchDraw(canvas); + } + + /** + * Draws a divider for the given child in the given bounds. + * + * @param canvas The canvas to draw to. + * @param bounds The bounds of the divider. + * @param childIndex The index of child (of the View) above the divider. + * This will be -1 if there is no child above the divider to be + * drawn. + */ + void drawDivider(Canvas canvas, Rect bounds, int childIndex) { + // This widget draws the same divider for all children + final Drawable divider = mDivider; + final boolean clipDivider = mClipDivider; + + if (!clipDivider) { + divider.setBounds(bounds); + } else { + canvas.save(); + canvas.clipRect(bounds); + } + + divider.draw(canvas); + + if (clipDivider) { + canvas.restore(); + } + } + + /** + * Returns the drawable that will be drawn between each item in the list. + * + * @return the current drawable drawn between list elements + */ + public Drawable getDivider() { + return mDivider; + } + + /** + * Sets the drawable that will be drawn between each item in the list. If the drawable does + * not have an intrinsic height, you should also call {@link #setDividerHeight(int)} + * + * @param divider The drawable to use. + */ + public void setDivider(Drawable divider) { + if (divider != null) { + mDividerHeight = divider.getIntrinsicHeight(); + mClipDivider = divider instanceof ColorDrawable; + } else { + mDividerHeight = 0; + mClipDivider = false; + } + mDivider = divider; + mDividerIsOpaque = divider == null || divider.getOpacity() == PixelFormat.OPAQUE; + requestLayoutIfNecessary(); + } + + /** + * @return Returns the height of the divider that will be drawn between each item in the list. + */ + public int getDividerHeight() { + return mDividerHeight; + } + + /** + * Sets the height of the divider that will be drawn between each item in the list. Calling + * this will override the intrinsic height as set by {@link #setDivider(Drawable)} + * + * @param height The new height of the divider in pixels. + */ + public void setDividerHeight(int height) { + mDividerHeight = height; + requestLayoutIfNecessary(); + } + + /** + * Enables or disables the drawing of the divider for header views. + * + * @param headerDividersEnabled True to draw the headers, false otherwise. + * + * @see #setFooterDividersEnabled(boolean) + * @see #addHeaderView(android.view.View) + */ + public void setHeaderDividersEnabled(boolean headerDividersEnabled) { + mHeaderDividersEnabled = headerDividersEnabled; + invalidate(); + } + + /** + * Enables or disables the drawing of the divider for footer views. + * + * @param footerDividersEnabled True to draw the footers, false otherwise. + * + * @see #setHeaderDividersEnabled(boolean) + * @see #addFooterView(android.view.View) + */ + public void setFooterDividersEnabled(boolean footerDividersEnabled) { + mFooterDividersEnabled = footerDividersEnabled; + invalidate(); + } + + /** + * Sets the drawable that will be drawn above all other list content. + * This area can become visible when the user overscrolls the list. + * + * @param header The drawable to use + */ + public void setOverscrollHeader(Drawable header) { + mOverScrollHeader = header; + // if (mScrollY < 0) { + // invalidate(); + // } + + if (getScrollY() < 0) { + invalidate(); + } + } + + /** + * @return The drawable that will be drawn above all other list content + */ + public Drawable getOverscrollHeader() { + return mOverScrollHeader; + } + + /** + * Sets the drawable that will be drawn below all other list content. + * This area can become visible when the user overscrolls the list, + * or when the list's content does not fully fill the container area. + * + * @param footer The drawable to use + */ + public void setOverscrollFooter(Drawable footer) { + mOverScrollFooter = footer; + invalidate(); + } + + /** + * @return The drawable that will be drawn below all other list content + */ + public Drawable getOverscrollFooter() { + return mOverScrollFooter; + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + + int closetChildIndex = -1; + if (gainFocus && previouslyFocusedRect != null) { + // previouslyFocusedRect.offset(mScrollX, mScrollY); + previouslyFocusedRect.offset(getScrollX(), getScrollY()); + + final ListAdapter adapter = mAdapter; + // Don't cache the result of getChildCount or mFirstPosition here, + // it could change in layoutChildren. + if (adapter.getCount() < getChildCount() + mFirstPosition) { + mLayoutMode = LAYOUT_NORMAL; + layoutChildren(); + } + + // figure out which item should be selected based on previously + // focused rect + Rect otherRect = mTempRect; + int minDistance = Integer.MAX_VALUE; + final int childCount = getChildCount(); + final int firstPosition = mFirstPosition; + + for (int i = 0; i < childCount; i++) { + // only consider selectable views + if (!adapter.isEnabled(firstPosition + i)) { + continue; + } + + View other = getChildAt(i); + other.getDrawingRect(otherRect); + offsetDescendantRectToMyCoords(other, otherRect); + int distance = getDistance(previouslyFocusedRect, otherRect, direction); + + if (distance < minDistance) { + minDistance = distance; + closetChildIndex = i; + } + } + } + + if (closetChildIndex >= 0) { + setSelection(closetChildIndex + mFirstPosition); + } else { + requestLayout(); + } + } + + + /* + * (non-Javadoc) + * + * Children specified in XML are assumed to be header views. After we have + * parsed them move them out of the children list and into mHeaderViews. + */ + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + int count = getChildCount(); + if (count > 0) { + for (int i = 0; i < count; ++i) { + addHeaderView(getChildAt(i)); + } + removeAllViews(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mItemsCanFocus && ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + return super.onTouchEvent(ev); + } + + + @Override + public boolean performItemClick(View view, int position, long id) { + boolean handled = false; + + handled |= super.performItemClick(view, position, id); + + return handled; + } + + /** + * Sets the checked state of the specified position. The is only valid if + * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or + * {@link #CHOICE_MODE_MULTIPLE}. + * + * @param position The item whose checked state is to be checked + * @param value The new checked state for the item + */ + public void setItemChecked(int position, boolean value) { + } + + /** + * Returns the checked state of the specified position. The result is only + * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE} + * or {@link #CHOICE_MODE_MULTIPLE}. + * + * @param position The item whose checked state to return + * @return The item's checked state or false if choice mode + * is invalid + * + * @see #setChoiceMode(int) + */ + public boolean isItemChecked(int position) { + return false; + } + + /** + * Returns the currently checked item. The result is only valid if the choice + * mode has been set to {@link #CHOICE_MODE_SINGLE}. + * + * @return The position of the currently checked item or + * {@link #INVALID_POSITION} if nothing is selected + * + * @see #setChoiceMode(int) + */ + public int getCheckedItemPosition() { + return INVALID_POSITION; + } + + /** + * Returns the set of checked items in the list. The result is only valid if + * the choice mode has not been set to {@link #CHOICE_MODE_NONE}. + * + * @return A SparseBooleanArray which will return true for each call to + * get(int position) where position is a position in the list, + * or null if the choice mode is set to + * {@link #CHOICE_MODE_NONE}. + */ + public SparseBooleanArray getCheckedItemPositions() { + return null; + } + + /** + * Returns the set of checked items ids. The result is only valid if the + * choice mode has not been set to {@link #CHOICE_MODE_NONE}. + * + * @return A new array which contains the id of each checked item in the + * list. + * + * @deprecated Use {@link #getCheckedItemIds()} instead. + */ + @Deprecated + public long[] getCheckItemIds() { + // Use new behavior that correctly handles stable ID mapping. + if (mAdapter != null && mAdapter.hasStableIds()) { + return getCheckedItemIds(); + } + + return new long[0]; + } + + /** + * Returns the set of checked items ids. The result is only valid if the + * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter + * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true}) + * + * @return A new array which contains the id of each checked item in the + * list. + */ + public long[] getCheckedItemIds() { + return new long[0]; + } + + /** + * Clear any choices previously set + */ + public void clearChoices() { + } + + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return new SavedState(superState); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + } + +}//end of class diff --git a/src/com/huewu/pla/sample/SampleActivity.java b/src/com/huewu/pla/sample/SampleActivity.java new file mode 100644 index 0000000..c1e2057 --- /dev/null +++ b/src/com/huewu/pla/sample/SampleActivity.java @@ -0,0 +1,62 @@ +package com.huewu.pla.sample; + +import java.util.Arrays; +import java.util.Random; + +import com.huewu.lib.pla.R; +import com.huewu.pla.lib.internal.PLA_AdapterView; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.widget.Adapter; +import android.widget.ArrayAdapter; + +public class SampleActivity extends Activity { + + private class MySimpleAdapter extends ArrayAdapter { + + public MySimpleAdapter(Context context, int layoutRes) { + super(context, layoutRes, android.R.id.text1); + } + } + + private PLA_AdapterView mAdapterView = null; + private MySimpleAdapter mAdapter = null; + + @SuppressWarnings("unchecked") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.act_sample); + mAdapterView = (PLA_AdapterView) findViewById(R.id.list); + } + + @Override + protected void onResume() { + super.onResume(); + initAdapter(); + mAdapterView.setAdapter(mAdapter); + } + + private Random mRand = new Random(); + private void initAdapter() { + mAdapter = new MySimpleAdapter(this, R.layout.item_sample); + + for( int i = 0; i < 100; ++i){ + //generate 100 random items. + + StringBuilder builder = new StringBuilder(); + builder.append("Hello!!["); + builder.append(i); + builder.append("] "); + + char[] chars = new char[mRand.nextInt(100)]; + Arrays.fill(chars, '1'); + builder.append(chars); + mAdapter.add(builder.toString()); + } + + } + +}//end of class