diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | 54b6cfa9a9e5b861a9930af873580d6dc20f773c (patch) | |
tree | 35051494d2af230dce54d6b31c6af8fc24091316 /core/java/android/widget | |
download | frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.zip frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.gz frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.bz2 |
Initial Contribution
Diffstat (limited to 'core/java/android/widget')
82 files changed, 40447 insertions, 0 deletions
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java new file mode 100644 index 0000000..19b1ce0 --- /dev/null +++ b/core/java/android/widget/AbsListView.java @@ -0,0 +1,3196 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.Configuration; +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.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +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.view.WindowManagerImpl; +import android.view.ContextMenu.ContextMenuInfo; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Common code shared between ListView and GridView + * + * @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 + */ +public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, + ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, + ViewTreeObserver.OnTouchModeChangeListener { + + /** + * 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. + */ + 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 + */ + 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 + */ + static final int TOUCH_MODE_DONE_WAITING = 2; + + /** + * Indicates the touch gesture is a scroll + */ + static final int TOUCH_MODE_SCROLL = 3; + + /** + * Indicates the view is in the process of being flung + */ + 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 + */ + 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 + */ + Rect mListPadding = new Rect(); + + /** + * Subclasses must retain their measure spec from onMeasure() into this member + */ + 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 + */ + boolean mCachingStarted; + + /** + * The position of the view that received the down motion event + */ + 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 + */ + 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; + + /** + * 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; + + /** + * Keeps track of our accessory window + */ + PopupWindow mPopup; + + /** + * Used with type filter window + */ + EditText mTextFilter; + + /** + * Indicates that this view supports filtering + */ + private boolean mTextFilterEnabled; + + /** + * Indicates that this view is currently displaying a filtered view of the data + */ + private boolean mFiltered; + + /** + * 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; + + // TODO: REMOVE WHEN WE'RE DONE WITH PROFILING + 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 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; + + /** + * 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(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(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount); + } + + public AbsListView(Context context) { + super(context); + initAbsListView(); + + setVerticalScrollBarEnabled(true); + TypedArray a = context.obtainStyledAttributes(R.styleable.View); + initializeScrollbars(a); + a.recycle(); + } + + public AbsListView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.absListViewStyle); + } + + public AbsListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initAbsListView(); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AbsListView, defStyle, 0); + + Drawable d = a.getDrawable(com.android.internal.R.styleable.AbsListView_listSelector); + if (d != null) { + setSelector(d); + } + + mDrawSelectorOnTop = a.getBoolean( + com.android.internal.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); + + boolean useTextFilter = a.getBoolean(R.styleable.AbsListView_textFilterEnabled, false); + setTextFilterEnabled(useTextFilter); + + int transcriptMode = a.getInt(R.styleable.AbsListView_transcriptMode, + TRANSCRIPT_MODE_DISABLED); + setTranscriptMode(transcriptMode); + + int color = a.getColor(R.styleable.AbsListView_cacheColorHint, 0); + setCacheColorHint(color); + + a.recycle(); + } + + /** + * 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) + */ + 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; + } + + /** + * Enables or disables the type filter window. If enabled, typing when + * this view has focus will filter the children to match the users input. + * Note that the {@link Adapter} used by this view must implement the + * {@link Filterable} interface. + * + * @param textFilterEnabled true to enable type filtering, false otherwise + * + * @see Filterable + */ + public void setTextFilterEnabled(boolean textFilterEnabled) { + mTextFilterEnabled = textFilterEnabled; + } + + /** + * Indicates whether type filtering is enabled for this view + * + * @return true if type filtering is enabled, false otherwise + * + * @see #setTextFilterEnabled(boolean) + * @see Filterable + */ + public boolean isTextFilterEnabled() { + return mTextFilterEnabled; + } + + @Override + public void getFocusedRect(Rect r) { + View view = getSelectedView(); + if (view != null) { + // 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 initAbsListView() { + // Setting focusable in touch mode will set the focusable property to true + setFocusableInTouchMode(true); + setWillNotDraw(false); + setAlwaysDrawnWithCacheEnabled(false); + setScrollingCacheEnabled(true); + } + + private void useDefaultSelector() { + setSelector(getResources().getDrawable(com.android.internal.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 + */ + 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; + String filter; + + /** + * Constructor called from {@link 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(); + filter = in.readString(); + } + + @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); + out.writeString(filter); + } + + @Override + public String toString() { + return "AbsListView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " selectedId=" + selectedId + + " firstId=" + firstId + + " viewTop=" + viewTop + + " position=" + position + + " height=" + height + + " filter=" + filter + "}"; + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + 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 + * popup, 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. + */ + dismissPopup(); + + 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; + } + } + + ss.filter = null; + if (mFiltered) { + final EditText textFilter = mTextFilter; + if (textFilter != null) { + Editable filterText = textFilter.getText(); + if (filterText != null) { + ss.filter = filterText.toString(); + } + } + } + + 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; + } + + // Don't restore the type filter window when there is no keyboard + int keyboardHidden = getContext().getResources().getConfiguration().keyboardHidden; + if (keyboardHidden != Configuration.KEYBOARDHIDDEN_YES) { + String filterText = ss.filter; + setFilterText(filterText); + } + requestLayout(); + } + + /** + * Sets the initial value for the text filter. + * @param filterText The text to use for the filter. + * + * @see #setTextFilterEnabled + */ + public void setFilterText(String filterText) { + if (mTextFilterEnabled && filterText != null && filterText.length() > 0) { + createTextFilter(false); + // This is going to call our listener onTextChanged, but we are + // not ready to bring up a window yet + mTextFilter.setText(filterText); + mTextFilter.setSelection(filterText.length()); + if (mAdapter instanceof Filterable) { + Filter f = ((Filterable) mAdapter).getFilter(); + f.filter(filterText); + // Set filtered to true so we will display the filter window when our main + // window is ready + mFiltered = true; + mDataSetObserver.clearSavedState(); + } + } + } + + @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) { + int extent = count * 100; + + View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + extent += (top * 100) / height; + } + + view = getChildAt(count - 1); + final int bottom = view.getBottom(); + height = view.getHeight(); + if (height > 0) { + extent -= ((bottom - getHeight()) * 100) / height; + } + + return extent; + } + return 0; + } + + @Override + protected int computeVerticalScrollOffset() { + if (mFirstPosition >= 0 && getChildCount() > 0) { + final View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + return Math.max(mFirstPosition * 100 - (top * 100) / height, 0); + } + } + return 0; + } + + @Override + protected int computeVerticalScrollRange() { + return Math.max(mItemCount * 100, 0); + } + + @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; + } + } + + @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; + } + } + + @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; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mInLayout = true; + layoutChildren(); + mInLayout = false; + } + + 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; + } + + 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 + * @return A view displaying the data associated with the specified position + */ + View obtainView(int position) { + 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 { + 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) { + int saveCount = 0; + final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; + if (clipToPadding) { + saveCount = canvas.save(); + final int scrollX = mScrollX; + final int scrollY = mScrollY; + canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop, + scrollX + mRight - mLeft - mPaddingRight, + scrollY + mBottom - mTop - mPaddingBottom); + mGroupFlags &= ~CLIP_TO_PADDING_MASK; + } + + final boolean drawSelectorOnTop = mDrawSelectorOnTop; + if (!drawSelectorOnTop) { + drawSelector(canvas); + } + + super.dispatchDraw(canvas); + + if (drawSelectorOnTop) { + drawSelector(canvas); + } + + if (clipToPadding) { + canvas.restoreToCount(saveCount); + mGroupFlags |= CLIP_TO_PADDING_MASK; + } + } + + @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 + */ + 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() { + Drawable selector = mSelector; + Rect selectorRect = mSelectorRect; + if (selector != null && (isFocused() || touchModeDrawsInPressedState()) + && selectorRect != null && !selectorRect.isEmpty()) { + 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(); + + 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); + removeCallbacks(mFlingRunnable); + // Always hide the type filter + dismissPopup(); + + if (touchMode == TOUCH_MODE_OFF) { + // Remember the last selected element + mResurrectToPosition = mSelectedPosition; + } + } else { + if (mFiltered) { + // Show the type filter only if a filter is in effect + showPopup(); + } + + // 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; + + if (mAdapter != null && mItemCount > 0 && + mClickMotionPosition < mAdapter.getCount() && sameWindow()) { + performItemClick(mChild, mClickMotionPosition, getAdapter().getItemId( + mClickMotionPosition)); + } + } + } + + 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(AbsListView.this, child, + longPressPosition, longPressId); + } + if (!handled) { + mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); + handled = super.showContextMenuForChild(AbsListView.this); + } + 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(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 onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (isPressed() && mSelectedPosition >= 0 && mAdapter != null && + mSelectedPosition < mAdapter.getCount()) { + final int index = mSelectedPosition - mFirstPosition; + performItemClick(getChildAt(index), mSelectedPosition, mSelectedRowId); + setPressed(false); + return true; + } + } + 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); + int touchSlop = ViewConfiguration.getTouchSlop(); + if (distance > touchSlop) { + 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 = this.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. + mLayoutMode = LAYOUT_NORMAL; + layoutChildren(); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + + View v; + int deltaY; + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + int motionPosition = pointToPosition(x, y); + 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; + } + // User clicked on whitespace, or stopped a fling. It is a scroll. + createScrollingCache(); + mTouchMode = TOUCH_MODE_SCROLL; + 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: { + 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; + trackMotionScroll(deltaY, incrementalDeltaY); + + // Check to see if we have bumped into the scroll limit + View motionView = this.getChildAt(mMotionPosition - mFirstPosition); + if (motionView != null) { + // Check if the top of the motion view is where it is + // supposed to be + if (motionView.getTop() != mMotionViewNewTop) { + // We did not scroll the full amount. Treat this essentially like the + // start of a new touch scroll + final int motionPosition = findMotionRow(y); + + mMotionCorrection = 0; + motionView = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = motionView.getTop(); + mMotionY = y; + mMotionPosition = motionPosition; + } + } + 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 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; + mTouchMode = TOUCH_MODE_TAP; + if (!mDataChanged) { + 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()); + } + return true; + } else { + if (!mDataChanged) { + post(performClick); + } + } + } + mTouchMode = TOUCH_MODE_REST; + break; + case TOUCH_MODE_SCROLL: + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000); + int initialVelocity = (int)velocityTracker.getYVelocity(); + + if ((Math.abs(initialVelocity) > ViewConfiguration.getMinimumFlingVelocity()) && + (getChildCount() > 0)){ + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + mFlingRunnable.start(-initialVelocity); + } else { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } + } + + 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; + } + + 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; + } + } + + } + + return true; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + int action = ev.getAction(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + View v; + switch (action) { + case MotionEvent.ACTION_DOWN: { + int motionPosition = findMotionRow(y); + if (mTouchMode != 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; + break; + } + + case MotionEvent.ACTION_MOVE: { + switch (mTouchMode) { + case TOUCH_MODE_DOWN: + if (startScrollIfNeeded(y - mMotionY)) { + return true; + } + break; + } + break; + } + + case MotionEvent.ACTION_UP: { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + break; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void addTouchables(ArrayList<View> 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); + } + } + + private 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 Scroller mScroller; + + /** + * Y value reported by mScroller on the previous fling + */ + private int mLastFlingY; + + public FlingRunnable() { + mScroller = new Scroller(getContext()); + } + + public 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; + } + } + } + + private void endFling() { + mTouchMode = TOUCH_MODE_REST; + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChanged(AbsListView.this, + OnScrollListener.SCROLL_STATE_IDLE); + } + clearScrollingCache(); + } + + public void run() { + if (mTouchMode != TOUCH_MODE_FLING) { + return; + } + + 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); + } 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); + } + + trackMotionScroll(delta, delta); + + // Check to see if we have bumped into the scroll limit + View motionView = getChildAt(mMotionPosition - mFirstPosition); + if (motionView != null) { + // Check if the top of the motion view is where it is + // supposed to be + if (motionView.getTop() != mMotionViewNewTop) { + more = false; + } + } + + if (more) { + mLastFlingY = y; + post(this); + } else { + endFling(); + if (PROFILE_FLINGING) { + if (mFlingProfilingStarted) { + Debug.stopMethodTracing(); + mFlingProfilingStarted = false; + } + } + } + } + } + + private void createScrollingCache() { + if (mScrollingCacheEnabled && !mCachingStarted) { + setChildrenDrawnWithCacheEnabled(true); + setChildrenDrawingCacheEnabled(true); + mCachingStarted = true; + } + } + + private void clearScrollingCache() { + if (mCachingStarted) { + setChildrenDrawnWithCacheEnabled(false); + if ((mPersistentDrawingCache & PERSISTENT_SCROLLING_CACHE) == 0) { + setChildrenDrawingCacheEnabled(false); + } + if (!isAlwaysDrawnWithCacheEnabled()) { + invalidate(); + } + mCachingStarted = false; + } + } + + /** + * 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. + */ + void trackMotionScroll(int deltaY, int incrementalDeltaY) { + final int childCount = getChildCount(); + if (childCount == 0) { + return; + } + + final int firstTop = getChildAt(0).getTop(); + final int lastBottom = getChildAt(childCount - 1).getBottom(); + + 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; + 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 absIncrementalDeltaY = Math.abs(incrementalDeltaY); + + if (spaceAbove >= absIncrementalDeltaY && spaceBelow >= absIncrementalDeltaY) { + hideSelector(); + offsetChildrenTopAndBottom(incrementalDeltaY); + invalidate(); + mMotionViewNewTop = mMotionViewOriginalTop + deltaY; + } else { + 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; + } + + 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; + } + + final boolean down = incrementalDeltaY < 0; + + 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; + detachViewsFromParent(start, count); + offsetChildrenTopAndBottom(incrementalDeltaY); + + if (down) { + mFirstPosition += count; + } + + invalidate(); + fillGap(down); + mBlockLayoutRequests = false; + + invokeOnItemScrollListener(); + } + } + + /** + * 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) { + 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 closest to y + */ + abstract int findMotionRow(int y); + + /** + * 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; + 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 >= 0) { + mLayoutMode = LAYOUT_SPECIFIC; + setSelectionInt(selectedPos); + } + + 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(); + } + + /** + * Removes the filter window + */ + void dismissPopup() { + if (mPopup != null) { + mPopup.dismiss(); + } + } + + /** + * Shows the filter window + */ + private void showPopup() { + // Make sure we have a window before showing the popup + if (getWindowVisibility() == View.VISIBLE) { + int screenHeight = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight(); + final int[] xy = mLocation; + getLocationOnScreen(xy); + int bottomGap = screenHeight - xy[1] - getHeight() + 20; + mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, + xy[0], bottomGap); + // Make sure we get focus if we are showing the popup + checkFocus(); + } + } + + /** + * 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 + protected boolean isInFilterMode() { + return mFiltered; + } + + /** + * Sends a key to the text filter window + * + * @param keyCode The keycode for the event + * @param event The actual key event + * + * @return True if the text filter handled the event, false otherwise. + */ + boolean sendToTextFilter(int keyCode, int count, KeyEvent event) { + if (!mTextFilterEnabled || !(getAdapter() instanceof Filterable) || + ((Filterable) getAdapter()).getFilter() == null) { + return false; + } + + boolean handled = false; + boolean okToSend = true; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + okToSend = false; + break; + case KeyEvent.KEYCODE_BACK: + if (mFiltered && mPopup != null && mPopup.isShowing() && + event.getAction() == KeyEvent.ACTION_DOWN) { + handled = true; + mTextFilter.setText(""); + } + okToSend = false; + break; + case KeyEvent.KEYCODE_SPACE: + // Only send spaces once we are filtered + okToSend = mFiltered = true; + break; + } + + if (okToSend) { + createTextFilter(true); + + KeyEvent forwardEvent = event; + if (forwardEvent.getRepeatCount() > 0) { + forwardEvent = new KeyEvent(event, event.getEventTime(), 0); + } + + int action = event.getAction(); + switch (action) { + case KeyEvent.ACTION_DOWN: + handled = mTextFilter.onKeyDown(keyCode, forwardEvent); + break; + + case KeyEvent.ACTION_UP: + handled = mTextFilter.onKeyUp(keyCode, forwardEvent); + break; + + case KeyEvent.ACTION_MULTIPLE: + handled = mTextFilter.onKeyMultiple(keyCode, count, event); + break; + } + } + return handled; + } + + /** + * Creates the window for the text filter and populates it with an EditText field; + * + * @param animateEntrance true if the window should appear with an animation + */ + private void createTextFilter(boolean animateEntrance) { + if (mPopup == null) { + Context c = getContext(); + PopupWindow p = new PopupWindow(c); + LayoutInflater layoutInflater = (LayoutInflater) c + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mTextFilter = (EditText) layoutInflater.inflate( + com.android.internal.R.layout.typing_filter, null); + mTextFilter.addTextChangedListener(this); + p.setFocusable(false); + p.setContentView(mTextFilter); + p.setWidth(LayoutParams.WRAP_CONTENT); + p.setHeight(LayoutParams.WRAP_CONTENT); + p.setBackgroundDrawable(null); + mPopup = p; + getViewTreeObserver().addOnGlobalLayoutListener(this); + } + if (animateEntrance) { + mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter); + } else { + mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilterRestore); + } + } + + /** + * Clear the text filter. + */ + public void clearTextFilter() { + if (mFiltered) { + mTextFilter.setText(""); + mFiltered = false; + if (mPopup != null && mPopup.isShowing()) { + dismissPopup(); + } + } + } + + /** + * Returns if the ListView currently has a text filter. + */ + public boolean hasTextFilter() { + return mFiltered; + } + + public void onGlobalLayout() { + if (isShown()) { + // Show the popup if we are filtered + if (mFiltered && mPopup != null && !mPopup.isShowing()) { + showPopup(); + } + } else { + // Hide the popup when we are no longer visible + if (mPopup.isShowing()) { + dismissPopup(); + } + } + + } + + /** + * For our text watcher that associated with the text filter + */ + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + /** + * For our text watcher that associated with the text filter. Performs the actual + * filtering as the text changes. + */ + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (mPopup != null) { + int length = s.length(); + boolean showing = mPopup.isShowing(); + if (!showing && length > 0) { + // Show the filter popup if necessary + showPopup(); + mFiltered = true; + } else if (showing && length == 0) { + // Remove the filter popup if the user has cleared all text + mPopup.dismiss(); + mFiltered = false; + } + if (mAdapter instanceof Filterable) { + Filter f = ((Filterable) mAdapter).getFilter(); + // Filter should not be null when we reach this part + if (f != null) { + f.filter(s, this); + } else { + throw new IllegalStateException("You cannot call onTextChanged with a non " + + "filterable adapter"); + } + } + } + } + + /** + * For our text watcher that associated with the text filter + */ + public void afterTextChanged(Editable s) { + } + + public void onFilterComplete(int count) { + if (mSelectedPosition < 0 && count > 0) { + mResurrectToPosition = INVALID_POSITION; + resurrectSelection(); + } + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new AbsListView.LayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof 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) { + mCacheColorHint = 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<View> views) { + int childCount = getChildCount(); + RecyclerListener listener = mRecycler.mRecyclerListener; + + // Reclaim views on screen + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + AbsListView.LayoutParams lp = (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(); + } + + /** + * 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.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) } + */ + int viewType; + + 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.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<View>[] mScrapViews; + + private int mViewTypeCount; + + private ArrayList<View> mCurrentScrap; + + public void setViewTypeCount(int viewTypeCount) { + if (viewTypeCount < 1) { + throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); + } + //noinspection unchecked + ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; + for (int i = 0; i < viewTypeCount; i++) { + scrapViews[i] = new ArrayList<View>(); + } + mViewTypeCount = viewTypeCount; + mCurrentScrap = scrapViews[0]; + mScrapViews = scrapViews; + } + + public boolean shouldRecycleViewType(int viewType) { + return viewType >= 0; + } + + /** + * Clears the scrap heap. + */ + void clear() { + if (mViewTypeCount == 1) { + final ArrayList<View> 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<View> 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); + AbsListView.LayoutParams lp = (AbsListView.LayoutParams)child.getLayoutParams(); + // Don't put header or footer views into the scrap heap + if (lp != null && lp.viewType != AdapterView.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] = getChildAt(i); + } + } + } + + /** + * 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<View> 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) { + AbsListView.LayoutParams lp = (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)) { + return; + } + + if (mViewTypeCount == 1) { + mCurrentScrap.add(scrap); + } else { + mScrapViews[viewType].add(scrap); + } + + if (mRecyclerListener != null) { + mRecyclerListener.onMovedToScrapHeap(scrap); + } + } + + /** + * Move all views remaining in mActiveViews to mScrapViews. + */ + void scrapActiveViews() { + final View[] activeViews = mActiveViews; + final boolean hasListener = mRecyclerListener != null; + final boolean multipleScraps = mViewTypeCount > 1; + + ArrayList<View> scrapViews = mCurrentScrap; + final int count = activeViews.length; + for (int i = 0; i < count; ++i) { + final View victim = activeViews[i]; + if (victim != null) { + int whichScrap = ((AbsListView.LayoutParams) + victim.getLayoutParams()).viewType; + + activeViews[i] = null; + + if (whichScrap == AdapterView.ITEM_VIEW_TYPE_IGNORE) { + // Do not move views that should be ignored + continue; + } + + if (multipleScraps) { + scrapViews = mScrapViews[whichScrap]; + } + 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<View>[] scrapViews = mScrapViews; + for (int i = 0; i < viewTypeCount; ++i) { + final ArrayList<View> 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<View> views) { + if (mViewTypeCount == 1) { + views.addAll(mCurrentScrap); + } else { + final int viewTypeCount = mViewTypeCount; + final ArrayList<View>[] scrapViews = mScrapViews; + for (int i = 0; i < viewTypeCount; ++i) { + final ArrayList<View> scrapPile = scrapViews[i]; + views.addAll(scrapPile); + } + } + } + } +} diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java new file mode 100644 index 0000000..1fa7318 --- /dev/null +++ b/core/java/android/widget/AbsSeekBar.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; + +public abstract class AbsSeekBar extends ProgressBar { + + private Drawable mThumb; + private int mThumbOffset; + + /** + * On touch, this offset plus the scaled value from the position of the + * touch will form the progress value. Usually 0. + */ + float mTouchProgressOffset; + + /** + * Whether this is user seekable. + */ + boolean mIsUserSeekable = true; + + private static final int NO_ALPHA = 0xFF; + float mDisabledAlpha; + + public AbsSeekBar(Context context) { + super(context); + } + + public AbsSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.SeekBar, defStyle, 0); + Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb); + setThumb(thumb); + int thumbOffset = + a.getDimensionPixelOffset(com.android.internal.R.styleable.SeekBar_thumbOffset, 0); + setThumbOffset(thumbOffset); + a.recycle(); + + a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Theme, 0, 0); + mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f); + a.recycle(); + } + + /** + * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar + * + * @param thumb Drawable representing the thumb + */ + public void setThumb(Drawable thumb) { + if (thumb != null) { + thumb.setCallback(this); + } + mThumb = thumb; + invalidate(); + } + + /** + * @see #setThumbOffset(int) + */ + public int getThumbOffset() { + return mThumbOffset; + } + + /** + * Sets the thumb offset that allows the thumb to extend out of the range of + * the track. + * + * @param thumbOffset The offset amount in pixels. + */ + public void setThumbOffset(int thumbOffset) { + mThumbOffset = thumbOffset; + invalidate(); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mThumb || super.verifyDrawable(who); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + Drawable progressDrawable = getProgressDrawable(); + if (progressDrawable != null) { + progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha)); + } + } + + @Override + void onProgressRefresh(float scale, boolean fromTouch) { + Drawable thumb = mThumb; + if (thumb != null) { + setThumbPos(getWidth(), getHeight(), thumb, scale, Integer.MIN_VALUE); + /* + * Since we draw translated, the drawable's bounds that it signals + * for invalidation won't be the actual bounds we want invalidated, + * so just invalidate this whole view. + */ + invalidate(); + } + } + + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + Drawable d = getCurrentDrawable(); + Drawable thumb = mThumb; + int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); + // The max height does not incorporate padding, whereas the height + // parameter does + int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom); + + int max = getMax(); + float scale = max > 0 ? (float) getProgress() / (float) max : 0; + + if (thumbHeight > trackHeight) { + if (thumb != null) { + setThumbPos(w, h, thumb, scale, 0); + } + int gapForCenteringTrack = (thumbHeight - trackHeight) / 2; + if (d != null) { + // Canvas will be translated by the padding, so 0,0 is where we start drawing + d.setBounds(0, gapForCenteringTrack, + w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack + - mPaddingTop); + } + } else { + if (d != null) { + // Canvas will be translated by the padding, so 0,0 is where we start drawing + d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom + - mPaddingTop); + } + int gap = (trackHeight - thumbHeight) / 2; + if (thumb != null) { + setThumbPos(w, h, thumb, scale, gap); + } + } + } + + /** + * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and + * the old vertical bounds will be used. + */ + private void setThumbPos(int w, int h, Drawable thumb, float scale, int gap) { + int available = w - mPaddingLeft - mPaddingRight; + int thumbWidth = thumb.getIntrinsicWidth(); + int thumbHeight = thumb.getIntrinsicHeight(); + available -= thumbWidth; + + // The extra space for the thumb to move on the track + available += mThumbOffset * 2; + + int thumbPos = (int) (scale * available); + + int topBound, bottomBound; + if (gap == Integer.MIN_VALUE) { + Rect oldBounds = thumb.getBounds(); + topBound = oldBounds.top; + bottomBound = oldBounds.bottom; + } else { + topBound = gap; + bottomBound = gap + thumbHeight; + } + + // Canvas will be translated, so 0,0 is where we start drawing + thumb.setBounds(thumbPos, topBound, thumbPos + thumbWidth, bottomBound); + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mThumb != null) { + canvas.save(); + // Translate the padding. For the x, we need to allow the thumb to + // draw in its extra space + canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop); + mThumb.draw(canvas); + canvas.restore(); + } + } + + @Override + protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Drawable d = getCurrentDrawable(); + + int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight(); + int dw = 0; + int dh = 0; + if (d != null) { + dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); + dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); + dh = Math.max(thumbHeight, dh); + } + dw += mPaddingLeft + mPaddingRight; + dh += mPaddingTop + mPaddingBottom; + + setMeasuredDimension(resolveSize(dw, widthMeasureSpec), + resolveSize(dh, heightMeasureSpec)); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!mIsUserSeekable || !isEnabled()) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + onStartTrackingTouch(); + trackTouchEvent(event); + break; + + case MotionEvent.ACTION_MOVE: + trackTouchEvent(event); + break; + + case MotionEvent.ACTION_UP: + trackTouchEvent(event); + onStopTrackingTouch(); + break; + + case MotionEvent.ACTION_CANCEL: + onStopTrackingTouch(); + break; + } + return true; + } + + private void trackTouchEvent(MotionEvent event) { + final int width = getWidth(); + final int available = width - mPaddingLeft - mPaddingRight; + int x = (int)event.getX(); + float scale; + float progress = 0; + if (x < mPaddingLeft) { + scale = 0.0f; + } else if (x > width - mPaddingRight) { + scale = 1.0f; + } else { + scale = (float)(x - mPaddingLeft) / (float)available; + progress = mTouchProgressOffset; + } + + final int max = getMax(); + progress += scale * max; + if (progress < 0) { + progress = 0; + } else if (progress > max) { + progress = max; + } + + setProgress((int) progress, true); + } + + /** + * This is called when the user has started touching this widget. + */ + void onStartTrackingTouch() { + } + + /** + * This is called when the user either releases his touch or the touch is + * canceled. + */ + void onStopTrackingTouch() { + } + +} diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java new file mode 100644 index 0000000..424a936 --- /dev/null +++ b/core/java/android/widget/AbsSpinner.java @@ -0,0 +1,490 @@ +/* + * 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 android.widget; + +import com.android.internal.R; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; + + +/** + * An abstract base class for spinner widgets. SDK users will probably not + * need to use this class. + * + * @attr ref android.R.styleable#AbsSpinner_entries + */ +public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { + + SpinnerAdapter mAdapter; + + int mHeightMeasureSpec; + int mWidthMeasureSpec; + boolean mBlockLayoutRequests; + int mSelectionLeftPadding = 0; + int mSelectionTopPadding = 0; + int mSelectionRightPadding = 0; + int mSelectionBottomPadding = 0; + Rect mSpinnerPadding = new Rect(); + View mSelectedView = null; + Interpolator mInterpolator; + + RecycleBin mRecycler = new RecycleBin(); + private DataSetObserver mDataSetObserver; + + + /** Temporary frame to hold a child View's frame rectangle */ + private Rect mTouchFrame; + + public AbsSpinner(Context context) { + super(context); + initAbsSpinner(); + } + + public AbsSpinner(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AbsSpinner(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initAbsSpinner(); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AbsSpinner, defStyle, 0); + + CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries); + if (entries != null) { + ArrayAdapter<CharSequence> adapter = + new ArrayAdapter<CharSequence>(context, + R.layout.simple_spinner_item, entries); + adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item); + setAdapter(adapter); + } + + a.recycle(); + } + + /** + * Common code for different constructor flavors + */ + private void initAbsSpinner() { + setFocusable(true); + setWillNotDraw(false); + } + + + /** + * The Adapter is used to provide the data which backs this Spinner. + * It also provides methods to transform spinner items based on their position + * relative to the selected item. + * @param adapter The SpinnerAdapter to use for this Spinner + */ + @Override + public void setAdapter(SpinnerAdapter adapter) { + if (null != mAdapter) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + resetList(); + } + + mAdapter = adapter; + + mOldSelectedPosition = INVALID_POSITION; + mOldSelectedRowId = INVALID_ROW_ID; + + if (mAdapter != null) { + mOldItemCount = mItemCount; + mItemCount = mAdapter.getCount(); + checkFocus(); + + mDataSetObserver = new AdapterDataSetObserver(); + mAdapter.registerDataSetObserver(mDataSetObserver); + + int position = mItemCount > 0 ? 0 : INVALID_POSITION; + + setSelectedPositionInt(position); + setNextSelectedPositionInt(position); + + if (mItemCount == 0) { + // Nothing selected + checkSelectionChanged(); + } + + } else { + checkFocus(); + resetList(); + // Nothing selected + checkSelectionChanged(); + } + + requestLayout(); + } + + /** + * Clear out all children from the list + */ + void resetList() { + mDataChanged = false; + mNeedSync = false; + + removeAllViewsInLayout(); + mOldSelectedPosition = INVALID_POSITION; + mOldSelectedRowId = INVALID_ROW_ID; + + setSelectedPositionInt(INVALID_POSITION); + setNextSelectedPositionInt(INVALID_POSITION); + invalidate(); + } + + /** + * @see android.view.View#measure(int, int) + * + * Figure out the dimensions of this Spinner. The width comes from + * the widthMeasureSpec as Spinnners can't have their width set to + * UNSPECIFIED. The height is based on the height of the selected item + * plus padding. + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize; + int heightSize; + + mSpinnerPadding.left = mPaddingLeft > mSelectionLeftPadding ? mPaddingLeft + : mSelectionLeftPadding; + mSpinnerPadding.top = mPaddingTop > mSelectionTopPadding ? mPaddingTop + : mSelectionTopPadding; + mSpinnerPadding.right = mPaddingRight > mSelectionRightPadding ? mPaddingRight + : mSelectionRightPadding; + mSpinnerPadding.bottom = mPaddingBottom > mSelectionBottomPadding ? mPaddingBottom + : mSelectionBottomPadding; + + if (mDataChanged) { + handleDataChanged(); + } + + int preferredHeight = 0; + int preferredWidth = 0; + boolean needsMeasuring = true; + + int selectedPosition = getSelectedItemPosition(); + if (selectedPosition >= 0 && mAdapter != null) { + // Try looking in the recycler. (Maybe we were measured once already) + View view = mRecycler.get(selectedPosition); + if (view == null) { + // Make a new one + view = mAdapter.getView(selectedPosition, null, this); + } + + if (view != null) { + // Put in recycler for re-measuring and/or layout + mRecycler.put(selectedPosition, view); + } + + if (view != null) { + if (view.getLayoutParams() == null) { + mBlockLayoutRequests = true; + view.setLayoutParams(generateDefaultLayoutParams()); + mBlockLayoutRequests = false; + } + measureChild(view, widthMeasureSpec, heightMeasureSpec); + + preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom; + preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right; + + needsMeasuring = false; + } + } + + if (needsMeasuring) { + // No views -- just use padding + preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom; + if (widthMode == MeasureSpec.UNSPECIFIED) { + preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right; + } + } + + preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight()); + preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth()); + + heightSize = resolveSize(preferredHeight, heightMeasureSpec); + widthSize = resolveSize(preferredWidth, widthMeasureSpec); + + setMeasuredDimension(widthSize, heightSize); + mHeightMeasureSpec = heightMeasureSpec; + mWidthMeasureSpec = widthMeasureSpec; + } + + + int getChildHeight(View child) { + return child.getMeasuredHeight(); + } + + int getChildWidth(View child) { + return child.getMeasuredWidth(); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + void recycleAllViews() { + int childCount = getChildCount(); + final AbsSpinner.RecycleBin recycleBin = mRecycler; + + // All views go in recycler + for (int i=0; i<childCount; i++) { + View v = getChildAt(i); + int index = mFirstPosition + i; + recycleBin.put(index, v); + } + } + + @Override + void handleDataChanged() { + // FIXME -- this is called from both measure and layout. + // This is harmless right now, but we don't want to do redundant work if + // this gets more complicated + super.handleDataChanged(); + } + + + + /** + * Jump directly to a specific item in the adapter data. + */ + public void setSelection(int position, boolean animate) { + // Animate only if requested position is already on screen somewhere + boolean shouldAnimate = animate && mFirstPosition <= position && + position <= mFirstPosition + getChildCount() - 1; + setSelectionInt(position, shouldAnimate); + } + + + @Override + public void setSelection(int position) { + setNextSelectedPositionInt(position); + requestLayout(); + invalidate(); + } + + + /** + * Makes the item at the supplied position selected. + * + * @param position Position to select + * @param animate Should the transition be animated + * + */ + void setSelectionInt(int position, boolean animate) { + if (position != mOldSelectedPosition) { + mBlockLayoutRequests = true; + int delta = position - mSelectedPosition; + setNextSelectedPositionInt(position); + layout(delta, animate); + mBlockLayoutRequests = false; + } + } + + abstract void layout(int delta, boolean animate); + + @Override + public View getSelectedView() { + if (mItemCount > 0 && mSelectedPosition >= 0) { + return getChildAt(mSelectedPosition - mFirstPosition); + } else { + return null; + } + } + + /** + * Override to prevent spamming ourselves with layout requests + * as we place views + * + * @see android.view.View#requestLayout() + */ + @Override + public void requestLayout() { + if (!mBlockLayoutRequests) { + super.requestLayout(); + } + } + + + + @Override + public SpinnerAdapter getAdapter() { + return mAdapter; + } + + @Override + public int getCount() { + return mItemCount; + } + + /** + * 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--) { + View child = getChildAt(i); + if (child.getVisibility() == View.VISIBLE) { + child.getHitRect(frame); + if (frame.contains(x, y)) { + return mFirstPosition + i; + } + } + } + return INVALID_POSITION; + } + + static class SavedState extends BaseSavedState { + long selectedId; + int position; + + /** + * Constructor called from {@link AbsSpinner#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + selectedId = in.readLong(); + position = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeLong(selectedId); + out.writeInt(position); + } + + @Override + public String toString() { + return "AbsSpinner.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " selectedId=" + selectedId + + " position=" + position + "}"; + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.selectedId = getSelectedItemId(); + if (ss.selectedId >= 0) { + ss.position = getSelectedItemPosition(); + } else { + ss.position = INVALID_POSITION; + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + + if (ss.selectedId >= 0) { + mDataChanged = true; + mNeedSync = true; + mSyncRowId = ss.selectedId; + mSyncPosition = ss.position; + mSyncMode = SYNC_SELECTED_POSITION; + requestLayout(); + } + } + + class RecycleBin { + private SparseArray<View> mScrapHeap = new SparseArray<View>(); + + public void put(int position, View v) { + mScrapHeap.put(position, v); + } + + View get(int position) { + // System.out.print("Looking for " + position); + View result = mScrapHeap.get(position); + if (result != null) { + // System.out.println(" HIT"); + mScrapHeap.delete(position); + } else { + // System.out.println(" MISS"); + } + return result; + } + + View peek(int position) { + // System.out.print("Looking for " + position); + return mScrapHeap.get(position); + } + + void clear() { + final SparseArray<View> scrapHeap = mScrapHeap; + final int count = scrapHeap.size(); + for (int i = 0; i < count; i++) { + final View view = scrapHeap.valueAt(i); + if (view != null) { + removeDetachedView(view, true); + } + } + scrapHeap.clear(); + } + } +} diff --git a/core/java/android/widget/AbsoluteLayout.java b/core/java/android/widget/AbsoluteLayout.java new file mode 100644 index 0000000..36a3b10 --- /dev/null +++ b/core/java/android/widget/AbsoluteLayout.java @@ -0,0 +1,216 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RemoteViews.RemoteView; + + +/** + * A layout that lets you specify exact locations (x/y coordinates) of its + * children. Absolute layouts are less flexible and harder to maintain than + * other types of layouts without absolute positioning. + * + * <p><strong>XML attributes</strong></p> <p> See {@link + * android.R.styleable#ViewGroup ViewGroup Attributes}, {@link + * android.R.styleable#View View Attributes}</p> + */ +@RemoteView +public class AbsoluteLayout extends ViewGroup { + public AbsoluteLayout(Context context) { + super(context); + } + + public AbsoluteLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AbsoluteLayout(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int count = getChildCount(); + + int maxHeight = 0; + int maxWidth = 0; + + // Find out how big everyone wants to be + measureChildren(widthMeasureSpec, heightMeasureSpec); + + // Find rightmost and bottom-most child + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + int childRight; + int childBottom; + + AbsoluteLayout.LayoutParams lp + = (AbsoluteLayout.LayoutParams) child.getLayoutParams(); + + childRight = lp.x + child.getMeasuredWidth(); + childBottom = lp.y + child.getMeasuredHeight(); + + maxWidth = Math.max(maxWidth, childRight); + maxHeight = Math.max(maxHeight, childBottom); + } + } + + // Account for padding too + maxWidth += mPaddingLeft + mPaddingRight; + maxHeight += mPaddingTop + mPaddingBottom; + + // Check against minimum height and width + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), + resolveSize(maxHeight, heightMeasureSpec)); + } + + /** + * Returns a set of layout parameters with a width of + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}, + * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * and with the coordinates (0, 0). + */ + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0); + } + + @Override + protected void onLayout(boolean changed, int l, int t, + int r, int b) { + int count = getChildCount(); + + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + + AbsoluteLayout.LayoutParams lp = + (AbsoluteLayout.LayoutParams) child.getLayoutParams(); + + int childLeft = mPaddingLeft + lp.x; + int childTop = mPaddingTop + lp.y; + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + + } + } + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new AbsoluteLayout.LayoutParams(getContext(), attrs); + } + + // Override to allow type-checking of LayoutParams. + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof AbsoluteLayout.LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + /** + * Per-child layout information associated with AbsoluteLayout. + * See + * {@link android.R.styleable#AbsoluteLayout_Layout Absolute Layout Attributes} + * for a list of all child view attributes that this class supports. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * The horizontal, or X, location of the child within the view group. + */ + public int x; + /** + * The vertical, or Y, location of the child within the view group. + */ + public int y; + + /** + * Creates a new set of layout parameters with the specified width, + * height and location. + * + * @param width the width, either {@link #FILL_PARENT}, + {@link #WRAP_CONTENT} or a fixed size in pixels + * @param height the height, either {@link #FILL_PARENT}, + {@link #WRAP_CONTENT} or a fixed size in pixels + * @param x the X location of the child + * @param y the Y location of the child + */ + public LayoutParams(int width, int height, int x, int y) { + super(width, height); + this.x = x; + this.y = y; + } + + /** + * Creates a new set of layout parameters. The values are extracted from + * the supplied attributes set and context. The XML attributes mapped + * to this set of layout parameters are: + * + * <ul> + * <li><code>layout_x</code>: the X location of the child</li> + * <li><code>layout_y</code>: the Y location of the child</li> + * <li>All the XML attributes from + * {@link android.view.ViewGroup.LayoutParams}</li> + * </ul> + * + * @param c the application environment + * @param attrs the set of attributes fom which to extract the layout + * parameters values + */ + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + TypedArray a = c.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AbsoluteLayout_Layout); + x = a.getDimensionPixelOffset( + com.android.internal.R.styleable.AbsoluteLayout_Layout_layout_x, 0); + y = a.getDimensionPixelOffset( + com.android.internal.R.styleable.AbsoluteLayout_Layout_layout_y, 0); + a.recycle(); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + @Override + public String debug(String output) { + return output + "Absolute.LayoutParams={width=" + + sizeToString(width) + ", height=" + sizeToString(height) + + " x=" + x + " y=" + y + "}"; + } + } +} + + diff --git a/core/java/android/widget/Adapter.java b/core/java/android/widget/Adapter.java new file mode 100644 index 0000000..e952dd5 --- /dev/null +++ b/core/java/android/widget/Adapter.java @@ -0,0 +1,149 @@ +/* + * 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 android.widget; + +import android.database.DataSetObserver; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +/** + * An Adapter object acts as a bridge between an {@link AdapterView} and the + * underlying data for that view. The Adapter provides access to the data items. + * The Adapter is also responsible for making a {@link android.view.View} for + * each item in the data set. + * + * @see android.widget.ArrayAdapter + * @see android.widget.CursorAdapter + * @see android.widget.SimpleCursorAdapter + */ +public interface Adapter { + /** + * Register an observer that is called when changes happen to the data used by this adapter. + * + * @param observer the object that gets notified when the data set changes. + */ + void registerDataSetObserver(DataSetObserver observer); + + /** + * Unregister an observer that has previously been registered with this + * adapter via {@link #registerDataSetObserver}. + * + * @param observer the object to unregister. + */ + void unregisterDataSetObserver(DataSetObserver observer); + + /** + * How many items are in the data set represented by this Adapter. + * + * @return Count of items. + */ + int getCount(); + + /** + * Get the data item associated with the specified position in the data set. + * + * @param position Position of the item whose data we want within the adapter's + * data set. + * @return The data at the specified position. + */ + Object getItem(int position); + + /** + * Get the row id associated with the specified position in the list. + * + * @param position The position of the item within the adapter's data set whose row id we want. + * @return The id of the item at the specified position. + */ + long getItemId(int position); + + /** + * Indicated whether the item ids are stable across changes to the + * underlying data. + * + * @return True if the same id always refers to the same object. + */ + boolean hasStableIds(); + + /** + * Get a View that displays the data at the specified position in the data set. You can either + * create a View manually or inflate it from an XML layout file. When the View is inflated, the + * parent View (GridView, ListView...) will apply default layout parameters unless you use + * {@link android.view.LayoutInflater#inflate(int, android.view.ViewGroup, boolean)} + * to specify a root view and to prevent attachment to the root. + * + * @param position The position of the item within the adapter's data set of the item whose view + * we want. + * @param convertView The old view to reuse, if possible. Note: You should check that this view + * is non-null and of an appropriate type before using. If it is not possible to convert + * this view to display the correct data, this method can create a new view. + * @param parent The parent that this view will eventually be attached to + * @return A View corresponding to the data at the specified position. + */ + View getView(int position, View convertView, ViewGroup parent); + + /** + * An item view type that causes the {@link AdapterView} to ignore the item + * view. For example, this can be used if the client does not want a + * particular view to be given for conversion in + * {@link #getView(int, View, ViewGroup)}. + * + * @see #getItemViewType(int) + * @see #getViewTypeCount() + */ + static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE; + + /** + * Get the type of View that will be created by {@link #getView} for the specified item. + * + * @param position The position of the item within the adapter's data set whose view type we + * want. + * @return An integer representing the type of View. Two views should share the same type if one + * can be converted to the other in {@link #getView}. Note: Integers must be in the + * range 0 to {@link #getViewTypeCount} - 1. {@link #IGNORE_ITEM_VIEW_TYPE} can + * also be returned. + * @see IGNORE_ITEM_VIEW_TYPE + */ + int getItemViewType(int position); + + /** + * <p> + * Returns the number of types of Views that will be created by + * {@link #getView}. Each type represents a set of views that can be + * converted in {@link #getView}. If the adapter always returns the same + * type of View for all items, this method should return 1. + * </p> + * <p> + * This method will only be called when when the adapter is set on the + * the {@link AdapterView}. + * </p> + * + * @return The number of types of Views that will be created by this adapter + */ + int getViewTypeCount(); + + static final int NO_SELECTION = Integer.MIN_VALUE; + + /** + * @return true if this adapter doesn't contain any data. This is used to determine + * whether the empty view should be displayed. A typical implementation will return + * getCount() == 0 but since getCount() includes the headers and footers, specialized + * adapters might want a different behavior. + */ + boolean isEmpty(); +} + diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java new file mode 100644 index 0000000..e096612 --- /dev/null +++ b/core/java/android/widget/AdapterView.java @@ -0,0 +1,1094 @@ +/* + * 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 android.widget; + +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.View; +import android.view.ViewGroup; +import android.view.ViewDebug; +import android.view.SoundEffectConstants; +import android.view.ContextMenu.ContextMenuInfo; + + +/** + * An AdapterView is a view whose children are determined by an {@link Adapter}. + * + * <p> + * See {@link ListView}, {@link GridView}, {@link Spinner} and + * {@link Gallery} for commonly used subclasses of AdapterView. + */ +public abstract class AdapterView<T extends Adapter> 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. + */ + 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 AdapterView(Context context) { + super(context); + } + + public AdapterView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public 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. + * <p> + * 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(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(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(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(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. + */ + public int getSelectedItemPosition() { + return mNextSelectedPosition; + } + + /** + * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID} + * if nothing is selected. + */ + 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.) + */ + 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 + * @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, mLeft, mTop, mRight, mBottom); + } + } 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<Parcelable> container) { + dispatchFreezeSelfOnly(container); + } + + /** + * Override to prevent thawing of any views created by the adapter. + */ + @Override + protected void dispatchRestoreInstanceState(SparseArray<Parcelable> 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 (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null + && mOldItemCount == 0 && mItemCount > 0) { + AdapterView.this.onRestoreInstanceState(mInstanceState); + mInstanceState = null; + } else { + rememberSyncState(); + } + checkFocus(); + requestLayout(); + } + + @Override + public void onInvalidated() { + mDataChanged = true; + + if (AdapterView.this.getAdapter().hasStableIds()) { + // Remember the current state for the case where our hosting activity is being + // stopped and later restarted + mInstanceState = 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(); + } + } + } + + 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 + 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/core/java/android/widget/AnalogClock.java b/core/java/android/widget/AnalogClock.java new file mode 100644 index 0000000..808104e --- /dev/null +++ b/core/java/android/widget/AnalogClock.java @@ -0,0 +1,241 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.View; +import android.pim.Time; + +import java.util.TimeZone; + +/** + * This widget display an analogic clock with two hands for hours and + * minutes. + */ +public class AnalogClock extends View { + private Time mCalendar; + + private Drawable mHourHand; + private Drawable mMinuteHand; + private Drawable mDial; + + private int mDialWidth; + private int mDialHeight; + + private boolean mAttached; + private long mLastTime; + + private final Handler mHandler = new Handler(); + private float mMinutes; + private float mHour; + private boolean mChanged; + + public AnalogClock(Context context) { + this(context, null); + } + + public AnalogClock(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AnalogClock(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + Resources r = mContext.getResources(); + TypedArray a = + context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.AnalogClock, defStyle, 0); + + mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial); + if (mDial == null) { + mDial = r.getDrawable(com.android.internal.R.drawable.clock_dial); + } + + mHourHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_hour); + if (mHourHand == null) { + mHourHand = r.getDrawable(com.android.internal.R.drawable.clock_hand_hour); + } + + mMinuteHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_minute); + if (mMinuteHand == null) { + mMinuteHand = r.getDrawable(com.android.internal.R.drawable.clock_hand_minute); + } + + mCalendar = new Time(); + + mDialWidth = mDial.getIntrinsicWidth(); + mDialHeight = mDial.getIntrinsicHeight(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + onTimeChanged(); + if (!mAttached) { + mAttached = true; + IntentFilter filter = new IntentFilter(); + + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + + getContext().registerReceiver(mIntentReceiver, filter, null, mHandler); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mAttached) { + getContext().unregisterReceiver(mIntentReceiver); + mAttached = false; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + float hScale = 1.0f; + float vScale = 1.0f; + + if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) { + hScale = (float) widthSize / (float) mDialWidth; + } + + if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) { + vScale = (float )heightSize / (float) mDialHeight; + } + + float scale = Math.min(hScale, vScale); + + setMeasuredDimension(resolveSize((int) (mDialWidth * scale), widthMeasureSpec), + resolveSize((int) (mDialHeight * scale), heightMeasureSpec)); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mChanged = true; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + boolean changed = mChanged; + if (changed) { + mChanged = false; + } + + int availableWidth = mRight - mLeft; + int availableHeight = mBottom - mTop; + + int x = availableWidth / 2; + int y = availableHeight / 2; + + final Drawable dial = mDial; + int w = dial.getIntrinsicWidth(); + int h = dial.getIntrinsicHeight(); + + boolean scaled = false; + + if (availableWidth < w || availableHeight < h) { + scaled = true; + float scale = Math.min((float) availableWidth / (float) w, + (float) availableHeight / (float) h); + canvas.save(); + canvas.scale(scale, scale, x, y); + } + + if (changed) { + dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); + } + dial.draw(canvas); + + canvas.save(); + canvas.rotate(mHour / 12.0f * 360.0f, x, y); + final Drawable hourHand = mHourHand; + if (changed) { + w = hourHand.getIntrinsicWidth(); + h = hourHand.getIntrinsicHeight(); + hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); + } + hourHand.draw(canvas); + canvas.restore(); + + canvas.save(); + canvas.rotate(mMinutes / 60.0f * 360.0f, x, y); + + final Drawable minuteHand = mMinuteHand; + if (changed) { + w = minuteHand.getIntrinsicWidth(); + h = minuteHand.getIntrinsicHeight(); + minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); + } + minuteHand.draw(canvas); + canvas.restore(); + + if (scaled) { + canvas.restore(); + } + } + + private void onTimeChanged() { + long time = System.currentTimeMillis(); + mCalendar.set(time); + mLastTime = time; + + int hour = mCalendar.hour; + int minute = mCalendar.minute; + int second = mCalendar.second; + + mMinutes = minute + second / 60.0f; + mHour = hour + mMinutes / 60.0f; + mChanged = true; + } + + private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) { + String tz = intent.getStringExtra("time-zone"); + mCalendar = new Time(TimeZone.getTimeZone(tz).getID()); + } else { + mCalendar = new Time(); + } + + onTimeChanged(); + + invalidate(); + } + }; +} diff --git a/core/java/android/widget/AppSecurityPermissions.java b/core/java/android/widget/AppSecurityPermissions.java new file mode 100755 index 0000000..582117f --- /dev/null +++ b/core/java/android/widget/AppSecurityPermissions.java @@ -0,0 +1,383 @@ +/* +** +** Copyright 2007, 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 android.widget; + +import com.android.internal.R; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageParser; +import android.content.pm.PermissionGroupInfo; +import android.content.pm.PermissionInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Set; + +/** + * This class contains the SecurityPermissions view implementation. + * Initially the package's advanced or dangerous security permissions + * are displayed under categorized + * groups. Clicking on the additional permissions presents + * extended information consisting of all groups and permissions. + * To use this view define a LinearLayout or any ViewGroup and add this + * view by instantiating AppSecurityPermissions and invoking getPermissionsView. + * + * {@hide} + */ +public class AppSecurityPermissions implements View.OnClickListener { + + private enum State { + NO_PERMS, + DANGEROUS_ONLY, + NORMAL_ONLY, + BOTH + } + + private final String TAG = "AppSecurityPermissions"; + private boolean localLOGV = false; + private Context mContext; + private LayoutInflater mInflater; + private PackageManager mPm; + private LinearLayout mPermsView; + private HashMap<String, String> mDangerousMap; + private HashMap<String, String> mNormalMap; + private ArrayList<PermissionInfo> mPermsList; + private String mDefaultGrpLabel; + private String mDefaultGrpName="DefaultGrp"; + private String mPermFormat; + private Drawable mNormalIcon; + private Drawable mDangerousIcon; + private boolean mExpanded; + private Drawable mShowMaxIcon; + private Drawable mShowMinIcon; + private View mShowMore; + private TextView mShowMoreText; + private ImageView mShowMoreIcon; + private State mCurrentState; + private LinearLayout mNonDangerousList; + private LinearLayout mDangerousList; + private HashMap<String, String> mGroupLabelCache; + private View mNoPermsView; + + public AppSecurityPermissions(Context context) { + this(context, null); + } + + public AppSecurityPermissions(Context context, ArrayList<PermissionInfo> permList) { + mContext = context; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mPm = context.getPackageManager(); + mPermsList = permList; + mPermsView = (LinearLayout) mInflater.inflate(R.layout.app_perms_summary, null); + mShowMore = mPermsView.findViewById(R.id.show_more); + mShowMoreIcon = (ImageView) mShowMore.findViewById(R.id.show_more_icon); + mShowMoreText = (TextView) mShowMore.findViewById(R.id.show_more_text); + mDangerousList = (LinearLayout) mPermsView.findViewById(R.id.dangerous_perms_list); + mNonDangerousList = (LinearLayout) mPermsView.findViewById(R.id.non_dangerous_perms_list); + mNoPermsView = mPermsView.findViewById(R.id.no_permissions); + + // Set up the LinearLayout that acts like a list item. + mShowMore.setClickable(true); + mShowMore.setOnClickListener(this); + mShowMore.setFocusable(true); + mShowMore.setBackgroundResource(android.R.drawable.list_selector_background); + + // Pick up from framework resources instead. + mDefaultGrpLabel = mContext.getString(R.string.default_permission_group); + mPermFormat = mContext.getString(R.string.permissions_format); + mNormalIcon = mContext.getResources().getDrawable(R.drawable.ic_text_dot); + mDangerousIcon = mContext.getResources().getDrawable(R.drawable.ic_bullet_key_permission); + mShowMaxIcon = mContext.getResources().getDrawable(R.drawable.expander_ic_maximized); + mShowMinIcon = mContext.getResources().getDrawable(R.drawable.expander_ic_minimized); + } + + public void setSecurityPermissionsView() { + setPermissions(mPermsList); + } + + public void setSecurityPermissionsView(Uri pkgURI) { + final String archiveFilePath = pkgURI.getPath(); + PackageParser packageParser = new PackageParser(archiveFilePath); + File sourceFile = new File(archiveFilePath); + DisplayMetrics metrics = new DisplayMetrics(); + metrics.setToDefaults(); + PackageParser.Package pkgInfo = packageParser.parsePackage(sourceFile, + archiveFilePath, metrics, 0); + mPermsList = generatePermissionsInfo(pkgInfo.requestedPermissions); + //For packages that havent been installed we need the application info object + //to load the labels and other resources. + setPermissions(mPermsList, pkgInfo.applicationInfo); + } + + public void setSecurityPermissionsView(PackageInfo pInfo) { + mPermsList = generatePermissionsInfo(pInfo.requestedPermissions); + setPermissions(mPermsList); + } + + public View getPermissionsView() { + return mPermsView; + } + + /** + * Canonicalizes the group description before it is displayed to the user. + * + * TODO check for internationalization issues remove trailing '.' in str1 + */ + private String canonicalizeGroupDesc(String groupDesc) { + if ((groupDesc == null) || (groupDesc.length() == 0)) { + return null; + } + // Both str1 and str2 are non-null and are non-zero in size. + int len = groupDesc.length(); + if(groupDesc.charAt(len-1) == '.') { + groupDesc = groupDesc.substring(0, len-1); + } + return groupDesc; + } + + /** + * Utility method that concatenates two strings defined by mPermFormat. + * a null value is returned if both str1 and str2 are null, if one of the strings + * is null the other non null value is returned without formatting + * this is to placate initial error checks + */ + private String formatPermissions(String groupDesc, String permDesc) { + if(groupDesc == null) { + return permDesc; + } + groupDesc = canonicalizeGroupDesc(groupDesc); + if(permDesc == null) { + return groupDesc; + } + return String.format(mPermFormat, groupDesc, permDesc); + } + + /** + * Utility method that concatenates two strings defined by mPermFormat. + */ + private String formatPermissions(String groupDesc, CharSequence permDesc) { + groupDesc = canonicalizeGroupDesc(groupDesc); + if(permDesc == null) { + return groupDesc; + } + // Format only if str1 and str2 are not null. + return formatPermissions(groupDesc, permDesc.toString()); + } + + private ArrayList<PermissionInfo> generatePermissionsInfo(String[] strList) { + ArrayList<PermissionInfo> permInfoList = new ArrayList<PermissionInfo>(); + if(strList == null) { + return permInfoList; + } + PermissionInfo tmpPermInfo = null; + for(int i = 0; i < strList.length; i++) { + try { + tmpPermInfo = mPm.getPermissionInfo(strList[i], 0); + permInfoList.add(tmpPermInfo); + } catch (NameNotFoundException e) { + Log.i(TAG, "Ignoring unknown permisison:"+strList[i]); + continue; + } + } + return permInfoList; + } + + private ArrayList<PermissionInfo> generatePermissionsInfo(ArrayList<String> strList) { + ArrayList<PermissionInfo> permInfoList = new ArrayList<PermissionInfo>(); + if(strList != null) { + PermissionInfo tmpPermInfo = null; + for(String permName:strList) { + try { + tmpPermInfo = mPm.getPermissionInfo(permName, 0); + permInfoList.add(tmpPermInfo); + } catch (NameNotFoundException e) { + Log.i(TAG, "Ignoring unknown permisison:"+permName); + continue; + } + } + } + return permInfoList; + } + + private String getGroupLabel(String grpName) { + if (grpName == null) { + //return default label + return mDefaultGrpLabel; + } + String cachedLabel = mGroupLabelCache.get(grpName); + if (cachedLabel != null) { + return cachedLabel; + } + + PermissionGroupInfo pgi; + try { + pgi = mPm.getPermissionGroupInfo(grpName, 0); + } catch (NameNotFoundException e) { + Log.i(TAG, "Invalid group name:" + grpName); + return null; + } + String label = pgi.loadLabel(mPm).toString(); + mGroupLabelCache.put(grpName, label); + return label; + } + + /** + * Utility method that displays permissions from a map containing group name and + * list of permission descriptions. + */ + private void displayPermissions(boolean dangerous) { + HashMap<String, String> permInfoMap = dangerous ? mDangerousMap : mNormalMap; + LinearLayout permListView = dangerous ? mDangerousList : mNonDangerousList; + permListView.removeAllViews(); + + Set<String> permInfoStrSet = permInfoMap.keySet(); + for (String loopPermGrpInfoStr : permInfoStrSet) { + String grpLabel = getGroupLabel(loopPermGrpInfoStr); + //guaranteed that grpLabel wont be null since permissions without groups + //will belong to the default group + if(localLOGV) Log.i(TAG, "Adding view group:" + grpLabel + ", desc:" + + permInfoMap.get(loopPermGrpInfoStr)); + permListView.addView(getPermissionItemView(grpLabel, + permInfoMap.get(loopPermGrpInfoStr), dangerous)); + } + } + + private void displayNoPermissions() { + mNoPermsView.setVisibility(View.VISIBLE); + } + + private View getPermissionItemView(String grpName, String permList, + boolean dangerous) { + View permView = mInflater.inflate(R.layout.app_permission_item, null); + Drawable icon = dangerous ? mDangerousIcon : mNormalIcon; + int grpColor = dangerous ? R.color.perms_dangerous_grp_color : + R.color.perms_normal_grp_color; + int permColor = dangerous ? R.color.perms_dangerous_perm_color : + R.color.perms_normal_perm_color; + + TextView permGrpView = (TextView) permView.findViewById(R.id.permission_group); + TextView permDescView = (TextView) permView.findViewById(R.id.permission_list); + permGrpView.setTextColor(mContext.getResources().getColor(grpColor)); + permDescView.setTextColor(mContext.getResources().getColor(permColor)); + + ImageView imgView = (ImageView)permView.findViewById(R.id.perm_icon); + imgView.setImageDrawable(icon); + if(grpName != null) { + permGrpView.setText(grpName); + permDescView.setText(permList); + } else { + permGrpView.setText(permList); + permDescView.setVisibility(View.GONE); + } + return permView; + } + + private void showPermissions() { + + switch(mCurrentState) { + case NO_PERMS: + displayNoPermissions(); + break; + + case DANGEROUS_ONLY: + displayPermissions(true); + break; + + case NORMAL_ONLY: + displayPermissions(false); + break; + + case BOTH: + displayPermissions(true); + if (mExpanded) { + displayPermissions(false); + mShowMoreIcon.setImageDrawable(mShowMaxIcon); + mShowMoreText.setText(R.string.perms_hide); + mNonDangerousList.setVisibility(View.VISIBLE); + } else { + mShowMoreIcon.setImageDrawable(mShowMinIcon); + mShowMoreText.setText(R.string.perms_show_all); + mNonDangerousList.setVisibility(View.GONE); + } + mShowMore.setVisibility(View.VISIBLE); + break; + } + } + + private boolean isDisplayablePermission(PermissionInfo pInfo) { + if(pInfo.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS || + pInfo.protectionLevel == PermissionInfo.PROTECTION_NORMAL) { + return true; + } + return false; + } + + private void setPermissions(ArrayList<PermissionInfo> permList) { + setPermissions(permList, null); + } + + private void setPermissions(ArrayList<PermissionInfo> permList, ApplicationInfo appInfo) { + mDangerousMap = new HashMap<String, String>(); + mNormalMap = new HashMap<String, String>(); + mGroupLabelCache = new HashMap<String, String>(); + //add the default label so that uncategorized permissions can go here + mGroupLabelCache.put(mDefaultGrpName, mDefaultGrpLabel); + if (permList != null) { + for (PermissionInfo pInfo : permList) { + if(!isDisplayablePermission(pInfo)) { + continue; + } + String grpName = (pInfo.group == null) ? mDefaultGrpName : pInfo.group; + HashMap<String, String> permInfoMap = + (pInfo.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS) ? + mDangerousMap : mNormalMap; + // Check to make sure we have a label for the group + if (getGroupLabel(grpName) == null) { + continue; + } + CharSequence permDesc = pInfo.loadLabel(mPm); + String grpDesc = permInfoMap.get(grpName); + permInfoMap.put(grpName, formatPermissions(grpDesc, permDesc)); + if(localLOGV) Log.i(TAG, pInfo.name + " : " + permDesc+" : " + grpName); + } + } + + mCurrentState = State.NO_PERMS; + if(mDangerousMap.size() > 0) { + mCurrentState = (mNormalMap.size() > 0) ? State.BOTH : State.DANGEROUS_ONLY; + } else if(mNormalMap.size() > 0) { + mCurrentState = State.NORMAL_ONLY; + } + if(localLOGV) Log.i(TAG, "mCurrentState=" + mCurrentState); + showPermissions(); + } + + public void onClick(View v) { + if(localLOGV) Log.i(TAG, "mExpanded="+mExpanded); + mExpanded = !mExpanded; + showPermissions(); + } +} diff --git a/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java new file mode 100644 index 0000000..fe50a01 --- /dev/null +++ b/core/java/android/widget/ArrayAdapter.java @@ -0,0 +1,455 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A ListAdapter that manages a ListView backed by an array of arbitrary + * objects. By default this class expects that the provided resource id referecnes + * a single TextView. If you want to use a more complex layout, use the constructors that + * also takes a field id. That field id should reference a TextView in the larger layout + * resource. + * + * However the TextView is referenced, it will be filled with the toString() of each object in + * the array. You can add lists or arrays of custom objects. Override the toString() method + * of your objects to determine what text will be displayed for the item in the list. + * + * To use something other than TextViews for the array display, for instance, ImageViews, + * or to have some of data besides toString() results fill the views, + * override {@link #getView(int, View, ViewGroup)} to return the type of view you want. + */ +public class ArrayAdapter<T> extends BaseAdapter implements Filterable { + /** + * Contains the list of objects that represent the data of this ArrayAdapter. + * The content of this list is referred to as "the array" in the documentation. + */ + private List<T> mObjects; + + /** + * Lock used to modify the content of {@link #mObjects}. Any write operation + * performed on the array should be synchronized on this lock. This lock is also + * used by the filter (see {@link #getFilter()} to make a synchronized copy of + * the original array of data. + */ + private final Object mLock = new Object(); + + /** + * The resource indicating what views to inflate to display the content of this + * array adapter. + */ + private int mResource; + + /** + * The resource indicating what views to inflate to display the content of this + * array adapter in a drop down widget. + */ + private int mDropDownResource; + + /** + * If the inflated resource is not a TextView, {@link #mFieldId} is used to find + * a TextView inside the inflated views hierarchy. This field must contain the + * identifier that matches the one defined in the resource file. + */ + private int mFieldId = 0; + + /** + * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever + * {@link #mObjects} is modified. + */ + private boolean mNotifyOnChange = true; + + private Context mContext; + + private ArrayList<T> mOriginalValues; + private ArrayFilter mFilter; + + private LayoutInflater mInflater; + + /** + * Constructor + * + * @param context The current context. + * @param textViewResourceId The resource ID for a layout file containing a TextView to use when + * instantiating views. + */ + public ArrayAdapter(Context context, int textViewResourceId) { + init(context, textViewResourceId, 0, new ArrayList<T>()); + } + + /** + * Constructor + * + * @param context The current context. + * @param resource The resource ID for a layout file containing a layout to use when + * instantiating views. + * @param textViewResourceId The id of the TextView within the layout resource to be populated + */ + public ArrayAdapter(Context context, int resource, int textViewResourceId) { + init(context, resource, textViewResourceId, new ArrayList<T>()); + } + + /** + * Constructor + * + * @param context The current context. + * @param textViewResourceId The resource ID for a layout file containing a TextView to use when + * instantiating views. + * @param objects The objects to represent in the ListView. + */ + public ArrayAdapter(Context context, int textViewResourceId, T[] objects) { + init(context, textViewResourceId, 0, Arrays.asList(objects)); + } + + /** + * Constructor + * + * @param context The current context. + * @param resource The resource ID for a layout file containing a layout to use when + * instantiating views. + * @param textViewResourceId The id of the TextView within the layout resource to be populated + * @param objects The objects to represent in the ListView. + */ + public ArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects) { + init(context, resource, textViewResourceId, Arrays.asList(objects)); + } + + /** + * Constructor + * + * @param context The current context. + * @param textViewResourceId The resource ID for a layout file containing a TextView to use when + * instantiating views. + * @param objects The objects to represent in the ListView. + */ + public ArrayAdapter(Context context, int textViewResourceId, List<T> objects) { + init(context, textViewResourceId, 0, objects); + } + + /** + * Constructor + * + * @param context The current context. + * @param resource The resource ID for a layout file containing a layout to use when + * instantiating views. + * @param textViewResourceId The id of the TextView within the layout resource to be populated + * @param objects The objects to represent in the ListView. + */ + public ArrayAdapter(Context context, int resource, int textViewResourceId, List<T> objects) { + init(context, resource, textViewResourceId, objects); + } + + /** + * Adds the specified object at the end of the array. + * + * @param object The object to add at the end of the array. + */ + public void add(T object) { + if (mOriginalValues != null) { + synchronized (mLock) { + mOriginalValues.add(object); + if (mNotifyOnChange) notifyDataSetChanged(); + } + } else { + mObjects.add(object); + if (mNotifyOnChange) notifyDataSetChanged(); + } + } + + /** + * Inserts the spcified object at the specified index in the array. + * + * @param object The object to insert into the array. + * @param index The index at which the object must be inserted. + */ + public void insert(T object, int index) { + if (mOriginalValues != null) { + synchronized (mLock) { + mOriginalValues.add(index, object); + if (mNotifyOnChange) notifyDataSetChanged(); + } + } else { + mObjects.add(index, object); + if (mNotifyOnChange) notifyDataSetChanged(); + } + } + + /** + * Removes the specified object from the array. + * + * @param object The object to remove. + */ + public void remove(T object) { + if (mOriginalValues != null) { + synchronized (mLock) { + mOriginalValues.remove(object); + } + } else { + mObjects.remove(object); + } + if (mNotifyOnChange) notifyDataSetChanged(); + } + + /** + * Remove all elements from the list. + */ + public void clear() { + if (mOriginalValues != null) { + synchronized (mLock) { + mOriginalValues.clear(); + } + } else { + mObjects.clear(); + } + if (mNotifyOnChange) notifyDataSetChanged(); + } + + /** + * {@inheritDoc} + */ + @Override + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + mNotifyOnChange = true; + } + + /** + * Control whether methods that change the list ({@link #add}, + * {@link #insert}, {@link #remove}, {@link #clear}) automatically call + * {@link #notifyDataSetChanged}. If set to false, caller must + * manually call notifyDataSetChanged() to have the changes + * reflected in the attached view. + * + * The default is true, and calling notifyDataSetChanged() + * resets the flag to true. + * + * @param notifyOnChange if true, modifications to the list will + * automatically call {@link + * #notifyDataSetChanged} + */ + public void setNotifyOnChange(boolean notifyOnChange) { + mNotifyOnChange = notifyOnChange; + } + + private void init(Context context, int resource, int textViewResourceId, List<T> objects) { + mContext = context; + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mResource = mDropDownResource = resource; + mObjects = objects; + mFieldId = textViewResourceId; + } + + /** + * Returns the context associated with this array adapter. The context is used + * to create views from the resource passed to the constructor. + * + * @return The Context associated with this adapter. + */ + public Context getContext() { + return mContext; + } + + /** + * {@inheritDoc} + */ + public int getCount() { + return mObjects.size(); + } + + /** + * {@inheritDoc} + */ + public T getItem(int position) { + return mObjects.get(position); + } + + /** + * Returns the position of the specified item in the array. + * + * @param item The item to retrieve the position of. + * + * @return The position of the specified item. + */ + public int getPosition(T item) { + return mObjects.indexOf(item); + } + + /** + * {@inheritDoc} + */ + public long getItemId(int position) { + return position; + } + + /** + * {@inheritDoc} + */ + public View getView(int position, View convertView, ViewGroup parent) { + return createViewFromResource(position, convertView, parent, mResource); + } + + private View createViewFromResource(int position, View convertView, ViewGroup parent, + int resource) { + View view; + TextView text; + + if (convertView == null) { + view = mInflater.inflate(resource, parent, false); + } else { + view = convertView; + } + + try { + if (mFieldId == 0) { + // If no custom field is assigned, assume the whole resource is a TextView + text = (TextView) view; + } else { + // Otherwise, find the TextView field within the layout + text = (TextView) view.findViewById(mFieldId); + } + } catch (ClassCastException e) { + Log.e("ArrayAdapter", "You must supply a resource ID for a TextView"); + throw new IllegalStateException( + "ArrayAdapter requires the resource ID to be a TextView", e); + } + + text.setText(getItem(position).toString()); + + return view; + } + + /** + * <p>Sets the layout resource to create the drop down views.</p> + * + * @param resource the layout resource defining the drop down views + * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) + */ + public void setDropDownViewResource(int resource) { + this.mDropDownResource = resource; + } + + /** + * {@inheritDoc} + */ + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return createViewFromResource(position, convertView, parent, mDropDownResource); + } + + /** + * Creates a new ArrayAdapter from external resources. The content of the array is + * obtained through {@link android.content.res.Resources#getTextArray(int)}. + * + * @param context The application's environment. + * @param textArrayResId The identifier of the array to use as the data source. + * @param textViewResId The identifier of the layout used to create views. + * + * @return An ArrayAdapter<CharSequence>. + */ + public static ArrayAdapter<CharSequence> createFromResource(Context context, + int textArrayResId, int textViewResId) { + CharSequence[] strings = context.getResources().getTextArray(textArrayResId); + return new ArrayAdapter<CharSequence>(context, textViewResId, strings); + } + + /** + * {@inheritDoc} + */ + public Filter getFilter() { + if (mFilter == null) { + mFilter = new ArrayFilter(); + } + return mFilter; + } + + /** + * <p>An array filters constrains the content of the array adapter with + * a prefix. Each item that does not start with the supplied prefix + * is removed from the list.</p> + */ + private class ArrayFilter extends Filter { + @Override + protected FilterResults performFiltering(CharSequence prefix) { + FilterResults results = new FilterResults(); + + if (mOriginalValues == null) { + synchronized (mLock) { + mOriginalValues = new ArrayList<T>(mObjects); + } + } + + if (prefix == null || prefix.length() == 0) { + synchronized (mLock) { + ArrayList<T> list = new ArrayList<T>(mOriginalValues); + results.values = list; + results.count = list.size(); + } + } else { + String prefixString = prefix.toString().toLowerCase(); + + final ArrayList<T> values = mOriginalValues; + final int count = values.size(); + + final ArrayList<T> newValues = new ArrayList<T>(count); + + for (int i = 0; i < count; i++) { + final T value = values.get(i); + final String valueText = value.toString().toLowerCase(); + + // First match against the whole, non-splitted value + if (valueText.startsWith(prefixString)) { + newValues.add(value); + } else { + final String[] words = valueText.split(" "); + final int wordCount = words.length; + + for (int k = 0; k < wordCount; k++) { + if (words[k].startsWith(prefixString)) { + newValues.add(value); + break; + } + } + } + } + + results.values = newValues; + results.count = newValues.size(); + } + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + //noinspection unchecked + mObjects = (List<T>) results.values; + if (results.count > 0) { + notifyDataSetChanged(); + } else { + notifyDataSetInvalidated(); + } + } + } +} diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java new file mode 100644 index 0000000..e1f6fa8 --- /dev/null +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -0,0 +1,762 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.Editable; +import android.text.Selection; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.internal.R; + + +/** + * <p>An editable text view that shows completion suggestions automatically + * while the user is typing. The list of suggestions is displayed in a drop + * down menu from which the user can choose an item to replace the content + * of the edit box with.</p> + * + * <p>The drop down can be dismissed at any time by pressing the back key or, + * if no item is selected in the drop down, by pressing the enter/dpad center + * key.</p> + * + * <p>The list of suggestions is obtained from a data adapter and appears + * only after a given number of characters defined by + * {@link #getThreshold() the threshold}.</p> + * + * <p>The following code snippet shows how to create a text view which suggests + * various countries names while the user is typing:</p> + * + * <pre class="prettyprint"> + * public class CountriesActivity extends Activity { + * protected void onCreate(Bundle icicle) { + * super.onCreate(icicle); + * setContentView(R.layout.countries); + * + * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + * android.R.layout.simple_dropdown_item_1line, COUNTRIES); + * AutoCompleteTextView textView = (AutoCompleteTextView) + * findViewById(R.id.countries_list); + * textView.setAdapter(adapter); + * } + * + * private static final String[] COUNTRIES = new String[] { + * "Belgium", "France", "Italy", "Germany", "Spain" + * }; + * } + * </pre> + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionHint + * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold + * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector + */ +public class AutoCompleteTextView extends EditText implements Filter.FilterListener { + private static final int HINT_VIEW_ID = 0x17; + + private CharSequence mHintText; + private int mHintResource; + + private ListAdapter mAdapter; + private Filter mFilter; + private int mThreshold; + + private PopupWindow mPopup; + private DropDownListView mDropDownList; + + private Drawable mDropDownListHighlight; + + private AdapterView.OnItemClickListener mItemClickListener; + private AdapterView.OnItemSelectedListener mItemSelectedListener; + + private final DropDownItemClickListener mDropDownItemClickListener = + new DropDownItemClickListener(); + + private boolean mTextChanged; + + public AutoCompleteTextView(Context context) { + this(context, null); + } + + public AutoCompleteTextView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); + } + + public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mPopup = new PopupWindow(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); + + TypedArray a = + context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0); + + mThreshold = a.getInt( + R.styleable.AutoCompleteTextView_completionThreshold, 2); + + mHintText = a.getText(R.styleable.AutoCompleteTextView_completionHint); + + mDropDownListHighlight = a.getDrawable( + R.styleable.AutoCompleteTextView_dropDownSelector); + + mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, + R.layout.simple_dropdown_hint); + + a.recycle(); + + setFocusable(true); + } + + /** + * Sets this to be single line; a separate method so + * MultiAutoCompleteTextView can skip this. + */ + /* package */ void finishInit() { + setSingleLine(); + } + + /** + * <p>Sets the optional hint text that is displayed at the bottom of the + * the matching list. This can be used as a cue to the user on how to + * best use the list, or to provide extra information.</p> + * + * @param hint the text to be displayed to the user + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionHint + */ + public void setCompletionHint(CharSequence hint) { + mHintText = hint; + } + + /** + * <p>Returns the number of characters the user must type before the drop + * down list is shown.</p> + * + * @return the minimum number of characters to type to show the drop down + * + * @see #setThreshold(int) + */ + public int getThreshold() { + return mThreshold; + } + + /** + * <p>Specifies the minimum number of characters the user has to type in the + * edit box before the drop down list is shown.</p> + * + * <p>When <code>threshold</code> is less than or equals 0, a threshold of + * 1 is applied.</p> + * + * @param threshold the number of characters to type before the drop down + * is shown + * + * @see #getThreshold() + * + * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold + */ + public void setThreshold(int threshold) { + if (threshold <= 0) { + threshold = 1; + } + + mThreshold = threshold; + } + + /** + * <p>Sets the listener that will be notified when the user clicks an item + * in the drop down list.</p> + * + * @param l the item click listener + */ + public void setOnItemClickListener(AdapterView.OnItemClickListener l) { + mItemClickListener = l; + } + + /** + * <p>Sets the listener that will be notified when the user selects an item + * in the drop down list.</p> + * + * @param l the item selected listener + */ + public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) { + mItemSelectedListener = l; + } + + /** + * <p>Returns the listener that is notified whenever the user clicks an item + * in the drop down list.</p> + * + * @return the item click listener + */ + public AdapterView.OnItemClickListener getItemClickListener() { + return mItemClickListener; + } + + /** + * <p>Returns the listener that is notified whenever the user selects an + * item in the drop down list.</p> + * + * @return the item selected listener + */ + public AdapterView.OnItemSelectedListener getItemSelectedListener() { + return mItemSelectedListener; + } + + /** + * <p>Returns a filterable list adapter used for auto completion.</p> + * + * @return a data adapter used for auto completion + */ + public ListAdapter getAdapter() { + return mAdapter; + } + + /** + * <p>Changes the list of data used for auto completion. The provided list + * must be a filterable list adapter.</p> + * + * @param adapter the adapter holding the auto completion data + * + * @see #getAdapter() + * @see android.widget.Filterable + * @see android.widget.ListAdapter + */ + public <T extends ListAdapter & Filterable> void setAdapter(T adapter) { + mAdapter = adapter; + if (mAdapter != null) { + //noinspection unchecked + mFilter = ((Filterable) mAdapter).getFilter(); + } else { + mFilter = null; + } + + if (mDropDownList != null) { + mDropDownList.setAdapter(mAdapter); + } + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (isPopupShowing()) { + boolean consumed = mDropDownList.onKeyUp(keyCode, event); + if (consumed) { + switch (keyCode) { + // if the list accepts the key events and the key event + // was a click, the text view gets the selected item + // from the drop down as its content + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + performCompletion(); + return true; + } + } + } + return super.onKeyUp(keyCode, event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // when the drop down is shown, we drive it directly + if (isPopupShowing()) { + // special case for the back key, we do not even try to send it + // to the drop down list but instead, consume it immediately + if (keyCode == KeyEvent.KEYCODE_BACK) { + dismissDropDown(); + return true; + + // the key events are forwarded to the list in the drop down view + // note that ListView handles space but we don't want that to happen + } else if (keyCode != KeyEvent.KEYCODE_SPACE) { + boolean consumed = mDropDownList.onKeyDown(keyCode, event); + + if (consumed) { + switch (keyCode) { + // avoid passing the focus from the text view to the + // next component + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_UP: + return true; + } + } else{ + int index = mDropDownList.getSelectedItemPosition(); + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + if (index == 0) { + return true; + } + break; + // when the selection is at the bottom, we block the + // event to avoid going to the next focusable widget + case KeyEvent.KEYCODE_DPAD_DOWN: + Adapter adapter = mDropDownList.getAdapter(); + if (index == adapter.getCount() - 1) { + return true; + } + break; + } + } + } + } else { + switch(keyCode) { + case KeyEvent.KEYCODE_DPAD_DOWN: + performValidation(); + } + } + + // when text is changed, inserted or deleted, we attempt to show + // the drop down + boolean openBefore = isPopupShowing(); + mTextChanged = false; + + boolean handled = super.onKeyDown(keyCode, event); + + // if the list was open before the keystroke, but closed afterwards, + // then something in the keystroke processing (an input filter perhaps) + // called performCompletion() and we shouldn't do any more processing. + if (openBefore && !isPopupShowing()) { + return handled; + } + + if (mTextChanged) { // would have been set in onTextChanged() + // the drop down is shown only when a minimum number of characters + // was typed in the text view + if (enoughToFilter()) { + if (mFilter != null) { + performFiltering(getText(), keyCode); + } + } else { + // drop down is automatically dismissed when enough characters + // are deleted from the text view + dismissDropDown(); + if (mFilter != null) { + mFilter.filter(null); + } + } + return true; + } + + return handled; + } + + /** + * Returns <code>true</code> if the amount of text in the field meets + * or exceeds the {@link #getThreshold} requirement. You can override + * this to impose a different standard for when filtering will be + * triggered. + */ + public boolean enoughToFilter() { + return getText().length() >= mThreshold; + } + + @Override + protected void onTextChanged(CharSequence text, int start, int before, + int after) { + super.onTextChanged(text, start, before, after); + mTextChanged = true; + } + + /** + * <p>Indicates whether the popup menu is showing.</p> + * + * @return true if the popup menu is showing, false otherwise + */ + public boolean isPopupShowing() { + return mPopup.isShowing(); + } + + /** + * <p>Converts the selected item from the drop down list into a sequence + * of character that can be used in the edit box.</p> + * + * @param selectedItem the item selected by the user for completion + * + * @return a sequence of characters representing the selected suggestion + */ + protected CharSequence convertSelectionToString(Object selectedItem) { + return mFilter.convertResultToString(selectedItem); + } + + /** + * <p>Starts filtering the content of the drop down list. The filtering + * pattern is the content of the edit box. Subclasses should override this + * method to filter with a different pattern, for instance a substring of + * <code>text</code>.</p> + * + * @param text the filtering pattern + * @param keyCode the last character inserted in the edit box + */ + @SuppressWarnings({ "UnusedDeclaration" }) + protected void performFiltering(CharSequence text, int keyCode) { + mFilter.filter(text, this); + } + + /** + * <p>Performs the text completion by converting the selected item from + * the drop down list into a string, replacing the text box's content with + * this string and finally dismissing the drop down menu.</p> + */ + public void performCompletion() { + performCompletion(null, -1, -1); + } + + private void performCompletion(View selectedView, int position, long id) { + if (isPopupShowing()) { + Object selectedItem; + if (position == -1) { + selectedItem = mDropDownList.getSelectedItem(); + } else { + selectedItem = mAdapter.getItem(position); + } + replaceText(convertSelectionToString(selectedItem)); + + if (mItemClickListener != null) { + final DropDownListView list = mDropDownList; + + if (selectedView == null || position == -1) { + selectedView = list.getSelectedView(); + position = list.getSelectedItemPosition(); + id = list.getSelectedItemId(); + } + mItemClickListener.onItemClick(list, selectedView, position, id); + } + } + + dismissDropDown(); + } + + /** + * <p>Performs the text completion by replacing the current text by the + * selected item. Subclasses should override this method to avoid replacing + * the whole content of the edit box.</p> + * + * @param text the selected suggestion in the drop down list + */ + protected void replaceText(CharSequence text) { + setText(text); + // make sure we keep the caret at the end of the text view + Editable spannable = getText(); + Selection.setSelection(spannable, spannable.length()); + } + + public void onFilterComplete(int count) { + /* + * This checks enoughToFilter() again because filtering requests + * are asynchronous, so the result may come back after enough text + * has since been deleted to make it no longer appropriate + * to filter. + */ + + if (count > 0 && enoughToFilter()) { + if (hasFocus() && hasWindowFocus()) { + showDropDown(); + } + } else { + dismissDropDown(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + performValidation(); + if (!hasWindowFocus) { + dismissDropDown(); + } + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + performValidation(); + if (!focused) { + dismissDropDown(); + } + } + + /** + * <p>Closes the drop down if present on screen.</p> + */ + public void dismissDropDown() { + mPopup.dismiss(); + if (mDropDownList != null) { + // start next time with no selection + mDropDownList.hideSelector(); + } + } + + @Override + protected boolean setFrame(int l, int t, int r, int b) { + boolean result = super.setFrame(l, t, r, b); + + mPopup.update(this, getMeasuredWidth(), -1); + + return result; + } + + /** + * <p>Displays the drop down on screen.</p> + */ + public void showDropDown() { + int height = buildDropDown(); + if (mPopup.isShowing()) { + mPopup.update(this, getMeasuredWidth() - mPaddingLeft - mPaddingRight, height); + } else { + mPopup.setHeight(height); + mPopup.setWidth(getMeasuredWidth() - mPaddingLeft - mPaddingRight); + mPopup.showAsDropDown(this); + } + } + + /** + * <p>Builds the popup window's content and returns the height the popup + * should have. Returns -1 when the content already exists.</p> + * + * @return the content's height or -1 if content already exists + */ + private int buildDropDown() { + ViewGroup dropDownView; + int otherHeights = 0; + + if (mDropDownList == null) { + Context context = getContext(); + + mDropDownList = new DropDownListView(context); + mDropDownList.setSelector(mDropDownListHighlight); + mDropDownList.setAdapter(mAdapter); + mDropDownList.setVerticalFadingEdgeEnabled(true); + mDropDownList.setOnItemClickListener(mDropDownItemClickListener); + + if (mItemSelectedListener != null) { + mDropDownList.setOnItemSelectedListener(mItemSelectedListener); + } + + dropDownView = mDropDownList; + + View hintView = getHintView(context); + if (hintView != null) { + // if an hint has been specified, we accomodate more space for it and + // add a text view in the drop down menu, at the bottom of the list + LinearLayout hintContainer = new LinearLayout(context); + hintContainer.setOrientation(LinearLayout.VERTICAL); + + LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, 0, 1.0f + ); + hintContainer.addView(dropDownView, hintParams); + hintContainer.addView(hintView); + + // measure the hint's height to find how much more vertical space + // we need to add to the drop down's height + int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.UNSPECIFIED; + hintView.measure(widthSpec, heightSpec); + + hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); + otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + + dropDownView = hintContainer; + } + + mPopup.setContentView(dropDownView); + } else { + dropDownView = (ViewGroup) mPopup.getContentView(); + final View view = dropDownView.findViewById(HINT_VIEW_ID); + if (view != null) { + LinearLayout.LayoutParams hintParams = + (LinearLayout.LayoutParams) view.getLayoutParams(); + otherHeights = view.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + } + } + + // Max height available on the screen for a popup anchored to us + final int maxHeight = mPopup.getMaxAvailableHeight(this); + otherHeights += dropDownView.getPaddingTop() + dropDownView.getPaddingBottom(); + + return mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, + 0, ListView.NO_POSITION, maxHeight - otherHeights, 2) + otherHeights; + } + + private View getHintView(Context context) { + if (mHintText != null && mHintText.length() > 0) { + final TextView hintView = (TextView) LayoutInflater.from(context).inflate( + mHintResource, null).findViewById(com.android.internal.R.id.text1); + hintView.setText(mHintText); + hintView.setId(HINT_VIEW_ID); + return hintView; + } else { + return null; + } + } + + private class DropDownItemClickListener implements AdapterView.OnItemClickListener { + public void onItemClick(AdapterView parent, View v, int position, long id) { + performCompletion(v, position, id); + } + } + + /** + * <p>Wrapper class for a ListView. This wrapper hijacks the focus to + * make sure the list uses the appropriate drawables and states when + * displayed on screen within a drop down. The focus is never actually + * passed to the drop down; the list only looks focused.</p> + */ + private static class DropDownListView extends ListView { + /** + * <p>Creates a new list view wrapper.</p> + * + * @param context this view's context + */ + public DropDownListView(Context context) { + super(context, null, com.android.internal.R.attr.dropDownListViewStyle); + } + + /** + * <p>Avoids jarring scrolling effect by ensuring that list elements + * made of a text view fit on a single line.</p> + * + * @param position the item index in the list to get a view for + * @return the view for the specified item + */ + @Override + protected View obtainView(int position) { + View view = super.obtainView(position); + + if (view instanceof TextView) { + ((TextView) view).setHorizontallyScrolling(true); + } + + return view; + } + + /** + * <p>Returns the top padding of the currently selected view.</p> + * + * @return the height of the top padding for the selection + */ + public int getSelectionPaddingTop() { + return mSelectionTopPadding; + } + + /** + * <p>Returns the bottom padding of the currently selected view.</p> + * + * @return the height of the bottom padding for the selection + */ + public int getSelectionPaddingBottom() { + return mSelectionBottomPadding; + } + + /** + * <p>Returns the focus state in the drop down.</p> + * + * @return true always + */ + @Override + public boolean hasWindowFocus() { + return true; + } + + /** + * <p>Returns the focus state in the drop down.</p> + * + * @return true always + */ + @Override + public boolean isFocused() { + return true; + } + + /** + * <p>Returns the focus state in the drop down.</p> + * + * @return true always + */ + @Override + public boolean hasFocus() { + return true; + } + } + + /** + * This interface is used to make sure that the text entered in this TextView complies to + * a certain format. Since there is no foolproof way to prevent the user from leaving + * this View with an incorrect value in it, all we can do is try to fix it ourselves + * when this happens. + */ + static public interface Validator { + /** + * @return true if the text currently in the text editor is valid. + */ + boolean isValid(CharSequence text); + + /** + * @param invalidText a string that doesn't pass validation: + * isValid(invalidText) returns false + * @return a string based on invalidText such as invoking isValid() on it returns true. + */ + CharSequence fixText(CharSequence invalidText); + } + + private Validator mValidator = null; + + public void setValidator(Validator validator) { + mValidator = validator; + } + + /** + * Returns the Validator set with {@link #setValidator}, + * or <code>null</code> if it was not set. + */ + public Validator getValidator() { + return mValidator; + } + + /** + * If a validator was set on this view and the current string is not valid, + * ask the validator to fix it. + */ + public void performValidation() { + if (mValidator == null) return; + + CharSequence text = getText(); + + if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) { + setText(mValidator.fixText(text)); + } + } + + /** + * Returns the Filter obtained from {@link Filterable#getFilter}, + * or <code>null</code> if {@link #setAdapter} was not called with + * a Filterable. + */ + protected Filter getFilter() { + return mFilter; + } +} diff --git a/core/java/android/widget/BaseAdapter.java b/core/java/android/widget/BaseAdapter.java new file mode 100644 index 0000000..1921d73 --- /dev/null +++ b/core/java/android/widget/BaseAdapter.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; + +/** + * Common base class of common implementation for an {@link Adapter} that can be + * used in both {@link ListView} (by implementing the specialized + * {@link ListAdapter} interface} and {@link Spinner} (by implementing the + * specialized {@link SpinnerAdapter} interface. + */ +public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter { + private final DataSetObservable mDataSetObservable = new DataSetObservable(); + + public boolean hasStableIds() { + return false; + } + + public void registerDataSetObserver(DataSetObserver observer) { + mDataSetObservable.registerObserver(observer); + } + + public void unregisterDataSetObserver(DataSetObserver observer) { + mDataSetObservable.unregisterObserver(observer); + } + + public void notifyDataSetChanged() { + mDataSetObservable.notifyChanged(); + } + + public void notifyDataSetInvalidated() { + mDataSetObservable.notifyInvalidated(); + } + + public boolean areAllItemsEnabled() { + return true; + } + + public boolean isEnabled(int position) { + return true; + } + + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return getView(position, convertView, parent); + } + + public int getItemViewType(int position) { + return 0; + } + + public int getViewTypeCount() { + return 1; + } + + public boolean isEmpty() { + return getCount() == 0; + } +} diff --git a/core/java/android/widget/BaseExpandableListAdapter.java b/core/java/android/widget/BaseExpandableListAdapter.java new file mode 100644 index 0000000..3a8bb2a --- /dev/null +++ b/core/java/android/widget/BaseExpandableListAdapter.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.view.KeyEvent; + +/** + * Base class for a {@link ExpandableListAdapter} used to provide data and Views + * from some data to an expandable list view. + * <p> + * Adapters inheriting this class should verify that the base implementations of + * {@link #getCombinedChildId(long, long)} and {@link #getCombinedGroupId(long)} + * are correct in generating unique IDs from the group/children IDs. + * <p> + * @see SimpleExpandableListAdapter + * @see SimpleCursorTreeAdapter + */ +public abstract class BaseExpandableListAdapter implements ExpandableListAdapter { + private final DataSetObservable mDataSetObservable = new DataSetObservable(); + + public void registerDataSetObserver(DataSetObserver observer) { + mDataSetObservable.registerObserver(observer); + } + + public void unregisterDataSetObserver(DataSetObserver observer) { + mDataSetObservable.unregisterObserver(observer); + } + + /** + * {@see DataSetObservable#notifyInvalidated()} + */ + public void notifyDataSetInvalidated() { + mDataSetObservable.notifyInvalidated(); + } + + /** + * {@see DataSetObservable#notifyChanged()} + */ + public void notifyDataSetChanged() { + mDataSetObservable.notifyChanged(); + } + + public boolean areAllItemsEnabled() { + return true; + } + + public void onGroupCollapsed(int groupPosition) { + } + + public void onGroupExpanded(int groupPosition) { + } + + /** + * Override this method if you foresee a clash in IDs based on this scheme: + * <p> + * Base implementation returns a long: + * <li> bit 0: Whether this ID points to a child (unset) or group (set), so for this method + * this bit will be 0. + * <li> bit 1-31: Lower 31 bits of the groupId + * <li> bit 32-63: Lower 32 bits of the childId. + * <p> + * {@inheritDoc} + */ + public long getCombinedChildId(long groupId, long childId) { + return 0x8000000000000000L | ((groupId & 0x7FFFFFFF) << 32) | (childId & 0xFFFFFFFF); + } + + /** + * Override this method if you foresee a clash in IDs based on this scheme: + * <p> + * Base implementation returns a long: + * <li> bit 0: Whether this ID points to a child (unset) or group (set), so for this method + * this bit will be 1. + * <li> bit 1-31: Lower 31 bits of the groupId + * <li> bit 32-63: Lower 32 bits of the childId. + * <p> + * {@inheritDoc} + */ + public long getCombinedGroupId(long groupId) { + return (groupId & 0x7FFFFFFF) << 32; + } + + /** + * {@inheritDoc} + */ + public boolean isEmpty() { + return getGroupCount() == 0; + } + +} diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java new file mode 100644 index 0000000..f2868af --- /dev/null +++ b/core/java/android/widget/Button.java @@ -0,0 +1,69 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.KeyEvent; + + +/** + * <p> + * <code>Button</code> represents a push-button widget. Push-buttons can be + * pressed, or clicked, by the user to perform an action. A typical use of a + * push-button in an activity would be the following: + * </p> + * + * <pre class="prettyprint"> + * public class MyActivity extends Activity { + * protected void onCreate(Bundle icicle) { + * super.onCreate(icicle); + * + * setContentView(R.layout.content_layout_id); + * + * final Button button = (Button) findViewById(R.id.button_id); + * button.setOnClickListener(new View.OnClickListener() { + * public void onClick(View v) { + * // Perform action on click + * } + * }); + * } + * } + * </pre> + * + * <p><strong>XML attributes</strong></p> + * <p> + * See {@link android.R.styleable#Button Button Attributes}, + * {@link android.R.styleable#TextView TextView Attributes}, + * {@link android.R.styleable#View View Attributes} + * </p> + */ +public class Button extends TextView { + public Button(Context context) { + this(context, null); + } + + public Button(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.buttonStyle); + } + + public Button(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } +} diff --git a/core/java/android/widget/CheckBox.java b/core/java/android/widget/CheckBox.java new file mode 100644 index 0000000..ff63a24 --- /dev/null +++ b/core/java/android/widget/CheckBox.java @@ -0,0 +1,65 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.util.AttributeSet; + + +/** + * <p> + * A checkbox is a specific type of two-states button that can be either + * checked or unchecked. A example usage of a checkbox inside your activity + * would be the following: + * </p> + * + * <pre class="prettyprint"> + * public class MyActivity extends Activity { + * protected void onCreate(Bundle icicle) { + * super.onCreate(icicle); + * + * setContentView(R.layout.content_layout_id); + * + * final CheckBox checkBox = (CheckBox) findViewById(R.id.checkbox_id); + * if (checkBox.isChecked()) { + * checkBox.setChecked(false); + * } + * } + * } + * </pre> + * + * <p><strong>XML attributes</strong></p> + * <p> + * See {@link android.R.styleable#CompoundButton CompoundButton Attributes}, + * {@link android.R.styleable#Button Button Attributes}, + * {@link android.R.styleable#TextView TextView Attributes}, + * {@link android.R.styleable#View View Attributes} + * </p> + */ +public class CheckBox extends CompoundButton { + public CheckBox(Context context) { + this(context, null); + } + + public CheckBox(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.checkboxStyle); + } + + public CheckBox(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } +} diff --git a/core/java/android/widget/Checkable.java b/core/java/android/widget/Checkable.java new file mode 100644 index 0000000..eb97b4a --- /dev/null +++ b/core/java/android/widget/Checkable.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2007 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 android.widget; + +/** + * Defines an extension for views that make them checkable. + * + */ +public interface Checkable { + + /** + * Change the checked state of the view + * + * @param checked The new checked state + */ + void setChecked(boolean checked); + + /** + * @return The current checked state of the view + */ + boolean isChecked(); + + /** + * Change the checked state of the view to the inverse of its current state + * + */ + void toggle(); +} diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java new file mode 100644 index 0000000..f5a0b1c --- /dev/null +++ b/core/java/android/widget/CheckedTextView.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.Gravity; + +import com.android.internal.R; + + +/** + * An extension to TextView that supports the {@link android.widget.Checkable} interface. + * This is useful when used in a {@link android.widget.ListView ListView} where the it's + * {@link android.widget.ListView#setChoiceMode(int) setChoiceMode} has been set to + * something other than {@link android.widget.ListView#CHOICE_MODE_NONE CHOICE_MODE_NONE}. + * + */ +public abstract class CheckedTextView extends TextView implements Checkable { + private boolean mChecked; + private int mCheckMarkResource; + private Drawable mCheckMarkDrawable; + private int mBasePaddingRight; + private int mCheckMarkWidth; + + private static final int[] CHECKED_STATE_SET = { + R.attr.state_checked + }; + + public CheckedTextView(Context context) { + this(context, null); + } + + public CheckedTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CheckedTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.CheckedTextView, defStyle, 0); + + Drawable d = a.getDrawable(R.styleable.CheckedTextView_checkMark); + if (d != null) { + setCheckMarkDrawable(d); + } + + boolean checked = a.getBoolean(R.styleable.CheckedTextView_checked, false); + setChecked(checked); + + a.recycle(); + } + + public void toggle() { + setChecked(!mChecked); + } + + public boolean isChecked() { + return mChecked; + } + + /** + * <p>Changes the checked state of this text view.</p> + * + * @param checked true to check the text, false to uncheck it + */ + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + refreshDrawableState(); + } + } + + + /** + * Set the checkmark to a given Drawable, identified by its resourece id. This will be drawn + * when {@link #isChecked()} is true. + * + * @param resid The Drawable to use for the checkmark. + */ + public void setCheckMarkDrawable(int resid) { + if (resid != 0 && resid == mCheckMarkResource) { + return; + } + + mCheckMarkResource = resid; + + Drawable d = null; + if (mCheckMarkResource != 0) { + d = getResources().getDrawable(mCheckMarkResource); + } + setCheckMarkDrawable(d); + } + + /** + * Set the checkmark to a given Drawable. This will be drawn when {@link #isChecked()} is true. + * + * @param d The Drawable to use for the checkmark. + */ + public void setCheckMarkDrawable(Drawable d) { + if (d != null) { + if (mCheckMarkDrawable != null) { + mCheckMarkDrawable.setCallback(null); + unscheduleDrawable(mCheckMarkDrawable); + } + d.setCallback(this); + d.setVisible(getVisibility() == VISIBLE, false); + d.setState(CHECKED_STATE_SET); + setMinHeight(d.getIntrinsicHeight()); + + mCheckMarkWidth = d.getIntrinsicWidth(); + mPaddingRight = mCheckMarkWidth + mBasePaddingRight; + d.setState(getDrawableState()); + mCheckMarkDrawable = d; + } else { + mPaddingRight = mBasePaddingRight; + } + requestLayout(); + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + super.setPadding(left, top, right, bottom); + mBasePaddingRight = mPaddingRight; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final Drawable checkMarkDrawable = mCheckMarkDrawable; + if (checkMarkDrawable != null) { + final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; + final int height = checkMarkDrawable.getIntrinsicHeight(); + + int y = 0; + + switch (verticalGravity) { + case Gravity.BOTTOM: + y = getHeight() - height; + break; + case Gravity.CENTER_VERTICAL: + y = (getHeight() - height) / 2; + break; + } + + int right = getWidth(); + checkMarkDrawable.setBounds( + right - mCheckMarkWidth - mBasePaddingRight, + y, + right - mBasePaddingRight, + y + height); + checkMarkDrawable.draw(canvas); + } + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + if (mCheckMarkDrawable != null) { + int[] myDrawableState = getDrawableState(); + + // Set the state of the Drawable + mCheckMarkDrawable.setState(myDrawableState); + + invalidate(); + } + } + +} diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java new file mode 100644 index 0000000..31d2063 --- /dev/null +++ b/core/java/android/widget/Chronometer.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2008 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.pim.DateUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.RemoteViews.RemoteView; + +import java.util.Formatter; +import java.util.IllegalFormatException; +import java.util.Locale; + +/** + * Class that implements a simple timer. + * <p> + * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase, + * and it counts up from that, or if you don't give it a base time, it will use the + * time at which you call {@link #start}. By default it will display the current + * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat} + * to format the timer value into an arbitrary string. + * + * @attr ref android.R.styleable#Chronometer_format + */ +@RemoteView +public class Chronometer extends TextView { + private static final String TAG = "Chronometer"; + + private long mBase; + private boolean mVisible; + private boolean mStarted; + private boolean mRunning; + private boolean mLogged; + private String mFormat; + private Formatter mFormatter; + private Locale mFormatterLocale; + private Object[] mFormatterArgs = new Object[1]; + private StringBuilder mFormatBuilder; + + /** + * Initialize this Chronometer object. + * Sets the base to the current time. + */ + public Chronometer(Context context) { + this(context, null, 0); + } + + /** + * Initialize with standard view layout information. + * Sets the base to the current time. + */ + public Chronometer(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + /** + * Initialize with standard view layout information and style. + * Sets the base to the current time. + */ + public Chronometer(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes( + attrs, + com.android.internal.R.styleable.Chronometer, defStyle, 0); + setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format)); + a.recycle(); + + init(); + } + + private void init() { + mBase = SystemClock.elapsedRealtime(); + updateText(mBase); + } + + /** + * Set the time that the count-up timer is in reference to. + * + * @param base Use the {@link SystemClock#elapsedRealtime} time base. + */ + public void setBase(long base) { + mBase = base; + updateText(SystemClock.elapsedRealtime()); + } + + /** + * Return the base time as set through {@link #setBase}. + */ + public long getBase() { + return mBase; + } + + /** + * Sets the format string used for display. The Chronometer will display + * this string, with the first "%s" replaced by the current timer value in + * "MM:SS" or "H:MM:SS" form. + * + * If the format string is null, or if you never call setFormat(), the + * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS" + * form. + * + * @param format the format string. + */ + public void setFormat(String format) { + mFormat = format; + if (format != null && mFormatBuilder == null) { + mFormatBuilder = new StringBuilder(format.length() * 2); + } + } + + /** + * Returns the current format string as set through {@link #setFormat}. + */ + public String getFormat() { + return mFormat; + } + + /** + * Start counting up. This does not affect the base as set from {@link #setBase}, just + * the view display. + * + * Chronometer works by regularly scheduling messages to the handler, even when the + * Widget is not visible. To make sure resource leaks do not occur, the user should + * make sure that each start() call has a reciprocal call to {@link #stop}. + */ + public void start() { + mStarted = true; + updateRunning(); + } + + /** + * Stop counting up. This does not affect the base as set from {@link #setBase}, just + * the view display. + * + * This stops the messages to the handler, effectively releasing resources that would + * be held as the chronometer is running, via {@link #start}. + */ + public void stop() { + mStarted = false; + updateRunning(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mVisible = false; + updateRunning(); + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + mVisible = visibility == VISIBLE; + updateRunning(); + } + + private void updateText(long now) { + long seconds = now - mBase; + seconds /= 1000; + String text = DateUtils.formatElapsedTime(seconds); + + if (mFormat != null) { + Locale loc = Locale.getDefault(); + if (mFormatter == null || !loc.equals(mFormatterLocale)) { + mFormatterLocale = loc; + mFormatter = new Formatter(mFormatBuilder, loc); + } + mFormatBuilder.setLength(0); + mFormatterArgs[0] = text; + try { + mFormatter.format(mFormat, mFormatterArgs); + text = mFormatBuilder.toString(); + } catch (IllegalFormatException ex) { + if (!mLogged) { + Log.w(TAG, "Illegal format string: " + mFormat); + mLogged = true; + } + } + } + setText(text); + } + + private void updateRunning() { + boolean running = mVisible && mStarted; + if (running != mRunning) { + if (running) { + updateText(SystemClock.elapsedRealtime()); + mHandler.sendMessageDelayed(Message.obtain(), 1000); + } + mRunning = running; + } + } + + private Handler mHandler = new Handler() { + public void handleMessage(Message m) { + if (mStarted) { + updateText(SystemClock.elapsedRealtime()); + sendMessageDelayed(Message.obtain(), 1000); + } + } + }; +} diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java new file mode 100644 index 0000000..e56a741 --- /dev/null +++ b/core/java/android/widget/CompoundButton.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import com.android.internal.R; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.Gravity; + + +/** + * <p> + * A button with two states, checked and unchecked. When the button is pressed + * or clicked, the state changes automatically. + * </p> + * + * <p><strong>XML attributes</strong></p> + * <p> + * See {@link android.R.styleable#CompoundButton + * CompoundButton Attributes}, {@link android.R.styleable#Button Button + * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link + * android.R.styleable#View View Attributes} + * </p> + */ +public abstract class CompoundButton extends Button implements Checkable { + private boolean mChecked; + private int mButtonResource; + private boolean mBroadcasting; + private Drawable mButtonDrawable; + private OnCheckedChangeListener mOnCheckedChangeListener; + private OnCheckedChangeListener mOnCheckedChangeWidgetListener; + + private static final int[] CHECKED_STATE_SET = { + R.attr.state_checked + }; + + public CompoundButton(Context context) { + this(context, null); + } + + public CompoundButton(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CompoundButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = + context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.CompoundButton, defStyle, 0); + + Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button); + if (d != null) { + setButtonDrawable(d); + } + + boolean checked = a + .getBoolean(com.android.internal.R.styleable.CompoundButton_checked, false); + setChecked(checked); + + a.recycle(); + } + + public void toggle() { + setChecked(!mChecked); + } + + @Override + public boolean performClick() { + /* + * XXX: These are tiny, need some surrounding 'expanded touch area', + * which will need to be implemented in Button if we only override + * performClick() + */ + + /* When clicked, toggle the state */ + toggle(); + return super.performClick(); + } + + public boolean isChecked() { + return mChecked; + } + + /** + * <p>Changes the checked state of this button.</p> + * + * @param checked true to check the button, false to uncheck it + */ + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + refreshDrawableState(); + + // Avoid infinite recursions if setChecked() is called from a listener + if (mBroadcasting) { + return; + } + + mBroadcasting = true; + if (mOnCheckedChangeListener != null) { + mOnCheckedChangeListener.onCheckedChanged(this, mChecked); + } + if (mOnCheckedChangeWidgetListener != null) { + mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked); + } + mBroadcasting = false; + } + } + + /** + * Register a callback to be invoked when the checked state of this button + * changes. + * + * @param listener the callback to call on checked state change + */ + public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { + mOnCheckedChangeListener = listener; + } + + /** + * Register a callback to be invoked when the checked state of this button + * changes. This callback is used for internal purpose only. + * + * @param listener the callback to call on checked state change + * @hide + */ + void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) { + mOnCheckedChangeWidgetListener = listener; + } + + /** + * Interface definition for a callback to be invoked when the checked state + * of a compound button changed. + */ + public static interface OnCheckedChangeListener { + /** + * Called when the checked state of a compound button has changed. + * + * @param buttonView The compound button view whose state has changed. + * @param isChecked The new checked state of buttonView. + */ + void onCheckedChanged(CompoundButton buttonView, boolean isChecked); + } + + /** + * Set the background to a given Drawable, identified by its resource id. + * + * @param resid the resource id of the drawable to use as the background + */ + public void setButtonDrawable(int resid) { + if (resid != 0 && resid == mButtonResource) { + return; + } + + mButtonResource = resid; + + Drawable d = null; + if (mButtonResource != 0) { + d = getResources().getDrawable(mButtonResource); + } + setButtonDrawable(d); + } + + /** + * Set the background to a given Drawable + * + * @param d The Drawable to use as the background + */ + public void setButtonDrawable(Drawable d) { + if (d != null) { + if (mButtonDrawable != null) { + mButtonDrawable.setCallback(null); + unscheduleDrawable(mButtonDrawable); + } + d.setCallback(this); + d.setState(getDrawableState()); + d.setVisible(getVisibility() == VISIBLE, false); + mButtonDrawable = d; + mButtonDrawable.setState(null); + setMinHeight(mButtonDrawable.getIntrinsicHeight()); + } + + refreshDrawableState(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final Drawable buttonDrawable = mButtonDrawable; + if (buttonDrawable != null) { + final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; + final int height = buttonDrawable.getIntrinsicHeight(); + + int y = 0; + + switch (verticalGravity) { + case Gravity.BOTTOM: + y = getHeight() - height; + break; + case Gravity.CENTER_VERTICAL: + y = (getHeight() - height) / 2; + break; + } + + buttonDrawable.setBounds(0, y, buttonDrawable.getIntrinsicWidth(), y + height); + buttonDrawable.draw(canvas); + } + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + if (mButtonDrawable != null) { + int[] myDrawableState = getDrawableState(); + + // Set the state of the Drawable + mButtonDrawable.setState(myDrawableState); + + invalidate(); + } + } + + static class SavedState extends BaseSavedState { + boolean checked; + + /** + * Constructor called from {@link CompoundButton#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + checked = (Boolean)in.readValue(null); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeValue(checked); + } + + @Override + public String toString() { + return "CompoundButton.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " checked=" + checked + "}"; + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + // Force our ancestor class to save its state + setFreezesText(true); + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + + ss.checked = isChecked(); + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + setChecked(ss.checked); + requestLayout(); + } +} diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java new file mode 100644 index 0000000..cacaeab --- /dev/null +++ b/core/java/android/widget/CursorAdapter.java @@ -0,0 +1,382 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Handler; +import android.util.Config; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * Adapter that exposes data from a {@link android.database.Cursor Cursor} to a + * {@link android.widget.ListView ListView} widget. The Cursor must include + * a column named "_id" or this class will not work. + */ +public abstract class CursorAdapter extends BaseAdapter implements Filterable, + CursorFilter.CursorFilterClient { + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected boolean mDataValid; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected boolean mAutoRequery; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected Cursor mCursor; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected Context mContext; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int mRowIDColumn; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected ChangeObserver mChangeObserver; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected DataSetObserver mDataSetObserver = new MyDataSetObserver(); + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected CursorFilter mCursorFilter; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected FilterQueryProvider mFilterQueryProvider; + + /** + * Constructor. The adapter will call requery() on the cursor whenever + * it changes so that the most recent data is always displayed. + * + * @param c The cursor from which to get the data. + * @param context The context + */ + public CursorAdapter(Context context, Cursor c) { + init(context, c, true); + } + + /** + * Constructor + * @param c The cursor from which to get the data. + * @param context The context + * @param autoRequery If true the adapter will call requery() on the + * cursor whenever it changes so the most recent + * data is always displayed. + */ + public CursorAdapter(Context context, Cursor c, boolean autoRequery) { + init(context, c, autoRequery); + } + + protected void init(Context context, Cursor c, boolean autoRequery) { + boolean cursorPresent = c != null; + mAutoRequery = autoRequery; + mCursor = c; + mDataValid = cursorPresent; + mContext = context; + mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; + mChangeObserver = new ChangeObserver(); + if (cursorPresent) { + c.registerContentObserver(mChangeObserver); + c.registerDataSetObserver(mDataSetObserver); + } + } + + /** + * Returns the cursor. + * @return the cursor. + */ + public Cursor getCursor() { + return mCursor; + } + + /** + * @see android.widget.ListAdapter#getCount() + */ + public final int getCount() { + if (mDataValid && mCursor != null) { + return mCursor.getCount(); + } else { + return 0; + } + } + + /** + * @see android.widget.ListAdapter#getItem(int) + */ + public final Object getItem(int position) { + if (mDataValid && mCursor != null) { + mCursor.moveToPosition(position); + return mCursor; + } else { + return null; + } + } + + /** + * @see android.widget.ListAdapter#getItemId(int) + */ + public final long getItemId(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getLong(mRowIDColumn); + } else { + return 0; + } + } else { + return 0; + } + } + + @Override + public boolean hasStableIds() { + return true; + } + + /** + * @see android.widget.ListAdapter#getView(int, View, ViewGroup) + */ + public View getView(int position, View convertView, ViewGroup parent) { + if (!mDataValid) { + throw new IllegalStateException("this should only be called when the cursor is valid"); + } + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + View v; + if (convertView == null) { + v = newView(mContext, mCursor, parent); + } else { + v = convertView; + } + bindView(v, mContext, mCursor); + return v; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + if (mDataValid) { + mCursor.moveToPosition(position); + View v; + if (convertView == null) { + v = newDropDownView(mContext, mCursor, parent); + } else { + v = convertView; + } + bindView(v, mContext, mCursor); + return v; + } else { + return null; + } + } + + /** + * Makes a new view to hold the data pointed to by cursor. + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + * @param parent The parent to which the new view is attached to + * @return the newly created view. + */ + public abstract View newView(Context context, Cursor cursor, ViewGroup parent); + + /** + * Makes a new drop down view to hold the data pointed to by cursor. + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + * @param parent The parent to which the new view is attached to + * @return the newly created view. + */ + public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { + return newView(context, cursor, parent); + } + + /** + * Bind an existing view to the data pointed to by cursor + * @param view Existing view, returned earlier by newView + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + */ + public abstract void bindView(View view, Context context, Cursor cursor); + + /** + * Change the underlying cursor to a new cursor. If there is an existing cursor it will be + * closed. + * + * @param cursor the new cursor to be used + */ + public void changeCursor(Cursor cursor) { + if (mCursor != null) { + mCursor.unregisterContentObserver(mChangeObserver); + mCursor.unregisterDataSetObserver(mDataSetObserver); + mCursor.close(); + } + mCursor = cursor; + if (cursor != null) { + cursor.registerContentObserver(mChangeObserver); + cursor.registerDataSetObserver(mDataSetObserver); + mRowIDColumn = cursor.getColumnIndexOrThrow("_id"); + mDataValid = true; + // notify the observers about the new cursor + notifyDataSetChanged(); + } else { + mRowIDColumn = -1; + mDataValid = false; + // notify the observers about the lack of a data set + notifyDataSetInvalidated(); + } + } + + /** + * <p>Converts the cursor into a CharSequence. Subclasses should override this + * method to convert their results. The default implementation returns an + * empty String for null values or the default String representation of + * the value.</p> + * + * @param cursor the cursor to convert to a CharSequence + * @return a CharSequence representing the value + */ + public CharSequence convertToString(Cursor cursor) { + return cursor == null ? "" : cursor.toString(); + } + + /** + * Runs a query with the specified constraint. This query is requested + * by the filter attached to this adapter. + * + * The query is provided by a + * {@link android.widget.FilterQueryProvider}. + * If no provider is specified, the current cursor is not filtered and returned. + * + * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)} + * and the previous cursor is closed. + * + * This method is always executed on a background thread, not on the + * application's main thread (or UI thread.) + * + * Contract: when constraint is null or empty, the original results, + * prior to any filtering, must be returned. + * + * @param constraint the constraint with which the query must be filtered + * + * @return a Cursor representing the results of the new query + * + * @see #getFilter() + * @see #getFilterQueryProvider() + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + */ + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + if (mFilterQueryProvider != null) { + return mFilterQueryProvider.runQuery(constraint); + } + + return mCursor; + } + + public Filter getFilter() { + if (mCursorFilter == null) { + mCursorFilter = new CursorFilter(this); + } + return mCursorFilter; + } + + /** + * Returns the query filter provider used for filtering. When the + * provider is null, no filtering occurs. + * + * @return the current filter query provider or null if it does not exist + * + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public FilterQueryProvider getFilterQueryProvider() { + return mFilterQueryProvider; + } + + /** + * Sets the query filter provider used to filter the current Cursor. + * The provider's + * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)} + * method is invoked when filtering is requested by a client of + * this adapter. + * + * @param filterQueryProvider the filter query provider or null to remove it + * + * @see #getFilterQueryProvider() + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { + mFilterQueryProvider = filterQueryProvider; + } + + private class ChangeObserver extends ContentObserver { + public ChangeObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + if (mAutoRequery && mCursor != null) { + if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + + " due to update"); + mDataValid = mCursor.requery(); + } + } + } + + private class MyDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + mDataValid = true; + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + mDataValid = false; + notifyDataSetInvalidated(); + } + } + +} diff --git a/core/java/android/widget/CursorFilter.java b/core/java/android/widget/CursorFilter.java new file mode 100644 index 0000000..afd5b10 --- /dev/null +++ b/core/java/android/widget/CursorFilter.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.database.Cursor; + +/** + * <p>The CursorFilter delegates most of the work to the CursorAdapter. + * Subclasses should override these delegate methods to run the queries + * and convert the results into String that can be used by auto-completion + * widgets.</p> + */ +class CursorFilter extends Filter { + + CursorFilterClient mClient; + + interface CursorFilterClient { + CharSequence convertToString(Cursor cursor); + Cursor runQueryOnBackgroundThread(CharSequence constraint); + Cursor getCursor(); + void changeCursor(Cursor cursor); + } + + CursorFilter(CursorFilterClient client) { + mClient = client; + } + + @Override + public CharSequence convertResultToString(Object resultValue) { + return mClient.convertToString((Cursor) resultValue); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + Cursor cursor = mClient.runQueryOnBackgroundThread(constraint); + + FilterResults results = new FilterResults(); + if (cursor != null) { + results.count = cursor.getCount(); + results.values = cursor; + } else { + results.count = 0; + results.values = null; + } + return results; + } + + @Override + protected void publishResults(CharSequence constraint, + FilterResults results) { + Cursor oldCursor = mClient.getCursor(); + + if (results.values != oldCursor) { + mClient.changeCursor((Cursor) results.values); + } + } +} diff --git a/core/java/android/widget/CursorTreeAdapter.java b/core/java/android/widget/CursorTreeAdapter.java new file mode 100644 index 0000000..fa8fd4b --- /dev/null +++ b/core/java/android/widget/CursorTreeAdapter.java @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.app.Activity; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Handler; +import android.util.Config; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +/** + * An adapter that exposes data from a series of {@link Cursor}s to an + * {@link ExpandableListView} widget. The top-level {@link Cursor} (that is + * given in the constructor) exposes the groups, while subsequent {@link Cursor}s + * returned from {@link #getChildrenCursor(Cursor)} expose children within a + * particular group. The Cursors must include a column named "_id" or this class + * will not work. + */ +public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implements Filterable, + CursorFilter.CursorFilterClient { + private Context mContext; + private Handler mHandler; + private boolean mAutoRequery; + + /** The cursor helper that is used to get the groups */ + MyCursorHelper mGroupCursorHelper; + + /** + * The map of a group position to the group's children cursor helper (the + * cursor helper that is used to get the children for that group) + */ + SparseArray<MyCursorHelper> mChildrenCursorHelpers; + + // Filter related + CursorFilter mCursorFilter; + FilterQueryProvider mFilterQueryProvider; + + /** + * Constructor. The adapter will call {@link Cursor#requery()} on the cursor whenever + * it changes so that the most recent data is always displayed. + * + * @param cursor The cursor from which to get the data for the groups. + */ + public CursorTreeAdapter(Cursor cursor, Context context) { + init(cursor, context, true); + } + + /** + * Constructor. + * + * @param cursor The cursor from which to get the data for the groups. + * @param context The context + * @param autoRequery If true the adapter will call {@link Cursor#requery()} + * on the cursor whenever it changes so the most recent data is + * always displayed. + */ + public CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery) { + init(cursor, context, autoRequery); + } + + private void init(Cursor cursor, Context context, boolean autoRequery) { + mContext = context; + mHandler = new Handler(); + mAutoRequery = autoRequery; + + mGroupCursorHelper = new MyCursorHelper(cursor); + mChildrenCursorHelpers = new SparseArray<MyCursorHelper>(); + } + + /** + * Gets the cursor helper for the children in the given group. + * + * @param groupPosition The group whose children will be returned + * @param requestCursor Whether to request a Cursor via + * {@link #getChildrenCursor(Cursor)} (true), or to assume a call + * to {@link #setChildrenCursor(int, Cursor)} will happen shortly + * (false). + * @return The cursor helper for the children of the given group + */ + synchronized MyCursorHelper getChildrenCursorHelper(int groupPosition, boolean requestCursor) { + MyCursorHelper cursorHelper = mChildrenCursorHelpers.get(groupPosition); + + if (cursorHelper == null) { + if (mGroupCursorHelper.moveTo(groupPosition) == null) return null; + + final Cursor cursor = getChildrenCursor(mGroupCursorHelper.getCursor()); + cursorHelper = new MyCursorHelper(cursor); + mChildrenCursorHelpers.put(groupPosition, cursorHelper); + } + + return cursorHelper; + } + + /** + * Gets the Cursor for the children at the given group. Subclasses must + * implement this method to return the children data for a particular group. + * <p> + * If you want to asynchronously query a provider to prevent blocking the + * UI, it is possible to return null and at a later time call + * {@link #setChildrenCursor(int, Cursor)}. + * <p> + * It is your responsibility to manage this Cursor through the Activity + * lifecycle. It is a good idea to use {@link Activity#managedQuery} which + * will handle this for you. In some situations, the adapter will deactivate + * the Cursor on its own, but this will not always be the case, so please + * ensure the Cursor is properly managed. + * + * @param groupCursor The cursor pointing to the group whose children cursor + * should be returned + * @return The cursor for the children of a particular group, or null. + */ + abstract protected Cursor getChildrenCursor(Cursor groupCursor); + + /** + * Sets the group Cursor. + * + * @param cursor The Cursor to set for the group. + */ + public void setGroupCursor(Cursor cursor) { + mGroupCursorHelper.changeCursor(cursor, false); + } + + /** + * Sets the children Cursor for a particular group. + * <p> + * This is useful when asynchronously querying to prevent blocking the UI. + * + * @param groupPosition The group whose children are being set via this Cursor. + * @param childrenCursor The Cursor that contains the children of the group. + */ + public void setChildrenCursor(int groupPosition, Cursor childrenCursor) { + + /* + * Don't request a cursor from the subclass, instead we will be setting + * the cursor ourselves. + */ + MyCursorHelper childrenCursorHelper = getChildrenCursorHelper(groupPosition, false); + + /* + * Don't release any cursor since we know exactly what data is changing + * (this cursor, which is still valid). + */ + childrenCursorHelper.changeCursor(childrenCursor, false); + } + + public Cursor getChild(int groupPosition, int childPosition) { + // Return this group's children Cursor pointing to the particular child + return getChildrenCursorHelper(groupPosition, true).moveTo(childPosition); + } + + public long getChildId(int groupPosition, int childPosition) { + return getChildrenCursorHelper(groupPosition, true).getId(childPosition); + } + + public int getChildrenCount(int groupPosition) { + MyCursorHelper helper = getChildrenCursorHelper(groupPosition, true); + return (mGroupCursorHelper.isValid() && helper != null) ? helper.getCount() : 0; + } + + public Cursor getGroup(int groupPosition) { + // Return the group Cursor pointing to the given group + return mGroupCursorHelper.moveTo(groupPosition); + } + + public int getGroupCount() { + return mGroupCursorHelper.getCount(); + } + + public long getGroupId(int groupPosition) { + return mGroupCursorHelper.getId(groupPosition); + } + + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { + Cursor cursor = mGroupCursorHelper.moveTo(groupPosition); + if (cursor == null) { + throw new IllegalStateException("this should only be called when the cursor is valid"); + } + + View v; + if (convertView == null) { + v = newGroupView(mContext, cursor, isExpanded, parent); + } else { + v = convertView; + } + bindGroupView(v, mContext, cursor, isExpanded); + return v; + } + + /** + * Makes a new group view to hold the group data pointed to by cursor. + * + * @param context Interface to application's global information + * @param cursor The group cursor from which to get the data. The cursor is + * already moved to the correct position. + * @param isExpanded Whether the group is expanded. + * @param parent The parent to which the new view is attached to + * @return The newly created view. + */ + protected abstract View newGroupView(Context context, Cursor cursor, boolean isExpanded, + ViewGroup parent); + + /** + * Bind an existing view to the group data pointed to by cursor. + * + * @param view Existing view, returned earlier by newGroupView. + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is + * already moved to the correct position. + * @param isExpanded Whether the group is expanded. + */ + protected abstract void bindGroupView(View view, Context context, Cursor cursor, + boolean isExpanded); + + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { + MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true); + + Cursor cursor = cursorHelper.moveTo(childPosition); + if (cursor == null) { + throw new IllegalStateException("this should only be called when the cursor is valid"); + } + + View v; + if (convertView == null) { + v = newChildView(mContext, cursor, isLastChild, parent); + } else { + v = convertView; + } + bindChildView(v, mContext, cursor, isLastChild); + return v; + } + + /** + * Makes a new child view to hold the data pointed to by cursor. + * + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is + * already moved to the correct position. + * @param isLastChild Whether the child is the last child within its group. + * @param parent The parent to which the new view is attached to + * @return the newly created view. + */ + protected abstract View newChildView(Context context, Cursor cursor, boolean isLastChild, + ViewGroup parent); + + /** + * Bind an existing view to the child data pointed to by cursor + * + * @param view Existing view, returned earlier by newChildView + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is + * already moved to the correct position. + * @param isLastChild Whether the child is the last child within its group. + */ + protected abstract void bindChildView(View view, Context context, Cursor cursor, + boolean isLastChild); + + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + public boolean hasStableIds() { + return true; + } + + private synchronized void releaseCursorHelpers() { + for (int pos = mChildrenCursorHelpers.size() - 1; pos >= 0; pos--) { + mChildrenCursorHelpers.valueAt(pos).deactivate(); + } + + mChildrenCursorHelpers.clear(); + } + + @Override + public void notifyDataSetChanged() { + notifyDataSetChanged(true); + } + + /** + * Notifies a data set change, but with the option of not releasing any + * cached cursors. + * + * @param releaseCursors Whether to release and deactivate any cached + * cursors. + */ + public void notifyDataSetChanged(boolean releaseCursors) { + + if (releaseCursors) { + releaseCursorHelpers(); + } + + super.notifyDataSetChanged(); + } + + @Override + public void notifyDataSetInvalidated() { + releaseCursorHelpers(); + super.notifyDataSetInvalidated(); + } + + @Override + public void onGroupCollapsed(int groupPosition) { + deactivateChildrenCursorHelper(groupPosition); + } + + /** + * Deactivates the Cursor and removes the helper from cache. + * + * @param groupPosition The group whose children Cursor and helper should be + * deactivated. + */ + synchronized void deactivateChildrenCursorHelper(int groupPosition) { + MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true); + mChildrenCursorHelpers.remove(groupPosition); + cursorHelper.deactivate(); + } + + /** + * @see CursorAdapter#convertToString(Cursor) + */ + public String convertToString(Cursor cursor) { + return cursor == null ? "" : cursor.toString(); + } + + /** + * @see CursorAdapter#runQueryOnBackgroundThread(CharSequence) + */ + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + if (mFilterQueryProvider != null) { + return mFilterQueryProvider.runQuery(constraint); + } + + return mGroupCursorHelper.getCursor(); + } + + public Filter getFilter() { + if (mCursorFilter == null) { + mCursorFilter = new CursorFilter(this); + } + return mCursorFilter; + } + + /** + * @see CursorAdapter#getFilterQueryProvider() + */ + public FilterQueryProvider getFilterQueryProvider() { + return mFilterQueryProvider; + } + + /** + * @see CursorAdapter#setFilterQueryProvider(FilterQueryProvider) + */ + public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { + mFilterQueryProvider = filterQueryProvider; + } + + /** + * @see CursorAdapter#changeCursor(Cursor) + */ + public void changeCursor(Cursor cursor) { + mGroupCursorHelper.changeCursor(cursor, true); + } + + /** + * @see CursorAdapter#getCursor() + */ + public Cursor getCursor() { + return mGroupCursorHelper.getCursor(); + } + + /** + * Helper class for Cursor management: + * <li> Data validity + * <li> Funneling the content and data set observers from a Cursor to a + * single data set observer for widgets + * <li> ID from the Cursor for use in adapter IDs + * <li> Swapping cursors but maintaining other metadata + */ + class MyCursorHelper { + private Cursor mCursor; + private boolean mDataValid; + private int mRowIDColumn; + private MyContentObserver mContentObserver; + private MyDataSetObserver mDataSetObserver; + + MyCursorHelper(Cursor cursor) { + final boolean cursorPresent = cursor != null; + mCursor = cursor; + mDataValid = cursorPresent; + mRowIDColumn = cursorPresent ? cursor.getColumnIndex("_id") : -1; + mContentObserver = new MyContentObserver(); + mDataSetObserver = new MyDataSetObserver(); + if (cursorPresent) { + cursor.registerContentObserver(mContentObserver); + cursor.registerDataSetObserver(mDataSetObserver); + } + } + + Cursor getCursor() { + return mCursor; + } + + int getCount() { + if (mDataValid && mCursor != null) { + return mCursor.getCount(); + } else { + return 0; + } + } + + long getId(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getLong(mRowIDColumn); + } else { + return 0; + } + } else { + return 0; + } + } + + Cursor moveTo(int position) { + if (mDataValid && (mCursor != null) && mCursor.moveToPosition(position)) { + return mCursor; + } else { + return null; + } + } + + void changeCursor(Cursor cursor, boolean releaseCursors) { + if (mCursor != null) { + mCursor.unregisterContentObserver(mContentObserver); + mCursor.unregisterDataSetObserver(mDataSetObserver); + } + mCursor = cursor; + if (cursor != null) { + cursor.registerContentObserver(mContentObserver); + cursor.registerDataSetObserver(mDataSetObserver); + mRowIDColumn = cursor.getColumnIndex("_id"); + mDataValid = true; + // notify the observers about the new cursor + notifyDataSetChanged(releaseCursors); + } else { + mRowIDColumn = -1; + mDataValid = false; + // notify the observers about the lack of a data set + notifyDataSetInvalidated(); + } + } + + void deactivate() { + if (mCursor == null) { + return; + } + + mCursor.unregisterContentObserver(mContentObserver); + mCursor.unregisterDataSetObserver(mDataSetObserver); + mCursor.deactivate(); + mCursor = null; + } + + boolean isValid() { + return mDataValid && mCursor != null; + } + + private class MyContentObserver extends ContentObserver { + public MyContentObserver() { + super(mHandler); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + if (mAutoRequery && mCursor != null) { + if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + + " due to update"); + mDataValid = mCursor.requery(); + } + } + } + + private class MyDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + mDataValid = true; + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + mDataValid = false; + notifyDataSetInvalidated(); + } + } + } +} diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java new file mode 100644 index 0000000..c03bd32 --- /dev/null +++ b/core/java/android/widget/DatePicker.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.annotation.Widget; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.pim.DateFormat; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.LayoutInflater; + +import com.android.internal.R; +import com.android.internal.widget.NumberPicker; +import com.android.internal.widget.NumberPicker.OnChangedListener; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +/** + * A view for selecting a month / year / day based on a calendar like layout. + * + * For a dialog using this view, see {@link android.app.DatePickerDialog}. + */ +@Widget +public class DatePicker extends FrameLayout { + + private static final int DEFAULT_START_YEAR = 1900; + private static final int DEFAULT_END_YEAR = 2100; + + /* UI Components */ + private final NumberPicker mDayPicker; + private final NumberPicker mMonthPicker; + private final NumberPicker mYearPicker; + + private final int mStartYear; + private final int mEndYear; + + /** + * How we notify users the date has changed. + */ + private OnDateChangedListener mOnDateChangedListener; + + private int mDay; + private int mMonth; + private int mYear; + + /** + * The callback used to indicate the user changes the date. + */ + public interface OnDateChangedListener { + + /** + * @param view The view associated with this listener. + * @param year The year that was set. + * @param monthOfYear The month that was set (0-11) for compatibility + * with {@link java.util.Calendar}. + * @param dayOfMonth The day of the month that was set. + */ + void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth); + } + + public DatePicker(Context context) { + this(context, null); + } + + public DatePicker(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DatePicker(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.date_picker, + this, // we are the parent + true); + + mDayPicker = (NumberPicker) findViewById(R.id.day); + mDayPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); + mDayPicker.setSpeed(100); + mDayPicker.setOnChangeListener(new OnChangedListener() { + public void onChanged(NumberPicker picker, int oldVal, int newVal) { + mDay = newVal; + if (mOnDateChangedListener != null) { + mOnDateChangedListener.onDateChanged(DatePicker.this, mYear, mMonth, mDay); + } + } + }); + mMonthPicker = (NumberPicker) findViewById(R.id.month); + mMonthPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); + DateFormatSymbols dfs = new DateFormatSymbols(); + mMonthPicker.setRange(1, 12, dfs.getShortMonths()); + mMonthPicker.setSpeed(200); + mMonthPicker.setOnChangeListener(new OnChangedListener() { + public void onChanged(NumberPicker picker, int oldVal, int newVal) { + + /* We display the month 1-12 but store it 0-11 so always + * subtract by one to ensure our internal state is always 0-11 + */ + mMonth = newVal - 1; + if (mOnDateChangedListener != null) { + mOnDateChangedListener.onDateChanged(DatePicker.this, mYear, mMonth, mDay); + } + updateDaySpinner(); + } + }); + mYearPicker = (NumberPicker) findViewById(R.id.year); + mYearPicker.setSpeed(100); + mYearPicker.setOnChangeListener(new OnChangedListener() { + public void onChanged(NumberPicker picker, int oldVal, int newVal) { + mYear = newVal; + if (mOnDateChangedListener != null) { + mOnDateChangedListener.onDateChanged(DatePicker.this, mYear, mMonth, mDay); + } + } + }); + + // attributes + TypedArray a = context + .obtainStyledAttributes(attrs, R.styleable.DatePicker); + + mStartYear = a.getInt(R.styleable.DatePicker_startYear, DEFAULT_START_YEAR); + mEndYear = a.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); + mYearPicker.setRange(mStartYear, mEndYear); + + a.recycle(); + + // initialize to current date + Calendar cal = Calendar.getInstance(); + init(cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH), + cal.get(Calendar.DAY_OF_MONTH), null); + + // re-order the number pickers to match the current date format + reorderPickers(); + + if (!isEnabled()) { + setEnabled(false); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mDayPicker.setEnabled(enabled); + mMonthPicker.setEnabled(enabled); + mYearPicker.setEnabled(enabled); + } + + private void reorderPickers() { + char[] order = DateFormat.getDateFormatOrder(mContext); + + /* Default order is month, date, year so if that's the order then + * do nothing. + */ + if ((order[0] == DateFormat.MONTH) && (order[1] == DateFormat.DATE)) { + return; + } + + /* Remove the 3 pickers from their parent and then add them back in the + * required order. + */ + LinearLayout parent = (LinearLayout) findViewById(R.id.parent); + parent.removeAllViews(); + for (char c : order) { + if (c == DateFormat.DATE) { + parent.addView(mDayPicker); + } else if (c == DateFormat.MONTH) { + parent.addView(mMonthPicker); + } else { + parent.addView (mYearPicker); + } + } + } + + public void updateDate(int year, int monthOfYear, int dayOfMonth) { + mYear = year; + mMonth = monthOfYear; + mDay = dayOfMonth; + updateSpinners(); + } + + private static class SavedState extends BaseSavedState { + + private final int mYear; + private final int mMonth; + private final int mDay; + + /** + * Constructor called from {@link DatePicker#onSaveInstanceState()} + */ + private SavedState(Parcelable superState, int year, int month, int day) { + super(superState); + mYear = year; + mMonth = month; + mDay = day; + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + mYear = in.readInt(); + mMonth = in.readInt(); + mDay = in.readInt(); + } + + public int getYear() { + return mYear; + } + + public int getMonth() { + return mMonth; + } + + public int getDay() { + return mDay; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mYear); + dest.writeInt(mMonth); + dest.writeInt(mDay); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Creator<SavedState>() { + + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + + /** + * Override so we are in complete control of save / restore for this widget. + */ + @Override + protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { + dispatchThawSelfOnly(container); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + return new SavedState(superState, mYear, mMonth, mDay); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mYear = ss.getYear(); + mMonth = ss.getMonth(); + mDay = ss.getDay(); + } + + /** + * Initialize the state. + * @param year The initial year. + * @param monthOfYear The initial month. + * @param dayOfMonth The initial day of the month. + * @param onDateChangedListener How user is notified date is changed by user, can be null. + */ + public void init(int year, int monthOfYear, int dayOfMonth, + OnDateChangedListener onDateChangedListener) { + mYear = year; + mMonth = monthOfYear; + mDay = dayOfMonth; + mOnDateChangedListener = onDateChangedListener; + updateSpinners(); + } + + private void updateSpinners() { + updateDaySpinner(); + mYearPicker.setCurrent(mYear); + + /* The month display uses 1-12 but our internal state stores it + * 0-11 so add one when setting the display. + */ + mMonthPicker.setCurrent(mMonth + 1); + } + + private void updateDaySpinner() { + Calendar cal = Calendar.getInstance(); + cal.set(mYear, mMonth, mDay); + int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH); + mDayPicker.setRange(1, max); + mDayPicker.setCurrent(mDay); + } + + public int getYear() { + return mYear; + } + + public int getMonth() { + return mMonth; + } + + public int getDayOfMonth() { + return mDay; + } +} diff --git a/core/java/android/widget/DialerFilter.java b/core/java/android/widget/DialerFilter.java new file mode 100644 index 0000000..a23887f --- /dev/null +++ b/core/java/android/widget/DialerFilter.java @@ -0,0 +1,431 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.view.KeyEvent; +import android.text.Editable; +import android.text.InputFilter; +import android.text.Selection; +import android.text.Spannable; +import android.text.Spanned; +import android.text.TextWatcher; +import android.text.method.DialerKeyListener; +import android.text.method.KeyListener; +import android.text.method.TextKeyListener; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.View; +import android.graphics.Rect; + + + +public class DialerFilter extends RelativeLayout +{ + public DialerFilter(Context context) { + super(context); + } + + public DialerFilter(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + // Setup the filter view + mInputFilters = new InputFilter[] { new InputFilter.AllCaps() }; + + mHint = (EditText) findViewById(com.android.internal.R.id.hint); + if (mHint == null) { + throw new IllegalStateException("DialerFilter must have a child EditText named hint"); + } + mHint.setFilters(mInputFilters); + + mLetters = mHint; + mLetters.setKeyListener(TextKeyListener.getInstance()); + mLetters.setMovementMethod(null); + mLetters.setFocusable(false); + + // Setup the digits view + mPrimary = (EditText) findViewById(com.android.internal.R.id.primary); + if (mPrimary == null) { + throw new IllegalStateException("DialerFilter must have a child EditText named primary"); + } + mPrimary.setFilters(mInputFilters); + + mDigits = mPrimary; + mDigits.setKeyListener(DialerKeyListener.getInstance()); + mDigits.setMovementMethod(null); + mDigits.setFocusable(false); + + // Look for an icon + mIcon = (ImageView) findViewById(com.android.internal.R.id.icon); + + // Setup focus & highlight for this view + setFocusable(true); + + // Default the mode based on the keyboard + KeyCharacterMap kmap + = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); + mIsQwerty = kmap.getKeyboardType() != KeyCharacterMap.NUMERIC; + if (mIsQwerty) { + Log.i("DialerFilter", "This device looks to be QWERTY"); +// setMode(DIGITS_AND_LETTERS); + } else { + Log.i("DialerFilter", "This device looks to be 12-KEY"); +// setMode(DIGITS_ONLY); + } + + // XXX Force the mode to QWERTY for now, since 12-key isn't supported + mIsQwerty = true; + setMode(DIGITS_AND_LETTERS); + } + + /** + * Only show the icon view when focused, if there is one. + */ + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + + if (mIcon != null) { + mIcon.setVisibility(focused ? View.VISIBLE : View.GONE); + } + } + + + public boolean isQwertyKeyboard() { + return mIsQwerty; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + boolean handled = false; + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + break; + + case KeyEvent.KEYCODE_DEL: + switch (mMode) { + case DIGITS_AND_LETTERS: + handled = mDigits.onKeyDown(keyCode, event); + handled &= mLetters.onKeyDown(keyCode, event); + break; + + case DIGITS_AND_LETTERS_NO_DIGITS: + handled = mLetters.onKeyDown(keyCode, event); + if (mLetters.getText().length() == mDigits.getText().length()) { + setMode(DIGITS_AND_LETTERS); + } + break; + + case DIGITS_AND_LETTERS_NO_LETTERS: + if (mDigits.getText().length() == mLetters.getText().length()) { + mLetters.onKeyDown(keyCode, event); + setMode(DIGITS_AND_LETTERS); + } + handled = mDigits.onKeyDown(keyCode, event); + break; + + case DIGITS_ONLY: + handled = mDigits.onKeyDown(keyCode, event); + break; + + case LETTERS_ONLY: + handled = mLetters.onKeyDown(keyCode, event); + break; + } + break; + + default: + //mIsQwerty = msg.getKeyIsQwertyKeyboard(); + + switch (mMode) { + case DIGITS_AND_LETTERS: + handled = mLetters.onKeyDown(keyCode, event); + + // pass this throw so the shift state is correct (for example, + // on a standard QWERTY keyboard, * and 8 are on the same key) + if (KeyEvent.isModifierKey(keyCode)) { + mDigits.onKeyDown(keyCode, event); + handled = true; + break; + } + + // Only check to see if the digit is valid if the key is a printing key + // in the TextKeyListener. This prevents us from hiding the digits + // line when keys like UP and DOWN are hit. + // XXX note that KEYCODE_TAB is special-cased here for + // devices that share tab and 0 on a single key. + boolean isPrint = event.isPrintingKey(); + if (isPrint || keyCode == KeyEvent.KEYCODE_SPACE + || keyCode == KeyEvent.KEYCODE_TAB) { + char c = event.getMatch(DialerKeyListener.CHARACTERS); + if (c != 0) { + handled &= mDigits.onKeyDown(keyCode, event); + } else { + setMode(DIGITS_AND_LETTERS_NO_DIGITS); + } + } + break; + + case DIGITS_AND_LETTERS_NO_LETTERS: + case DIGITS_ONLY: + handled = mDigits.onKeyDown(keyCode, event); + break; + + case DIGITS_AND_LETTERS_NO_DIGITS: + case LETTERS_ONLY: + handled = mLetters.onKeyDown(keyCode, event); + break; + } + } + + if (!handled) { + return super.onKeyDown(keyCode, event); + } else { + return true; + } + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + boolean a = mLetters.onKeyUp(keyCode, event); + boolean b = mDigits.onKeyUp(keyCode, event); + return a || b; + } + + public int getMode() { + return mMode; + } + + /** + * Change the mode of the widget. + * + * @param newMode The mode to switch to. + */ + public void setMode(int newMode) { + switch (newMode) { + case DIGITS_AND_LETTERS: + makeDigitsPrimary(); + mLetters.setVisibility(View.VISIBLE); + mDigits.setVisibility(View.VISIBLE); + break; + + case DIGITS_ONLY: + makeDigitsPrimary(); + mLetters.setVisibility(View.GONE); + mDigits.setVisibility(View.VISIBLE); + break; + + case LETTERS_ONLY: + makeLettersPrimary(); + mLetters.setVisibility(View.VISIBLE); + mDigits.setVisibility(View.GONE); + break; + + case DIGITS_AND_LETTERS_NO_LETTERS: + makeDigitsPrimary(); + mLetters.setVisibility(View.INVISIBLE); + mDigits.setVisibility(View.VISIBLE); + break; + + case DIGITS_AND_LETTERS_NO_DIGITS: + makeLettersPrimary(); + mLetters.setVisibility(View.VISIBLE); + mDigits.setVisibility(View.INVISIBLE); + break; + + } + int oldMode = mMode; + mMode = newMode; + onModeChange(oldMode, newMode); + } + + private void makeLettersPrimary() { + if (mPrimary == mDigits) { + swapPrimaryAndHint(true); + } + } + + private void makeDigitsPrimary() { + if (mPrimary == mLetters) { + swapPrimaryAndHint(false); + } + } + + private void swapPrimaryAndHint(boolean makeLettersPrimary) { + Editable lettersText = mLetters.getText(); + Editable digitsText = mDigits.getText(); + KeyListener lettersInput = mLetters.getKeyListener(); + KeyListener digitsInput = mDigits.getKeyListener(); + + if (makeLettersPrimary) { + mLetters = mPrimary; + mDigits = mHint; + } else { + mLetters = mHint; + mDigits = mPrimary; + } + + mLetters.setKeyListener(lettersInput); + mLetters.setText(lettersText); + lettersText = mLetters.getText(); + Selection.setSelection(lettersText, lettersText.length()); + + mDigits.setKeyListener(digitsInput); + mDigits.setText(digitsText); + digitsText = mDigits.getText(); + Selection.setSelection(digitsText, digitsText.length()); + + // Reset the filters + mPrimary.setFilters(mInputFilters); + mHint.setFilters(mInputFilters); + } + + + public CharSequence getLetters() { + if (mLetters.getVisibility() == View.VISIBLE) { + return mLetters.getText(); + } else { + return ""; + } + } + + public CharSequence getDigits() { + if (mDigits.getVisibility() == View.VISIBLE) { + return mDigits.getText(); + } else { + return ""; + } + } + + public CharSequence getFilterText() { + if (mMode != DIGITS_ONLY) { + return getLetters(); + } else { + return getDigits(); + } + } + + public void append(String text) { + switch (mMode) { + case DIGITS_AND_LETTERS: + mDigits.getText().append(text); + mLetters.getText().append(text); + break; + + case DIGITS_AND_LETTERS_NO_LETTERS: + case DIGITS_ONLY: + mDigits.getText().append(text); + break; + + case DIGITS_AND_LETTERS_NO_DIGITS: + case LETTERS_ONLY: + mLetters.getText().append(text); + break; + } + } + + /** + * Clears both the digits and the filter text. + */ + public void clearText() { + Editable text; + + text = mLetters.getText(); + text.clear(); + + text = mDigits.getText(); + text.clear(); + + // Reset the mode based on the hardware type + if (mIsQwerty) { + setMode(DIGITS_AND_LETTERS); + } else { + setMode(DIGITS_ONLY); + } + } + + public void setLettersWatcher(TextWatcher watcher) { + CharSequence text = mLetters.getText(); + Spannable span = (Spannable)text; + span.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + public void setDigitsWatcher(TextWatcher watcher) { + CharSequence text = mDigits.getText(); + Spannable span = (Spannable)text; + span.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + public void setFilterWatcher(TextWatcher watcher) { + if (mMode != DIGITS_ONLY) { + setLettersWatcher(watcher); + } else { + setDigitsWatcher(watcher); + } + } + + public void removeFilterWatcher(TextWatcher watcher) { + Spannable text; + if (mMode != DIGITS_ONLY) { + text = mLetters.getText(); + } else { + text = mDigits.getText(); + } + text.removeSpan(watcher); + } + + /** + * Called right after the mode changes to give subclasses the option to + * restyle, etc. + */ + protected void onModeChange(int oldMode, int newMode) { + } + + /** This mode has both lines */ + public static final int DIGITS_AND_LETTERS = 1; + /** This mode is when after starting in {@link #DIGITS_AND_LETTERS} mode the filter + * has removed all possibility of the digits matching, leaving only the letters line */ + public static final int DIGITS_AND_LETTERS_NO_DIGITS = 2; + /** This mode is when after starting in {@link #DIGITS_AND_LETTERS} mode the filter + * has removed all possibility of the letters matching, leaving only the digits line */ + public static final int DIGITS_AND_LETTERS_NO_LETTERS = 3; + /** This mode has only the digits line */ + public static final int DIGITS_ONLY = 4; + /** This mode has only the letters line */ + public static final int LETTERS_ONLY = 5; + + EditText mLetters; + EditText mDigits; + EditText mPrimary; + EditText mHint; + InputFilter mInputFilters[]; + ImageView mIcon; + int mMode; + private boolean mIsQwerty; +} diff --git a/core/java/android/widget/DigitalClock.java b/core/java/android/widget/DigitalClock.java new file mode 100644 index 0000000..3ca2c81 --- /dev/null +++ b/core/java/android/widget/DigitalClock.java @@ -0,0 +1,135 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.SystemClock; +import android.pim.DateFormat; +import android.provider.Settings; +import android.util.AttributeSet; + +import java.util.Calendar; + +/** + * Like AnalogClock, but digital. Shows seconds. + * + * FIXME: implement separate views for hours/minutes/seconds, so + * proportional fonts don't shake rendering + */ + +public class DigitalClock extends TextView { + + Calendar mCalendar; + private final static String m12 = "h:mm:ss aa"; + private final static String m24 = "k:mm:ss"; + private FormatChangeObserver mFormatChangeObserver; + + private Runnable mTicker; + private Handler mHandler; + + private boolean mTickerStopped = false; + + String mFormat; + + public DigitalClock(Context context) { + super(context); + initClock(context); + } + + public DigitalClock(Context context, AttributeSet attrs) { + super(context, attrs); + initClock(context); + } + + private void initClock(Context context) { + Resources r = mContext.getResources(); + + if (mCalendar == null) { + mCalendar = Calendar.getInstance(); + } + + mFormatChangeObserver = new FormatChangeObserver(); + getContext().getContentResolver().registerContentObserver( + Settings.System.CONTENT_URI, true, mFormatChangeObserver); + + setFormat(); + } + + @Override + protected void onAttachedToWindow() { + mTickerStopped = false; + super.onAttachedToWindow(); + mHandler = new Handler(); + + /** + * requests a tick on the next hard-second boundary + */ + mTicker = new Runnable() { + public void run() { + if (mTickerStopped) return; + mCalendar.setTimeInMillis(System.currentTimeMillis()); + setText(DateFormat.format(mFormat, mCalendar)); + invalidate(); + long now = SystemClock.uptimeMillis(); + long next = now + (1000 - now % 1000); + mHandler.postAtTime(mTicker, next); + } + }; + mTicker.run(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mTickerStopped = true; + } + + /** + * Pulls 12/24 mode from system settings + */ + private boolean get24HourMode() { + String value = Settings.System.getString( + getContext().getContentResolver(), + Settings.System.TIME_12_24); + + if (value == null || value.equals("12")) + return false; + return true; + } + + private void setFormat() { + if (get24HourMode()) { + mFormat = m24; + } else { + mFormat = m12; + } + } + + private class FormatChangeObserver extends ContentObserver { + public FormatChangeObserver() { + super(new Handler()); + } + + @Override + public void onChange(boolean selfChange) { + setFormat(); + } + } +} diff --git a/core/java/android/widget/DoubleDigitManager.java b/core/java/android/widget/DoubleDigitManager.java new file mode 100644 index 0000000..1eea1fb --- /dev/null +++ b/core/java/android/widget/DoubleDigitManager.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.os.Handler; + +/** + * Provides callbacks indicating the steps in two digit pressing within a + * timeout. + * + * Package private: only relevant in helping {@link TimeSpinnerHelper}. + */ +class DoubleDigitManager { + + private final long timeoutInMillis; + private final CallBack mCallBack; + + private Integer intermediateDigit; + + /** + * @param timeoutInMillis How long after the first digit is pressed does + * the user have to press the second digit? + * @param callBack The callback to indicate what's going on with the user. + */ + public DoubleDigitManager(long timeoutInMillis, CallBack callBack) { + this.timeoutInMillis = timeoutInMillis; + mCallBack = callBack; + } + + /** + * Report to this manager that a digit was pressed. + * @param digit + */ + public void reportDigit(int digit) { + if (intermediateDigit == null) { + intermediateDigit = digit; + + new Handler().postDelayed(new Runnable() { + public void run() { + if (intermediateDigit != null) { + mCallBack.singleDigitFinal(intermediateDigit); + intermediateDigit = null; + } + } + }, timeoutInMillis); + + if (!mCallBack.singleDigitIntermediate(digit)) { + + // this wasn't a good candidate for the intermediate digit, + // make it the final digit (since there is no opportunity to + // reject the final digit). + intermediateDigit = null; + mCallBack.singleDigitFinal(digit); + } + } else if (mCallBack.twoDigitsFinal(intermediateDigit, digit)) { + intermediateDigit = null; + } + } + + /** + * The callback to indicate what is going on with the digits pressed. + */ + static interface CallBack { + + /** + * A digit was pressed, and there are no intermediate digits. + * @param digit The digit pressed. + * @return Whether the digit was accepted; how the user of this manager + * tells us that the intermediate digit is acceptable as an + * intermediate digit. + */ + boolean singleDigitIntermediate(int digit); + + /** + * A single digit was pressed, and it is 'the final answer'. + * - a single digit pressed, and the timeout expires. + * - a single digit pressed, and {@link #singleDigitIntermediate} + * returned false. + * @param digit The digit. + */ + void singleDigitFinal(int digit); + + /** + * The user pressed digit1, then digit2 within the timeout. + * @param digit1 + * @param digit2 + */ + boolean twoDigitsFinal(int digit1, int digit2); + } + +} diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java new file mode 100644 index 0000000..e89a2bd --- /dev/null +++ b/core/java/android/widget/EditText.java @@ -0,0 +1,102 @@ +/* + * 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 android.widget; + +import android.text.*; +import android.text.method.*; +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; + + +/* + * This is supposed to be a *very* thin veneer over TextView. + * Do not make any changes here that do anything that a TextView + * with a key listener and a movement method wouldn't do! + */ + +/** + * EditText is a thin veneer over TextView that configures itself + * to be editable. + * <p> + * <b>XML attributes</b> + * <p> + * See {@link android.R.styleable#EditText EditText Attributes}, + * {@link android.R.styleable#TextView TextView Attributes}, + * {@link android.R.styleable#View View Attributes} + */ +public class EditText extends TextView { + public EditText(Context context) { + this(context, null); + } + + public EditText(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.editTextStyle); + } + + public EditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected boolean getDefaultEditable() { + return true; + } + + @Override + protected MovementMethod getDefaultMovementMethod() { + return ArrowKeyMovementMethod.getInstance(); + } + + @Override + public Editable getText() { + return (Editable) super.getText(); + } + + @Override + public void setText(CharSequence text, BufferType type) { + super.setText(text, BufferType.EDITABLE); + } + + /** + * Convenience for {@link Selection#setSelection(Spannable, int, int)}. + */ + public void setSelection(int start, int stop) { + Selection.setSelection(getText(), start, stop); + } + + /** + * Convenience for {@link Selection#setSelection(Spannable, int)}. + */ + public void setSelection(int index) { + Selection.setSelection(getText(), index); + } + + /** + * Convenience for {@link Selection#selectAll}. + */ + public void selectAll() { + Selection.selectAll(getText()); + } + + /** + * Convenience for {@link Selection#extendSelection}. + */ + public void extendSelection(int index) { + Selection.extendSelection(getText(), index); + } +} diff --git a/core/java/android/widget/ExpandableListAdapter.java b/core/java/android/widget/ExpandableListAdapter.java new file mode 100644 index 0000000..b75983c --- /dev/null +++ b/core/java/android/widget/ExpandableListAdapter.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.database.DataSetObserver; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +/** + * An adapter that links a {@link ExpandableListView} with the underlying + * data. The implementation of this interface will provide access + * to the data of the children (categorized by groups), and also instantiate + * {@link View}s for children and groups. + */ +public interface ExpandableListAdapter { + /** + * @see Adapter#registerDataSetObserver(DataSetObserver) + */ + void registerDataSetObserver(DataSetObserver observer); + + /** + * @see Adapter#unregisterDataSetObserver(DataSetObserver) + */ + void unregisterDataSetObserver(DataSetObserver observer); + + /** + * Gets the number of groups. + * + * @return the number of groups + */ + int getGroupCount(); + + /** + * Gets the number of children in a specified group. + * + * @param groupPosition the position of the group for which the children + * count should be returned + * @return the children count in the specified group + */ + int getChildrenCount(int groupPosition); + + /** + * Gets the data associated with the given group. + * + * @param groupPosition the position of the group + * @return the data child for the specified group + */ + Object getGroup(int groupPosition); + + /** + * Gets the data associated with the given child within the given group. + * + * @param groupPosition the position of the group that the child resides in + * @param childPosition the position of the child with respect to other + * children in the group + * @return the data of the child + */ + Object getChild(int groupPosition, int childPosition); + + /** + * Gets the ID for the group at the given position. This group ID must be + * unique across groups. The combined ID (see + * {@link #getCombinedGroupId(long)}) must be unique across ALL items + * (groups and all children). + * + * @param groupPosition the position of the group for which the ID is wanted + * @return the ID associated with the group + */ + long getGroupId(int groupPosition); + + /** + * Gets the ID for the given child within the given group. This ID must be + * unique across all children within the group. The combined ID (see + * {@link #getCombinedChildId(long, long)}) must be unique across ALL items + * (groups and all children). + * + * @param groupPosition the position of the group that contains the child + * @param childPosition the position of the child within the group for which + * the ID is wanted + * @return the ID associated with the child + */ + long getChildId(int groupPosition, int childPosition); + + /** + * Indicates whether the child and group IDs are stable across changes to the + * underlying data. + * + * @return whether or not the same ID always refers to the same object + * @see Adapter#hasStableIds() + */ + boolean hasStableIds(); + + /** + * Gets a View that displays the given group. This View is only for the + * group--the Views for the group's children will be fetched using + * getChildrenView. + * + * @param groupPosition the position of the group for which the View is + * returned + * @param isExpanded whether the group is expanded or collapsed + * @param convertView the old view to reuse, if possible. You should check + * that this view is non-null and of an appropriate type before + * using. If it is not possible to convert this view to display + * the correct data, this method can create a new view. It is not + * guaranteed that the convertView will have been previously + * created by + * {@link #getGroupView(int, boolean, View, ViewGroup)}. + * @param parent the parent that this view will eventually be attached to + * @return the View corresponding to the group at the specified position + */ + View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent); + + /** + * Gets a View that displays the data for the given child within the given + * group. + * + * @param groupPosition the position of the group that contains the child + * @param childPosition the position of the child (for which the View is + * returned) within the group + * @param isLastChild Whether the child is the last child within the group + * @param convertView the old view to reuse, if possible. You should check + * that this view is non-null and of an appropriate type before + * using. If it is not possible to convert this view to display + * the correct data, this method can create a new view. It is not + * guaranteed that the convertView will have been previously + * created by + * {@link #getChildView(int, int, boolean, View, ViewGroup)}. + * @param parent the parent that this view will eventually be attached to + * @return the View corresponding to the child at the specified position + */ + View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent); + + /** + * Whether the child at the specified position is selectable. + * + * @param groupPosition the position of the group that contains the child + * @param childPosition the position of the child within the group + * @return whether the child is selectable. + */ + boolean isChildSelectable(int groupPosition, int childPosition); + + /** + * @see ListAdapter#areAllItemsEnabled() + */ + boolean areAllItemsEnabled(); + + /** + * @see ListAdapter#isEmpty() + */ + boolean isEmpty(); + + /** + * Called when a group is expanded. + * + * @param groupPosition The group being expanded. + */ + void onGroupExpanded(int groupPosition); + + /** + * Called when a group is collapsed. + * + * @param groupPosition The group being collapsed. + */ + void onGroupCollapsed(int groupPosition); + + /** + * Gets an ID for a child that is unique across any item (either group or + * child) that is in this list. Expandable lists require each item (group or + * child) to have a unique ID among all children and groups in the list. + * This method is responsible for returning that unique ID given a child's + * ID and its group's ID. Furthermore, if {@link #hasStableIds()} is true, the + * returned ID must be stable as well. + * + * @param groupId The ID of the group that contains this child. + * @param childId The ID of the child. + * @return The unique (and possibly stable) ID of the child across all + * groups and children in this list. + */ + long getCombinedChildId(long groupId, long childId); + + /** + * Gets an ID for a group that is unique across any item (either group or + * child) that is in this list. Expandable lists require each item (group or + * child) to have a unique ID among all children and groups in the list. + * This method is responsible for returning that unique ID given a group's + * ID. Furthermore, if {@link #hasStableIds()} is true, the returned ID must be + * stable as well. + * + * @param groupId The ID of the group + * @return The unique (and possibly stable) ID of the group across all + * groups and children in this list. + */ + long getCombinedGroupId(long groupId); +} diff --git a/core/java/android/widget/ExpandableListConnector.java b/core/java/android/widget/ExpandableListConnector.java new file mode 100644 index 0000000..ddedea3 --- /dev/null +++ b/core/java/android/widget/ExpandableListConnector.java @@ -0,0 +1,797 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.database.DataSetObserver; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +/* + * Implementation notes: + * + * <p> + * Terminology: + * <li> flPos - Flat list position, the position used by ListView + * <li> gPos - Group position, the position of a group among all the groups + * <li> cPos - Child position, the position of a child among all the children + * in a group + */ + +/** + * A {@link BaseAdapter} that provides data/Views in an expandable list (offers + * features such as collapsing/expanding groups containing children). By + * itself, this adapter has no data and is a connector to a + * {@link ExpandableListAdapter} which provides the data. + * <p> + * Internally, this connector translates the flat list position that the + * ListAdapter expects to/from group and child positions that the ExpandableListAdapter + * expects. + */ +class ExpandableListConnector extends BaseAdapter implements Filterable { + /** + * The ExpandableListAdapter to fetch the data/Views for this expandable list + */ + private ExpandableListAdapter mExpandableListAdapter; + + /** + * List of metadata for the currently expanded groups. The metadata consists + * of data essential for efficiently translating between flat list positions + * and group/child positions. See {@link GroupMetadata}. + */ + private ArrayList<GroupMetadata> mExpGroupMetadataList; + + /** The number of children from all currently expanded groups */ + private int mTotalExpChildrenCount; + + /** The maximum number of allowable expanded groups. Defaults to 'no limit' */ + private int mMaxExpGroupCount = Integer.MAX_VALUE; + + /** Change observer used to have ExpandableListAdapter changes pushed to us */ + private DataSetObserver mDataSetObserver = new MyDataSetObserver(); + + /** + * Constructs the connector + */ + public ExpandableListConnector(ExpandableListAdapter expandableListAdapter) { + mExpGroupMetadataList = new ArrayList<GroupMetadata>(); + + setExpandableListAdapter(expandableListAdapter); + } + + /** + * Point to the {@link ExpandableListAdapter} that will give us data/Views + * + * @param expandableListAdapter the adapter that supplies us with data/Views + */ + public void setExpandableListAdapter(ExpandableListAdapter expandableListAdapter) { + if (mExpandableListAdapter != null) { + mExpandableListAdapter.unregisterDataSetObserver(mDataSetObserver); + } + + mExpandableListAdapter = expandableListAdapter; + expandableListAdapter.registerDataSetObserver(mDataSetObserver); + } + + /** + * Translates a flat list position to either a) group pos if the specified + * flat list position corresponds to a group, or b) child pos if it + * corresponds to a child. Performs a binary search on the expanded + * groups list to find the flat list pos if it is an exp group, otherwise + * finds where the flat list pos fits in between the exp groups. + * + * @param flPos the flat list position to be translated + * @return the group position or child position of the specified flat list + * position encompassed in a {@link PositionMetadata} object + * that contains additional useful info for insertion, etc. + */ + PositionMetadata getUnflattenedPos(final int flPos) { + /* Keep locally since frequent use */ + final ArrayList<GroupMetadata> egml = mExpGroupMetadataList; + final int numExpGroups = egml.size(); + + /* Binary search variables */ + int leftExpGroupIndex = 0; + int rightExpGroupIndex = numExpGroups - 1; + int midExpGroupIndex = 0; + GroupMetadata midExpGm; + + if (numExpGroups == 0) { + /* + * There aren't any expanded groups (hence no visible children + * either), so flPos must be a group and its group pos will be the + * same as its flPos + */ + return new PositionMetadata(flPos, ExpandableListPosition.GROUP, flPos, + -1, null, 0); + } + + /* + * Binary search over the expanded groups to find either the exact + * expanded group (if we're looking for a group) or the group that + * contains the child we're looking for. If we are looking for a + * collapsed group, we will not have a direct match here, but we will + * find the expanded group just before the group we're searching for (so + * then we can calculate the group position of the group we're searching + * for). If there isn't an expanded group prior to the group being + * searched for, then the group being searched for's group position is + * the same as the flat list position (since there are no children before + * it, and all groups before it are collapsed). + */ + while (leftExpGroupIndex <= rightExpGroupIndex) { + midExpGroupIndex = + (rightExpGroupIndex - leftExpGroupIndex) / 2 + + leftExpGroupIndex; + midExpGm = egml.get(midExpGroupIndex); + + if (flPos > midExpGm.lastChildFlPos) { + /* + * The flat list position is after the current middle group's + * last child's flat list position, so search right + */ + leftExpGroupIndex = midExpGroupIndex + 1; + } else if (flPos < midExpGm.flPos) { + /* + * The flat list position is before the current middle group's + * flat list position, so search left + */ + rightExpGroupIndex = midExpGroupIndex - 1; + } else if (flPos == midExpGm.flPos) { + /* + * The flat list position is this middle group's flat list + * position, so we've found an exact hit + */ + return new PositionMetadata(flPos, ExpandableListPosition.GROUP, + midExpGm.gPos, -1, midExpGm, midExpGroupIndex); + } else if (flPos <= midExpGm.lastChildFlPos + /* && flPos > midGm.flPos as deduced from previous + * conditions */) { + /* The flat list position is a child of the middle group */ + + /* + * Subtract the first child's flat list position from the + * specified flat list pos to get the child's position within + * the group + */ + final int childPos = flPos - (midExpGm.flPos + 1); + return new PositionMetadata(flPos, ExpandableListPosition.CHILD, + midExpGm.gPos, childPos, midExpGm, midExpGroupIndex); + } + } + + /* + * If we've reached here, it means the flat list position must be a + * group that is not expanded, since otherwise we would have hit it + * in the above search. + */ + + + /* If we are to expand this group later, where would it go in the + * mExpGroupMetadataList ? */ + int insertPosition = 0; + + /* What is its group position from the list of all groups? */ + int groupPos = 0; + + /* + * To figure out exact insertion and prior group positions, we need to + * determine how we broke out of the binary search. We backtrack + * to see this. + */ + if (leftExpGroupIndex > midExpGroupIndex) { + + /* + * This would occur in the first conditional, so the flat list + * insertion position is after the left group. Also, the + * leftGroupPos is one more than it should be (since that broke out + * of our binary search), so we decrement it. + */ + final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1); + + insertPosition = leftExpGroupIndex; + + /* + * Sums the number of groups between the prior exp group and this + * one, and then adds it to the prior group's group pos + */ + groupPos = + (flPos - leftExpGm.lastChildFlPos) + leftExpGm.gPos; + } else if (rightExpGroupIndex < midExpGroupIndex) { + + /* + * This would occur in the second conditional, so the flat list + * insertion position is before the right group. Also, the + * rightGroupPos is one less than it should be, so increment it. + */ + final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex); + + insertPosition = rightExpGroupIndex; + + /* + * Subtracts this group's flat list pos from the group after's flat + * list position to find out how many groups are in between the two + * groups. Then, subtracts that number from the group after's group + * pos to get this group's pos. + */ + groupPos = rightExpGm.gPos - (rightExpGm.flPos - flPos); + } else { + // TODO: clean exit + throw new RuntimeException("Unknown state"); + } + + return new PositionMetadata(flPos, ExpandableListPosition.GROUP, groupPos, -1, + null, insertPosition); + } + + /** + * Translates either a group pos or a child pos (+ group it belongs to) to a + * flat list position. If searching for a child and its group is not expanded, this will + * return null since the child isn't being shown in the ListView, and hence it has no + * position. + * + * @param pos a {@link ExpandableListPosition} representing either a group position + * or child position + * @return the flat list position encompassed in a {@link PositionMetadata} + * object that contains additional useful info for insertion, etc. + */ + PositionMetadata getFlattenedPos(final ExpandableListPosition pos) { + final ArrayList<GroupMetadata> egml = mExpGroupMetadataList; + final int numExpGroups = egml.size(); + + /* Binary search variables */ + int leftExpGroupIndex = 0; + int rightExpGroupIndex = numExpGroups - 1; + int midExpGroupIndex = 0; + GroupMetadata midExpGm; + + if (numExpGroups == 0) { + /* + * There aren't any expanded groups, so flPos must be a group and + * its flPos will be the same as its group pos. The + * insert position is 0 (since the list is empty). + */ + return new PositionMetadata(pos.groupPos, pos.type, + pos.groupPos, pos.childPos, null, 0); + } + + /* + * Binary search over the expanded groups to find either the exact + * expanded group (if we're looking for a group) or the group that + * contains the child we're looking for. + */ + while (leftExpGroupIndex <= rightExpGroupIndex) { + midExpGroupIndex = (rightExpGroupIndex - leftExpGroupIndex)/2 + leftExpGroupIndex; + midExpGm = egml.get(midExpGroupIndex); + + if (pos.groupPos > midExpGm.gPos) { + /* + * It's after the current middle group, so search right + */ + leftExpGroupIndex = midExpGroupIndex + 1; + } else if (pos.groupPos < midExpGm.gPos) { + /* + * It's before the current middle group, so search left + */ + rightExpGroupIndex = midExpGroupIndex - 1; + } else if (pos.groupPos == midExpGm.gPos) { + /* + * It's this middle group, exact hit + */ + + if (pos.type == ExpandableListPosition.GROUP) { + /* If it's a group, give them this matched group's flPos */ + return new PositionMetadata(midExpGm.flPos, pos.type, + pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex); + } else if (pos.type == ExpandableListPosition.CHILD) { + /* If it's a child, calculate the flat list pos */ + return new PositionMetadata(midExpGm.flPos + pos.childPos + + 1, pos.type, pos.groupPos, pos.childPos, + midExpGm, midExpGroupIndex); + } else { + return null; + } + } + } + + /* + * If we've reached here, it means there was no match in the expanded + * groups, so it must be a collapsed group that they're search for + */ + if (pos.type != ExpandableListPosition.GROUP) { + /* If it isn't a group, return null */ + return null; + } + + /* + * To figure out exact insertion and prior group positions, we need to + * determine how we broke out of the binary search. We backtrack to see + * this. + */ + if (leftExpGroupIndex > midExpGroupIndex) { + + /* + * This would occur in the first conditional, so the flat list + * insertion position is after the left group. + * + * The leftGroupPos is one more than it should be (from the binary + * search loop) so we subtract 1 to get the actual left group. Since + * the insertion point is AFTER the left group, we keep this +1 + * value as the insertion point + */ + final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1); + final int flPos = + leftExpGm.lastChildFlPos + + (pos.groupPos - leftExpGm.gPos); + + return new PositionMetadata(flPos, pos.type, pos.groupPos, + pos.childPos, null, leftExpGroupIndex); + } else if (rightExpGroupIndex < midExpGroupIndex) { + + /* + * This would occur in the second conditional, so the flat list + * insertion position is before the right group. Also, the + * rightGroupPos is one less than it should be (from binary search + * loop), so we increment to it. + */ + final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex); + final int flPos = + rightExpGm.flPos + - (rightExpGm.gPos - pos.groupPos); + return new PositionMetadata(flPos, pos.type, pos.groupPos, + pos.childPos, null, rightExpGroupIndex); + } else { + return null; + } + } + + @Override + public boolean areAllItemsEnabled() { + return mExpandableListAdapter.areAllItemsEnabled(); + } + + @Override + public boolean isEnabled(int flatListPos) { + final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position; + + if (pos.type == ExpandableListPosition.CHILD) { + return mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos); + } else { + // Groups are always selectable + return true; + } + } + + public int getCount() { + /* + * Total count for the list view is the number groups plus the + * number of children from currently expanded groups (a value we keep + * cached in this class) + */ + return mExpandableListAdapter.getGroupCount() + mTotalExpChildrenCount; + } + + public Object getItem(int flatListPos) { + final PositionMetadata posMetadata = getUnflattenedPos(flatListPos); + + if (posMetadata.position.type == ExpandableListPosition.GROUP) { + return mExpandableListAdapter + .getGroup(posMetadata.position.groupPos); + } else if (posMetadata.position.type == ExpandableListPosition.CHILD) { + return mExpandableListAdapter.getChild(posMetadata.position.groupPos, + posMetadata.position.childPos); + } else { + // TODO: clean exit + throw new RuntimeException("Flat list position is of unknown type"); + } + } + + public long getItemId(int flatListPos) { + final PositionMetadata posMetadata = getUnflattenedPos(flatListPos); + final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos); + + if (posMetadata.position.type == ExpandableListPosition.GROUP) { + return mExpandableListAdapter.getCombinedGroupId(groupId); + } else if (posMetadata.position.type == ExpandableListPosition.CHILD) { + final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos, + posMetadata.position.childPos); + return mExpandableListAdapter.getCombinedChildId(groupId, childId); + } else { + // TODO: clean exit + throw new RuntimeException("Flat list position is of unknown type"); + } + } + + public View getView(int flatListPos, View convertView, ViewGroup parent) { + final PositionMetadata posMetadata = getUnflattenedPos(flatListPos); + + if (posMetadata.position.type == ExpandableListPosition.GROUP) { + return mExpandableListAdapter.getGroupView(posMetadata.position.groupPos, posMetadata + .isExpanded(), convertView, parent); + } else if (posMetadata.position.type == ExpandableListPosition.CHILD) { + final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos; + + final View view = mExpandableListAdapter.getChildView(posMetadata.position.groupPos, + posMetadata.position.childPos, isLastChild, convertView, parent); + + return view; + } else { + // TODO: clean exit + throw new RuntimeException("Flat list position is of unknown type"); + } + } + + @Override + public int getItemViewType(int flatListPos) { + final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position; + + if (pos.type == ExpandableListPosition.GROUP) { + return 0; + } else { + return 1; + } + } + + @Override + public int getViewTypeCount() { + return 2; + } + + @Override + public boolean hasStableIds() { + return mExpandableListAdapter.hasStableIds(); + } + + /** + * Traverses the expanded group metadata list and fills in the flat list + * positions. + * + * @param forceChildrenCountRefresh Forces refreshing of the children count + * for all expanded groups. + */ + private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh) { + final ArrayList<GroupMetadata> egml = mExpGroupMetadataList; + final int egmlSize = egml.size(); + int curFlPos = 0; + + /* Update child count as we go through */ + mTotalExpChildrenCount = 0; + + GroupMetadata curGm; + int gChildrenCount; + int lastGPos = 0; + for (int i = 0; i < egmlSize; i++) { + /* Store in local variable since we'll access freq */ + curGm = egml.get(i); + + /* + * Get the number of children, try to refrain from calling + * another class's method unless we have to (so do a subtraction) + */ + if ((curGm.lastChildFlPos == GroupMetadata.REFRESH) || forceChildrenCountRefresh) { + gChildrenCount = mExpandableListAdapter.getChildrenCount(curGm.gPos); + } else { + /* Num children for this group is its last child's fl pos minus + * the group's fl pos + */ + gChildrenCount = curGm.lastChildFlPos - curGm.flPos; + } + + /* Update */ + mTotalExpChildrenCount += gChildrenCount; + + /* + * This skips the collapsed groups and increments the flat list + * position (for subsequent exp groups) by accounting for the collapsed + * groups + */ + curFlPos += (curGm.gPos - lastGPos); + lastGPos = curGm.gPos; + + /* Update the flat list positions, and the current flat list pos */ + curGm.flPos = curFlPos; + curFlPos += gChildrenCount; + curGm.lastChildFlPos = curFlPos; + } + } + + /** + * Collapse a group in the grouped list view + * + * @param groupPos position of the group to collapse + */ + boolean collapseGroup(int groupPos) { + return collapseGroup(getFlattenedPos(new ExpandableListPosition(ExpandableListPosition.GROUP, + groupPos, -1, -1))); + } + + boolean collapseGroup(PositionMetadata posMetadata) { + /* + * Collapsing requires removal from mExpGroupMetadataList + */ + + /* + * If it is null, it must be already collapsed. This group metadata + * object should have been set from the search that returned the + * position metadata object. + */ + if (posMetadata.groupMetadata == null) return false; + + // Remove the group from the list of expanded groups + mExpGroupMetadataList.remove(posMetadata.groupMetadata); + + // Refresh the metadata + refreshExpGroupMetadataList(false); + + // Notify of change + notifyDataSetChanged(); + + // Give the callback + mExpandableListAdapter.onGroupCollapsed(posMetadata.groupMetadata.gPos); + + return true; + } + + /** + * Expand a group in the grouped list view + * @param groupPos the group to be expanded + */ + boolean expandGroup(int groupPos) { + return expandGroup(getFlattenedPos(new ExpandableListPosition(ExpandableListPosition.GROUP, + groupPos, -1, -1))); + } + + boolean expandGroup(PositionMetadata posMetadata) { + /* + * Expanding requires insertion into the mExpGroupMetadataList + */ + + if (posMetadata.position.groupPos < 0) { + // TODO clean exit + throw new RuntimeException("Need group"); + } + + if (mMaxExpGroupCount == 0) return false; + + // Check to see if it's already expanded + if (posMetadata.groupMetadata != null) return false; + + /* Restrict number of exp groups to mMaxExpGroupCount */ + if (mExpGroupMetadataList.size() >= mMaxExpGroupCount) { + /* Collapse a group */ + // TODO: Collapse something not on the screen instead of the first one? + // TODO: Could write overloaded function to take GroupMetadata to collapse + GroupMetadata collapsedGm = mExpGroupMetadataList.get(0); + + int collapsedIndex = mExpGroupMetadataList.indexOf(collapsedGm); + + collapseGroup(collapsedGm.gPos); + + /* Decrement index if it is after the group we removed */ + if (posMetadata.groupInsertIndex > collapsedIndex) { + posMetadata.groupInsertIndex--; + } + } + + GroupMetadata expandedGm = new GroupMetadata(); + + expandedGm.gPos = posMetadata.position.groupPos; + expandedGm.flPos = GroupMetadata.REFRESH; + expandedGm.lastChildFlPos = GroupMetadata.REFRESH; + + mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm); + + // Refresh the metadata + refreshExpGroupMetadataList(false); + + // Notify of change + notifyDataSetChanged(); + + // Give the callback + mExpandableListAdapter.onGroupExpanded(expandedGm.gPos); + + return true; + } + + /** + * Whether the given group is currently expanded. + * @param groupPosition The group to check. + * @return Whether the group is currently expanded. + */ + public boolean isGroupExpanded(int groupPosition) { + GroupMetadata groupMetadata; + for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) { + groupMetadata = mExpGroupMetadataList.get(i); + + if (groupMetadata.gPos == groupPosition) { + return true; + } + } + + return false; + } + + /** + * Set the maximum number of groups that can be expanded at any given time + */ + public void setMaxExpGroupCount(int maxExpGroupCount) { + mMaxExpGroupCount = maxExpGroupCount; + } + + ExpandableListAdapter getAdapter() { + return mExpandableListAdapter; + } + + public Filter getFilter() { + ExpandableListAdapter adapter = getAdapter(); + if (adapter instanceof Filterable) { + return ((Filterable) adapter).getFilter(); + } else { + return null; + } + } + + ArrayList<GroupMetadata> getExpandedGroupMetadataList() { + return mExpGroupMetadataList; + } + + void setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList) { + + if ((expandedGroupMetadataList == null) || (mExpandableListAdapter == null)) { + return; + } + + // Make sure our current data set is big enough for the previously + // expanded groups, if not, ignore this request + int numGroups = mExpandableListAdapter.getGroupCount(); + for (int i = expandedGroupMetadataList.size() - 1; i >= 0; i--) { + if (expandedGroupMetadataList.get(i).gPos >= numGroups) { + // Doh, for some reason the client doesn't have some of the groups + return; + } + } + + mExpGroupMetadataList = expandedGroupMetadataList; + refreshExpGroupMetadataList(true); + } + + @Override + public boolean isEmpty() { + ExpandableListAdapter adapter = getAdapter(); + return adapter != null ? adapter.isEmpty() : true; + } + + protected class MyDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + refreshExpGroupMetadataList(true); + + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + refreshExpGroupMetadataList(true); + + notifyDataSetInvalidated(); + } + } + + /** + * Metadata about an expanded group to help convert from a flat list + * position to either a) group position for groups, or b) child position for + * children + */ + static class GroupMetadata implements Parcelable { + final static int REFRESH = -1; + + /** This group's flat list position */ + int flPos; + + /* firstChildFlPos isn't needed since it's (flPos + 1) */ + + /** + * This group's last child's flat list position, so basically + * the range of this group in the flat list + */ + int lastChildFlPos; + + /** + * This group's group position + */ + int gPos; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(flPos); + dest.writeInt(lastChildFlPos); + dest.writeInt(gPos); + } + + public static final Parcelable.Creator<GroupMetadata> CREATOR = + new Parcelable.Creator<GroupMetadata>() { + + public GroupMetadata createFromParcel(Parcel in) { + GroupMetadata gm = new GroupMetadata(); + gm.flPos = in.readInt(); + gm.lastChildFlPos = in.readInt(); + gm.gPos = in.readInt(); + return gm; + } + + public GroupMetadata[] newArray(int size) { + return new GroupMetadata[size]; + } + }; + + } + + /** + * Data type that contains an expandable list position (can refer to either a group + * or child) and some extra information regarding referred item (such as + * where to insert into the flat list, etc.) + */ + static public class PositionMetadata { + /** Data type to hold the position and its type (child/group) */ + public ExpandableListPosition position; + + /** + * Link back to the expanded GroupMetadata for this group. Useful for + * removing the group from the list of expanded groups inside the + * connector when we collapse the group, and also as a check to see if + * the group was expanded or collapsed (this will be null if the group + * is collapsed since we don't keep that group's metadata) + */ + public GroupMetadata groupMetadata; + + /** + * For groups that are collapsed, we use this as the index (in + * mExpGroupMetadataList) to insert this group when we are expanding + * this group. + */ + public int groupInsertIndex; + + public PositionMetadata(int flatListPos, int type, int groupPos, + int childPos) { + position = new ExpandableListPosition(type, groupPos, childPos, flatListPos); + } + + protected PositionMetadata(int flatListPos, int type, int groupPos, + int childPos, GroupMetadata groupMetadata, int groupInsertIndex) { + position = new ExpandableListPosition(type, groupPos, childPos, flatListPos); + + this.groupMetadata = groupMetadata; + this.groupInsertIndex = groupInsertIndex; + } + + /** + * Checks whether the group referred to in this object is expanded, + * or not (at the time this object was created) + * + * @return whether the group at groupPos is expanded or not + */ + public boolean isExpanded() { + return groupMetadata != null; + } + } +} diff --git a/core/java/android/widget/ExpandableListPosition.java b/core/java/android/widget/ExpandableListPosition.java new file mode 100644 index 0000000..71e970c --- /dev/null +++ b/core/java/android/widget/ExpandableListPosition.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2007 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 android.widget; + +/** + * ExpandableListPosition can refer to either a group's position or a child's + * position. Referring to a child's position requires both a group position (the + * group containing the child) and a child position (the child's position within + * that group). To create objects, use {@link #obtainChildPosition(int, int)} or + * {@link #obtainGroupPosition(int)}. + */ +class ExpandableListPosition { + /** + * This data type represents a child position + */ + public final static int CHILD = 1; + + /** + * This data type represents a group position + */ + public final static int GROUP = 2; + + /** + * The position of either the group being referred to, or the parent + * group of the child being referred to + */ + public int groupPos; + + /** + * The position of the child within its parent group + */ + public int childPos; + + /** + * The position of the item in the flat list (optional, used internally when + * the corresponding flat list position for the group or child is known) + */ + int flatListPos; + + /** + * What type of position this ExpandableListPosition represents + */ + public int type; + + ExpandableListPosition(int type, int groupPos, int childPos, int flatListPos) { + this.type = type; + this.flatListPos = flatListPos; + this.groupPos = groupPos; + this.childPos = childPos; + } + + /** + * Used internally by the {@link #obtainChildPosition} and + * {@link #obtainGroupPosition} methods to construct a new object. + */ + private ExpandableListPosition(int type, int groupPos, int childPos) { + this.type = type; + this.groupPos = groupPos; + this.childPos = childPos; + } + + long getPackedPosition() { + if (type == CHILD) return ExpandableListView.getPackedPositionForChild(groupPos, childPos); + else return ExpandableListView.getPackedPositionForGroup(groupPos); + } + + static ExpandableListPosition obtainGroupPosition(int groupPosition) { + return new ExpandableListPosition(GROUP, groupPosition, 0); + } + + static ExpandableListPosition obtainChildPosition(int groupPosition, int childPosition) { + return new ExpandableListPosition(CHILD, groupPosition, childPosition); + } + + static ExpandableListPosition obtainPosition(long packedPosition) { + if (packedPosition == ExpandableListView.PACKED_POSITION_VALUE_NULL) { + return null; + } + + final int type = ExpandableListView.getPackedPositionType(packedPosition) == + ExpandableListView.PACKED_POSITION_TYPE_CHILD ? CHILD : GROUP; + + return new ExpandableListPosition(type, ExpandableListView + .getPackedPositionGroup(packedPosition), ExpandableListView + .getPackedPositionChild(packedPosition)); + } + +} diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java new file mode 100644 index 0000000..138cace --- /dev/null +++ b/core/java/android/widget/ExpandableListView.java @@ -0,0 +1,1057 @@ +/* + * 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 android.widget; + +import com.android.internal.R; + +import java.util.ArrayList; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.ContextMenu; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.ExpandableListConnector.PositionMetadata; + +/** + * A view that shows items in a vertically scrolling two-level list. This + * differs from the {@link ListView} by allowing two levels: groups which can + * individually be expanded to show its children. The items come from the + * {@link ExpandableListAdapter} associated with this view. + * <p> + * Expandable lists are able to show an indicator beside each item to display + * the item's current state (the states are usually one of expanded group, + * collapsed group, child, or last child). Use + * {@link #setChildIndicator(Drawable)} or {@link #setGroupIndicator(Drawable)} + * (or the corresponding XML attributes) to set these indicators (see the docs + * for each method to see additional state that each Drawable can have). The + * default style for an {@link ExpandableListView} provides indicators which + * will be shown next to Views given to the {@link ExpandableListView}. The + * layouts android.R.layout.simple_expandable_list_item_1 and + * android.R.layout.simple_expandable_list_item_2 (which should be used with + * {@link SimpleCursorTreeAdapter}) contain the preferred position information + * for indicators. + * <p> + * The context menu information set by an {@link ExpandableListView} will be a + * {@link ExpandableListContextMenuInfo} object with + * {@link ExpandableListContextMenuInfo#packedPosition} being a packed position + * that can be used with {@link #getPackedPositionType(long)} and the other + * similar methods. + * <p> + * <em><b>Note:</b></em> You cannot use the value <code>wrap_content</code> + * for the <code>android:layout_height</code> attribute of a + * ExpandableListView in XML if the parent's size is also not strictly specified + * (for example, if the parent were ScrollView you could not specify + * wrap_content since it also can be any length. However, you can use + * wrap_content if the ExpandableListView parent has a specific size, such as + * 100 pixels. + * + * @attr ref android.R.styleable#ExpandableListView_groupIndicator + * @attr ref android.R.styleable#ExpandableListView_indicatorLeft + * @attr ref android.R.styleable#ExpandableListView_indicatorRight + * @attr ref android.R.styleable#ExpandableListView_childIndicator + * @attr ref android.R.styleable#ExpandableListView_childIndicatorLeft + * @attr ref android.R.styleable#ExpandableListView_childIndicatorRight + * @attr ref android.R.styleable#ExpandableListView_childDivider + */ +public class ExpandableListView extends ListView { + + /** + * The packed position represents a group. + */ + public static final int PACKED_POSITION_TYPE_GROUP = 0; + + /** + * The packed position represents a child. + */ + public static final int PACKED_POSITION_TYPE_CHILD = 1; + + /** + * The packed position represents a neither/null/no preference. + */ + public static final int PACKED_POSITION_TYPE_NULL = 2; + + /** + * The value for a packed position that represents neither/null/no + * preference. This value is not otherwise possible since a group type + * (first bit 0) should not have a child position filled. + */ + public static final long PACKED_POSITION_VALUE_NULL = 0x00000000FFFFFFFFL; + + /** The mask (in packed position representation) for the child */ + private static final long PACKED_POSITION_MASK_CHILD = 0x00000000FFFFFFFFL; + + /** The mask (in packed position representation) for the group */ + private static final long PACKED_POSITION_MASK_GROUP = 0x7FFFFFFF00000000L; + + /** The mask (in packed position representation) for the type */ + private static final long PACKED_POSITION_MASK_TYPE = 0x8000000000000000L; + + /** The shift amount (in packed position representation) for the group */ + private static final long PACKED_POSITION_SHIFT_GROUP = 32; + + /** The shift amount (in packed position representation) for the type */ + private static final long PACKED_POSITION_SHIFT_TYPE = 63; + + /** The mask (in integer child position representation) for the child */ + private static final long PACKED_POSITION_INT_MASK_CHILD = 0xFFFFFFFF; + + /** The mask (in integer group position representation) for the group */ + private static final long PACKED_POSITION_INT_MASK_GROUP = 0x7FFFFFFF; + + /** Serves as the glue/translator between a ListView and an ExpandableListView */ + private ExpandableListConnector mConnector; + + /** Gives us Views through group+child positions */ + private ExpandableListAdapter mAdapter; + + /** Left bound for drawing the indicator. */ + private int mIndicatorLeft; + + /** Right bound for drawing the indicator. */ + private int mIndicatorRight; + + /** + * Left bound for drawing the indicator of a child. Value of + * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorLeft. + */ + private int mChildIndicatorLeft; + + /** + * Right bound for drawing the indicator of a child. Value of + * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorRight. + */ + private int mChildIndicatorRight; + + /** + * Denotes when a child indicator should inherit this bound from the generic + * indicator bounds + */ + public static final int CHILD_INDICATOR_INHERIT = -1; + + /** The indicator drawn next to a group. */ + private Drawable mGroupIndicator; + + /** The indicator drawn next to a child. */ + private Drawable mChildIndicator; + + private static final int[] EMPTY_STATE_SET = {}; + + /** State indicating the group is expanded. */ + private static final int[] GROUP_EXPANDED_STATE_SET = + {R.attr.state_expanded}; + + /** State indicating the group is empty (has no children). */ + private static final int[] GROUP_EMPTY_STATE_SET = + {R.attr.state_empty}; + + /** State indicating the group is expanded and empty (has no children). */ + private static final int[] GROUP_EXPANDED_EMPTY_STATE_SET = + {R.attr.state_expanded, R.attr.state_empty}; + + /** States for the group where the 0th bit is expanded and 1st bit is empty. */ + private static final int[][] GROUP_STATE_SETS = { + EMPTY_STATE_SET, // 00 + GROUP_EXPANDED_STATE_SET, // 01 + GROUP_EMPTY_STATE_SET, // 10 + GROUP_EXPANDED_EMPTY_STATE_SET // 11 + }; + + /** State indicating the child is the last within its group. */ + private static final int[] CHILD_LAST_STATE_SET = + {R.attr.state_last}; + + /** Drawable to be used as a divider when it is adjacent to any children */ + private Drawable mChildDivider; + + public ExpandableListView(Context context) { + this(context, null); + } + + public ExpandableListView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.expandableListViewStyle); + } + + public ExpandableListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ExpandableListView, defStyle, + 0); + + mGroupIndicator = a + .getDrawable(com.android.internal.R.styleable.ExpandableListView_groupIndicator); + mChildIndicator = a + .getDrawable(com.android.internal.R.styleable.ExpandableListView_childIndicator); + mIndicatorLeft = a + .getDimensionPixelSize(com.android.internal.R.styleable.ExpandableListView_indicatorLeft, 0); + mIndicatorRight = a + .getDimensionPixelSize(com.android.internal.R.styleable.ExpandableListView_indicatorRight, 0); + mChildIndicatorLeft = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_childIndicatorLeft, CHILD_INDICATOR_INHERIT); + mChildIndicatorRight = a.getDimensionPixelSize( + com.android.internal.R.styleable.ExpandableListView_childIndicatorRight, CHILD_INDICATOR_INHERIT); + mChildDivider = a.getDrawable(com.android.internal.R.styleable.ExpandableListView_childDivider); + + a.recycle(); + } + + + @Override + protected void dispatchDraw(Canvas canvas) { + // Draw children, etc. + super.dispatchDraw(canvas); + + // If we have any indicators to draw, we do it here + if ((mChildIndicator == null) && (mGroupIndicator == null)) { + return; + } + + int saveCount = 0; + final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; + if (clipToPadding) { + saveCount = canvas.save(); + final int scrollX = mScrollX; + final int scrollY = mScrollY; + canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop, + scrollX + mRight - mLeft - mPaddingRight, + scrollY + mBottom - mTop - mPaddingBottom); + } + + final int headerViewsCount = getHeaderViewsCount(); + + final int lastChildFlPos = mItemCount - getFooterViewsCount() - headerViewsCount - 1; + + final int myB = mBottom; + + PositionMetadata pos; + View item; + Drawable indicator; + int t, b; + + // Start at a value that is neither child nor group + int lastItemType = ~(ExpandableListPosition.CHILD | ExpandableListPosition.GROUP); + + // Bounds of the indicator to be drawn + Rect indicatorRect = new Rect(); + + // The "child" mentioned in the following two lines is this + // View's child, not referring to an expandable list's + // notion of a child (as opposed to a group) + final int childCount = getChildCount(); + for (int i = 0, childFlPos = mFirstPosition - headerViewsCount; i < childCount; + i++, childFlPos++) { + + if (childFlPos < 0) { + // This child is header + continue; + } else if (childFlPos > lastChildFlPos) { + // This child is footer, so are all subsequent children + break; + } + + item = getChildAt(i); + t = item.getTop(); + b = item.getBottom(); + + // This item isn't on the screen + if ((b < 0) || (t > myB)) continue; + + // Get more expandable list-related info for this item + pos = mConnector.getUnflattenedPos(childFlPos); + + // If this item type and the previous item type are different, then we need to change + // the left & right bounds + if (pos.position.type != lastItemType) { + if (pos.position.type == ExpandableListPosition.CHILD) { + indicatorRect.left = (mChildIndicatorLeft == CHILD_INDICATOR_INHERIT) ? + mIndicatorLeft : mChildIndicatorLeft; + indicatorRect.right = (mChildIndicatorRight == CHILD_INDICATOR_INHERIT) ? + mIndicatorRight : mChildIndicatorRight; + } else { + indicatorRect.left = mIndicatorLeft; + indicatorRect.right = mIndicatorRight; + } + + lastItemType = pos.position.type; + } + + if (indicatorRect.left == indicatorRect.right) { + // The left and right bounds are the same, so nothing will be drawn + continue; + } + + // Use item's full height + the divider height + if (mStackFromBottom) { + // See ListView#dispatchDraw + indicatorRect.top = t - mDividerHeight; + indicatorRect.bottom = b; + } else { + indicatorRect.top = t; + indicatorRect.bottom = b + mDividerHeight; + } + + // Get the indicator (with its state set to the item's state) + indicator = getIndicator(pos); + if (indicator == null) continue; + + // Draw the indicator + indicator.setBounds(indicatorRect); + indicator.draw(canvas); + } + + if (clipToPadding) { + canvas.restoreToCount(saveCount); + } + } + + /** + * Gets the indicator for the item at the given position. If the indicator + * is stateful, the state will be given to the indicator. + * + * @param pos The flat list position of the item whose indicator + * should be returned. + * @return The indicator in the proper state. + */ + private Drawable getIndicator(PositionMetadata pos) { + Drawable indicator; + + if (pos.position.type == ExpandableListPosition.GROUP) { + indicator = mGroupIndicator; + + if (indicator != null && indicator.isStateful()) { + // Empty check based on availability of data. If the groupMetadata isn't null, + // we do a check on it. Otherwise, the group is collapsed so we consider it + // empty for performance reasons. + boolean isEmpty = (pos.groupMetadata == null) || + (pos.groupMetadata.lastChildFlPos == pos.groupMetadata.flPos); + + final int stateSetIndex = + (pos.isExpanded() ? 1 : 0) | // Expanded? + (isEmpty ? 2 : 0); // Empty? + indicator.setState(GROUP_STATE_SETS[stateSetIndex]); + } + } else { + indicator = mChildIndicator; + + if (indicator != null && indicator.isStateful()) { + // No need for a state sets array for the child since it only has two states + final int stateSet[] = pos.position.flatListPos == pos.groupMetadata.lastChildFlPos + ? CHILD_LAST_STATE_SET + : EMPTY_STATE_SET; + indicator.setState(stateSet); + } + } + + return indicator; + } + + /** + * Sets the drawable that will be drawn adjacent to every child in the list. This will + * be drawn using the same height as the normal divider ({@link #setDivider(Drawable)}) or + * if it does not have an intrinsic height, the height set by {@link #setDividerHeight(int)}. + * + * @param childDivider The drawable to use. + */ + public void setChildDivider(Drawable childDivider) { + mChildDivider = childDivider; + } + + @Override + void drawDivider(Canvas canvas, Rect bounds, int childIndex) { + int flatListPosition = childIndex + mFirstPosition; + + // Only proceed as possible child if the divider isn't above all items (if it is above + // all items, then the item below it has to be a group) + if (flatListPosition >= 0) { + PositionMetadata pos = mConnector.getUnflattenedPos(flatListPosition); + // If this item is a child, or it is a non-empty group that is expanded + if ((pos.position.type == ExpandableListPosition.CHILD) + || (pos.isExpanded() && + pos.groupMetadata.lastChildFlPos != pos.groupMetadata.flPos)) { + // These are the cases where we draw the child divider + mChildDivider.setBounds(bounds); + mChildDivider.draw(canvas); + return; + } + } + + // Otherwise draw the default divider + super.drawDivider(canvas, bounds, flatListPosition); + } + + /** + * This overloaded method should not be used, instead use + * {@link #setAdapter(ExpandableListAdapter)}. + * <p> + * {@inheritDoc} + */ + @Override + public void setAdapter(ListAdapter adapter) { + throw new RuntimeException( + "For ExpandableListView, use setAdapter(ExpandableListAdapter) instead of " + + "setAdapter(ListAdapter)"); + } + + /** + * This method should not be used, use {@link #getExpandableListAdapter()}. + */ + @Override + public ListAdapter getAdapter() { + /* + * The developer should never really call this method on an + * ExpandableListView, so it would be nice to throw a RuntimeException, + * but AdapterView calls this + */ + return super.getAdapter(); + } + + /** + * Register a callback to be invoked when an item has been clicked and the + * caller prefers to receive a ListView-style position instead of a group + * and/or child position. In most cases, the caller should use + * {@link #setOnGroupClickListener} and/or {@link #setOnChildClickListener}. + * <p /> + * {@inheritDoc} + */ + @Override + public void setOnItemClickListener(OnItemClickListener l) { + super.setOnItemClickListener(l); + } + + /** + * Sets the adapter that provides data to this view. + * @param adapter The adapter that provides data to this view. + */ + public void setAdapter(ExpandableListAdapter adapter) { + // Set member variable + mAdapter = adapter; + + if (adapter != null) { + // Create the connector + mConnector = new ExpandableListConnector(adapter); + } else { + mConnector = null; + } + + // Link the ListView (superclass) to the expandable list data through the connector + super.setAdapter(mConnector); + } + + /** + * Gets the adapter that provides data to this view. + * @return The adapter that provides data to this view. + */ + public ExpandableListAdapter getExpandableListAdapter() { + return mAdapter; + } + + @Override + public boolean performItemClick(View v, int position, long id) { + // Ignore clicks in header/footers + final int headerViewsCount = getHeaderViewsCount(); + final int footerViewsStart = mItemCount - getFooterViewsCount(); + + if (position < headerViewsCount || position >= footerViewsStart) { + // Clicked on a header/footer, so ignore pass it on to super + return super.performItemClick(v, position, id); + } + + // Internally handle the item click + return handleItemClick(v, position - headerViewsCount, id); + } + + /** + * This will either expand/collapse groups (if a group was clicked) or pass + * on the click to the proper child (if a child was clicked) + * + * @param position The flat list position. This has already been factored to + * remove the header/footer. + * @param id The ListAdapter ID, not the group or child ID. + */ + boolean handleItemClick(View v, int position, long id) { + final PositionMetadata posMetadata = mConnector.getUnflattenedPos(position); + + id = getChildOrGroupId(posMetadata.position); + + if (posMetadata.position.type == ExpandableListPosition.GROUP) { + /* It's a group, so handle collapsing/expanding */ + + if (posMetadata.isExpanded()) { + /* Collapse it */ + mConnector.collapseGroup(posMetadata); + + playSoundEffect(SoundEffectConstants.CLICK); + + if (mOnGroupCollapseListener != null) { + mOnGroupCollapseListener.onGroupCollapse(posMetadata.position.groupPos); + } + + } else { + /* It's a group click, so pass on event */ + if (mOnGroupClickListener != null) { + if (mOnGroupClickListener.onGroupClick(this, v, + posMetadata.position.groupPos, id)) { + return true; + } + } + + /* Expand it */ + mConnector.expandGroup(posMetadata); + + playSoundEffect(SoundEffectConstants.CLICK); + + if (mOnGroupExpandListener != null) { + mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos); + } + } + + return true; + } else { + /* It's a child, so pass on event */ + if (mOnChildClickListener != null) { + playSoundEffect(SoundEffectConstants.CLICK); + return mOnChildClickListener.onChildClick(this, v, posMetadata.position.groupPos, + posMetadata.position.childPos, id); + } + + return false; + } + } + + /** + * Expand a group in the grouped list view + * + * @param groupPos the group to be expanded + * @return True if the group was expanded, false otherwise (if the group + * was already expanded, this will return false) + */ + public boolean expandGroup(int groupPos) { + boolean retValue = mConnector.expandGroup(groupPos); + + if (mOnGroupExpandListener != null) { + mOnGroupExpandListener.onGroupExpand(groupPos); + } + + return retValue; + } + + /** + * Collapse a group in the grouped list view + * + * @param groupPos position of the group to collapse + * @return True if the group was collapsed, false otherwise (if the group + * was already collapsed, this will return false) + */ + public boolean collapseGroup(int groupPos) { + boolean retValue = mConnector.collapseGroup(groupPos); + + if (mOnGroupCollapseListener != null) { + mOnGroupCollapseListener.onGroupCollapse(groupPos); + } + + return retValue; + } + + /** Used for being notified when a group is collapsed */ + public interface OnGroupCollapseListener { + /** + * Callback method to be invoked when a group in this expandable list has + * been collapsed. + * + * @param groupPosition The group position that was collapsed + */ + void onGroupCollapse(int groupPosition); + } + + private OnGroupCollapseListener mOnGroupCollapseListener; + + public void setOnGroupCollapseListener( + OnGroupCollapseListener onGroupCollapseListener) { + mOnGroupCollapseListener = onGroupCollapseListener; + } + + /** Used for being notified when a group is expanded */ + public interface OnGroupExpandListener { + /** + * Callback method to be invoked when a group in this expandable list has + * been expanded. + * + * @param groupPosition The group position that was expanded + */ + void onGroupExpand(int groupPosition); + } + + private OnGroupExpandListener mOnGroupExpandListener; + + public void setOnGroupExpandListener( + OnGroupExpandListener onGroupExpandListener) { + mOnGroupExpandListener = onGroupExpandListener; + } + + /** + * Interface definition for a callback to be invoked when a group in this + * expandable list has been clicked. + */ + public interface OnGroupClickListener { + /** + * Callback method to be invoked when a group in this expandable list has + * been clicked. + * + * @param parent The ExpandableListConnector where the click happened + * @param v The view within the expandable list/ListView that was clicked + * @param groupPosition The group position that was clicked + * @param id The row id of the group that was clicked + * @return True if the click was handled + */ + boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, + long id); + } + + private OnGroupClickListener mOnGroupClickListener; + + public void setOnGroupClickListener(OnGroupClickListener onGroupClickListener) { + mOnGroupClickListener = onGroupClickListener; + } + + /** + * Interface definition for a callback to be invoked when a child in this + * expandable list has been clicked. + */ + public interface OnChildClickListener { + /** + * Callback method to be invoked when a child in this expandable list has + * been clicked. + * + * @param parent The ExpandableListView where the click happened + * @param v The view within the expandable list/ListView that was clicked + * @param groupPosition The group position that contains the child that + * was clicked + * @param childPosition The child position within the group + * @param id The row id of the child that was clicked + * @return True if the click was handled + */ + boolean onChildClick(ExpandableListView parent, View v, int groupPosition, + int childPosition, long id); + } + + private OnChildClickListener mOnChildClickListener; + + public void setOnChildClickListener(OnChildClickListener onChildClickListener) { + mOnChildClickListener = onChildClickListener; + } + + /** + * Converts a flat list position (the raw position of an item (child or + * group) in the list) to an group and/or child position (represented in a + * packed position). This is useful in situations where the caller needs to + * use the underlying {@link ListView}'s methods. Use + * {@link ExpandableListView#getPackedPositionType} , + * {@link ExpandableListView#getPackedPositionChild}, + * {@link ExpandableListView#getPackedPositionGroup} to unpack. + * + * @param flatListPosition The flat list position to be converted. + * @return The group and/or child position for the given flat list position + * in packed position representation. + */ + public long getExpandableListPosition(int flatListPosition) { + return mConnector.getUnflattenedPos(flatListPosition).position.getPackedPosition(); + } + + /** + * Converts a group and/or child position to a flat list position. This is + * useful in situations where the caller needs to use the underlying + * {@link ListView}'s methods. + * + * @param packedPosition The group and/or child positions to be converted in + * packed position representation. Use + * {@link #getPackedPositionForChild(int, int)} or + * {@link #getPackedPositionForGroup(int)}. + * @return The flat list position for the given child or group. + */ + public int getFlatListPosition(long packedPosition) { + return mConnector.getFlattenedPos(ExpandableListPosition.obtainPosition(packedPosition)). + position.flatListPos; + } + + /** + * Gets the position of the currently selected group or child (along with + * its type). Can return {@link #PACKED_POSITION_VALUE_NULL} if no selection. + * + * @return A packed position containing the currently selected group or + * child's position and type. #PACKED_POSITION_VALUE_NULL if no selection. + */ + public long getSelectedPosition() { + final int selectedPos = getSelectedItemPosition(); + if (selectedPos == -1) return PACKED_POSITION_VALUE_NULL; + + return getExpandableListPosition(selectedPos); + } + + /** + * Gets the ID of the currently selected group or child. Can return -1 if no + * selection. + * + * @return The ID of the currently selected group or child. -1 if no + * selection. + */ + public long getSelectedId() { + long packedPos = getSelectedPosition(); + if (packedPos == PACKED_POSITION_VALUE_NULL) return -1; + + int groupPos = getPackedPositionGroup(packedPos); + + if (getPackedPositionType(packedPos) == PACKED_POSITION_TYPE_GROUP) { + // It's a group + return mAdapter.getGroupId(groupPos); + } else { + // It's a child + return mAdapter.getChildId(groupPos, getPackedPositionChild(packedPos)); + } + } + + /** + * Sets the selection to the specified group. + * @param groupPosition The position of the group that should be selected. + */ + public void setSelectedGroup(int groupPosition) { + ExpandableListPosition elGroupPos = ExpandableListPosition + .obtainGroupPosition(groupPosition); + super.setSelection(mConnector.getFlattenedPos(elGroupPos).position.flatListPos); + } + + /** + * Sets the selection to the specified child. If the child is in a collapsed + * group, the group will only be expanded and child subsequently selected if + * shouldExpandGroup is set to true, otherwise the method will return false. + * + * @param groupPosition The position of the group that contains the child. + * @param childPosition The position of the child within the group. + * @param shouldExpandGroup Whether the child's group should be expanded if + * it is collapsed. + * @return Whether the selection was successfully set on the child. + */ + public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) { + ExpandableListPosition elChildPos = ExpandableListPosition.obtainChildPosition( + groupPosition, childPosition); + PositionMetadata flatChildPos = mConnector.getFlattenedPos(elChildPos); + + if (flatChildPos == null) { + // The child's group isn't expanded + + // Shouldn't expand the group, so return false for we didn't set the selection + if (!shouldExpandGroup) return false; + + expandGroup(groupPosition); + + flatChildPos = mConnector.getFlattenedPos(elChildPos); + + // Sanity check + if (flatChildPos == null) { + throw new IllegalStateException("Could not find child"); + } + } + + super.setSelection(flatChildPos.position.flatListPos); + + return true; + } + + /** + * Whether the given group is currently expanded. + * + * @param groupPosition The group to check. + * @return Whether the group is currently expanded. + */ + public boolean isGroupExpanded(int groupPosition) { + return mConnector.isGroupExpanded(groupPosition); + } + + /** + * Gets the type of a packed position. See + * {@link #getPackedPositionForChild(int, int)}. + * + * @param packedPosition The packed position for which to return the type. + * @return The type of the position contained within the packed position, + * either {@link #PACKED_POSITION_TYPE_CHILD}, {@link #PACKED_POSITION_TYPE_GROUP}, or + * {@link #PACKED_POSITION_TYPE_NULL}. + */ + public static int getPackedPositionType(long packedPosition) { + if (packedPosition == PACKED_POSITION_VALUE_NULL) { + return PACKED_POSITION_TYPE_NULL; + } + + return (packedPosition & PACKED_POSITION_MASK_TYPE) == PACKED_POSITION_MASK_TYPE + ? PACKED_POSITION_TYPE_CHILD + : PACKED_POSITION_TYPE_GROUP; + } + + /** + * Gets the group position from a packed position. See + * {@link #getPackedPositionForChild(int, int)}. + * + * @param packedPosition The packed position from which the group position + * will be returned. + * @return The group position portion of the packed position. If this does + * not contain a group, returns -1. + */ + public static int getPackedPositionGroup(long packedPosition) { + // Null + if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1; + + return (int) ((packedPosition & PACKED_POSITION_MASK_GROUP) >> PACKED_POSITION_SHIFT_GROUP); + } + + /** + * Gets the child position from a packed position that is of + * {@link #PACKED_POSITION_TYPE_CHILD} type (use {@link #getPackedPositionType(long)}). + * To get the group that this child belongs to, use + * {@link #getPackedPositionGroup(long)}. See + * {@link #getPackedPositionForChild(int, int)}. + * + * @param packedPosition The packed position from which the child position + * will be returned. + * @return The child position portion of the packed position. If this does + * not contain a child, returns -1. + */ + public static int getPackedPositionChild(long packedPosition) { + // Null + if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1; + + // Group since a group type clears this bit + if ((packedPosition & PACKED_POSITION_MASK_TYPE) != PACKED_POSITION_MASK_TYPE) return -1; + + return (int) (packedPosition & PACKED_POSITION_MASK_CHILD); + } + + /** + * Returns the packed position representation of a child's position. + * <p> + * In general, a packed position should be used in + * situations where the position given to/returned from an + * {@link ExpandableListAdapter} or {@link ExpandableListView} method can + * either be a child or group. The two positions are packed into a single + * long which can be unpacked using + * {@link #getPackedPositionChild(long)}, + * {@link #getPackedPositionGroup(long)}, and + * {@link #getPackedPositionType(long)}. + * + * @param groupPosition The child's parent group's position. + * @param childPosition The child position within the group. + * @return The packed position representation of the child (and parent group). + */ + public static long getPackedPositionForChild(int groupPosition, int childPosition) { + return (((long)PACKED_POSITION_TYPE_CHILD) << PACKED_POSITION_SHIFT_TYPE) + | ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP) + << PACKED_POSITION_SHIFT_GROUP) + | (childPosition & PACKED_POSITION_INT_MASK_CHILD); + } + + /** + * Returns the packed position representation of a group's position. See + * {@link #getPackedPositionForChild(int, int)}. + * + * @param groupPosition The child's parent group's position. + * @return The packed position representation of the group. + */ + public static long getPackedPositionForGroup(int groupPosition) { + // No need to OR a type in because PACKED_POSITION_GROUP == 0 + return ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP) + << PACKED_POSITION_SHIFT_GROUP); + } + + @Override + ContextMenuInfo createContextMenuInfo(View view, int flatListPosition, long id) { + ExpandableListPosition pos = mConnector.getUnflattenedPos(flatListPosition).position; + + id = getChildOrGroupId(pos); + + return new ExpandableListContextMenuInfo(view, pos.getPackedPosition(), id); + } + + /** + * Gets the ID of the group or child at the given <code>position</code>. + * This is useful since there is no ListAdapter ID -> ExpandableListAdapter + * ID conversion mechanism (in some cases, it isn't possible). + * + * @param position The position of the child or group whose ID should be + * returned. + */ + private long getChildOrGroupId(ExpandableListPosition position) { + if (position.type == ExpandableListPosition.CHILD) { + return mAdapter.getChildId(position.groupPos, position.childPos); + } else { + return mAdapter.getGroupId(position.groupPos); + } + } + + /** + * Sets the indicator to be drawn next to a child. + * + * @param childIndicator The drawable to be used as an indicator. If the + * child is the last child for a group, the state + * {@link android.R.attr#state_last} will be set. + */ + public void setChildIndicator(Drawable childIndicator) { + mChildIndicator = childIndicator; + } + + /** + * Sets the drawing bounds for the child indicator. For either, you can + * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general + * indicator's bounds. + * + * @see #setIndicatorBounds(int, int) + * @param left The left position (relative to the left bounds of this View) + * to start drawing the indicator. + * @param right The right position (relative to the left bounds of this + * View) to end the drawing of the indicator. + */ + public void setChildIndicatorBounds(int left, int right) { + mChildIndicatorLeft = left; + mChildIndicatorRight = right; + } + + /** + * Sets the indicator to be drawn next to a group. + * + * @param groupIndicator The drawable to be used as an indicator. If the + * group is empty, the state {@link android.R.attr#state_empty} will be + * set. If the group is expanded, the state + * {@link android.R.attr#state_expanded} will be set. + */ + public void setGroupIndicator(Drawable groupIndicator) { + mGroupIndicator = groupIndicator; + } + + /** + * Sets the drawing bounds for the indicators (at minimum, the group indicator + * is affected by this; the child indicator is affected by this if the + * child indicator bounds are set to inherit). + * + * @see #setChildIndicatorBounds(int, int) + * @param left The left position (relative to the left bounds of this View) + * to start drawing the indicator. + * @param right The right position (relative to the left bounds of this + * View) to end the drawing of the indicator. + */ + public void setIndicatorBounds(int left, int right) { + mIndicatorLeft = left; + mIndicatorRight = right; + } + + /** + * Extra menu information specific to an {@link ExpandableListView} 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 ExpandableListContextMenuInfo implements ContextMenu.ContextMenuInfo { + + public ExpandableListContextMenuInfo(View targetView, long packedPosition, long id) { + this.targetView = targetView; + this.packedPosition = packedPosition; + this.id = id; + } + + /** + * The view for which the context menu is being displayed. This + * will be one of the children Views of this {@link ExpandableListView}. + */ + public View targetView; + + /** + * The packed position in the list represented by the adapter for which + * the context menu is being displayed. Use the methods + * {@link ExpandableListView#getPackedPositionType}, + * {@link ExpandableListView#getPackedPositionChild}, and + * {@link ExpandableListView#getPackedPositionGroup} to unpack this. + */ + public long packedPosition; + + /** + * The ID of the item (group or child) for which the context menu is + * being displayed. + */ + public long id; + } + + static class SavedState extends BaseSavedState { + ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList; + + /** + * Constructor called from {@link ExpandableListView#onSaveInstanceState()} + */ + SavedState( + Parcelable superState, + ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList) { + super(superState); + this.expandedGroupMetadataList = expandedGroupMetadataList; + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + expandedGroupMetadataList = new ArrayList<ExpandableListConnector.GroupMetadata>(); + in.readList(expandedGroupMetadataList, ExpandableListConnector.class.getClassLoader()); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeList(expandedGroupMetadataList); + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return new SavedState(superState, + mConnector != null ? mConnector.getExpandedGroupMetadataList() : null); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mConnector != null && ss.expandedGroupMetadataList != null) { + mConnector.setExpandedGroupMetadataList(ss.expandedGroupMetadataList); + } + } + +} diff --git a/core/java/android/widget/Filter.java b/core/java/android/widget/Filter.java new file mode 100644 index 0000000..49888f7 --- /dev/null +++ b/core/java/android/widget/Filter.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +/** + * <p>A filter constrains data with a filtering pattern.</p> + * + * <p>Filters are usually created by {@link android.widget.Filterable} + * classes.</p> + * + * <p>Filtering operations performed by calling {@link #filter(CharSequence)} or + * {@link #filter(CharSequence, android.widget.Filter.FilterListener)} are + * performed asynchronously. When these methods are called, a filtering request + * is posted in a request queue and processed later. Any call to one of these + * methods will cancel any previous non-executed filtering request.</p> + * + * @see android.widget.Filterable + */ +public abstract class Filter { + private static final String THREAD_NAME = "Filter"; + private static final int FILTER_TOKEN = 0xD0D0F00D; + private static final int FINISH_TOKEN = 0xDEADBEEF; + + private Handler mThreadHandler; + private Handler mResultHandler; + + /** + * <p>Creates a new asynchronous filter.</p> + */ + public Filter() { + mResultHandler = new ResultsHandler(); + } + + /** + * <p>Starts an asynchronous filtering operation. Calling this method + * cancels all previous non-executed filtering requests and posts a new + * filtering request that will be executed later.</p> + * + * @param constraint the constraint used to filter the data + * + * @see #filter(CharSequence, android.widget.Filter.FilterListener) + */ + public final void filter(CharSequence constraint) { + filter(constraint, null); + } + + /** + * <p>Starts an asynchronous filtering operation. Calling this method + * cancels all previous non-executed filtering requests and posts a new + * filtering request that will be executed later.</p> + * + * <p>Upon completion, the listener is notified.</p> + * + * @param constraint the constraint used to filter the data + * @param listener a listener notified upon completion of the operation + * + * @see #filter(CharSequence) + * @see #performFiltering(CharSequence) + * @see #publishResults(CharSequence, android.widget.Filter.FilterResults) + */ + public final void filter(CharSequence constraint, FilterListener listener) { + synchronized (this) { + if (mThreadHandler == null) { + HandlerThread thread = new HandlerThread(THREAD_NAME); + thread.start(); + mThreadHandler = new RequestHandler(thread.getLooper()); + } + + Message message = mThreadHandler.obtainMessage(FILTER_TOKEN); + + RequestArguments args = new RequestArguments(); + args.constraint = constraint; + args.listener = listener; + message.obj = args; + + mThreadHandler.removeMessages(FILTER_TOKEN); + mThreadHandler.removeMessages(FINISH_TOKEN); + mThreadHandler.sendMessage(message); + } + } + + /** + * <p>Invoked in a worker thread to filter the data according to the + * constraint. Subclasses must implement this method to perform the + * filtering operation. Results computed by the filtering operation + * must be returned as a {@link android.widget.Filter.FilterResults} that + * will then be published in the UI thread through + * {@link #publishResults(CharSequence, + * android.widget.Filter.FilterResults)}.</p> + * + * <p><strong>Contract:</strong> When the constraint is null, the original + * data must be restored.</p> + * + * @param constraint the constraint used to filter the data + * @return the results of the filtering operation + * + * @see #filter(CharSequence, android.widget.Filter.FilterListener) + * @see #publishResults(CharSequence, android.widget.Filter.FilterResults) + * @see android.widget.Filter.FilterResults + */ + protected abstract FilterResults performFiltering(CharSequence constraint); + + /** + * <p>Invoked in the UI thread to publish the filtering results in the + * user interface. Subclasses must implement this method to display the + * results computed in {@link #performFiltering}.</p> + * + * @param constraint the constraint used to filter the data + * @param results the results of the filtering operation + * + * @see #filter(CharSequence, android.widget.Filter.FilterListener) + * @see #performFiltering(CharSequence) + * @see android.widget.Filter.FilterResults + */ + protected abstract void publishResults(CharSequence constraint, + FilterResults results); + + /** + * <p>Converts a value from the filtered set into a CharSequence. Subclasses + * should override this method to convert their results. The default + * implementation returns an empty String for null values or the default + * String representation of the value.</p> + * + * @param resultValue the value to convert to a CharSequence + * @return a CharSequence representing the value + */ + public CharSequence convertResultToString(Object resultValue) { + return resultValue == null ? "" : resultValue.toString(); + } + + /** + * <p>Holds the results of a filtering operation. The results are the values + * computed by the filtering operation and the number of these values.</p> + */ + protected static class FilterResults { + public FilterResults() { + // nothing to see here + } + + /** + * <p>Contains all the values computed by the filtering operation.</p> + */ + public Object values; + + /** + * <p>Contains the number of values computed by the filtering + * operation.</p> + */ + public int count; + } + + /** + * <p>Listener used to receive a notification upon completion of a filtering + * operation.</p> + */ + public static interface FilterListener { + /** + * <p>Notifies the end of a filtering operation.</p> + * + * @param count the number of values computed by the filter + */ + public void onFilterComplete(int count); + } + + /** + * <p>Worker thread handler. When a new filtering request is posted from + * {@link android.widget.Filter#filter(CharSequence, android.widget.Filter.FilterListener)}, + * it is sent to this handler.</p> + */ + private class RequestHandler extends Handler { + public RequestHandler(Looper looper) { + super(looper); + } + + /** + * <p>Handles filtering requests by calling + * {@link Filter#performFiltering} and then sending a message + * with the results to the results handler.</p> + * + * @param msg the filtering request + */ + public void handleMessage(Message msg) { + int what = msg.what; + Message message; + switch (what) { + case FILTER_TOKEN: + RequestArguments args = (RequestArguments) msg.obj; + try { + args.results = performFiltering(args.constraint); + } finally { + message = mResultHandler.obtainMessage(what); + message.obj = args; + message.sendToTarget(); + } + + synchronized (this) { + if (mThreadHandler != null) { + Message finishMessage = mThreadHandler.obtainMessage(FINISH_TOKEN); + mThreadHandler.sendMessageDelayed(finishMessage, 3000); + } + } + break; + case FINISH_TOKEN: + synchronized (this) { + if (mThreadHandler != null) { + mThreadHandler.getLooper().quit(); + mThreadHandler = null; + } + } + break; + } + } + } + + /** + * <p>Handles the results of a filtering operation. The results are + * handled in the UI thread.</p> + */ + private class ResultsHandler extends Handler { + /** + * <p>Messages received from the request handler are processed in the + * UI thread. The processing involves calling + * {@link Filter#publishResults(CharSequence, + * android.widget.Filter.FilterResults)} + * to post the results back in the UI and then notifying the listener, + * if any.</p> + * + * @param msg the filtering results + */ + @Override + public void handleMessage(Message msg) { + RequestArguments args = (RequestArguments) msg.obj; + + publishResults(args.constraint, args.results); + if (args.listener != null) { + int count = args.results != null ? args.results.count : -1; + args.listener.onFilterComplete(count); + } + } + } + + /** + * <p>Holds the arguments of a filtering request as well as the results + * of the request.</p> + */ + private static class RequestArguments { + /** + * <p>The constraint used to filter the data.</p> + */ + CharSequence constraint; + + /** + * <p>The listener to notify upon completion. Can be null.</p> + */ + FilterListener listener; + + /** + * <p>The results of the filtering operation.</p> + */ + FilterResults results; + } +} diff --git a/core/java/android/widget/FilterQueryProvider.java b/core/java/android/widget/FilterQueryProvider.java new file mode 100644 index 0000000..740d2f0 --- /dev/null +++ b/core/java/android/widget/FilterQueryProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.database.Cursor; + +/** + * This class can be used by external clients of CursorAdapter and + * CursorTreeAdapter to define how the content of the adapter should be + * filtered. + * + * @see #runQuery(CharSequence) + */ +public interface FilterQueryProvider { + /** + * Runs a query with the specified constraint. This query is requested + * by the filter attached to this adapter. + * + * Contract: when constraint is null or empty, the original results, + * prior to any filtering, must be returned. + * + * @param constraint the constraint with which the query must + * be filtered + * + * @return a Cursor representing the results of the new query + */ + Cursor runQuery(CharSequence constraint); +} diff --git a/core/java/android/widget/Filterable.java b/core/java/android/widget/Filterable.java new file mode 100644 index 0000000..f7c8d59 --- /dev/null +++ b/core/java/android/widget/Filterable.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2007 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 android.widget; + +/** + * <p>Defines a filterable behavior. A filterable class can have its data + * constrained by a filter. Filterable classes are usually + * {@link android.widget.Adapter} implementations.</p> + * + * @see android.widget.Filter + */ +public interface Filterable { + /** + * <p>Returns a filter that can be used to constrain data with a filtering + * pattern.</p> + * + * <p>This method is usually implemented by {@link android.widget.Adapter} + * classes.</p> + * + * @return a filter used to constrain data + */ + Filter getFilter(); +} diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java new file mode 100644 index 0000000..b4ed3ba --- /dev/null +++ b/core/java/android/widget/FrameLayout.java @@ -0,0 +1,448 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.Gravity; +import android.widget.RemoteViews.RemoteView; + + +/** + * FrameLayout is designed to block out an area on the screen to display + * a single item. You can add multiple children to a FrameLayout, but all + * children are pegged to the top left of the screen. + * Children are drawn in a stack, with the most recently added child on top. + * The size of the frame layout is the size of its largest child (plus padding), visible + * or not (if the FrameLayout's parent permits). Views that are GONE are used for sizing + * only if {@link #setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring()} + * is set to true. + * + * @attr ref android.R.styleable#FrameLayout_foreground + * @attr ref android.R.styleable#FrameLayout_foregroundGravity + * @attr ref android.R.styleable#FrameLayout_measureAllChildren + */ +@RemoteView +public class FrameLayout extends ViewGroup { + boolean mMeasureAllChildren = false; + + private Drawable mForeground; + private int mForegroundPaddingLeft = 0; + private int mForegroundPaddingTop = 0; + private int mForegroundPaddingRight = 0; + private int mForegroundPaddingBottom = 0; + + private final Rect mSelfBounds = new Rect(); + private final Rect mOverlayBounds = new Rect(); + private int mForegroundGravity = Gravity.FILL; + + public FrameLayout(Context context) { + super(context); + } + + public FrameLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FrameLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.FrameLayout, + defStyle, 0); + + final Drawable d = a.getDrawable(com.android.internal.R.styleable.FrameLayout_foreground); + if (d != null) { + setForeground(d); + } + + if (a.getBoolean(com.android.internal.R.styleable.FrameLayout_measureAllChildren, false)) { + setMeasureAllChildren(true); + } + + mForegroundGravity = a.getInt(com.android.internal.R.styleable.FrameLayout_foregroundGravity, + mForegroundGravity); + + a.recycle(); + } + + /** + * Describes how the foreground is positioned. Defaults to FILL. + * + * @param foregroundGravity See {@link android.view.Gravity} + * + * @attr ref android.R.styleable#FrameLayout_foregroundGravity + */ + public void setForegroundGravity(int foregroundGravity) { + if (mForegroundGravity != foregroundGravity) { + if ((foregroundGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { + foregroundGravity |= Gravity.LEFT; + } + + if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { + foregroundGravity |= Gravity.TOP; + } + + mForegroundGravity = foregroundGravity; + requestLayout(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || (who == mForeground); + } + + /** + * {@inheritDoc} + */ + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mForeground != null && mForeground.isStateful()) { + mForeground.setState(getDrawableState()); + } + } + + /** + * Returns a set of layout parameters with a width of + * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}, + * and a height of {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}. + */ + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + } + + /** + * Supply a Drawable that is to be rendered on top of all of the child + * views in the frame layout. Any padding in the Drawable will be taken + * into account by ensuring that the children are inset to be placed + * inside of the padding area. + * + * @param drawable The Drawable to be drawn on top of the children. + * + * @attr ref android.R.styleable#FrameLayout_foreground + */ + public void setForeground(Drawable drawable) { + if (mForeground != drawable) { + if (mForeground != null) { + mForeground.setCallback(null); + unscheduleDrawable(mForeground); + } + + mForeground = drawable; + mForegroundPaddingLeft = 0; + mForegroundPaddingTop = 0; + mForegroundPaddingRight = 0; + mForegroundPaddingBottom = 0; + + if (drawable != null) { + setWillNotDraw(false); + drawable.setCallback(this); + if (drawable.isStateful()) { + drawable.setState(getDrawableState()); + } + Rect padding = new Rect(); + if (drawable.getPadding(padding)) { + mForegroundPaddingLeft = padding.left; + mForegroundPaddingTop = padding.top; + mForegroundPaddingRight = padding.right; + mForegroundPaddingBottom = padding.bottom; + } + } else { + setWillNotDraw(true); + } + requestLayout(); + invalidate(); + } + } + + /** + * Returns the drawable used as the foreground of this FrameLayout. The + * foreground drawable, if non-null, is always drawn on top of the children. + * + * @return A Drawable or null if no foreground was set. + */ + public Drawable getForeground() { + return mForeground; + } + + /** + * {@inheritDoc} + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int count = getChildCount(); + + int maxHeight = 0; + int maxWidth = 0; + + // Find rightmost and bottommost child + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (mMeasureAllChildren || child.getVisibility() != GONE) { + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); + maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); + } + } + + // Account for padding too + maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight; + maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom; + + // Check against our minimum height and width + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + // Check against our foreground's minimum height and width + final Drawable drawable = getForeground(); + if (drawable != null) { + maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); + maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); + } + + setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), + resolveSize(maxHeight, heightMeasureSpec)); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int count = getChildCount(); + + final int parentLeft = mPaddingLeft + mForegroundPaddingLeft; + final int parentRight = right - left - mPaddingRight - mForegroundPaddingRight; + + final int parentTop = mPaddingTop + mForegroundPaddingTop; + final int parentBottom = bottom - top - mPaddingBottom - mForegroundPaddingBottom; + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int width = child.getMeasuredWidth(); + final int height = child.getMeasuredHeight(); + + int childLeft = parentLeft; + int childTop = parentTop; + + final int gravity = lp.gravity; + + if (gravity != -1) { + final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (horizontalGravity) { + case Gravity.LEFT: + childLeft = parentLeft + lp.leftMargin; + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = parentLeft + (parentRight - parentLeft + lp.leftMargin + + lp.rightMargin - width) / 2; + break; + case Gravity.RIGHT: + childLeft = parentRight - width - lp.rightMargin; + break; + default: + childLeft = parentLeft + lp.leftMargin; + } + + switch (verticalGravity) { + case Gravity.TOP: + childTop = parentTop + lp.topMargin; + break; + case Gravity.CENTER_VERTICAL: + childTop = parentTop + (parentBottom - parentTop + lp.topMargin + + lp.bottomMargin - height) / 2; + break; + case Gravity.BOTTOM: + childTop = parentBottom - height - lp.bottomMargin; + break; + default: + childTop = parentTop + lp.topMargin; + } + } + + child.layout(childLeft, childTop, childLeft + width, childTop + height); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + final Drawable foreground = mForeground; + if (foreground != null) { + final Rect selfBounds = mSelfBounds; + final Rect overlayBounds = mOverlayBounds; + + selfBounds.set(0, 0, w, h); + Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), + foreground.getIntrinsicHeight(), selfBounds, overlayBounds); + + foreground.setBounds(overlayBounds); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (mForeground != null) { + mForeground.draw(canvas); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean gatherTransparentRegion(Region region) { + boolean opaque = super.gatherTransparentRegion(region); + if (region != null && mForeground != null) { + applyDrawableToTransparentRegion(mForeground, region); + } + return opaque; + } + + /** + * Determines whether to measure all children or just those in + * the VISIBLE or INVISIBLE state when measuring. Defaults to false. + * @param measureAll true to consider children marked GONE, false otherwise. + * Default value is false. + * + * @attr ref android.R.styleable#FrameLayout_measureAllChildren + */ + public void setMeasureAllChildren(boolean measureAll) { + mMeasureAllChildren = measureAll; + } + + /** + * Determines whether to measure all children or just those in + * the VISIBLE or INVISIBLE state when measuring. + */ + public boolean getConsiderGoneChildrenWhenMeasuring() { + return mMeasureAllChildren; + } + + /** + * {@inheritDoc} + */ + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new FrameLayout.LayoutParams(getContext(), attrs); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + /** + * Per-child layout information for layouts that support margins. + * See {@link android.R.styleable#FrameLayout_Layout FrameLayout Layout Attributes} + * for a list of all child view attributes that this class supports. + */ + public static class LayoutParams extends MarginLayoutParams { + /** + * The gravity to apply with the View to which these layout parameters + * are associated. + * + * @see android.view.Gravity + */ + public int gravity = -1; + + /** + * {@inheritDoc} + */ + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.FrameLayout_Layout); + gravity = a.getInt(com.android.internal.R.styleable.FrameLayout_Layout_layout_gravity, -1); + a.recycle(); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(int width, int height) { + super(width, height); + } + + /** + * Creates a new set of layout parameters with the specified width, height + * and weight. + * + * @param width the width, either {@link #FILL_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param height the height, either {@link #FILL_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param gravity the gravity + * + * @see android.view.Gravity + */ + public LayoutParams(int width, int height, int gravity) { + super(width, height); + this.gravity = gravity; + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + } +} + diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java new file mode 100644 index 0000000..acf9400 --- /dev/null +++ b/core/java/android/widget/Gallery.java @@ -0,0 +1,1338 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import com.android.internal.R; + +import android.annotation.Widget; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Config; +import android.util.Log; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.SoundEffectConstants; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.animation.Transformation; +import android.widget.AbsSpinner; +import android.widget.Scroller; + +/** + * A view that shows items in a center-locked, horizontally scrolling list. + * <p> + * The default values for the Gallery assume you will be using + * {@link android.R.styleable#Theme_galleryItemBackground} as the background for + * each View given to the Gallery from the Adapter. If you are not doing this, + * you may need to adjust some Gallery properties, such as the spacing. + * + * @attr ref android.R.styleable#Gallery_animationDuration + * @attr ref android.R.styleable#Gallery_spacing + * @attr ref android.R.styleable#Gallery_gravity + */ +@Widget +public class Gallery extends AbsSpinner implements GestureDetector.OnGestureListener { + + private static final String TAG = "Gallery"; + + private static final boolean localLOGV = Config.LOGV; + + /** + * Horizontal spacing between items. + */ + private int mSpacing = 0; + + /** + * How long the transition animation should run when a child view changes + * position, measured in milliseconds. + */ + private int mAnimationDuration = 400; + + /** + * The alpha of items that are not selected. + */ + private float mUnselectedAlpha; + + /** + * Left most edge of a child seen so far during layout. + */ + private int mLeftMost; + + /** + * Right most edge of a child seen so far during layout. + */ + private int mRightMost; + + private int mGravity; + + /** + * Helper for detecting touch gestures. + */ + private GestureDetector mGestureDetector; + + /** + * The position of the item that received the user's down touch. + */ + private int mDownTouchPosition; + + /** + * The view of the item that received the user's down touch. + */ + private View mDownTouchView; + + /** + * Executes the delta scrolls from a fling or scroll movement. + */ + private FlingRunnable mFlingRunnable = new FlingRunnable(); + + /** + * When fling runnable runs, it resets this to false. Any method along the + * path until the end of its run() can set this to true to abort any + * remaining fling. For example, if we've reached either the leftmost or + * rightmost item, we will set this to true. + */ + private boolean mShouldStopFling; + + /** + * The currently selected item's child. + */ + private View mSelectedChild; + + /** + * Whether to continuously callback on the item selected listener during a + * fling. + */ + private boolean mShouldCallbackDuringFling; + + /** + * Whether to callback when an item that is not selected is clicked. + */ + private boolean mShouldCallbackOnUnselectedItemClick = true; + + /** + * If true, do not callback to item selected listener. + */ + private boolean mSuppressSelectionChanged; + + private AdapterContextMenuInfo mContextMenuInfo; + + public Gallery(Context context) { + this(context, null); + } + + public Gallery(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.galleryStyle); + } + + public Gallery(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mGestureDetector = new GestureDetector(this); + mGestureDetector.setIsLongpressEnabled(true); + + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Gallery, defStyle, 0); + + int index = a.getInt(com.android.internal.R.styleable.Gallery_gravity, -1); + if (index >= 0) { + setGravity(index); + } + + int animationDuration = + a.getInt(com.android.internal.R.styleable.Gallery_animationDuration, -1); + if (animationDuration > 0) { + setAnimationDuration(animationDuration); + } + + int spacing = + a.getDimensionPixelOffset(com.android.internal.R.styleable.Gallery_spacing, 0); + setSpacing(spacing); + + float unselectedAlpha = a.getFloat( + com.android.internal.R.styleable.Gallery_unselectedAlpha, 0.5f); + setUnselectedAlpha(unselectedAlpha); + + a.recycle(); + + // We draw the selected item last (because otherwise the item to the + // right overlaps it) + mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER; + + mGroupFlags |= FLAG_SUPPORT_STATIC_TRANSFORMATIONS; + } + + /** + * Whether or not to callback on any {@link #getOnItemSelectedListener()} + * while the items are being flinged. If false, only the final selected item + * will cause the callback. If true, all items between the first and the + * final will cause callbacks. + * + * @param shouldCallback Whether or not to callback on the listener while + * the items are being flinged. + */ + public void setCallbackDuringFling(boolean shouldCallback) { + mShouldCallbackDuringFling = shouldCallback; + } + + /** + * Whether or not to callback when an item that is not selected is clicked. + * If false, the item will become selected (and re-centered). If true, the + * {@link #getOnItemClickListener()} will get the callback. + * + * @param shouldCallback Whether or not to callback on the listener when a + * item that is not selected is clicked. + * @hide + */ + public void setCallbackOnUnselectedItemClick(boolean shouldCallback) { + mShouldCallbackOnUnselectedItemClick = shouldCallback; + } + + /** + * Sets how long the transition animation should run when a child view + * changes position. Only relevant if animation is turned on. + * + * @param animationDurationMillis The duration of the transition, in + * milliseconds. + * + * @attr ref android.R.styleable#Gallery_animationDuration + */ + public void setAnimationDuration(int animationDurationMillis) { + mAnimationDuration = animationDurationMillis; + } + + /** + * Sets the spacing between items in a Gallery + * + * @param spacing The spacing in pixels between items in the Gallery + * + * @attr ref android.R.styleable#Gallery_spacing + */ + public void setSpacing(int spacing) { + mSpacing = spacing; + } + + /** + * Sets the alpha of items that are not selected in the Gallery. + * + * @param unselectedAlpha the alpha for the items that are not selected. + * + * @attr ref android.R.styleable#Gallery_unselectedAlpha + */ + public void setUnselectedAlpha(float unselectedAlpha) { + mUnselectedAlpha = unselectedAlpha; + } + + @Override + protected boolean getChildStaticTransformation(View child, Transformation t) { + + t.clear(); + t.setAlpha(child == mSelectedChild ? 1.0f : mUnselectedAlpha); + + return true; + } + + @Override + protected int computeHorizontalScrollExtent() { + // Only 1 item is considered to be selected + return 1; + } + + @Override + protected int computeHorizontalScrollOffset() { + // Current scroll position is the same as the selected position + return mSelectedPosition; + } + + @Override + protected int computeHorizontalScrollRange() { + // Scroll range is the same as the item count + return mItemCount; + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + /* + * Gallery expects Gallery.LayoutParams. + */ + return new Gallery.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + /* + * Remember that we are in layout to prevent more layout request from + * being generated. + */ + mInLayout = true; + layout(0, false); + mInLayout = false; + } + + @Override + int getChildHeight(View child) { + return child.getMeasuredHeight(); + } + + /** + * Tracks a motion scroll. In reality, this is used to do just about any + * movement to items (touch scroll, arrow-key scroll, set an item as selected). + * + * @param deltaX Change in X from the previous event. + */ + void trackMotionScroll(int deltaX) { + + if (getChildCount() == 0) { + return; + } + + boolean toLeft = deltaX < 0; + + int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX); + if (limitedDeltaX != deltaX) { + // The above call returned a limited amount, so stop any scrolls/flings + mFlingRunnable.endFling(false); + onFinishedMovement(); + } + + offsetChildrenLeftAndRight(limitedDeltaX); + + detachOffScreenChildren(toLeft); + + if (toLeft) { + // If moved left, there will be empty space on the right + fillToGalleryRight(); + } else { + // Similarly, empty space on the left + fillToGalleryLeft(); + } + + // Clear unused views + mRecycler.clear(); + + setSelectionToCenterChild(); + + invalidate(); + } + + int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) { + int extremeItemPosition = motionToLeft ? mItemCount - 1 : 0; + View extremeChild = getChildAt(extremeItemPosition - mFirstPosition); + + if (extremeChild == null) { + return deltaX; + } + + int extremeChildCenter = getCenterOfView(extremeChild); + int galleryCenter = getCenterOfGallery(); + + if (motionToLeft) { + if (extremeChildCenter <= galleryCenter) { + + // The extreme child is past his boundary point! + return 0; + } + } else { + if (extremeChildCenter >= galleryCenter) { + + // The extreme child is past his boundary point! + return 0; + } + } + + int centerDifference = galleryCenter - extremeChildCenter; + + return motionToLeft + ? Math.max(centerDifference, deltaX) + : Math.min(centerDifference, deltaX); + } + + /** + * Offset the horizontal location of all children of this view by the + * specified number of pixels. + * + * @param offset the number of pixels to offset + */ + private void offsetChildrenLeftAndRight(int offset) { + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).offsetLeftAndRight(offset); + } + } + + /** + * @return The center of this Gallery. + */ + private int getCenterOfGallery() { + return (getWidth() - mPaddingLeft - mPaddingRight) / 2 + mPaddingLeft; + } + + /** + * @return The center of the given view. + */ + private static int getCenterOfView(View view) { + return view.getLeft() + view.getWidth() / 2; + } + + /** + * Detaches children that are off the screen (i.e.: Gallery bounds). + * + * @param toLeft Whether to detach children to the left of the Gallery, or + * to the right. + */ + private void detachOffScreenChildren(boolean toLeft) { + int numChildren = getChildCount(); + int firstPosition = mFirstPosition; + int start = 0; + int count = 0; + + if (toLeft) { + final int galleryLeft = mPaddingLeft; + for (int i = 0; i < numChildren; i++) { + final View child = getChildAt(i); + if (child.getRight() >= galleryLeft) { + break; + } else { + count++; + mRecycler.put(firstPosition + i, child); + } + } + } else { + final int galleryRight = getWidth() - mPaddingRight; + for (int i = numChildren - 1; i >= 0; i--) { + final View child = getChildAt(i); + if (child.getLeft() <= galleryRight) { + break; + } else { + start = i; + count++; + mRecycler.put(firstPosition + i, child); + } + } + } + + detachViewsFromParent(start, count); + + if (toLeft) { + mFirstPosition += count; + } + } + + /** + * Scrolls the items so that the selected item is in its 'slot' (its center + * is the gallery's center). + */ + private void scrollIntoSlots() { + + if (getChildCount() == 0 || mSelectedChild == null) return; + + int selectedCenter = getCenterOfView(mSelectedChild); + int targetCenter = getCenterOfGallery(); + + int scrollAmount = targetCenter - selectedCenter; + if (scrollAmount != 0) { + mFlingRunnable.startUsingDistance(scrollAmount); + } else { + onFinishedMovement(); + } + } + + private void onFinishedMovement() { + if (mSuppressSelectionChanged) { + mSuppressSelectionChanged = false; + + // We haven't been callbacking during the fling, so do it now + super.selectionChanged(); + } + } + + @Override + void selectionChanged() { + if (!mSuppressSelectionChanged) { + super.selectionChanged(); + } + } + + /** + * Looks for the child that is closest to the center and sets it as the + * selected child. + */ + private void setSelectionToCenterChild() { + + View selView = mSelectedChild; + if (mSelectedChild == null) return; + + int galleryCenter = getCenterOfGallery(); + + if (selView != null) { + + // Common case where the current selected position is correct + if (selView.getLeft() <= galleryCenter && selView.getRight() >= galleryCenter) { + return; + } + } + + // TODO better search + int closestEdgeDistance = Integer.MAX_VALUE; + int newSelectedChildIndex = 0; + for (int i = getChildCount() - 1; i >= 0; i--) { + + View child = getChildAt(i); + + if (child.getLeft() <= galleryCenter && child.getRight() >= galleryCenter) { + // This child is in the center + newSelectedChildIndex = i; + break; + } + + int childClosestEdgeDistance = Math.min(Math.abs(child.getLeft() - galleryCenter), + Math.abs(child.getRight() - galleryCenter)); + if (childClosestEdgeDistance < closestEdgeDistance) { + closestEdgeDistance = childClosestEdgeDistance; + newSelectedChildIndex = i; + } + } + + int newPos = mFirstPosition + newSelectedChildIndex; + + if (newPos != mSelectedPosition) { + setSelectedPositionInt(newPos); + setNextSelectedPositionInt(newPos); + checkSelectionChanged(); + } + } + + /** + * Creates and positions all views for this Gallery. + * <p> + * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes + * care of repositioning, adding, and removing children. + * + * @param delta Change in the selected position. +1 means the selection is + * moving to the right, so views are scrolling to the left. -1 + * means the selection is moving to the left. + */ + @Override + void layout(int delta, boolean animate) { + + int childrenLeft = mSpinnerPadding.left; + int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right; + + if (mDataChanged) { + handleDataChanged(); + } + + // Handle an empty gallery by removing all views. + if (mItemCount == 0) { + resetList(); + return; + } + + // Update to the new selected position. + if (mNextSelectedPosition >= 0) { + setSelectedPositionInt(mNextSelectedPosition); + } + + // All views go in recycler while we are in layout + recycleAllViews(); + + // Clear out old views + //removeAllViewsInLayout(); + detachAllViewsFromParent(); + + /* + * These will be used to give initial positions to views entering the + * gallery as we scroll + */ + mRightMost = 0; + mLeftMost = 0; + + // Make selected view and center it + + /* + * mFirstPosition will be decreased as we add views to the left later + * on. The 0 for x will be offset in a couple lines down. + */ + mFirstPosition = mSelectedPosition; + View sel = makeAndAddView(mSelectedPosition, 0, 0, true); + + // Put the selected child in the center + Gallery.LayoutParams lp = (Gallery.LayoutParams) sel.getLayoutParams(); + int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2); + sel.offsetLeftAndRight(selectedOffset); + + fillToGalleryRight(); + fillToGalleryLeft(); + + // Flush any cached views that did not get reused above + mRecycler.clear(); + + invalidate(); + checkSelectionChanged(); + + mDataChanged = false; + mNeedSync = false; + setNextSelectedPositionInt(mSelectedPosition); + + updateSelectedItemMetadata(); + } + + private void fillToGalleryLeft() { + int itemSpacing = mSpacing; + int galleryLeft = mPaddingLeft; + + // Set state for initial iteration + View prevIterationView = getChildAt(0); + int curPosition; + int curRightEdge; + + if (prevIterationView != null) { + curPosition = mFirstPosition - 1; + curRightEdge = prevIterationView.getLeft() - itemSpacing; + } else { + // No children available! + curPosition = 0; + curRightEdge = mRight - mLeft - mPaddingRight; + mShouldStopFling = true; + } + + while (curRightEdge > galleryLeft && curPosition >= 0) { + prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, + curRightEdge, false); + + // Remember some state + mFirstPosition = curPosition; + + // Set state for next iteration + curRightEdge = prevIterationView.getLeft() - itemSpacing; + curPosition--; + } + } + + private void fillToGalleryRight() { + int itemSpacing = mSpacing; + int galleryRight = mRight - mLeft - mPaddingRight; + int numChildren = getChildCount(); + int numItems = mItemCount; + + // Set state for initial iteration + View prevIterationView = getChildAt(numChildren - 1); + int curPosition; + int curLeftEdge; + + if (prevIterationView != null) { + curPosition = mFirstPosition + numChildren; + curLeftEdge = prevIterationView.getRight() + itemSpacing; + } else { + mFirstPosition = curPosition = mItemCount - 1; + curLeftEdge = mPaddingLeft; + mShouldStopFling = true; + } + + while (curLeftEdge < galleryRight && curPosition < numItems) { + prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, + curLeftEdge, true); + + // Set state for next iteration + curLeftEdge = prevIterationView.getRight() + itemSpacing; + curPosition++; + } + } + + /** + * Obtain a view, either by pulling an existing view from the recycler or by + * getting a new one from the adapter. If we are animating, make sure there + * is enough information in the view's layout parameters to animate from the + * old to new positions. + * + * @param position Position in the gallery for the view to obtain + * @param offset Offset from the selected position + * @param x X-coordintate indicating where this view should be placed. This + * will either be the left or right edge of the view, depending on + * the fromLeft paramter + * @param fromLeft Are we posiitoning views based on the left edge? (i.e., + * building from left to right)? + * @return A view that has been added to the gallery + */ + private View makeAndAddView(int position, int offset, int x, + boolean fromLeft) { + + View child; + + if (!mDataChanged) { + child = mRecycler.get(position); + if (child != null) { + // Can reuse an existing view + Gallery.LayoutParams lp = (Gallery.LayoutParams) + child.getLayoutParams(); + + int childLeft = child.getLeft(); + + // Remember left and right edges of where views have been placed + mRightMost = Math.max(mRightMost, childLeft + + child.getMeasuredWidth()); + mLeftMost = Math.min(mLeftMost, childLeft); + + // Position the view + setUpChild(child, offset, x, fromLeft); + + return child; + } + } + + // Nothing found in the recycler -- ask the adapter for a view + child = mAdapter.getView(position, null, this); + + // Position the view + setUpChild(child, offset, x, fromLeft); + + return child; + } + + /** + * Helper for makeAndAddView to set the position of a view and fill out its + * layout paramters. + * + * @param child The view to position + * @param offset Offset from the selected position + * @param x X-coordintate indicating where this view should be placed. This + * will either be the left or right edge of the view, depending on + * the fromLeft paramter + * @param fromLeft Are we posiitoning views based on the left edge? (i.e., + * building from left to right)? + */ + private void setUpChild(View child, int offset, int x, boolean fromLeft) { + + // Respect layout params that are already in the view. Otherwise + // make some up... + Gallery.LayoutParams lp = (Gallery.LayoutParams) + child.getLayoutParams(); + if (lp == null) { + lp = (Gallery.LayoutParams) generateDefaultLayoutParams(); + } + + addViewInLayout(child, fromLeft ? -1 : 0, lp); + + child.setSelected(offset == 0); + + // Get measure specs + int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, + mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height); + int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, + mSpinnerPadding.left + mSpinnerPadding.right, lp.width); + + // Measure child + child.measure(childWidthSpec, childHeightSpec); + + int childLeft; + int childRight; + + // Position vertically based on gravity setting + int childTop = calculateTop(child, lp, true); + int childBottom = childTop + child.getMeasuredHeight(); + + int width = child.getMeasuredWidth(); + if (fromLeft) { + childLeft = x; + childRight = childLeft + width; + } else { + childLeft = x - width; + childRight = x; + } + + child.layout(childLeft, childTop, childRight, childBottom); + } + + /** + * Figure out vertical placement based on mGravity + * + * @param child Child to place + * @param lp LayoutParams for this view (just so we don't keep looking them + * up) + * @return Where the top of the child should be + */ + private int calculateTop(View child, Gallery.LayoutParams lp, boolean duringLayout) { + int myHeight = duringLayout ? mMeasuredHeight : getHeight(); + int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight(); + + int childTop = 0; + + switch (mGravity) { + case Gravity.TOP: + childTop = mSpinnerPadding.top; + break; + case Gravity.CENTER_VERTICAL: + int availableSpace = myHeight - mSpinnerPadding.bottom + - mSpinnerPadding.top - childHeight; + childTop = mSpinnerPadding.top + (availableSpace / 2); + break; + case Gravity.BOTTOM: + childTop = myHeight - mSpinnerPadding.bottom - childHeight; + break; + } + return childTop; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + + // Give everything to the gesture detector + boolean retValue = mGestureDetector.onTouchEvent(event); + + int action = event.getAction(); + if (action == MotionEvent.ACTION_UP) { + // Helper method for lifted finger + onUp(); + } else if (action == MotionEvent.ACTION_CANCEL) { + onCancel(); + } + + return retValue; + } + + /** + * {@inheritDoc} + */ + public boolean onSingleTapUp(MotionEvent e) { + + if (mDownTouchPosition >= 0) { + + // An item tap should make it selected, so scroll to this child. + scrollToChild(mDownTouchPosition - mFirstPosition); + + // Also pass the click so the client knows, if it wants to. + if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) { + performItemClick(mDownTouchView, mDownTouchPosition, mAdapter + .getItemId(mDownTouchPosition)); + } + + return true; + } + + return false; + } + + /** + * {@inheritDoc} + */ + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + + if (!mShouldCallbackDuringFling) { + // This will get reset once we scroll into slots + mSuppressSelectionChanged = true; + } + + // Fling the gallery! + mFlingRunnable.startUsingVelocity((int) -velocityX); + + return true; + } + + /** + * {@inheritDoc} + */ + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + + if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX())); + + /* + * Now's a good time to tell our parent to stop intercepting our events! + * The user has moved more than the slop amount, since GestureDetector + * ensures this before calling this method. Also, if a parent is more + * interested in this touch's events than we are, it would have + * intercepted them by now (for example, we can assume when a Gallery is + * in the ListView, a vertical scroll would not end up in this method + * since a ListView would have intercepted it by now). + */ + mParent.requestDisallowInterceptTouchEvent(true); + + // As the user scrolls, we want to callback selection changes so related + // into on the screen is up-to-date with the user's selection + if (mSuppressSelectionChanged) { + mSuppressSelectionChanged = false; + } + + // Track the motion + trackMotionScroll(-1 * (int) distanceX); + + return true; + } + + /** + * {@inheritDoc} + */ + public boolean onDown(MotionEvent e) { + + // Kill any existing fling/scroll + mFlingRunnable.stop(false); + + // Get the item's view that was touched + mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY()); + + if (mDownTouchPosition >= 0) { + mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition); + mDownTouchView.setPressed(true); + } + + // Must return true to get matching events for this down event. + return true; + } + + /** + * Called when a touch event's action is MotionEvent.ACTION_UP. + */ + void onUp() { + + if (mFlingRunnable.mScroller.isFinished()) { + scrollIntoSlots(); + } + + dispatchUnpress(); + } + + /** + * Called when a touch event's action is MotionEvent.ACTION_CANCEL. + */ + void onCancel() { + onUp(); + } + + /** + * {@inheritDoc} + */ + public void onLongPress(MotionEvent e) { + + if (mDownTouchPosition < 0) { + return; + } + + long id = getItemIdAtPosition(mDownTouchPosition); + dispatchLongPress(mDownTouchView, mDownTouchPosition, id); + } + + // Unused methods from GestureDetector.OnGestureListener below + + /** + * {@inheritDoc} + */ + public void onShowPress(MotionEvent e) { + } + + // Unused methods from GestureDetector.OnGestureListener above + + private void dispatchPress(View child) { + + if (child != null) { + child.setPressed(true); + } + + setPressed(true); + } + + private void dispatchUnpress() { + + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).setPressed(false); + } + + setPressed(false); + } + + @Override + public void dispatchSetSelected(boolean selected) { + /* + * We don't want to pass the selected state given from its parent to its + * children since this widget itself has a selected state to give to its + * children. + */ + } + + @Override + protected void dispatchSetPressed(boolean pressed) { + + // Show the pressed state on the selected child + if (mSelectedChild != null) { + mSelectedChild.setPressed(pressed); + } + } + + @Override + protected ContextMenuInfo getContextMenuInfo() { + return mContextMenuInfo; + } + + @Override + public boolean showContextMenuForChild(View originalView) { + + final int longPressPosition = getPositionForView(originalView); + if (longPressPosition < 0) { + return false; + } + + final long longPressId = mAdapter.getItemId(longPressPosition); + return dispatchLongPress(originalView, longPressPosition, longPressId); + } + + @Override + public boolean showContextMenu() { + + if (isPressed() && mSelectedPosition >= 0) { + int index = mSelectedPosition - mFirstPosition; + View v = getChildAt(index); + return dispatchLongPress(v, mSelectedPosition, mSelectedRowId); + } + + return false; + } + + private boolean dispatchLongPress(View view, int position, long id) { + boolean handled = false; + + if (mOnItemLongClickListener != null) { + handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView, + mDownTouchPosition, id); + } + + if (!handled) { + mContextMenuInfo = new AdapterContextMenuInfo(view, position, id); + handled = super.showContextMenuForChild(this); + } + + return handled; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Gallery steals all key events + return event.dispatch(this); + } + + /** + * Handles left, right, and clicking + * @see android.view.View#onKeyDown + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + + case KeyEvent.KEYCODE_DPAD_LEFT: + if (movePrevious()) { + playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); + } + return true; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (moveNext()) { + playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); + } + return true; + } + + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: { + if (mItemCount > 0) { + + dispatchPress(mSelectedChild); + postDelayed(new Runnable() { + public void run() { + dispatchUnpress(); + } + }, ViewConfiguration.getPressedStateDuration()); + + int selectedIndex = mSelectedPosition - mFirstPosition; + performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter + .getItemId(mSelectedPosition)); + } + return true; + } + } + + return super.onKeyUp(keyCode, event); + } + + boolean movePrevious() { + if (mItemCount > 0 && mSelectedPosition > 0) { + scrollToChild(mSelectedPosition - mFirstPosition - 1); + return true; + } else { + return false; + } + } + + boolean moveNext() { + if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) { + scrollToChild(mSelectedPosition - mFirstPosition + 1); + return true; + } else { + return false; + } + } + + private boolean scrollToChild(int childPosition) { + View child = getChildAt(childPosition); + + if (child != null) { + int distance = getCenterOfGallery() - getCenterOfView(child); + mFlingRunnable.startUsingDistance(distance); + return true; + } + + return false; + } + + @Override + void setSelectedPositionInt(int position) { + super.setSelectedPositionInt(position); + + // Updates any metadata we keep about the selected item. + updateSelectedItemMetadata(); + } + + private void updateSelectedItemMetadata() { + + View oldSelectedChild = mSelectedChild; + + View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition); + if (child == null) { + return; + } + + child.setSelected(true); + child.setFocusable(true); + + if (hasFocus()) { + child.requestFocus(); + } + + // We unfocus the old child down here so the above hasFocus check + // returns true + if (oldSelectedChild != null) { + + // Make sure its drawable state doesn't contain 'selected' + oldSelectedChild.setSelected(false); + + // Make sure it is not focusable anymore, since otherwise arrow keys + // can make this one be focused + oldSelectedChild.setFocusable(false); + } + + } + + /** + * Describes how the child views are aligned. + * @param gravity + * + * @attr ref android.R.styleable#Gallery_gravity + */ + public void setGravity(int gravity) + { + if (mGravity != gravity) { + mGravity = gravity; + requestLayout(); + } + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + int selectedIndex = mSelectedPosition - mFirstPosition; + + // Just to be safe + if (selectedIndex < 0) return i; + + if (i == childCount - 1) { + // Draw the selected child last + return selectedIndex; + } else if (i >= selectedIndex) { + // Move the children to the right of the selected child earlier one + return i + 1; + } else { + // Keep the children to the left of the selected child the same + return i; + } + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + + /* + * The gallery shows focus by focusing the selected item. So, give + * focus to our selected item instead. We steal keys from our + * selected item elsewhere. + */ + if (gainFocus && mSelectedChild != null) { + mSelectedChild.requestFocus(direction); + } + + } + + /** + * Responsible for fling behavior. Use {@link #startUsingVelocity(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 Scroller mScroller; + + /** + * X value reported by mScroller on the previous fling + */ + private int mLastFlingX; + + public FlingRunnable() { + mScroller = new Scroller(getContext()); + } + + private void startCommon() { + // Remove any pending flings + removeCallbacks(this); + } + + public void startUsingVelocity(int initialVelocity) { + if (initialVelocity == 0) return; + + startCommon(); + + int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0; + mLastFlingX = initialX; + mScroller.fling(initialX, 0, initialVelocity, 0, + 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); + post(this); + } + + public void startUsingDistance(int distance) { + if (distance == 0) return; + + startCommon(); + + mLastFlingX = 0; + mScroller.startScroll(0, 0, -distance, 0, mAnimationDuration); + post(this); + } + + public void stop(boolean scrollIntoSlots) { + removeCallbacks(this); + endFling(scrollIntoSlots); + } + + private void endFling(boolean scrollIntoSlots) { + /* + * Force the scroller's status to finished (without setting its + * position to the end) + */ + mScroller.forceFinished(true); + + if (scrollIntoSlots) scrollIntoSlots(); + } + + public void run() { + + if (mItemCount == 0) { + endFling(true); + return; + } + + mShouldStopFling = false; + + final Scroller scroller = mScroller; + boolean more = scroller.computeScrollOffset(); + final int x = scroller.getCurrX(); + + // Flip sign to convert finger direction to list items direction + // (e.g. finger moving down means list is moving towards the top) + int delta = mLastFlingX - x; + + // Pretend that each frame of a fling scroll is a touch scroll + if (delta > 0) { + // Moving towards the left. Use first view as mDownTouchPosition + mDownTouchPosition = mFirstPosition; + + // Don't fling more than 1 screen + delta = Math.min(getWidth() - mPaddingLeft - mPaddingRight - 1, delta); + } else { + // Moving towards the right. Use last view as mDownTouchPosition + int offsetToLast = getChildCount() - 1; + mDownTouchPosition = mFirstPosition + offsetToLast; + + // Don't fling more than 1 screen + delta = Math.max(-(getWidth() - mPaddingRight - mPaddingLeft - 1), delta); + } + + trackMotionScroll(delta); + + if (more && !mShouldStopFling) { + mLastFlingX = x; + post(this); + } else { + endFling(true); + } + } + + } + + /** + * Gallery extends LayoutParams to provide a place to hold current + * Transformation information along with previous position/transformation + * info. + * + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int w, int h) { + super(w, h); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + } +} diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java new file mode 100644 index 0000000..268bf84 --- /dev/null +++ b/core/java/android/widget/GridView.java @@ -0,0 +1,1828 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.SoundEffectConstants; +import android.view.animation.GridLayoutAnimationController; + + +/** + * A view that shows items in two-dimensional scrolling grid. The items in the + * grid come from the {@link ListAdapter} associated with this view. + */ +public class GridView extends AbsListView { + public static final int NO_STRETCH = 0; + public static final int STRETCH_SPACING = 1; + public static final int STRETCH_COLUMN_WIDTH = 2; + + public static final int AUTO_FIT = -1; + + private int mNumColumns = AUTO_FIT; + + private int mHorizontalSpacing = 0; + private int mRequestedHorizontalSpacing; + private int mVerticalSpacing = 0; + private int mStretchMode = STRETCH_COLUMN_WIDTH; + private int mColumnWidth; + private int mRequestedColumnWidth; + private int mRequestedNumColumns; + + private View mReferenceView = null; + private View mReferenceViewInSelectedRow = null; + + private int mGravity = Gravity.LEFT; + + private final Rect mTempRect = new Rect(); + + public GridView(Context context) { + super(context); + } + + public GridView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.gridViewStyle); + } + + public GridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.GridView, defStyle, 0); + + int hSpacing = a.getDimensionPixelOffset( + com.android.internal.R.styleable.GridView_horizontalSpacing, 0); + setHorizontalSpacing(hSpacing); + + int vSpacing = a.getDimensionPixelOffset( + com.android.internal.R.styleable.GridView_verticalSpacing, 0); + setVerticalSpacing(vSpacing); + + int index = a.getInt(com.android.internal.R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH); + if (index >= 0) { + setStretchMode(index); + } + + int columnWidth = a.getDimensionPixelOffset(com.android.internal.R.styleable.GridView_columnWidth, -1); + if (columnWidth > 0) { + setColumnWidth(columnWidth); + } + + int numColumns = a.getInt(com.android.internal.R.styleable.GridView_numColumns, 1); + setNumColumns(numColumns); + + index = a.getInt(com.android.internal.R.styleable.GridView_gravity, -1); + if (index >= 0) { + setGravity(index); + } + + a.recycle(); + } + + @Override + public ListAdapter getAdapter() { + return mAdapter; + } + + /** + * Sets the data behind this GridView. + * + * @param adapter the adapter providing the grid's data + */ + @Override + public void setAdapter(ListAdapter adapter) { + if (null != mAdapter) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + } + + resetList(); + mRecycler.clear(); + mAdapter = adapter; + + mOldSelectedPosition = INVALID_POSITION; + mOldSelectedRowId = INVALID_ROW_ID; + + if (mAdapter != null) { + mOldItemCount = mItemCount; + mItemCount = mAdapter.getCount(); + mDataChanged = true; + 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); + checkSelectionChanged(); + } else { + checkFocus(); + // Nothing selected + checkSelectionChanged(); + } + + requestLayout(); + } + + @Override + int lookForSelectablePosition(int position, boolean lookDown) { + final ListAdapter adapter = mAdapter; + if (adapter == null || isInTouchMode()) { + return INVALID_POSITION; + } + + if (position < 0 || position >= mItemCount) { + return INVALID_POSITION; + } + return position; + } + + /** + * {@inheritDoc} + */ + @Override + void fillGap(boolean down) { + final int numColumns = mNumColumns; + final int verticalSpacing = mVerticalSpacing; + + final int count = getChildCount(); + + if (down) { + final int startOffset = count > 0 ? + getChildAt(count - 1).getBottom() + verticalSpacing : getListPaddingTop(); + int position = mFirstPosition + count; + if (mStackFromBottom) { + position += numColumns - 1; + } + fillDown(position, startOffset); + correctTooHigh(numColumns, verticalSpacing, getChildCount()); + } else { + final int startOffset = count > 0 ? + getChildAt(0).getTop() - verticalSpacing : getHeight() - getListPaddingBottom(); + int position = mFirstPosition; + if (!mStackFromBottom) { + position -= numColumns; + } else { + position--; + } + fillUp(position, startOffset); + correctTooLow(numColumns, verticalSpacing, 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 nextTop) { + View selectedView = null; + + final int end = (mBottom - mTop) - mListPadding.bottom; + + while (nextTop < end && pos < mItemCount) { + View temp = makeRow(pos, nextTop, true); + if (temp != null) { + selectedView = temp; + } + + nextTop = mReferenceView.getBottom() + mVerticalSpacing; + + pos += mNumColumns; + } + + return selectedView; + } + + private View makeRow(int startPos, int y, boolean flow) { + int last; + int nextLeft = mListPadding.left; + + final int columnWidth = mColumnWidth; + final int horizontalSpacing = mHorizontalSpacing; + + if (!mStackFromBottom) { + last = Math.min(startPos + mNumColumns, mItemCount); + } else { + last = startPos + 1; + startPos = Math.max(0, startPos - mNumColumns + 1); + + if (last - startPos < mNumColumns) { + nextLeft += (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing); + } + } + + View selectedView = null; + + final boolean hasFocus = shouldShowSelector(); + final boolean inClick = touchModeDrawsInPressedState(); + final int selectedPosition = mSelectedPosition; + + mReferenceView = null; + + for (int pos = startPos; pos < last; pos++) { + // is this the selected item? + boolean selected = pos == selectedPosition; + // does the list view have focus or contain focus + + final int where = flow ? -1 : pos - startPos; + final View child = makeAndAddView(pos, y, flow, nextLeft, selected, where); + mReferenceView = child; + + nextLeft += columnWidth; + if (pos < last - 1) { + nextLeft += horizontalSpacing; + } + + if (selected && (hasFocus || inClick)) { + selectedView = child; + } + } + + if (selectedView != null) { + mReferenceViewInSelectedRow = mReferenceView; + } + + 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 nextBottom) { + View selectedView = null; + + final int end = mListPadding.top; + + while (nextBottom > end && pos >= 0) { + + View temp = makeRow(pos, nextBottom, false); + if (temp != null) { + selectedView = temp; + } + + nextBottom = mReferenceView.getTop() - mVerticalSpacing; + + mFirstPosition = pos; + + pos -= mNumColumns; + } + + if (mStackFromBottom) { + mFirstPosition = Math.max(0, 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; + } + mFirstPosition -= mFirstPosition % mNumColumns; + return fillDown(mFirstPosition, nextTop); + } + + private View fillFromBottom(int lastPosition, int nextBottom) { + lastPosition = Math.max(lastPosition, mSelectedPosition); + lastPosition = Math.min(lastPosition, mItemCount - 1); + + final int invertedPosition = mItemCount - 1 - lastPosition; + lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns)); + + return fillUp(lastPosition, nextBottom); + } + + private View fillSelection(int childrenTop, int childrenBottom) { + final int selectedPosition = reconcileSelectedPosition(); + final int numColumns = mNumColumns; + final int verticalSpacing = mVerticalSpacing; + + int rowStart; + int rowEnd = -1; + + if (!mStackFromBottom) { + rowStart = selectedPosition - (selectedPosition % numColumns); + } else { + final int invertedSelection = mItemCount - 1 - selectedPosition; + + rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + rowStart = Math.max(0, rowEnd - numColumns + 1); + } + + final int fadingEdgeLength = getVerticalFadingEdgeLength(); + final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); + + final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true); + mFirstPosition = rowStart; + + final View referenceView = mReferenceView; + + if (!mStackFromBottom) { + fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); + pinToBottom(childrenBottom); + fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); + adjustViewsUpOrDown(); + } else { + final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, + fadingEdgeLength, numColumns, rowStart); + final int offset = bottomSelectionPixel - referenceView.getBottom(); + offsetChildrenTopAndBottom(offset); + fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); + pinToTop(childrenTop); + fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); + adjustViewsUpOrDown(); + } + + return sel; + } + + private void pinToTop(int childrenTop) { + if (mFirstPosition == 0) { + final int top = getChildAt(0).getTop(); + final int offset = childrenTop - top; + if (offset < 0) { + offsetChildrenTopAndBottom(offset); + } + } + } + + private void pinToBottom(int childrenBottom) { + final int count = getChildCount(); + if (mFirstPosition + count == mItemCount) { + final int bottom = getChildAt(count - 1).getBottom(); + final int offset = childrenBottom - bottom; + if (offset > 0) { + offsetChildrenTopAndBottom(offset); + } + } + } + + @Override + int findMotionRow(int y) { + final int childCount = getChildCount(); + if (childCount > 0) { + + final int numColumns = mNumColumns; + if (!mStackFromBottom) { + for (int i = 0; i < childCount; i += numColumns) { + if (y <= getChildAt(i).getBottom()) { + return mFirstPosition + i; + } + } + } else { + for (int i = childCount - 1; i >= 0; i -= numColumns) { + if (y >= getChildAt(i).getTop()) { + return mFirstPosition + i; + } + } + } + + return mFirstPosition + childCount - 1; + } + return INVALID_POSITION; + } + + /** + * Layout during a scroll that results from tracking motion events. Places + * the mMotionPosition view at the offset specified by mMotionViewTop, and + * then build surrounding views from there. + * + * @param position the position at which to start filling + * @param top the top of the view at that position + * @return The selected view, or null if the selected view is outside the + * visible area. + */ + private View fillSpecific(int position, int top) { + final int numColumns = mNumColumns; + + int motionRowStart; + int motionRowEnd = -1; + + if (!mStackFromBottom) { + motionRowStart = position - (position % numColumns); + } else { + final int invertedSelection = mItemCount - 1 - position; + + motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + motionRowStart = Math.max(0, motionRowEnd - numColumns + 1); + } + + final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true); + + // Possibly changed again in fillUp if we add rows above this one. + mFirstPosition = motionRowStart; + + final View referenceView = mReferenceView; + final int verticalSpacing = mVerticalSpacing; + + View above; + View below; + + if (!mStackFromBottom) { + above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing); + adjustViewsUpOrDown(); + below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing); + // Check if we have dragged the bottom of the grid too high + final int childCount = getChildCount(); + if (childCount > 0) { + correctTooHigh(numColumns, verticalSpacing, childCount); + } + } else { + below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing); + adjustViewsUpOrDown(); + above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing); + // Check if we have dragged the bottom of the grid too high + final int childCount = getChildCount(); + if (childCount > 0) { + correctTooLow(numColumns, verticalSpacing, childCount); + } + } + + if (temp != null) { + return temp; + } else if (above != null) { + return above; + } else { + return below; + } + } + + private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) { + // First see if the last item is visible + final 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; + + // This is how far the bottom edge of the last view is from the bottom of the + // drawable area + int bottomOffset = end - lastBottom; + + final 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); + if (mFirstPosition > 0) { + // Fill the gap that was opened above mFirstPosition with more rows, if + // possible + fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns), + firstChild.getTop() - verticalSpacing); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + } + } + } + + private void correctTooLow(int numColumns, int verticalSpacing, int childCount) { + 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; + + // This is how far the top edge of the first view is from the top of the + // drawable area + int topOffset = firstTop - start; + final View lastChild = getChildAt(childCount - 1); + final int lastBottom = lastChild.getBottom(); + final 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 && (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); + if (lastPosition < mItemCount - 1) { + // Fill the gap that was opened below the last position with more rows, if + // possible + fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns), + lastChild.getBottom() + verticalSpacing); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + } + } + } + + /** + * 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) { + final int fadingEdgeLength = getVerticalFadingEdgeLength(); + final int selectedPosition = mSelectedPosition; + final int numColumns = mNumColumns; + final int verticalSpacing = mVerticalSpacing; + + int rowStart; + int rowEnd = -1; + + if (!mStackFromBottom) { + rowStart = selectedPosition - (selectedPosition % numColumns); + } else { + int invertedSelection = mItemCount - 1 - selectedPosition; + + rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + rowStart = Math.max(0, rowEnd - numColumns + 1); + } + + View sel; + View referenceView; + + int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); + int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, + numColumns, rowStart); + + sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true); + // Possibly changed again in fillUp if we add rows above this one. + mFirstPosition = rowStart; + + referenceView = mReferenceView; + adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); + adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); + + if (!mStackFromBottom) { + fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); + adjustViewsUpOrDown(); + fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); + } else { + fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); + adjustViewsUpOrDown(); + fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); + } + + + 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 numColumns Number of columns in the grid + * @param rowStart The start of the row that will contain the selection + * @return The bottom-most pixel we can draw the selection into + */ + private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, + int numColumns, int rowStart) { + // Last pixel we can draw the selection into + int bottomSelectionPixel = childrenBottom; + if (rowStart + numColumns - 1 < 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 rowStart The start of the row that will contain the selection + * @return The top-most pixel we can draw the selection into + */ + private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) { + // first pixel we can draw the selection into + int topSelectionPixel = childrenTop; + if (rowStart > 0) { + topSelectionPixel += fadingEdgeLength; + } + return topSelectionPixel; + } + + /** + * Move all views upwards so the selected row does not interesect the bottom + * fading edge (if necessary). + * + * @param childInSelectedRow A child in the row that contains the selection + * @param topSelectionPixel The topmost pixel we can draw the selection into + * @param bottomSelectionPixel The bottommost pixel we can draw the + * selection into + */ + private void adjustForBottomFadingEdge(View childInSelectedRow, + int topSelectionPixel, int bottomSelectionPixel) { + // Some of the newly selected item extends below the bottom of the + // list + if (childInSelectedRow.getBottom() > bottomSelectionPixel) { + + // Find space available above the selection into which we can + // scroll upwards + int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel; + + // Find space required to bring the bottom of the selected item + // fully into view + int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel; + int offset = Math.min(spaceAbove, spaceBelow); + + // Now offset the selected item to get it into view + offsetChildrenTopAndBottom(-offset); + } + } + + /** + * Move all views upwards so the selected row does not interesect the top + * fading edge (if necessary). + * + * @param childInSelectedRow A child in the row that contains the selection + * @param topSelectionPixel The topmost pixel we can draw the selection into + * @param bottomSelectionPixel The bottommost pixel we can draw the + * selection into + */ + private void adjustForTopFadingEdge(View childInSelectedRow, + int topSelectionPixel, int bottomSelectionPixel) { + // Some of the newly selected item extends above the top of the list + if (childInSelectedRow.getTop() < topSelectionPixel) { + // Find space required to bring the top of the selected item + // fully into view + int spaceAbove = topSelectionPixel - childInSelectedRow.getTop(); + + // Find space available below the selection into which we can + // scroll downwards + int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom(); + int offset = Math.min(spaceAbove, spaceBelow); + + // Now offset the selected item to get it into view + offsetChildrenTopAndBottom(offset); + } + } + + /** + * Fills the grid based on positioning the new selection relative to the old + * selection. The new selection will be placed at, above, or below the + * location of the new selection depending on how the selection is moving. + * The selection will then be pinned to the visible part of the screen, + * excluding the edges that are faded. The grid is then filled upwards and + * downwards from there. + * + * @param delta Which way we are moving + * @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 moveSelection(int delta, int childrenTop, int childrenBottom) { + final int fadingEdgeLength = getVerticalFadingEdgeLength(); + final int selectedPosition = mSelectedPosition; + final int numColumns = mNumColumns; + final int verticalSpacing = mVerticalSpacing; + + int oldRowStart; + int rowStart; + int rowEnd = -1; + + if (!mStackFromBottom) { + oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns); + + rowStart = selectedPosition - (selectedPosition % numColumns); + } else { + int invertedSelection = mItemCount - 1 - selectedPosition; + + rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + rowStart = Math.max(0, rowEnd - numColumns + 1); + + invertedSelection = mItemCount - 1 - (selectedPosition - delta); + oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + oldRowStart = Math.max(0, oldRowStart - numColumns + 1); + } + + final int rowDelta = rowStart - oldRowStart; + + final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); + final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, + numColumns, rowStart); + + // Possibly changed again in fillUp if we add rows above this one. + mFirstPosition = rowStart; + + View sel; + View referenceView; + + if (rowDelta > 0) { + /* + * Case 1: Scrolling down. + */ + + final int oldBottom = mReferenceViewInSelectedRow == null ? 0 : + mReferenceViewInSelectedRow.getBottom(); + + sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true); + referenceView = mReferenceView; + + adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); + } else if (rowDelta < 0) { + /* + * Case 2: Scrolling up. + */ + final int oldTop = mReferenceViewInSelectedRow == null ? + 0 : mReferenceViewInSelectedRow .getTop(); + + sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false); + referenceView = mReferenceView; + + adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); + } else { + /* + * Keep selection where it was + */ + final int oldTop = mReferenceViewInSelectedRow == null ? + 0 : mReferenceViewInSelectedRow .getTop(); + + sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true); + referenceView = mReferenceView; + } + + if (!mStackFromBottom) { + fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); + adjustViewsUpOrDown(); + fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); + } else { + fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); + adjustViewsUpOrDown(); + fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); + } + + return sel; + } + + private void determineColumns(int availableSpace) { + final int requestedHorizontalSpacing = mRequestedHorizontalSpacing; + final int stretchMode = mStretchMode; + final int requestedColumnWidth = mRequestedColumnWidth; + + if (mRequestedNumColumns == AUTO_FIT) { + if (requestedColumnWidth > 0) { + // Client told us to pick the number of columns + mNumColumns = (availableSpace + requestedHorizontalSpacing) / + (requestedColumnWidth + requestedHorizontalSpacing); + } else { + // Just make up a number if we don't have enough info + mNumColumns = 2; + } + } else { + // We picked the columns + mNumColumns = mRequestedNumColumns; + } + + if (mNumColumns <= 0) { + mNumColumns = 1; + } + + switch (stretchMode) { + case NO_STRETCH: + // Nobody stretches + mColumnWidth = requestedColumnWidth; + mHorizontalSpacing = requestedHorizontalSpacing; + break; + + default: + int spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - + ((mNumColumns - 1) * requestedHorizontalSpacing); + switch (stretchMode) { + case STRETCH_COLUMN_WIDTH: + // Stretch the columns + mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns; + mHorizontalSpacing = requestedHorizontalSpacing; + break; + + case STRETCH_SPACING: + // Stretch the spacing between columns + mColumnWidth = requestedColumnWidth; + if (mNumColumns > 1) { + mHorizontalSpacing = requestedHorizontalSpacing + + spaceLeftOver / (mNumColumns - 1); + } else { + mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; + } + break; + } + + break; + } + } + + @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); + + if (widthMode == MeasureSpec.UNSPECIFIED) { + if (mColumnWidth > 0) { + widthSize = mColumnWidth + mListPadding.left + mListPadding.right; + } else { + widthSize = mListPadding.left + mListPadding.right; + } + widthSize += getVerticalScrollbarWidth(); + } + + int childWidth = widthSize - mListPadding.left - mListPadding.right; + determineColumns(childWidth); + + int childHeight = 0; + + mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); + final int count = mItemCount; + if (count > 0) { + final View child = obtainView(0); + final int childViewType = mAdapter.getItemViewType(0); + + AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); + if (lp == null) { + lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + child.setLayoutParams(lp); + } + lp.viewType = childViewType; + + final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, + mListPadding.left + mListPadding.right, lp.width); + + int lpHeight = lp.height; + + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + + child.measure(childWidthSpec, childHeightSpec); + childHeight = child.getMeasuredHeight(); + + if (mRecycler.shouldRecycleViewType(childViewType)) { + mRecycler.addScrapView(child); + } + } + + if (heightMode == MeasureSpec.UNSPECIFIED) { + heightSize = mListPadding.top + mListPadding.bottom + childHeight + + getVerticalFadingEdgeLength() * 2; + } + + if (heightMode == MeasureSpec.AT_MOST) { + int ourSize = mListPadding.top + mListPadding.bottom; + + final int numColumns = mNumColumns; + for (int i = 0; i < count; i += numColumns) { + ourSize += childHeight; + if (i + numColumns < count) { + ourSize += mVerticalSpacing; + } + if (ourSize >= heightSize) { + ourSize = heightSize; + break; + } + } + heightSize = ourSize; + } + + setMeasuredDimension(widthSize, heightSize); + mWidthMeasureSpec = widthMeasureSpec; + } + + @Override + protected void attachLayoutAnimationParameters(View child, + ViewGroup.LayoutParams params, int index, int count) { + + GridLayoutAnimationController.AnimationParameters animationParams = + (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters; + + if (animationParams == null) { + animationParams = new GridLayoutAnimationController.AnimationParameters(); + params.layoutAnimationParameters = animationParams; + } + + animationParams.count = count; + animationParams.index = index; + animationParams.columnsCount = mNumColumns; + animationParams.rowsCount = count / mNumColumns; + + if (!mStackFromBottom) { + animationParams.column = index % mNumColumns; + animationParams.row = index / mNumColumns; + } else { + final int invertedIndex = count - 1 - index; + + animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns); + animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns; + } + } + + @Override + protected void layoutChildren() { + final boolean blockLayoutRequests = mBlockLayoutRequests; + if (!blockLayoutRequests) { + mBlockLayoutRequests = true; + } + + try { + super.layoutChildren(); + + invalidate(); + + if (mAdapter == null) { + resetList(); + invokeOnItemScrollListener(); + return; + } + + final int childrenTop = mListPadding.top; + final int childrenBottom = mBottom - mTop - mListPadding.bottom; + + int childCount = getChildCount(); + int index; + int delta = 0; + + View sel; + View oldSel = null; + View oldFirst = null; + View newSel = 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: + if (mNextSelectedPosition >= 0) { + delta = mNextSelectedPosition - mSelectedPosition; + } + break; + 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); + } + + 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; + } + + setSelectedPositionInt(mNextSelectedPosition); + + // Pull all children into the RecycleBin. + // These views will be reused if possible + final int firstPosition = mFirstPosition; + final RecycleBin recycleBin = mRecycler; + + if (dataChanged) { + for (int i = 0; i < childCount; i++) { + recycleBin.addScrapView(getChildAt(i)); + } + } else { + recycleBin.fillActiveViews(childCount, firstPosition); + } + + // Clear out old views + //removeAllViewsInLayout(); + detachAllViewsFromParent(); + + switch (mLayoutMode) { + case LAYOUT_SET_SELECTION: + if (newSel != null) { + sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); + } else { + sel = fillSelection(childrenTop, childrenBottom); + } + break; + case LAYOUT_FORCE_TOP: + mFirstPosition = 0; + sel = fillFromTop(childrenTop); + adjustViewsUpOrDown(); + break; + case LAYOUT_FORCE_BOTTOM: + sel = fillUp(mItemCount - 1, childrenBottom); + adjustViewsUpOrDown(); + break; + case LAYOUT_SPECIFIC: + sel = fillSpecific(mSelectedPosition, mSpecificTop); + break; + case LAYOUT_SYNC: + sel = fillSpecific(mSyncPosition, mSpecificTop); + break; + case LAYOUT_MOVE_SELECTION: + // Move the selection relative to its old position + sel = moveSelection(delta, childrenTop, childrenBottom); + break; + default: + if (childCount == 0) { + if (!mStackFromBottom) { + setSelectedPositionInt(0); + sel = fillFromTop(childrenTop); + } else { + final int last = mItemCount - 1; + setSelectedPositionInt(last); + sel = fillFromBottom(last, 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) { + positionSelector(sel); + mSelectedTop = sel.getTop(); + } else { + mSelectedTop = 0; + mSelectorRect.setEmpty(); + } + + mLayoutMode = LAYOUT_NORMAL; + mDataChanged = false; + mNeedSync = false; + setNextSelectedPositionInt(mSelectedPosition); + + updateScrollIndicators(); + + if (mItemCount > 0) { + checkSelectionChanged(); + } + + invokeOnItemScrollListener(); + } finally { + if (!blockLayoutRequests) { + mBlockLayoutRequests = 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 y Top or bottom edge of the view to add + * @param flow 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 where to add new item in the list + * @return View that was added + */ + private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, + boolean selected, int where) { + View child; + + if (!mDataChanged) { + // Try to use an exsiting view for this position + child = mRecycler.getActiveView(position); + if (child != null) { + // Found it -- we're using an existing child + // This just needs to be positioned + setupChild(child, position, y, flow, childrenLeft, selected, true, where); + return child; + } + } + + // Make a new view for this position, or convert an unused view if + // possible + child = obtainView(position); + + // This needs to be positioned and measured + setupChild(child, position, y, flow, childrenLeft, selected, false, where); + + return child; + } + + /** + * 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 the view + * @param y The y position relative to which this view will be positioned + * @param flow 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. + * @param where Where to add the item in the list + * + */ + private void setupChild(View child, int position, int y, boolean flow, int childrenLeft, + boolean selected, boolean recycled, int where) { + boolean isSelected = selected && shouldShowSelector(); + + final boolean updateChildSelected = isSelected != child.isSelected(); + boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); + + // Respect layout params that are already in the view. Otherwise make + // some up... + AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); + if (p == null) { + p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + p.viewType = mAdapter.getItemViewType(position); + + if (recycled) { + attachViewToParent(child, where, p); + } else { + addViewInLayout(child, where, p, true); + } + + if (updateChildSelected) { + child.setSelected(isSelected); + if (isSelected) { + requestFocus(); + } + } + + if (needToMeasure) { + int childHeightSpec = ViewGroup.getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); + + int childWidthSpec = ViewGroup.getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); + child.measure(childWidthSpec, childHeightSpec); + } else { + cleanupLayoutState(child); + } + + final int w = child.getMeasuredWidth(); + final int h = child.getMeasuredHeight(); + + int childLeft; + final int childTop = flow ? y : y - h; + + switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + childLeft = childrenLeft; + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = childrenLeft + ((mColumnWidth - w) / 2); + break; + case Gravity.RIGHT: + childLeft = childrenLeft + mColumnWidth - w; + break; + default: + childLeft = childrenLeft; + break; + } + + if (needToMeasure) { + final int childRight = childLeft + w; + final int childBottom = childTop + h; + child.layout(childLeft, childTop, childRight, childBottom); + } else { + child.offsetLeftAndRight(childLeft - child.getLeft()); + child.offsetTopAndBottom(childTop - child.getTop()); + } + + if (mCachingStarted) { + child.setDrawingCacheEnabled(true); + } + } + + /** + * Sets the currently selected item + * + * @param position Index (starting at 0) of the data item to be selected. + * + * If in touch mode, the item will not be selected but it will still be positioned + * appropriately. + */ + @Override + public void setSelection(int position) { + if (!isInTouchMode()) { + setNextSelectedPositionInt(position); + } else { + mResurrectToPosition = position; + } + mLayoutMode = LAYOUT_SET_SELECTION; + requestLayout(); + } + + /** + * Makes the item at the supplied position selected. + * + * @param position the position of the new selection + */ + @Override + void setSelectionInt(int position) { + mBlockLayoutRequests = true; + setNextSelectedPositionInt(position); + layoutChildren(); + + mBlockLayoutRequests = false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return commonKey(keyCode, 1, event); + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return commonKey(keyCode, repeatCount, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return commonKey(keyCode, 1, event); + } + + private boolean commonKey(int keyCode, int count, KeyEvent event) { + if (mAdapter == null) { + return false; + } + + if (mDataChanged) { + layoutChildren(); + } + + boolean handled = false; + int action = event.getAction(); + + if (action != KeyEvent.ACTION_UP) { + if (mSelectedPosition < 0) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_SPACE: + case KeyEvent.KEYCODE_ENTER: + resurrectSelection(); + return true; + } + } + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + handled = arrowScroll(FOCUS_LEFT); + break; + + + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled = arrowScroll(FOCUS_RIGHT); + break; + + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(FOCUS_UP); + + } else { + handled = fullScroll(FOCUS_UP); + } + break; + + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(FOCUS_DOWN); + } else { + handled = fullScroll(FOCUS_DOWN); + } + break; + + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: { + if (getChildCount() > 0 && event.getRepeatCount() == 0) { + keyPressed(); + } + + return true; + } + + case KeyEvent.KEYCODE_SPACE: + if (mPopup == null || !mPopup.isShowing()) { + if (!event.isShiftPressed()) { + handled = pageScroll(FOCUS_DOWN); + } else { + handled = pageScroll(FOCUS_UP); + } + } + break; + } + + } + + if (!handled) { + handled = sendToTextFilter(keyCode, count, event); + } + + if (handled) { + return true; + } else { + switch (action) { + case KeyEvent.ACTION_DOWN: + return super.onKeyDown(keyCode, event); + case KeyEvent.ACTION_UP: + return super.onKeyUp(keyCode, event); + case KeyEvent.ACTION_MULTIPLE: + return super.onKeyMultiple(keyCode, count, event); + default: + return false; + } + } + } + + /** + * 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; + + if (direction == FOCUS_UP) { + nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); + } else if (direction == FOCUS_DOWN) { + nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); + } + + if (nextPage >= 0) { + setSelectionInt(nextPage); + return true; + } + + return false; + } + + /** + * Go to the last or first item if possible. + * + * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}. + * + * @return Whether selection was moved. + */ + boolean fullScroll(int direction) { + boolean moved = false; + if (direction == FOCUS_UP) { + mLayoutMode = LAYOUT_SET_SELECTION; + setSelectionInt(0); + moved = true; + } else if (direction == FOCUS_DOWN) { + mLayoutMode = LAYOUT_SET_SELECTION; + setSelectionInt(mItemCount - 1); + moved = true; + } + + return moved; + } + + /** + * Scrolls to the next or previous item, horizontally or vertically. + * + * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} + * + * @return whether selection was moved + */ + boolean arrowScroll(int direction) { + final int selectedPosition = mSelectedPosition; + final int numColumns = mNumColumns; + + int startOfRowPos; + int endOfRowPos; + + boolean moved = false; + + if (!mStackFromBottom) { + startOfRowPos = (selectedPosition / numColumns) * numColumns; + endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1); + } else { + final int invertedSelection = mItemCount - 1 - selectedPosition; + endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns; + startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1); + } + + switch (direction) { + case FOCUS_UP: + if (startOfRowPos > 0) { + mLayoutMode = LAYOUT_MOVE_SELECTION; + setSelectionInt(Math.max(0, selectedPosition - numColumns)); + moved = true; + } + break; + case FOCUS_DOWN: + if (endOfRowPos < mItemCount - 1) { + mLayoutMode = LAYOUT_MOVE_SELECTION; + setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1)); + moved = true; + } + break; + case FOCUS_LEFT: + if (selectedPosition > startOfRowPos) { + mLayoutMode = LAYOUT_MOVE_SELECTION; + setSelectionInt(selectedPosition - 1); + moved = true; + } + break; + case FOCUS_RIGHT: + if (selectedPosition < endOfRowPos) { + mLayoutMode = LAYOUT_MOVE_SELECTION; + setSelectionInt(selectedPosition + 1); + moved = true; + } + break; + } + + if (moved) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + + return moved; + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + + int closestChildIndex = -1; + if (gainFocus && previouslyFocusedRect != null) { + previouslyFocusedRect.offset(mScrollX, mScrollY); + + // figure out which item should be selected based on previously + // focused rect + Rect otherRect = mTempRect; + int minDistance = Integer.MAX_VALUE; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + // only consider view's on appropriate edge of grid + if (!isCandidateSelection(i, direction)) { + continue; + } + + final View other = getChildAt(i); + other.getDrawingRect(otherRect); + offsetDescendantRectToMyCoords(other, otherRect); + int distance = getDistance(previouslyFocusedRect, otherRect, direction); + + if (distance < minDistance) { + minDistance = distance; + closestChildIndex = i; + } + } + } + + if (closestChildIndex >= 0) { + setSelection(closestChildIndex + mFirstPosition); + } else { + requestLayout(); + } + } + + /** + * Is childIndex a candidate for next focus given the direction the focus + * change is coming from? + * @param childIndex The index to check. + * @param direction The direction, one of + * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT} + * @return Whether childIndex is a candidate. + */ + private boolean isCandidateSelection(int childIndex, int direction) { + final int count = getChildCount(); + final int invertedIndex = count - 1 - childIndex; + + int rowStart; + int rowEnd; + + if (!mStackFromBottom) { + rowStart = childIndex - (childIndex % mNumColumns); + rowEnd = Math.max(rowStart + mNumColumns - 1, count); + } else { + rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns)); + rowStart = Math.max(0, rowEnd - mNumColumns + 1); + } + + switch (direction) { + case View.FOCUS_RIGHT: + // coming from left, selection is only valid if it is on left + // edge + return childIndex == rowStart; + case View.FOCUS_DOWN: + // coming from top; only valid if in top row + return rowStart == 0; + case View.FOCUS_LEFT: + // coming from right, must be on right edge + return childIndex == rowEnd; + case View.FOCUS_UP: + // coming from bottom, need to be in last row + return rowEnd == count - 1; + default: + throw new IllegalArgumentException("direction must be one of " + + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); + } + } + + /** + * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT + * + * @param gravity the gravity to apply to this grid's children + * + * @attr ref android.R.styleable#GridView_gravity + */ + public void setGravity(int gravity) { + if (mGravity != gravity) { + mGravity = gravity; + requestLayoutIfNecessary(); + } + } + + /** + * Set the amount of horizontal (x) spacing to place between each item + * in the grid. + * + * @param horizontalSpacing The amount of horizontal space between items, + * in pixels. + * + * @attr ref android.R.styleable#GridView_horizontalSpacing + */ + public void setHorizontalSpacing(int horizontalSpacing) { + if (horizontalSpacing != mRequestedHorizontalSpacing) { + mRequestedHorizontalSpacing = horizontalSpacing; + requestLayoutIfNecessary(); + } + } + + + /** + * Set the amount of vertical (y) spacing to place between each item + * in the grid. + * + * @param verticalSpacing The amount of vertical space between items, + * in pixels. + * + * @attr ref android.R.styleable#GridView_verticalSpacing + */ + public void setVerticalSpacing(int verticalSpacing) { + if (verticalSpacing != mVerticalSpacing) { + mVerticalSpacing = verticalSpacing; + requestLayoutIfNecessary(); + } + } + + /** + * Control how items are stretched to fill their space. + * + * @param stretchMode Either {@link #NO_STRETCH}, + * {@link #STRETCH_SPACING}, or {@link #STRETCH_COLUMN_WIDTH}. + * + * @attr ref android.R.styleable#GridView_stretchMode + */ + public void setStretchMode(int stretchMode) { + if (stretchMode != mStretchMode) { + mStretchMode = stretchMode; + requestLayoutIfNecessary(); + } + } + + public int getStretchMode() { + return mStretchMode; + } + + /** + * Set the width of columns in the grid. + * + * @param columnWidth The column width, in pixels. + * + * @attr ref android.R.styleable#GridView_columnWidth + */ + public void setColumnWidth(int columnWidth) { + if (columnWidth != mRequestedColumnWidth) { + mRequestedColumnWidth = columnWidth; + requestLayoutIfNecessary(); + } + } + + /** + * Set the number of columns in the grid + * + * @param numColumns The desired number of columns. + * + * @attr ref android.R.styleable#GridView_numColumns + */ + public void setNumColumns(int numColumns) { + if (numColumns != mRequestedNumColumns) { + mRequestedNumColumns = numColumns; + requestLayoutIfNecessary(); + } + } + + /** + * Make sure views are touching the top or bottom edge, as appropriate for + * our gravity + */ + private void adjustViewsUpOrDown() { + final int childCount = getChildCount(); + + if (childCount > 0) { + int delta; + 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 -= mVerticalSpacing; + } + 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 += mVerticalSpacing; + } + + if (delta > 0) { + // We only are looking to see if we are too high, not too low + delta = 0; + } + } + + if (delta != 0) { + offsetChildrenTopAndBottom(-delta); + } + } + } + + @Override + protected int computeVerticalScrollExtent() { + final int count = getChildCount(); + if (count > 0) { + final int numColumns = mNumColumns; + final int rowCount = (count + numColumns - 1) / numColumns; + + int extent = rowCount * 100; + + View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + extent += (top * 100) / height; + } + + view = getChildAt(count - 1); + final int bottom = view.getBottom(); + height = view.getHeight(); + if (height > 0) { + extent -= ((bottom - getHeight()) * 100) / height; + } + + return extent; + } + return 0; + } + + @Override + protected int computeVerticalScrollOffset() { + if (mFirstPosition >= 0 && getChildCount() > 0) { + final View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + final int whichRow = mFirstPosition / mNumColumns; + return Math.max(whichRow * 100 - (top * 100) / height, 0); + } + } + return 0; + } + + @Override + protected int computeVerticalScrollRange() { + // TODO: Account for vertical spacing too + final int numColumns = mNumColumns; + final int rowCount = (mItemCount + numColumns - 1) / numColumns; + return Math.max(rowCount * 100, 0); + } +} + diff --git a/core/java/android/widget/HeaderViewListAdapter.java b/core/java/android/widget/HeaderViewListAdapter.java new file mode 100644 index 0000000..b0e5f7e --- /dev/null +++ b/core/java/android/widget/HeaderViewListAdapter.java @@ -0,0 +1,241 @@ +/* + * 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 android.widget; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; + +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. + *<p>This is intended as a base class; you will probably not need to + * use this class directly in your own code. + * + */ +public class HeaderViewListAdapter implements WrapperListAdapter, Filterable { + + private ListAdapter mAdapter; + + ArrayList<ListView.FixedViewInfo> mHeaderViewInfos; + ArrayList<ListView.FixedViewInfo> mFooterViewInfos; + boolean mAreAllFixedViewsSelectable; + + private boolean mIsFilterable; + + public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos, + ArrayList<ListView.FixedViewInfo> footerViewInfos, + ListAdapter adapter) { + mAdapter = adapter; + mIsFilterable = adapter instanceof Filterable; + + mHeaderViewInfos = headerViewInfos; + mFooterViewInfos = footerViewInfos; + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + } + + public int getHeadersCount() { + return mHeaderViewInfos == null ? 0 : mHeaderViewInfos.size(); + } + + public int getFootersCount() { + return mFooterViewInfos == null ? 0 : mFooterViewInfos.size(); + } + + public boolean isEmpty() { + return mAdapter == null || mAdapter.isEmpty(); + } + + private boolean areAllListInfosSelectable(ArrayList<ListView.FixedViewInfo> infos) { + if (infos != null) { + for (ListView.FixedViewInfo info : infos) { + if (!info.isSelectable) { + return false; + } + } + } + return true; + } + + public boolean removeHeader(View v) { + for (int i = 0; i < mHeaderViewInfos.size(); i++) { + 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++) { + 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) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCount = mAdapter.getCount(); + if (adjPosition >= adapterCount && mFooterViewInfos != null) { + return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable; + } else { + return mAdapter.isEnabled(adjPosition); + } + } else if (position < numHeaders && mHeaderViewInfos != null) { + return mHeaderViewInfos.get(position).isSelectable; + } + return true; + } + + public Object getItem(int position) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCount = mAdapter.getCount(); + if (adjPosition >= adapterCount && mFooterViewInfos != null) { + return mFooterViewInfos.get(adjPosition - adapterCount).data; + } else { + return mAdapter.getItem(adjPosition); + } + } else if (position < numHeaders && mHeaderViewInfos != null) { + return mHeaderViewInfos.get(position).data; + } + return null; + } + + public long getItemId(int position) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCnt = mAdapter.getCount(); + if (adjPosition < adapterCnt) { + 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) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCount = mAdapter.getCount(); + if (adjPosition >= adapterCount) { + if (mFooterViewInfos != null) { + return mFooterViewInfos.get(adjPosition - adapterCount).view; + } + } else { + return mAdapter.getView(adjPosition, convertView, parent); + } + } else if (position < numHeaders) { + return mHeaderViewInfos.get(position).view; + } + return null; + } + + 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 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/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java new file mode 100644 index 0000000..5c56428 --- /dev/null +++ b/core/java/android/widget/ImageButton.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import java.util.Map; + +/** + * <p> + * An image button displays an image that can be pressed, or clicked, by the + * user. + * </p> + * + * <p><strong>XML attributes</strong></p> + * <p> + * See {@link android.R.styleable#ImageView Button Attributes}, + * {@link android.R.styleable#View View Attributes} + * </p> + */ +public class ImageButton extends ImageView { + public ImageButton(Context context) { + this(context, null); + } + + public ImageButton(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.imageButtonStyle); + } + + public ImageButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setFocusable(true); + } + + @Override + protected boolean onSetAlpha(int alpha) { + return false; + } +} diff --git a/core/java/android/widget/ImageSwitcher.java b/core/java/android/widget/ImageSwitcher.java new file mode 100644 index 0000000..bcb750a --- /dev/null +++ b/core/java/android/widget/ImageSwitcher.java @@ -0,0 +1,59 @@ +/* + * 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 android.widget; + +import java.util.Map; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.util.AttributeSet; + + +public class ImageSwitcher extends ViewSwitcher +{ + public ImageSwitcher(Context context) + { + super(context); + } + + public ImageSwitcher(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setImageResource(int resid) + { + ImageView image = (ImageView)this.getNextView(); + image.setImageResource(resid); + showNext(); + } + + public void setImageURI(Uri uri) + { + ImageView image = (ImageView)this.getNextView(); + image.setImageURI(uri); + showNext(); + } + + public void setImageDrawable(Drawable drawable) + { + ImageView image = (ImageView)this.getNextView(); + image.setImageDrawable(drawable); + showNext(); + } +} + diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java new file mode 100644 index 0000000..b5d4e2d --- /dev/null +++ b/core/java/android/widget/ImageView.java @@ -0,0 +1,883 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews.RemoteView; + + +/** + * Displays an arbitrary image, such as an icon. The ImageView class + * can load images from various sources (such as resources or content + * providers), takes care of computing its measurement from the image so that + * it can be used in any layout manager, and provides various display options + * such as scaling and tinting. + * + * @attr ref android.R.styleable#ImageView_adjustViewBounds + * @attr ref android.R.styleable#ImageView_src + * @attr ref android.R.styleable#ImageView_maxWidth + * @attr ref android.R.styleable#ImageView_maxHeight + * @attr ref android.R.styleable#ImageView_tint + * @attr ref android.R.styleable#ImageView_scaleType + */ +@RemoteView +public class ImageView extends View { + // settable by the client + private Uri mUri; + private int mResource = 0; + private Matrix mMatrix; + private ScaleType mScaleType; + private boolean mHaveFrame = false; + private boolean mAdjustViewBounds = false; + private int mMaxWidth = Integer.MAX_VALUE; + private int mMaxHeight = Integer.MAX_VALUE; + + // these are applied to the drawable + private ColorFilter mColorFilter; + private int mAlpha = 255; + private int mViewAlphaScale = 256; + + private Drawable mDrawable = null; + private int[] mState = null; + private boolean mMergeState = false; + private int mLevel = 0; + private int mDrawableWidth; + private int mDrawableHeight; + private Matrix mDrawMatrix = null; + + // Avoid allocations... + private RectF mTempSrc = new RectF(); + private RectF mTempDst = new RectF(); + + private boolean mCropToPadding; + + private boolean mBaselineAligned = false; + + private static final ScaleType[] sScaleTypeArray = { + ScaleType.MATRIX, + ScaleType.FIT_XY, + ScaleType.FIT_START, + ScaleType.FIT_CENTER, + ScaleType.FIT_END, + ScaleType.CENTER, + ScaleType.CENTER_CROP, + ScaleType.CENTER_INSIDE + }; + + public ImageView(Context context) { + super(context); + initImageView(); + } + + public ImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initImageView(); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ImageView, defStyle, 0); + + Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src); + if (d != null) { + setImageDrawable(d); + } + + mBaselineAligned = a.getBoolean( + com.android.internal.R.styleable.ImageView_baselineAlignBottom, false); + + setAdjustViewBounds( + a.getBoolean(com.android.internal.R.styleable.ImageView_adjustViewBounds, + false)); + + setMaxWidth(a.getDimensionPixelSize( + com.android.internal.R.styleable.ImageView_maxWidth, Integer.MAX_VALUE)); + + setMaxHeight(a.getDimensionPixelSize( + com.android.internal.R.styleable.ImageView_maxHeight, Integer.MAX_VALUE)); + + int index = a.getInt(com.android.internal.R.styleable.ImageView_scaleType, -1); + if (index >= 0) { + setScaleType(sScaleTypeArray[index]); + } + + int tint = a.getInt(com.android.internal.R.styleable.ImageView_tint, 0); + if (tint != 0) { + setColorFilter(tint, PorterDuff.Mode.SRC_ATOP); + } + + mCropToPadding = a.getBoolean( + com.android.internal.R.styleable.ImageView_cropToPadding, false); + + a.recycle(); + + //need inflate syntax/reader for matrix + } + + private void initImageView() { + mMatrix = new Matrix(); + mScaleType = ScaleType.FIT_CENTER; + } + + @Override + protected boolean verifyDrawable(Drawable dr) { + return mDrawable == dr || super.verifyDrawable(dr); + } + + @Override + public void invalidateDrawable(Drawable dr) { + if (dr == mDrawable) { + /* we invalidate the whole view in this case because it's very + * hard to know where the drawable actually is. This is made + * complicated because of the offsets and transformations that + * can be applied. In theory we could get the drawable's bounds + * and run them through the transformation and offsets, but this + * is probably not worth the effort. + */ + invalidate(); + } else { + super.invalidateDrawable(dr); + } + } + + @Override + protected boolean onSetAlpha(int alpha) { + if (getBackground() == null) { + int scale = alpha + (alpha >> 7); + if (mViewAlphaScale != scale) { + mViewAlphaScale = scale; + applyColorMod(); + } + return true; + } + return false; + } + + /** + * Set this to true if you want the ImageView to adjust its bounds + * to preserve the aspect ratio of its drawable. + * @param adjustViewBounds Whether to adjust the bounds of this view + * to presrve the original aspect ratio of the drawable + * + * @attr ref android.R.styleable#ImageView_adjustViewBounds + */ + public void setAdjustViewBounds(boolean adjustViewBounds) { + mAdjustViewBounds = adjustViewBounds; + if (adjustViewBounds) { + setScaleType(ScaleType.FIT_CENTER); + } + } + + /** + * An optional argument to supply a maximum width for this view. Only valid if + * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x + * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to + * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to + * WRAP_CONTENT. + * + * <p> + * Note that this view could be still smaller than 100 x 100 using this approach if the original + * image is small. To set an image to a fixed size, specify that size in the layout params and + * then use {@link #setScaleType} to determine how to fit the image within the bounds. + * </p> + * + * @param maxWidth maximum width for this view + * + * @attr ref android.R.styleable#ImageView_maxWidth + */ + public void setMaxWidth(int maxWidth) { + mMaxWidth = maxWidth; + } + + /** + * An optional argument to supply a maximum height for this view. Only valid if + * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x + * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to + * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to + * WRAP_CONTENT. + * + * <p> + * Note that this view could be still smaller than 100 x 100 using this approach if the original + * image is small. To set an image to a fixed size, specify that size in the layout params and + * then use {@link #setScaleType} to determine how to fit the image within the bounds. + * </p> + * + * @param maxHeight maximum height for this view + * + * @attr ref android.R.styleable#ImageView_maxHeight + */ + public void setMaxHeight(int maxHeight) { + mMaxHeight = maxHeight; + } + + /** Return the view's drawable, or null if no drawable has been + assigned. + */ + public Drawable getDrawable() { + return mDrawable; + } + + /** + * Sets a drawable as the content of this ImageView. + * + * @param resId the resource identifier of the the drawable + * + * @attr ref android.R.styleable#ImageView_src + */ + public void setImageResource(int resId) { + if (mUri != null || mResource != resId) { + updateDrawable(null); + mResource = resId; + mUri = null; + resolveUri(); + requestLayout(); + invalidate(); + } + } + + /** + * Sets the content of this ImageView to the specified Uri. + * + * @param uri The Uri of an image + */ + public void setImageURI(Uri uri) { + if (mResource != 0 || + (mUri != uri && + (uri == null || mUri == null || !uri.equals(mUri)))) { + updateDrawable(null); + mResource = 0; + mUri = uri; + resolveUri(); + requestLayout(); + invalidate(); + } + } + + + /** + * Sets a drawable as the content of this ImageView. + * + * @param drawable The drawable to set + */ + public void setImageDrawable(Drawable drawable) { + if (mDrawable != drawable) { + mResource = 0; + mUri = null; + updateDrawable(drawable); + requestLayout(); + invalidate(); + } + } + + /** + * Sets a Bitmap as the content of this ImageView. + * + * @param bm The bitmap to set + */ + public void setImageBitmap(Bitmap bm) { + // if this is used frequently, may handle bitmaps explicitly + // to reduce the intermediate drawable object + setImageDrawable(new BitmapDrawable(bm)); + } + + public void setImageState(int[] state, boolean merge) { + mState = state; + mMergeState = merge; + if (mDrawable != null) { + refreshDrawableState(); + resizeFromDrawable(); + } + } + + @Override + public void setSelected(boolean selected) { + super.setSelected(selected); + resizeFromDrawable(); + } + + public void setImageLevel(int level) { + mLevel = level; + if (mDrawable != null) { + mDrawable.setLevel(level); + resizeFromDrawable(); + } + } + + /** + * Options for scaling the bounds of an image to the bounds of this view. + */ + public enum ScaleType { + /** + * Scale using the image matrix when drawing. The image matrix can be set using + * {@link ImageView#setImageMatrix(Matrix)}. From XML, use this syntax: + * <code>android:scaleType="matrix"</code>. + */ + MATRIX (0), + /** + * Scale the image using {@link Matrix.ScaleToFit#FILL}. + * From XML, use this syntax: <code>android:scaleType="fitXY"</code>. + */ + FIT_XY (1), + /** + * Scale the image using {@link Matrix.ScaleToFit#START}. + * From XML, use this syntax: <code>android:scaleType="fitStart"</code>. + */ + FIT_START (2), + /** + * Scale the image using {@link Matrix.ScaleToFit#CENTER}. + * From XML, use this syntax: + * <code>android:scaleType="fitCenter"</code>. + */ + FIT_CENTER (3), + /** + * Scale the image using {@link Matrix.ScaleToFit#END}. + * From XML, use this syntax: <code>android:scaleType="fitEnd"</code>. + */ + FIT_END (4), + /** + * Center the image in the view, but perform no scaling. + * From XML, use this syntax: <code>android:scaleType="center"</code>. + */ + CENTER (5), + /** + * Scale the image uniformly (maintain the image's aspect ratio) so + * that both dimensions (width and height) of the image will be equal + * to or larger than the corresponding dimension of the view + * (minus padding). The image is then centered in the view. + * From XML, use this syntax: <code>android:scaleType="centerCrop"</code>. + */ + CENTER_CROP (6), + /** + * Scale the image uniformly (maintain the image's aspect ratio) so + * that both dimensions (width and height) of the image will be equal + * to or less than the corresponding dimension of the view + * (minus padding). The image is then centered in the view. + * From XML, use this syntax: <code>android:scaleType="centerInside"</code>. + */ + CENTER_INSIDE (7); + + ScaleType(int ni) { + nativeInt = ni; + } + final int nativeInt; + } + + /** + * Controls how the image should be resized or moved to match the size + * of this ImageView. + * + * @param scaleType The desired scaling mode. + * + * @attr ref android.R.styleable#ImageView_scaleType + */ + public void setScaleType(ScaleType scaleType) { + if (scaleType == null) { + throw new NullPointerException(); + } + + if (mScaleType != scaleType) { + mScaleType = scaleType; + + setWillNotCacheDrawing(mScaleType == ScaleType.CENTER); + + requestLayout(); + invalidate(); + } + } + + /** + * Return the current scale type in use by this ImageView. + * + * @see ImageView.ScaleType + * + * @attr ref android.R.styleable#ImageView_scaleType + */ + public ScaleType getScaleType() { + return mScaleType; + } + + /** Return the view's optional matrix. This is applied to the + view's drawable when it is drawn. If there is not matrix, + this method will return null. + Do not change this matrix in place. If you want a different matrix + applied to the drawable, be sure to call setImageMatrix(). + */ + public Matrix getImageMatrix() { + return mMatrix; + } + + public void setImageMatrix(Matrix matrix) { + // collaps null and identity to just null + if (matrix != null && matrix.isIdentity()) { + matrix = null; + } + + // don't invalidate unless we're actually changing our matrix + if (matrix == null && !mMatrix.isIdentity() || + matrix != null && !mMatrix.equals(matrix)) { + mMatrix.set(matrix); + invalidate(); + } + } + + private void resolveUri() { + if (mDrawable != null) { + return; + } + + Resources rsrc = getResources(); + if (rsrc == null) { + return; + } + + Drawable d = null; + + if (mResource != 0) { + try { + d = rsrc.getDrawable(mResource); + } catch (Exception e) { + Log.w("ImageView", "Unable to find resource: " + mResource, e); + // Don't try again. + mUri = null; + } + } else if (mUri != null) { + if ("content".equals(mUri.getScheme())) { + try { + d = Drawable.createFromStream( + mContext.getContentResolver().openInputStream(mUri), + null); + } catch (Exception e) { + Log.w("ImageView", "Unable to open content: " + mUri, e); + } + } else { + d = Drawable.createFromPath(mUri.toString()); + } + + if (d == null) { + System.out.println("resolveUri failed on bad bitmap uri: " + + mUri); + // Don't try again. + mUri = null; + } + } else { + return; + } + + updateDrawable(d); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + if (mState == null) { + return super.onCreateDrawableState(extraSpace); + } else if (!mMergeState) { + return mState; + } else { + return mergeDrawableStates( + super.onCreateDrawableState(extraSpace + mState.length), mState); + } + } + + private void updateDrawable(Drawable d) { + if (mDrawable != null) { + mDrawable.setCallback(null); + unscheduleDrawable(mDrawable); + } + mDrawable = d; + if (d != null) { + d.setCallback(this); + if (d.isStateful()) { + d.setState(getDrawableState()); + } + d.setLevel(mLevel); + mDrawableWidth = d.getIntrinsicWidth(); + mDrawableHeight = d.getIntrinsicHeight(); + applyColorMod(); + configureBounds(); + } + } + + private void resizeFromDrawable() { + Drawable d = mDrawable; + if (d != null) { + int w = d.getIntrinsicWidth(); + if (w < 0) w = mDrawableWidth; + int h = d.getIntrinsicHeight(); + if (h < 0) h = mDrawableHeight; + if (w != mDrawableWidth || h != mDrawableHeight) { + mDrawableWidth = w; + mDrawableHeight = h; + requestLayout(); + } + } + } + + private static final Matrix.ScaleToFit[] sS2FArray = { + Matrix.ScaleToFit.FILL, + Matrix.ScaleToFit.START, + Matrix.ScaleToFit.CENTER, + Matrix.ScaleToFit.END + }; + + private static Matrix.ScaleToFit scaleTypeToScaleToFit(ScaleType st) { + // ScaleToFit enum to their corresponding Matrix.ScaleToFit values + return sS2FArray[st.nativeInt - 1]; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + resolveUri(); + int w; + int h; + + // Desired aspect ratio of the view's contents (not including padding) + float desiredAspect = 0.0f; + + // We are allowed to change the view's width + boolean resizeWidth = false; + + // We are allowed to change the view's height + boolean resizeHeight = false; + + if (mDrawable == null) { + // If no drawable, its intrinsic size is 0. + mDrawableWidth = -1; + mDrawableHeight = -1; + w = h = 0; + } else { + w = mDrawableWidth; + h = mDrawableHeight; + if (w <= 0) w = 1; + if (h <= 0) h = 1; + + // We are supposed to adjust view bounds to match the aspect + // ratio of our drawable. See if that is possible. + if (mAdjustViewBounds) { + + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + + resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; + resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; + + desiredAspect = (float)w/(float)h; + } + } + + int pleft = mPaddingLeft; + int pright = mPaddingRight; + int ptop = mPaddingTop; + int pbottom = mPaddingBottom; + + int widthSize; + int heightSize; + + if (resizeWidth || resizeHeight) { + /* If we get here, it means we want to resize to match the + drawables aspect ratio, and we have the freedom to change at + least one dimension. + */ + + // Get the max possible width given our constraints + widthSize = resolveAdjustedSize(w + pleft + pright, + mMaxWidth, widthMeasureSpec); + + // Get the max possible height given our constraints + heightSize = resolveAdjustedSize(h + ptop + pbottom, + mMaxHeight, heightMeasureSpec); + + if (desiredAspect != 0.0f) { + // See what our actual aspect ratio is + float actualAspect = (float)(widthSize - pleft - pright) / + (heightSize - ptop - pbottom); + + if (Math.abs(actualAspect - desiredAspect) > 0.0000001) { + + boolean done = false; + + // Try adjusting width to be proportional to height + if (resizeWidth) { + int newWidth = (int)(desiredAspect * + (heightSize - ptop - pbottom)) + + pleft + pright; + if (newWidth <= widthSize) { + widthSize = newWidth; + done = true; + } + } + + // Try adjusting height to be proportional to width + if (!done && resizeHeight) { + int newHeight = (int)((widthSize - pleft - pright) + / desiredAspect) + ptop + pbottom; + if (newHeight <= heightSize) { + heightSize = newHeight; + } + } + } + } + } else { + /* We are either don't want to preserve the drawables aspect ratio, + or we are not allowed to change view dimensions. Just measure in + the normal way. + */ + w += pleft + pright; + h += ptop + pbottom; + + w = Math.max(w, getSuggestedMinimumWidth()); + h = Math.max(h, getSuggestedMinimumHeight()); + + widthSize = resolveSize(w, widthMeasureSpec); + heightSize = resolveSize(h, heightMeasureSpec); + } + + setMeasuredDimension(widthSize, heightSize); + } + + private int resolveAdjustedSize(int desiredSize, int maxSize, + int measureSpec) { + int result = desiredSize; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + /* Parent says we can be as big as we want. Just don't be larger + than max size imposed on ourselves. + */ + result = Math.min(desiredSize, maxSize); + break; + case MeasureSpec.AT_MOST: + // Parent says we can be as big as we want, up to specSize. + // Don't be larger than specSize, and don't be larger than + // the max size imposed on ourselves. + result = Math.min(Math.min(desiredSize, specSize), maxSize); + break; + case MeasureSpec.EXACTLY: + // No choice. Do what we are told. + result = specSize; + break; + } + return result; + } + + @Override + protected boolean setFrame(int l, int t, int r, int b) { + boolean changed = super.setFrame(l, t, r, b); + mHaveFrame = true; + configureBounds(); + return changed; + } + + private void configureBounds() { + if (mDrawable == null || !mHaveFrame) { + return; + } + + int dwidth = mDrawableWidth; + int dheight = mDrawableHeight; + + int vwidth = getWidth() - mPaddingLeft - mPaddingRight; + int vheight = getHeight() - mPaddingTop - mPaddingBottom; + + boolean fits = (dwidth < 0 || vwidth == dwidth) && + (dheight < 0 || vheight == dheight); + + if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) { + /* If the drawable has no intrinsic size, or we're told to + scaletofit, then we just fill our entire view. + */ + mDrawable.setBounds(0, 0, vwidth, vheight); + mDrawMatrix = null; + } else { + // We need to do the scaling ourself, so have the drawable + // use its native size. + mDrawable.setBounds(0, 0, dwidth, dheight); + + if (ScaleType.MATRIX == mScaleType) { + // Use the specified matrix as-is. + if (mMatrix.isIdentity()) { + mDrawMatrix = null; + } else { + mDrawMatrix = mMatrix; + } + } else if (fits) { + // The bitmap fits exactly, no transform needed. + mDrawMatrix = null; + } else if (ScaleType.CENTER == mScaleType) { + // Center bitmap in view, no scaling. + mDrawMatrix = mMatrix; + mDrawMatrix.setTranslate((vwidth - dwidth) * 0.5f, + (vheight - dheight) * 0.5f); + } else if (ScaleType.CENTER_CROP == mScaleType) { + mDrawMatrix = mMatrix; + + float scale; + float dx = 0, dy = 0; + + if (dwidth * vheight > vwidth * dheight) { + scale = (float) vheight / (float) dheight; + dx = (vwidth - dwidth * scale) * 0.5f; + } else { + scale = (float) vwidth / (float) dwidth; + dy = (vheight - dheight * scale) * 0.5f; + } + + mDrawMatrix.setScale(scale, scale); + mDrawMatrix.postTranslate(dx, dy); + } else if (ScaleType.CENTER_INSIDE == mScaleType) { + mDrawMatrix = mMatrix; + float scale; + float dx; + float dy; + + if (dwidth <= vwidth && dheight <= vheight) { + scale = 1.0f; + } else { + scale = Math.min((float) vwidth / (float) dwidth, + (float) vheight / (float) dheight); + } + + dx = (vwidth - dwidth * scale) * 0.5f; + dy = (vheight - dheight * scale) * 0.5f; + + mDrawMatrix.setScale(scale, scale); + mDrawMatrix.postTranslate(dx, dy); + } else { + // Generate the required transform. + mTempSrc.set(0, 0, dwidth, dheight); + mTempDst.set(0, 0, vwidth, vheight); + + mDrawMatrix = mMatrix; + mDrawMatrix.setRectToRect(mTempSrc, mTempDst, + scaleTypeToScaleToFit(mScaleType)); + } + } + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + Drawable d = mDrawable; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mDrawable == null) { + return; // couldn't resolve the URI + } + + if (mDrawableWidth == 0 || mDrawableHeight == 0) { + return; // nothing to draw (empty bounds) + } + + if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) { + mDrawable.draw(canvas); + } else { + int saveCount = canvas.getSaveCount(); + canvas.save(); + + if (mCropToPadding) { + final int scrollX = mScrollX; + final int scrollY = mScrollY; + canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop, + scrollX + mRight - mLeft - mPaddingRight, + scrollY + mBottom - mTop - mPaddingBottom); + } + + canvas.translate(mPaddingLeft, mPaddingTop); + + if (mDrawMatrix != null) { + canvas.concat(mDrawMatrix); + } + mDrawable.draw(canvas); + canvas.restoreToCount(saveCount); + } + } + + @Override + public int getBaseline() { + return mBaselineAligned ? getHeight() : -1; + } + + /** + * Set a tinting option for the image. + * + * @param color Color tint to apply. + * @param mode How to apply the color. The standard mode is + * {@link PorterDuff.Mode#SRC_ATOP} + * + * @attr ref android.R.styleable#ImageView_tint + */ + public final void setColorFilter(int color, PorterDuff.Mode mode) { + setColorFilter(new PorterDuffColorFilter(color, mode)); + } + + public final void clearColorFilter() { + setColorFilter(null); + } + + /** + * Apply an arbitrary colorfilter to the image. + * + * @param cf the colorfilter to apply (may be null) + */ + public void setColorFilter(ColorFilter cf) { + if (mColorFilter != cf) { + mColorFilter = cf; + applyColorMod(); + invalidate(); + } + } + + public void setAlpha(int alpha) { + alpha &= 0xFF; // keep it legal + if (mAlpha != alpha) { + mAlpha = alpha; + applyColorMod(); + invalidate(); + } + } + + private void applyColorMod() { + if (mDrawable != null) { + mDrawable.setColorFilter(mColorFilter); + mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8); + } + } +} diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java new file mode 100644 index 0000000..de74fa4 --- /dev/null +++ b/core/java/android/widget/LinearLayout.java @@ -0,0 +1,1315 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.widget.RemoteViews.RemoteView; + +import com.android.internal.R; + + +/** + * A Layout that arranges its children in a single column or a single row. The direction of + * the row can be set by calling {@link #setOrientation(int) setOrientation()}. + * You can also specify gravity, which specifies the alignment of all the child elements by + * calling {@link #setGravity(int) setGravity()} or specify that specific children + * grow to fill up any remaining space in the layout by setting the <em>weight</em> member of + * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}. + * The default orientation is horizontal. + * + * <p> + * Also see {@link LinearLayout.LayoutParams android.widget.LinearLayout.LayoutParams} + * for layout attributes </p> + */ +@RemoteView +public class LinearLayout extends ViewGroup { + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + /** + * Whether the children of this layout are baseline aligned. Only applicable + * if {@link #mOrientation} is horizontal. + */ + private boolean mBaselineAligned = true; + + /** + * If this layout is part of another layout that is baseline aligned, + * use the child at this index as the baseline. + * + * Note: this is orthogonal to {@link #mBaselineAligned}, which is concerned + * with whether the children of this layout are baseline aligned. + */ + private int mBaselineAlignedChildIndex = 0; + + /** + * The additional offset to the child's baseline. + * We'll calculate the baseline of this layout as we measure vertically; for + * horizontal linear layouts, the offset of 0 is appropriate. + */ + private int mBaselineChildTop = 0; + + private int mOrientation; + private int mGravity = Gravity.LEFT | Gravity.TOP; + private int mTotalLength; + + private float mWeightSum; + + private int[] mMaxAscent; + private int[] mMaxDescent; + + private static final int VERTICAL_GRAVITY_COUNT = 4; + + private static final int INDEX_CENTER_VERTICAL = 0; + private static final int INDEX_TOP = 1; + private static final int INDEX_BOTTOM = 2; + private static final int INDEX_FILL = 3; + + public LinearLayout(Context context) { + super(context); + } + + public LinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout); + + int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1); + if (index >= 0) { + setOrientation(index); + } + + index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1); + if (index >= 0) { + setGravity(index); + } + + boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true); + if (!baselineAligned) { + setBaselineAligned(baselineAligned); + } + + mWeightSum = a.getFloat(R.styleable.LinearLayout_weightSum, -1.0f); + + mBaselineAlignedChildIndex = + a.getInt(com.android.internal.R.styleable.LinearLayout_baselineAlignedChildIndex, -1); + + a.recycle(); + } + + /** + * <p>Indicates whether widgets contained within this layout are aligned + * on their baseline or not.</p> + * + * @return true when widgets are baseline-aligned, false otherwise + */ + public boolean isBaselineAligned() { + return mBaselineAligned; + } + + /** + * <p>Defines whether widgets contained in this layout are + * baseline-aligned or not.</p> + * + * @param baselineAligned true to align widgets on their baseline, + * false otherwise + * + * @attr ref android.R.styleable#LinearLayout_baselineAligned + */ + public void setBaselineAligned(boolean baselineAligned) { + mBaselineAligned = baselineAligned; + } + + @Override + public int getBaseline() { + if (mBaselineAlignedChildIndex < 0) { + return super.getBaseline(); + } + + if (getChildCount() <= mBaselineAlignedChildIndex) { + throw new RuntimeException("mBaselineAlignedChildIndex of LinearLayout " + + "set to an index that is out of bounds."); + } + + final View child = getChildAt(mBaselineAlignedChildIndex); + final int childBaseline = child.getBaseline(); + + if (childBaseline == -1) { + if (mBaselineAlignedChildIndex == 0) { + // this is just the default case, safe to return -1 + return -1; + } + // the user picked an index that points to something that doesn't + // know how to calculate its baseline. + throw new RuntimeException("mBaselineAlignedChildIndex of LinearLayout " + + "points to a View that doesn't know how to get its baseline."); + } + + // TODO: This should try to take into account the virtual offsets + // (See getNextLocationOffset and getLocationOffset) + // We should add to childTop: + // sum([getNextLocationOffset(getChildAt(i)) / i < mBaselineAlignedChildIndex]) + // and also add: + // getLocationOffset(child) + int childTop = mBaselineChildTop; + + if (mOrientation == VERTICAL) { + final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + if (majorGravity != Gravity.TOP) { + switch (majorGravity) { + case Gravity.BOTTOM: + childTop = mBottom - mTop - mPaddingBottom - mTotalLength; + break; + + case Gravity.CENTER_VERTICAL: + childTop += ((mBottom - mTop - mPaddingTop - mPaddingBottom) - + mTotalLength) / 2; + break; + } + } + } + + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); + return childTop + lp.topMargin + childBaseline; + } + + /** + * @return The index of the child that will be used if this layout is + * part of a larger layout that is baseline aligned, or -1 if none has + * been set. + */ + public int getBaselineAlignedChildIndex() { + return mBaselineAlignedChildIndex; + } + + /** + * @param i The index of the child that will be used if this layout is + * part of a larger layout that is baseline aligned. + * + * @attr ref android.R.styleable#LinearLayout_baselineAlignedChildIndex + */ + public void setBaselineAlignedChildIndex(int i) { + if ((i < 0) || (i >= getChildCount())) { + throw new IllegalArgumentException("base aligned child index out " + + "of range (0, " + getChildCount() + ")"); + } + mBaselineAlignedChildIndex = i; + } + + /** + * <p>Returns the view at the specified index. This method can be overriden + * to take into account virtual children. Refer to + * {@link android.widget.TableLayout} and {@link android.widget.TableRow} + * for an example.</p> + * + * @param index the child's index + * @return the child at the specified index + */ + View getVirtualChildAt(int index) { + return getChildAt(index); + } + + /** + * <p>Returns the virtual number of children. This number might be different + * than the actual number of children if the layout can hold virtual + * children. Refer to + * {@link android.widget.TableLayout} and {@link android.widget.TableRow} + * for an example.</p> + * + * @return the virtual number of children + */ + int getVirtualChildCount() { + return getChildCount(); + } + + /** + * Returns the desired weights sum. + * + * @return A number greater than 0.0f if the weight sum is defined, or + * a number lower than or equals to 0.0f if not weight sum is + * to be used. + */ + public float getWeightSum() { + return mWeightSum; + } + + /** + * Defines the desired weights sum. If unspecified the weights sum is computed + * at layout time by adding the layout_weight of each child. + * + * This can be used for instance to give a single child 50% of the total + * available space by giving it a layout_weight of 0.5 and setting the + * weightSum to 1.0. + * + * @param weightSum a number greater than 0.0f, or a number lower than or equals + * to 0.0f if the weight sum should be computed from the children's + * layout_weight + */ + public void setWeightSum(float weightSum) { + mWeightSum = Math.max(0.0f, weightSum); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mOrientation == VERTICAL) { + measureVertical(widthMeasureSpec, heightMeasureSpec); + } else { + measureHorizontal(widthMeasureSpec, heightMeasureSpec); + } + } + + /** + * Measures the children when the orientation of this LinearLayout is set + * to {@link #VERTICAL}. + * + * @param widthMeasureSpec Horizontal space requirements as imposed by the parent. + * @param heightMeasureSpec Vertical space requirements as imposed by the parent. + * + * @see #getOrientation() + * @see #setOrientation(int) + * @see #onMeasure(int, int) + */ + void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { + mTotalLength = 0; + int maxWidth = 0; + int alternativeMaxWidth = 0; + int weightedMaxWidth = 0; + boolean allFillParent = true; + float totalWeight = 0; + + final int count = getVirtualChildCount(); + + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + boolean matchWidth = false; + + final int baselineChildIndex = mBaselineAlignedChildIndex; + + // See how tall everyone is. Also remember max width. + for (int i = 0; i < count; ++i) { + final View child = getVirtualChildAt(i); + + if (child == null) { + mTotalLength += measureNullChild(i); + continue; + } + + if (child.getVisibility() == View.GONE) { + i += getChildrenSkipCount(child, i); + continue; + } + + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); + + totalWeight += lp.weight; + + if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) { + // Optimization: don't bother measuring children who are going to use + // leftover space. These views will get measured again down below if + // there is any leftover space. + mTotalLength += lp.topMargin + lp.bottomMargin; + } else { + int oldHeight = Integer.MIN_VALUE; + + if (lp.height == 0 && lp.weight > 0) { + // heightMode is either UNSPECIFIED OR AT_MOST, and this child + // wanted to stretch to fill available space. Translate that to + // WRAP_CONTENT so that it does not end up with a height of 0 + oldHeight = lp.height; + lp.height = LayoutParams.WRAP_CONTENT; + } + + // Determine how big this child would like to. If this or + // previous children have given a weight, then we allow it to + // use all available space (and we will shrink things later + // if needed). + measureChildBeforeLayout( + child, i, widthMeasureSpec, 0, heightMeasureSpec, + totalWeight == 0 ? mTotalLength : 0); + + if (oldHeight != Integer.MIN_VALUE) { + lp.height = oldHeight; + } + + mTotalLength += child.getMeasuredHeight() + lp.topMargin + + lp.bottomMargin + getNextLocationOffset(child); + } + + /** + * If applicable, compute the additional offset to the child's baseline + * we'll need later when asked {@link #getBaseline}. + */ + if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) { + mBaselineChildTop = mTotalLength; + } + + // if we are trying to use a child index for our baseline, the above + // book keeping only works if there are no children above it with + // weight. fail fast to aid the developer. + if (i < baselineChildIndex && lp.weight > 0) { + throw new RuntimeException("A child of LinearLayout with index " + + "less than mBaselineAlignedChildIndex has weight > 0, which " + + "won't work. Either remove the weight, or don't set " + + "mBaselineAlignedChildIndex."); + } + + boolean matchWidthLocally = false; + if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.FILL_PARENT) { + // The width of the linear layout will scale, and at least one + // child said it wanted to match our width. Set a flag + // indicating that we need to remeasure at least that view when + // we know our width. + matchWidth = true; + matchWidthLocally = true; + } + + final int margin = lp.leftMargin + lp.rightMargin; + final int measuredWidth = child.getMeasuredWidth() + margin; + maxWidth = Math.max(maxWidth, measuredWidth); + + allFillParent = allFillParent && lp.width == LayoutParams.FILL_PARENT; + if (lp.weight > 0) { + /* + * Widths of weighted Views are bogus if we end up + * remeasuring, so keep them separate. + */ + weightedMaxWidth = Math.max(weightedMaxWidth, + matchWidthLocally ? margin : measuredWidth); + } else { + alternativeMaxWidth = Math.max(alternativeMaxWidth, + matchWidthLocally ? margin : measuredWidth); + } + + i += getChildrenSkipCount(child, i); + } + + // Add in our padding + mTotalLength += mPaddingTop + mPaddingBottom; + + int heightSize = mTotalLength; + + // Check against our minimum height + heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); + + // Reconcile our calculated size with the heightMeasureSpec + heightSize = resolveSize(heightSize, heightMeasureSpec); + + // Either expand children with weight to take up available space or + // shrink them if they extend beyond our current bounds + int delta = heightSize - mTotalLength; + if (delta != 0 && totalWeight > 0.0f) { + float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; + + mTotalLength = 0; + + for (int i = 0; i < count; ++i) { + final View child = getVirtualChildAt(i); + + if (child.getVisibility() == View.GONE) { + continue; + } + + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); + + float childExtra = lp.weight; + if (childExtra > 0) { + // Child said it could absorb extra space -- give him his share + int share = (int) (childExtra * delta / weightSum); + weightSum -= childExtra; + delta -= share; + + final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + mPaddingLeft + mPaddingRight + + lp.leftMargin + lp.rightMargin, lp.width); + + // TODO: Use a field like lp.isMeasured to figure out if this + // child has been previously measured + if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) { + // child was measured once already above... + // base new measurement on stored values + int childHeight = child.getMeasuredHeight() + share; + if (childHeight < 0) { + childHeight = 0; + } + + child.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); + } else { + // child was skipped in the loop above. + // Measure for this first time here + child.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(share > 0 ? share : 0, + MeasureSpec.EXACTLY)); + } + } + + final int margin = lp.leftMargin + lp.rightMargin; + final int measuredWidth = child.getMeasuredWidth() + margin; + maxWidth = Math.max(maxWidth, measuredWidth); + + boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY && + lp.width == LayoutParams.FILL_PARENT; + + alternativeMaxWidth = Math.max(alternativeMaxWidth, + matchWidthLocally ? margin : measuredWidth); + + allFillParent = allFillParent && lp.width == LayoutParams.FILL_PARENT; + alternativeMaxWidth = Math.max(alternativeMaxWidth, + matchWidthLocally ? margin : measuredWidth); + + mTotalLength += child.getMeasuredHeight() + lp.topMargin + + lp.bottomMargin + getNextLocationOffset(child); + } + + // Add in our padding + mTotalLength += mPaddingTop + mPaddingBottom; + } else { + alternativeMaxWidth = Math.max(alternativeMaxWidth, + weightedMaxWidth); + } + + if (!allFillParent && widthMode != MeasureSpec.EXACTLY) { + maxWidth = alternativeMaxWidth; + } + + maxWidth += mPaddingLeft + mPaddingRight; + + // Check against our minimum width + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize); + + if (matchWidth) { + forceUniformWidth(count, heightMeasureSpec); + } + } + + private void forceUniformWidth(int count, int heightMeasureSpec) { + // Pretend that the linear layout has an exact size. + int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), + MeasureSpec.EXACTLY); + for (int i = 0; i< count; ++i) { + final View child = getVirtualChildAt(i); + if (child.getVisibility() != GONE) { + LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams()); + + if (lp.width == LayoutParams.FILL_PARENT) { + // Temporarily force children to reuse their old measured height + // FIXME: this may not be right for something like wrapping text? + int oldHeight = lp.height; + lp.height = child.getMeasuredHeight(); + + // Remeasue with new dimensions + measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0); + lp.height = oldHeight; + } + } + } + } + + /** + * Measures the children when the orientation of this LinearLayout is set + * to {@link #HORIZONTAL}. + * + * @param widthMeasureSpec Horizontal space requirements as imposed by the parent. + * @param heightMeasureSpec Vertical space requirements as imposed by the parent. + * + * @see #getOrientation() + * @see #setOrientation(int) + * @see #onMeasure(int, int) + */ + void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) { + mTotalLength = 0; + int maxHeight = 0; + int alternativeMaxHeight = 0; + int weightedMaxHeight = 0; + boolean allFillParent = true; + float totalWeight = 0; + + final int count = getVirtualChildCount(); + + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + boolean matchHeight = false; + + if (mMaxAscent == null || mMaxDescent == null) { + mMaxAscent = new int[VERTICAL_GRAVITY_COUNT]; + mMaxDescent = new int[VERTICAL_GRAVITY_COUNT]; + } + + final int[] maxAscent = mMaxAscent; + final int[] maxDescent = mMaxDescent; + + maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1; + maxDescent[0] = maxDescent[1] = maxDescent[2] = maxDescent[3] = -1; + + final boolean baselineAligned = mBaselineAligned; + + // See how wide everyone is. Also remember max height. + for (int i = 0; i < count; ++i) { + final View child = getVirtualChildAt(i); + + if (child == null) { + mTotalLength += measureNullChild(i); + continue; + } + + if (child.getVisibility() == GONE) { + i += getChildrenSkipCount(child, i); + continue; + } + + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); + + totalWeight += lp.weight; + + if (widthMode == MeasureSpec.EXACTLY && lp.width == 0 && lp.weight > 0) { + // Optimization: don't bother measuring children who are going to use + // leftover space. These views will get measured again down below if + // there is any leftover space. + mTotalLength += lp.leftMargin + lp.rightMargin; + + // Baseline alignment requires to measure widgets to obtain the + // baseline offset (in particular for TextViews). + // The following defeats the optimization mentioned above. + // Allow the child to use as much space as it wants because we + // can shrink things later (and re-measure). + if (baselineAligned) { + final int freeSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + child.measure(freeSpec, freeSpec); + } + } else { + int oldWidth = Integer.MIN_VALUE; + + if (lp.width == 0 && lp.weight > 0) { + // widthMode is either UNSPECIFIED OR AT_MOST, and this child + // wanted to stretch to fill available space. Translate that to + // WRAP_CONTENT so that it does not end up with a width of 0 + oldWidth = lp.width; + lp.width = LayoutParams.WRAP_CONTENT; + } + + // Determine how big this child would like to be. If this or + // previous children have given a weight, then we allow it to + // use all available space (and we will shrink things later + // if needed). + measureChildBeforeLayout(child, i, widthMeasureSpec, + totalWeight == 0 ? mTotalLength : 0, + heightMeasureSpec, 0); + + if (oldWidth != Integer.MIN_VALUE) { + lp.width = oldWidth; + } + + mTotalLength += child.getMeasuredWidth() + lp.leftMargin + + lp.rightMargin + getNextLocationOffset(child); + } + + boolean matchHeightLocally = false; + if (heightMode != MeasureSpec.EXACTLY && lp.height == LayoutParams.FILL_PARENT) { + // The height of the linear layout will scale, and at least one + // child said it wanted to match our height. Set a flag indicating that + // we need to remeasure at least that view when we know our height. + matchHeight = true; + matchHeightLocally = true; + } + + final int margin = lp.topMargin + lp.bottomMargin; + final int childHeight = child.getMeasuredHeight() + margin; + + if (baselineAligned) { + final int childBaseline = child.getBaseline(); + if (childBaseline != -1) { + // Translates the child's vertical gravity into an index + // in the range 0..VERTICAL_GRAVITY_COUNT + final int gravity = (lp.gravity < 0 ? mGravity : lp.gravity) + & Gravity.VERTICAL_GRAVITY_MASK; + final int index = ((gravity >> Gravity.AXIS_Y_SHIFT) + & ~Gravity.AXIS_SPECIFIED) >> 1; + + maxAscent[index] = Math.max(maxAscent[index], childBaseline); + maxDescent[index] = Math.max(maxDescent[index], childHeight - childBaseline); + } + } + + maxHeight = Math.max(maxHeight, childHeight); + + allFillParent = allFillParent && lp.height == LayoutParams.FILL_PARENT; + if (lp.weight > 0) { + /* + * Heights of weighted Views are bogus if we end up + * remeasuring, so keep them separate. + */ + weightedMaxHeight = Math.max(weightedMaxHeight, + matchHeightLocally ? margin : childHeight); + } else { + alternativeMaxHeight = Math.max(alternativeMaxHeight, + matchHeightLocally ? margin : childHeight); + } + + i += getChildrenSkipCount(child, i); + } + + // Check mMaxAscent[INDEX_TOP] first because it maps to Gravity.TOP, + // the most common case + if (maxAscent[INDEX_TOP] != -1 || + maxAscent[INDEX_CENTER_VERTICAL] != -1 || + maxAscent[INDEX_BOTTOM] != -1 || + maxAscent[INDEX_FILL] != -1) { + final int ascent = Math.max(maxAscent[INDEX_FILL], + Math.max(maxAscent[INDEX_CENTER_VERTICAL], + Math.max(maxAscent[INDEX_TOP], maxAscent[INDEX_BOTTOM]))); + final int descent = Math.max(maxDescent[INDEX_FILL], + Math.max(maxDescent[INDEX_CENTER_VERTICAL], + Math.max(maxDescent[INDEX_TOP], maxDescent[INDEX_BOTTOM]))); + maxHeight = Math.max(maxHeight, ascent + descent); + } + + // Add in our padding + mTotalLength += mPaddingLeft + mPaddingRight; + + int widthSize = mTotalLength; + + // Check against our minimum width + widthSize = Math.max(widthSize, getSuggestedMinimumWidth()); + + // Reconcile our calculated size with the widthMeasureSpec + widthSize = resolveSize(widthSize, widthMeasureSpec); + + // Either expand children with weight to take up available space or + // shrink them if they extend beyond our current bounds + int delta = widthSize - mTotalLength; + if (delta != 0 && totalWeight > 0.0f) { + float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; + + maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1; + maxDescent[0] = maxDescent[1] = maxDescent[2] = maxDescent[3] = -1; + maxHeight = -1; + + mTotalLength = 0; + + for (int i = 0; i < count; ++i) { + final View child = getVirtualChildAt(i); + + if (child == null || child.getVisibility() == View.GONE) { + continue; + } + + final LinearLayout.LayoutParams lp = + (LinearLayout.LayoutParams) child.getLayoutParams(); + + float childExtra = lp.weight; + if (childExtra > 0) { + // Child said it could absorb extra space -- give him his share + int share = (int) (childExtra * delta / weightSum); + weightSum -= childExtra; + delta -= share; + + final int childHeightMeasureSpec = getChildMeasureSpec( + heightMeasureSpec, + mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin, + lp.height); + + // TODO: Use a field like lp.isMeasured to figure out if this + // child has been previously measured + if ((lp.width != 0) || (widthMode != MeasureSpec.EXACTLY)) { + // child was measured once already above ... base new measurement + // on stored values + int childWidth = child.getMeasuredWidth() + share; + if (childWidth < 0) { + childWidth = 0; + } + + child.measure( + MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), + childHeightMeasureSpec); + } else { + // child was skipped in the loop above. Measure for this first time here + child.measure(MeasureSpec.makeMeasureSpec( + share > 0 ? share : 0, MeasureSpec.EXACTLY), + childHeightMeasureSpec); + } + } + + mTotalLength += child.getMeasuredWidth() + lp.leftMargin + + lp.rightMargin + getNextLocationOffset(child); + + boolean matchHeightLocally = heightMode != MeasureSpec.EXACTLY && + lp.height == LayoutParams.FILL_PARENT; + + final int margin = lp.topMargin + lp .bottomMargin; + int childHeight = child.getMeasuredHeight() + margin; + maxHeight = Math.max(maxHeight, childHeight); + alternativeMaxHeight = Math.max(alternativeMaxHeight, + matchHeightLocally ? margin : childHeight); + + allFillParent = allFillParent && lp.height == LayoutParams.FILL_PARENT; + alternativeMaxHeight = Math.max(alternativeMaxHeight, + matchHeightLocally ? margin : childHeight); + + if (baselineAligned) { + final int childBaseline = child.getBaseline(); + if (childBaseline != -1) { + // Translates the child's vertical gravity into an index in the range 0..2 + final int gravity = (lp.gravity < 0 ? mGravity : lp.gravity) + & Gravity.VERTICAL_GRAVITY_MASK; + final int index = ((gravity >> Gravity.AXIS_Y_SHIFT) + & ~Gravity.AXIS_SPECIFIED) >> 1; + + maxAscent[index] = Math.max(maxAscent[index], childBaseline); + maxDescent[index] = Math.max(maxDescent[index], + childHeight - childBaseline); + } + } + } + + // Add in our padding + mTotalLength += mPaddingLeft + mPaddingRight; + + // Check mMaxAscent[INDEX_TOP] first because it maps to Gravity.TOP, + // the most common case + if (maxAscent[INDEX_TOP] != -1 || + maxAscent[INDEX_CENTER_VERTICAL] != -1 || + maxAscent[INDEX_BOTTOM] != -1 || + maxAscent[INDEX_FILL] != -1) { + final int ascent = Math.max(maxAscent[INDEX_FILL], + Math.max(maxAscent[INDEX_CENTER_VERTICAL], + Math.max(maxAscent[INDEX_TOP], maxAscent[INDEX_BOTTOM]))); + final int descent = Math.max(maxDescent[INDEX_FILL], + Math.max(maxDescent[INDEX_CENTER_VERTICAL], + Math.max(maxDescent[INDEX_TOP], maxDescent[INDEX_BOTTOM]))); + maxHeight = Math.max(maxHeight, ascent + descent); + } + } else { + alternativeMaxHeight = Math.max(alternativeMaxHeight, + weightedMaxHeight); + } + + if (!allFillParent && heightMode != MeasureSpec.EXACTLY) { + maxHeight = alternativeMaxHeight; + } + + maxHeight += mPaddingTop + mPaddingBottom; + + // Check against our minimum height + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + + setMeasuredDimension(widthSize, resolveSize(maxHeight, heightMeasureSpec)); + + if (matchHeight) { + forceUniformHeight(count, widthMeasureSpec); + } + } + + private void forceUniformHeight(int count, int widthMeasureSpec) { + // Pretend that the linear layout has an exact size. This is the measured height of + // ourselves. The measured height should be the max height of the children, changed + // to accomodate the heightMesureSpec from the parent + int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), + MeasureSpec.EXACTLY); + for (int i = 0; i < count; ++i) { + final View child = getVirtualChildAt(i); + if (child.getVisibility() != GONE) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); + + if (lp.height == LayoutParams.FILL_PARENT) { + // Temporarily force children to reuse their old measured width + // FIXME: this may not be right for something like wrapping text? + int oldWidth = lp.width; + lp.width = child.getMeasuredWidth(); + + // Remeasure with new dimensions + measureChildWithMargins(child, widthMeasureSpec, 0, uniformMeasureSpec, 0); + lp.width = oldWidth; + } + } + } + } + + /** + * <p>Returns the number of children to skip after measuring/laying out + * the specified child.</p> + * + * @param child the child after which we want to skip children + * @param index the index of the child after which we want to skip children + * @return the number of children to skip, 0 by default + */ + int getChildrenSkipCount(View child, int index) { + return 0; + } + + /** + * <p>Returns the size (width or height) that should be occupied by a null + * child.</p> + * + * @param childIndex the index of the null child + * @return the width or height of the child depending on the orientation + */ + int measureNullChild(int childIndex) { + return 0; + } + + /** + * <p>Measure the child according to the parent's measure specs. This + * method should be overriden by subclasses to force the sizing of + * children. This method is called by {@link #measureVertical(int, int)} and + * {@link #measureHorizontal(int, int)}.</p> + * + * @param child the child to measure + * @param childIndex the index of the child in this view + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param totalWidth extra space that has been used up by the parent horizontally + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @param totalHeight extra space that has been used up by the parent vertically + */ + void measureChildBeforeLayout(View child, int childIndex, + int widthMeasureSpec, int totalWidth, int heightMeasureSpec, + int totalHeight) { + measureChildWithMargins(child, widthMeasureSpec, totalWidth, + heightMeasureSpec, totalHeight); + } + + /** + * <p>Return the location offset of the specified child. This can be used + * by subclasses to change the location of a given widget.</p> + * + * @param child the child for which to obtain the location offset + * @return the location offset in pixels + */ + int getLocationOffset(View child) { + return 0; + } + + /** + * <p>Return the size offset of the next sibling of the specified child. + * This can be used by subclasses to change the location of the widget + * following <code>child</code>.</p> + * + * @param child the child whose next sibling will be moved + * @return the location offset of the next child in pixels + */ + int getNextLocationOffset(View child) { + return 0; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (mOrientation == VERTICAL) { + layoutVertical(); + } else { + layoutHorizontal(); + } + } + + /** + * Position the children during a layout pass if the orientation of this + * LinearLayout is set to {@link #VERTICAL}. + * + * @see #getOrientation() + * @see #setOrientation(int) + * @see #onLayout(boolean, int, int, int, int) + */ + void layoutVertical() { + final int paddingLeft = mPaddingLeft; + + int childTop = mPaddingTop; + int childLeft = paddingLeft; + + // Where right end of child should go + final int width = mRight - mLeft; + int childRight = width - mPaddingRight; + + // Space available for child + int childSpace = width - paddingLeft - mPaddingRight; + + final int count = getVirtualChildCount(); + + final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + final int minorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + + if (majorGravity != Gravity.TOP) { + switch (majorGravity) { + case Gravity.BOTTOM: + childTop = mBottom - mTop - mPaddingBottom - mTotalLength; + break; + + case Gravity.CENTER_VERTICAL: + childTop += ((mBottom - mTop - mPaddingTop - mPaddingBottom) - + mTotalLength) / 2; + break; + } + + } + + for (int i = 0; i < count; i++) { + final View child = getVirtualChildAt(i); + if (child == null) { + childTop += measureNullChild(i); + } else if (child.getVisibility() != GONE) { + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + final LinearLayout.LayoutParams lp = + (LinearLayout.LayoutParams) child.getLayoutParams(); + + int gravity = lp.gravity; + if (gravity < 0) { + gravity = minorGravity; + } + + switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + childLeft = paddingLeft + lp.leftMargin; + break; + + case Gravity.CENTER_HORIZONTAL: + childLeft = paddingLeft + ((childSpace - childWidth) / 2) + + lp.leftMargin - lp.rightMargin; + break; + + case Gravity.RIGHT: + childLeft = childRight - childWidth - lp.rightMargin; + break; + } + + + childTop += lp.topMargin; + setChildFrame(child, childLeft, childTop + getLocationOffset(child), + childWidth, childHeight); + childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); + + i += getChildrenSkipCount(child, i); + } + } + } + + /** + * Position the children during a layout pass if the orientation of this + * LinearLayout is set to {@link #HORIZONTAL}. + * + * @see #getOrientation() + * @see #setOrientation(int) + * @see #onLayout(boolean, int, int, int, int) + */ + void layoutHorizontal() { + final int paddingTop = mPaddingTop; + + int childTop = paddingTop; + int childLeft = mPaddingLeft; + + // Where bottom of child should go + final int height = mBottom - mTop; + int childBottom = height - mPaddingBottom; + + // Space available for child + int childSpace = height - paddingTop - mPaddingBottom; + + final int count = getVirtualChildCount(); + + final int majorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + + final boolean baselineAligned = mBaselineAligned; + + final int[] maxAscent = mMaxAscent; + final int[] maxDescent = mMaxDescent; + + if (majorGravity != Gravity.LEFT) { + switch (majorGravity) { + case Gravity.RIGHT: + childLeft = mRight - mLeft - mPaddingRight - mTotalLength; + break; + + case Gravity.CENTER_HORIZONTAL: + childLeft += ((mRight - mLeft - mPaddingLeft - mPaddingRight) - + mTotalLength) / 2; + break; + } + } + + for (int i = 0; i < count; i++) { + final View child = getVirtualChildAt(i); + + if (child == null) { + childLeft += measureNullChild(i); + } else if (child.getVisibility() != GONE) { + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + int childBaseline = -1; + + final LinearLayout.LayoutParams lp = + (LinearLayout.LayoutParams) child.getLayoutParams(); + + if (baselineAligned && lp.height != LayoutParams.FILL_PARENT) { + childBaseline = child.getBaseline(); + } + + int gravity = lp.gravity; + if (gravity < 0) { + gravity = minorGravity; + } + + switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + childTop = paddingTop + lp.topMargin; + if (childBaseline != -1) { + childTop += maxAscent[INDEX_TOP] - childBaseline; + } + break; + + case Gravity.CENTER_VERTICAL: + // Removed support for baselign alignment when layout_gravity or + // gravity == center_vertical. See bug #1038483. + // Keep the code around if we need to re-enable this feature + // if (childBaseline != -1) { + // // Align baselines vertically only if the child is smaller than us + // if (childSpace - childHeight > 0) { + // childTop = paddingTop + (childSpace / 2) - childBaseline; + // } else { + // childTop = paddingTop + (childSpace - childHeight) / 2; + // } + // } else { + childTop = paddingTop + ((childSpace - childHeight) / 2) + + lp.topMargin - lp.bottomMargin; + break; + + case Gravity.BOTTOM: + childTop = childBottom - childHeight - lp.bottomMargin; + if (childBaseline != -1) { + int descent = child.getMeasuredHeight() - childBaseline; + childTop -= (maxDescent[INDEX_BOTTOM] - descent); + } + break; + } + + childLeft += lp.leftMargin; + setChildFrame(child, childLeft + getLocationOffset(child), childTop, + childWidth, childHeight); + childLeft += childWidth + lp.rightMargin + + getNextLocationOffset(child); + + i += getChildrenSkipCount(child, i); + } + } + } + + private void setChildFrame(View child, int left, int top, int width, int height) { + child.layout(left, top, left + width, top + height); + } + + /** + * Should the layout be a column or a row. + * @param orientation Pass HORIZONTAL or VERTICAL. Default + * value is HORIZONTAL. + * + * @attr ref android.R.styleable#LinearLayout_orientation + */ + public void setOrientation(int orientation) { + if (mOrientation != orientation) { + mOrientation = orientation; + requestLayout(); + } + } + + /** + * Returns the current orientation. + * + * @return either {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public int getOrientation() { + return mOrientation; + } + + /** + * Describes how the child views are positioned. Defaults to GRAVITY_TOP. If + * this layout has a VERTICAL orientation, this controls where all the child + * views are placed if there is extra vertical space. If this layout has a + * HORIZONTAL orientation, this controls the alignment of the children. + * + * @param gravity See {@link android.view.Gravity} + * + * @attr ref android.R.styleable#LinearLayout_gravity + */ + public void setGravity(int gravity) { + if (mGravity != gravity) { + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.LEFT; + } + + if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.TOP; + } + + mGravity = gravity; + requestLayout(); + } + } + + public void setHorizontalGravity(int horizontalGravity) { + final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { + mGravity = (mGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK) | gravity; + requestLayout(); + } + } + + public void setVerticalGravity(int verticalGravity) { + final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK; + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) { + mGravity = (mGravity & ~Gravity.VERTICAL_GRAVITY_MASK) | gravity; + requestLayout(); + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LinearLayout.LayoutParams(getContext(), attrs); + } + + /** + * Returns a set of layout parameters with a width of + * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT} + * and a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * when the layout's orientation is {@link #VERTICAL}. When the orientation is + * {@link #HORIZONTAL}, the width is set to {@link LayoutParams#WRAP_CONTENT} + * and the height to {@link LayoutParams#WRAP_CONTENT}. + */ + @Override + protected LayoutParams generateDefaultLayoutParams() { + if (mOrientation == HORIZONTAL) { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } else if (mOrientation == VERTICAL) { + return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT); + } + return null; + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + + // Override to allow type-checking of LayoutParams. + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LinearLayout.LayoutParams; + } + + /** + * Per-child layout information associated with ViewLinearLayout. + * + * @attr ref android.R.styleable#LinearLayout_Layout_layout_weight + * @attr ref android.R.styleable#LinearLayout_Layout_layout_gravity + */ + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + /** + * Indicates how much of the extra space in the LinearLayout will be + * allocated to the view associated with these LayoutParams. Specify + * 0 if the view should not be stretched. Otherwise the extra pixels + * will be pro-rated among all views whose weight is greater than 0. + */ + @ViewDebug.ExportedProperty + public float weight; + + /** + * Gravity for the view associated with these LayoutParams. + * + * @see android.view.Gravity + */ + @ViewDebug.ExportedProperty(mapping = { + @ViewDebug.IntToString(from = -1, to = "NONE"), + @ViewDebug.IntToString(from = Gravity.NO_GRAVITY, to = "NONE"), + @ViewDebug.IntToString(from = Gravity.TOP, to = "TOP"), + @ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"), + @ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"), + @ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"), + @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"), + @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"), + @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"), + @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL, to = "FILL_HORIZONTAL"), + @ViewDebug.IntToString(from = Gravity.CENTER, to = "CENTER"), + @ViewDebug.IntToString(from = Gravity.FILL, to = "FILL") + }) + public int gravity = -1; + + /** + * {@inheritDoc} + */ + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + TypedArray a = + c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout); + + weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0); + gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1); + + a.recycle(); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(int width, int height) { + super(width, height); + weight = 0; + } + + /** + * Creates a new set of layout parameters with the specified width, height + * and weight. + * + * @param width the width, either {@link #FILL_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param height the height, either {@link #FILL_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param weight the weight + */ + public LayoutParams(int width, int height, float weight) { + super(width, height); + this.weight = weight; + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams p) { + super(p); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + @Override + public String debug(String output) { + return output + "LinearLayout.LayoutParams={width=" + sizeToString(width) + + ", height=" + sizeToString(height) + " weight=" + weight + "}"; + } + } +} diff --git a/core/java/android/widget/ListAdapter.java b/core/java/android/widget/ListAdapter.java new file mode 100644 index 0000000..a035145 --- /dev/null +++ b/core/java/android/widget/ListAdapter.java @@ -0,0 +1,44 @@ +/* + * 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 android.widget; + +/** + * Extended {@link Adapter} that is the bridge between a {@link ListView} + * and the data that backs the list. Frequently that data comes from a Cursor, + * but that is not + * required. The ListView can display any data provided that it is wrapped in a + * ListAdapter. + */ +public interface ListAdapter extends Adapter { + + /** + * Are all items in this ListAdapter enabled? + * If yes it means all items are selectable and clickable. + * + * @return True if all items are enabled + */ + public boolean areAllItemsEnabled(); + + /** + * Returns true if the item at the specified position is not a separator. + * (A separator is a non-selectable, non-clickable item). + * + * @param position Index of the item + * @return True if the item is not a separator + */ + boolean isEnabled(int position); +} diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java new file mode 100644 index 0000000..d52e51f --- /dev/null +++ b/core/java/android/widget/ListView.java @@ -0,0 +1,3204 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.SparseBooleanArray; +import android.util.SparseArray; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.SoundEffectConstants; + +import com.google.android.collect.Lists; + +import java.util.ArrayList; + +/* + * 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. + * + * @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 + */ +public class ListView extends AbsListView { + /** + * Used to indicate a no preference for a position type. + */ + static final int NO_POSITION = -1; + + /** + * Normal list that does not indicate choices + */ + public static final int CHOICE_MODE_NONE = 0; + + /** + * The list allows up to one choice + */ + public static final int CHOICE_MODE_SINGLE = 1; + + /** + * The list allows multiple choices + */ + public static final int CHOICE_MODE_MULTIPLE = 2; + + /** + * 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; + + /** + * When arrow scrolling, need a certain amount of pixels to preview next + * items. This is usually the fading edge, but if that is small enough, + * we want to make sure we preview at least this many pixels. + */ + private static final int MIN_SCROLL_PREVIEW_PIXELS = 2; + + // TODO: document + class FixedViewInfo { + public View view; + public Object data; + public boolean isSelectable; + } + + private ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList(); + private ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList(); + + Drawable mDivider; + int mDividerHeight; + + private boolean mAreAllItemsSelectable = true; + + private boolean mItemsCanFocus = false; + + private int mChoiceMode = CHOICE_MODE_NONE; + + private SparseBooleanArray mCheckStates; + + // used for temporary calculations. + private Rect mTempRect = new Rect(); + + /** + * Used to save / restore the state of the focused child in {@link #layoutChildren()} + */ + private SparseArray<Parcelable> mfocusRestoreChildState = new SparseArray<Parcelable>(); + + + // the single allocated result per list view; kinda cheesey but avoids + // allocating these thingies too often. + private ArrowScrollFocusResult mArrowScrollFocusResult = new ArrowScrollFocusResult(); + + public ListView(Context context) { + this(context, null); + } + + public ListView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.listViewStyle); + } + + public ListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ListView, defStyle, 0); + + CharSequence[] entries = a.getTextArray( + com.android.internal.R.styleable.ListView_entries); + if (entries != null) { + setAdapter(new ArrayAdapter<CharSequence>(context, + com.android.internal.R.layout.simple_list_item_1, entries)); + } + + final Drawable d = a.getDrawable(com.android.internal.R.styleable.ListView_divider); + if (d != null) { + + // If a divider is specified use its intrinsic height for divider height + setDivider(d); + } else { + + // Else use the height specified, zero being the default + final int dividerHeight = a.getDimensionPixelSize( + com.android.internal.R.styleable.ListView_dividerHeight, 0); + if (dividerHeight != 0) { + setDividerHeight(dividerHeight); + } + } + + 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)); + } + + /** + * 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); + } + } + } + + /** + * 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. + * <p> + * NOTE: Call this before calling setAdapter. This is so ListView can wrap + * the supplied cursor with one that that will also account for header + * 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. + * <p> + * NOTE: Call this before calling setAdapter. This is so ListView can wrap + * the supplied cursor with one that that will also account for header + * 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 (((HeaderViewListAdapter) mAdapter).removeHeader(v)) { + mDataSetObserver.onChanged(); + result = true; + } + removeFixedViewInfo(v, mHeaderViewInfos); + return result; + } + return false; + } + + private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> 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. + * <p> + * NOTE: Call this before calling setAdapter. This is so ListView can wrap + * the supplied cursor with one that that will also account for header + * 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. + * <p>NOTE: Call this before calling setAdapter. This is so ListView can wrap the supplied + * cursor with one that that will also account for header 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 (((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 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(); + } + + if (mCheckStates != null) { + mCheckStates.clear(); + } + + requestLayout(); + } + + + /** + * The list is empty. Clear everything out. + */ + @Override + void resetList() { + super.resetList(); + mLayoutMode = LAYOUT_NORMAL; + } + + /** + * @return Whether the list needs to show the top fading edge + */ + private boolean showingTopFadingEdge() { + final int listTop = mScrollY + 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; + + 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; + } + + /** + * {@inheritDoc} + */ + @Override + 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 nextTop) { + View selectedView = null; + + int end = (mBottom - mTop) - mListPadding.bottom; + + while (nextTop < end && pos < mItemCount) { + // is this the selected item? + boolean selected = pos == mSelectedPosition; + View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); + + nextTop = child.getBottom() + 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 nextBottom) { + View selectedView = null; + + int end = mListPadding.top; + + while (nextBottom > end && pos >= 0) { + // is this the selected item? + boolean selected = pos == mSelectedPosition; + View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected); + nextBottom = child.getTop() - mDividerHeight; + 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, + mListPadding.left, 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, mListPadding.left, 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; + } + + + /** + * Fills the list based on positioning the new selection relative to the old + * selection. The new selection will be placed at, above, or below the + * location of the new selection depending on how the selection is moving. + * The selection will then be pinned to the visible part of the screen, + * excluding the edges that are faded. The list is then filled upwards and + * downwards from there. + * + * @param oldSel The old selected view. Useful for trying to put the new + * selection in the same place + * @param newSel The view that is to become selected. Useful for trying to + * put the new selection in the same place + * @param delta Which way we are moving + * @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 moveSelection(View oldSel, View newSel, int delta, int childrenTop, + int childrenBottom) { + int fadingEdgeLength = getVerticalFadingEdgeLength(); + final int selectedPosition = mSelectedPosition; + + View sel; + + final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, + selectedPosition); + final int bottomSelectionPixel = getBottomSelectionPixel(childrenTop, fadingEdgeLength, + selectedPosition); + + if (delta > 0) { + /* + * Case 1: Scrolling down. + */ + + /* + * Before After + * | | | | + * +-------+ +-------+ + * | A | | A | + * | 1 | => +-------+ + * +-------+ | B | + * | B | | 2 | + * +-------+ +-------+ + * | | | | + * + * Try to keep the top of the previously selected item where it was. + * oldSel = A + * sel = B + */ + + // Put oldSel (A) where it belongs + oldSel = makeAndAddView(selectedPosition - 1, oldSel.getTop(), true, + mListPadding.left, false); + + final int dividerHeight = mDividerHeight; + + // Now put the new selection (B) below that + sel = makeAndAddView(selectedPosition, oldSel.getBottom() + dividerHeight, true, + mListPadding.left, 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 + int spaceAbove = sel.getTop() - topSelectionPixel; + + // Find space required to bring the bottom of the selected item fully into view + int spaceBelow = sel.getBottom() - bottomSelectionPixel; + + // Don't scroll more than half the height of the list + int halfVerticalSpace = (childrenBottom - childrenTop) / 2; + int offset = Math.min(spaceAbove, spaceBelow); + offset = Math.min(offset, halfVerticalSpace); + + // We placed oldSel, so offset that item + oldSel.offsetTopAndBottom(-offset); + // Now offset the selected item to get it into view + sel.offsetTopAndBottom(-offset); + } + + // Fill in views above and below + if (!mStackFromBottom) { + fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight); + adjustViewsUpOrDown(); + fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight); + } else { + fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight); + adjustViewsUpOrDown(); + fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight); + } + } else if (delta < 0) { + /* + * Case 2: Scrolling up. + */ + + /* + * Before After + * | | | | + * +-------+ +-------+ + * | A | | A | + * +-------+ => | 1 | + * | B | +-------+ + * | 2 | | B | + * +-------+ +-------+ + * | | | | + * + * Try to keep the top of the item about to become selected where it was. + * newSel = A + * olSel = B + */ + + if (newSel != null) { + // Try to position the top of newSel (A) where it was before it was selected + sel = makeAndAddView(selectedPosition, newSel.getTop(), true, mListPadding.left, + true); + } else { + // If (A) was not on screen and so did not have a view, position + // it above the oldSel (B) + sel = makeAndAddView(selectedPosition, oldSel.getTop(), false, mListPadding.left, + true); + } + + // Some of the newly selected item extends above the top of the list + if (sel.getTop() < topSelectionPixel) { + // Find space required to bring the top of the selected item fully into view + int spaceAbove = topSelectionPixel - sel.getTop(); + + // Find space available below the selection into which we can scroll downwards + int spaceBelow = bottomSelectionPixel - sel.getBottom(); + + // Don't scroll more than half the height of the list + int halfVerticalSpace = (childrenBottom - childrenTop) / 2; + int offset = Math.min(spaceAbove, spaceBelow); + offset = Math.min(offset, halfVerticalSpace); + + // Offset the selected item to get it into view + sel.offsetTopAndBottom(offset); + } + + // Fill in views above and below + fillAboveAndBelow(sel, selectedPosition); + } else { + + int oldTop = oldSel.getTop(); + + /* + * Case 3: Staying still + */ + sel = makeAndAddView(selectedPosition, oldTop, true, mListPadding.left, true); + + // We're staying still... + if (oldTop < childrenTop) { + // ... but the top of the old selection was off screen. + // (This can happen if the data changes size out from under us) + int newBottom = sel.getBottom(); + if (newBottom < childrenTop + 20) { + // Not enough visible -- bring it onscreen + sel.offsetTopAndBottom(childrenTop - sel.getTop()); + } + } + + // Fill in views above and below + fillAboveAndBelow(sel, selectedPosition); + } + + return sel; + } + + @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); + final int childViewType = mAdapter.getItemViewType(0); + + AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); + if (lp == null) { + lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + child.setLayoutParams(lp); + } + lp.viewType = childViewType; + + final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, + mListPadding.left + mListPadding.right, lp.width); + + int lpHeight = lp.height; + + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + + child.measure(childWidthSpec, childHeightSpec); + + childWidth = child.getMeasuredWidth(); + childHeight = child.getMeasuredHeight(); + + if (mRecycler.shouldRecycleViewType(childViewType)) { + 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( + MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), + 0, NO_POSITION, heightSize, -1); + } + + setMeasuredDimension(widthSize, heightSize); + mWidthMeasureSpec = widthMeasureSpec; + } + + /** + * 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(final int widthMeasureSpec, final 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 AbsListView.RecycleBin recycleBin = mRecycler; + for (i = startPosition; i <= endPosition; ++i) { + child = obtainView(i); + final int childViewType = adapter.getItemViewType(i); + + AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); + if (lp == null) { + lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + child.setLayoutParams(lp); + } + lp.viewType = childViewType; + + if (i > 0) { + // Count the divider for all but one child + returnedHeight += dividerHeight; + } + + child.measure(widthMeasureSpec, lp.height >= 0 + ? MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY) + : MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + + // Recycle the view before we possibly return from the method + if (recycleBin.shouldRecycleViewType(childViewType)) { + 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) { + for (int i = 0; i < childCount; i++) { + View v = getChildAt(i); + if (y <= v.getBottom()) { + return mFirstPosition + i; + } + } + return mFirstPosition + childCount - 1; + } + 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, mListPadding.left, 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; + + // 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); + 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; + + // 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 && (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); + 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(); + } + } + } + } + + @Override + protected void layoutChildren() { + final boolean blockLayoutRequests = mBlockLayoutRequests; + if (!blockLayoutRequests) { + mBlockLayoutRequests = true; + } + + try { + super.layoutChildren(); + + invalidate(); + + if (mAdapter == null) { + resetList(); + invokeOnItemScrollListener(); + return; + } + + int childrenTop = mListPadding.top; + int childrenBottom = mBottom - mTop - mListPadding.bottom; + + int childCount = getChildCount(); + int index; + 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; + } + + 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 = getFocusedChild(); + if (focusLayoutRestoreDirectChild != null) { + + // remember its state + focusLayoutRestoreDirectChild.saveHierarchyState(mfocusRestoreChildState); + + // remember the specific view that had focus + focusLayoutRestoreView = findFocus(); + } + } + requestFocus(); + } + + // Clear out old views + //removeAllViewsInLayout(); + 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; + case LAYOUT_MOVE_SELECTION: + sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); + 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(); + } + + if (sel == focusLayoutRestoreDirectChild) { + focusLayoutRestoreDirectChild.restoreHierarchyState(mfocusRestoreChildState); + } + } else { + positionSelector(sel); + } + mSelectedTop = sel.getTop(); + } 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(); + focusLayoutRestoreDirectChild.restoreHierarchyState(mfocusRestoreChildState); + } + } + + 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<FixedViewInfo> headers = mHeaderViewInfos; + final int numHeaders = headers.size(); + for (int i = 0; i < numHeaders; i++) { + if (child == headers.get(i).view) { + return true; + } + } + final ArrayList<FixedViewInfo> 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 y 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 + */ + private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, + boolean selected) { + View child; + + + 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 + setupChild(child, position, y, flow, childrenLeft, selected, true); + + return child; + } + } + + // Make a new view for this position, or convert an unused view if possible + child = obtainView(position); + + // This needs to be positioned and measured + setupChild(child, position, y, flow, childrenLeft, selected, false); + + return child; + } + + /** + * 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) { + final boolean isSelected = selected && shouldShowSelector(); + final boolean updateChildSelected = isSelected != child.isSelected(); + final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); + + // Respect layout params that are already in the view. Otherwise make some up... + // noinspection unchecked + AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); + if (p == null) { + p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + p.viewType = mAdapter.getItemViewType(position); + + if (recycled) { + attachViewToParent(child, flowDown ? -1 : 0, p); + } else { + addViewInLayout(child, flowDown ? -1 : 0, p, true); + } + + if (updateChildSelected) { + child.setSelected(isSelected); + } + + if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { + if (child instanceof Checkable) { + ((Checkable)child).setChecked(mCheckStates.get(position)); + } + } + + 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); + } + 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); + } else { + child.offsetLeftAndRight(childrenLeft - child.getLeft()); + child.offsetTopAndBottom(childTop - child.getTop()); + } + + if (mCachingStarted && !child.isDrawingCacheEnabled()) { + child.setDrawingCacheEnabled(true); + } + } + + @Override + protected boolean canAnimate() { + return super.canAnimate() && mItemCount > 0; + } + + /** + * Sets the currently selected item + * + * @param position Index (starting at 0) of the data item to be selected. + * + * If in touch mode, the item will not be selected but it will still be positioned + * appropriately. + */ + @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) { + mBlockLayoutRequests = true; + setNextSelectedPositionInt(position); + layoutChildren(); + mBlockLayoutRequests = false; + } + + /** + * 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; + } + } + + /** + * 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; + } + + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Dispatch in the normal way + boolean handled = super.dispatchKeyEvent(event); + if (!handled) { + // If we didn't handle it... + View focused = getFocusedChild(); + if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) { + // ... and our focused child didn't handle it + // ... give it to ourselves so we can scroll if necessary + handled = onKeyDown(event.getKeyCode(), event); + } + } + return handled; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return commonKey(keyCode, 1, event); + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return commonKey(keyCode, repeatCount, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return commonKey(keyCode, 1, event); + } + + private boolean commonKey(int keyCode, int count, KeyEvent event) { + if (mAdapter == null) { + return false; + } + + if (mDataChanged) { + layoutChildren(); + } + + boolean handled = false; + int action = event.getAction(); + + if (action != KeyEvent.ACTION_UP) { + if (mSelectedPosition < 0) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_SPACE: + if (resurrectSelection()) { + return true; + } + } + } + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + while (count > 0) { + handled = arrowScroll(FOCUS_UP); + count--; + } + } else { + handled = fullScroll(FOCUS_UP); + } + break; + + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + while (count > 0) { + handled = arrowScroll(FOCUS_DOWN); + count--; + } + } else { + handled = fullScroll(FOCUS_DOWN); + } + break; + + case KeyEvent.KEYCODE_DPAD_LEFT: + handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT); + break; + + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (mItemCount > 0 && event.getRepeatCount() == 0) { + keyPressed(); + } + handled = true; + break; + + case KeyEvent.KEYCODE_SPACE: + if (mPopup == null || !mPopup.isShowing()) { + if (!event.isShiftPressed()) { + pageScroll(FOCUS_DOWN); + } else { + pageScroll(FOCUS_UP); + } + handled = true; + } + break; + } + } + + if (!handled) { + handled = sendToTextFilter(keyCode, count, event); + } + + if (handled) { + return true; + } else { + switch (action) { + case KeyEvent.ACTION_DOWN: + return super.onKeyDown(keyCode, event); + + case KeyEvent.ACTION_UP: + return super.onKeyUp(keyCode, event); + + case KeyEvent.ACTION_MULTIPLE: + return super.onKeyMultiple(keyCode, count, event); + + default: // shouldn't happen + return false; + } + } + } + + /** + * 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(); + + if (down && position > mItemCount - getChildCount()) { + mLayoutMode = LAYOUT_FORCE_BOTTOM; + } + + if (!down && position < getChildCount()) { + mLayoutMode = LAYOUT_FORCE_TOP; + } + + setSelectionInt(position); + 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 + */ + 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); + } + 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); + } + moved = true; + } + } + + if (moved) { + invalidate(); + } + + return moved; + } + + /** + * To avoid horizontal focus searches changing the selected item, we + * manually focus search within the selected item (as applicable), and + * prevent focus from jumping to something within another item. + * @param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT} + * @return Whether this consumes the key event. + */ + private boolean handleHorizontalFocusWithinListItem(int direction) { + if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { + throw new IllegalArgumentException("direction must be one of {View.FOCUS_LEFT, View.FOCUS_RIGHT}"); + } + + final int numChildren = getChildCount(); + if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) { + final View selectedView = getSelectedView(); + if (selectedView.hasFocus() && selectedView instanceof ViewGroup) { + final View currentFocus = selectedView.findFocus(); + final View nextFocus = FocusFinder.getInstance().findNextFocus( + (ViewGroup) selectedView, + currentFocus, + direction); + if (nextFocus != null) { + // do the math to get interesting rect in next focus' coordinates + currentFocus.getFocusedRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocus, mTempRect); + offsetRectIntoDescendantCoords(nextFocus, mTempRect); + if (nextFocus.requestFocus(direction, mTempRect)) { + return true; + } + } + // we are blocking the key from being handled (by returning true) + // if the global result is going to be some other view within this + // list. this is to acheive the overall goal of having + // horizontal d-pad navigation remain in the current item. + final View globalNextFocus = FocusFinder.getInstance() + .findNextFocus( + (ViewGroup) getRootView(), + currentFocus, + direction); + if (globalNextFocus != null) { + return isViewAncestorOf(globalNextFocus, this); + } + } + } + return false; + } + + /** + * Scrolls to the next or previous item if possible. + * + * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} + * + * @return whether selection was moved + */ + boolean arrowScroll(int direction) { + try { + mInLayout = true; + final boolean handled = arrowScrollImpl(direction); + if (handled) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + return handled; + } finally { + mInLayout = false; + } + } + + /** + * Handle an arrow scroll going up or down. Take into account whether items are selectable, + * whether there are focusable items etc. + * + * @param direction Either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN}. + * @return Whether any scrolling, selection or focus change occured. + */ + private boolean arrowScrollImpl(int direction) { + if (getChildCount() <= 0) { + return false; + } + + View selectedView = getSelectedView(); + + int nextSelectedPosition = lookForSelectablePositionOnScreen(direction); + int amountToScroll = amountToScroll(direction, nextSelectedPosition); + + // if we are moving focus, we may OVERRIDE the default behavior + final ArrowScrollFocusResult focusResult = mItemsCanFocus ? arrowScrollFocused(direction) : null; + if (focusResult != null) { + nextSelectedPosition = focusResult.getSelectedPosition(); + amountToScroll = focusResult.getAmountToScroll(); + } + + boolean needToRedraw = focusResult != null; + if (nextSelectedPosition != INVALID_POSITION) { + handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null); + setSelectedPositionInt(nextSelectedPosition); + setNextSelectedPositionInt(nextSelectedPosition); + selectedView = getSelectedView(); + if (mItemsCanFocus && focusResult == null) { + // there was no new view found to take focus, make sure we + // don't leave focus with the old selection + final View focused = getFocusedChild(); + if (focused != null) { + focused.clearFocus(); + } + } + needToRedraw = true; + checkSelectionChanged(); + } + + if (amountToScroll > 0) { + scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll); + needToRedraw = true; + } + + // if we didn't find a new focusable, make sure any existing focused + // item that was panned off screen gives up focus. + if (mItemsCanFocus && (focusResult == null) + && selectedView != null && selectedView.hasFocus()) { + final View focused = selectedView.findFocus(); + if (distanceToView(focused) > 0) { + focused.clearFocus(); + } + } + + // if the current selection is panned off, we need to remove the selection + if (nextSelectedPosition == INVALID_POSITION && selectedView != null + && !isViewAncestorOf(selectedView, this)) { + selectedView = null; + hideSelector(); + } + + if (needToRedraw) { + if (selectedView != null) { + positionSelector(selectedView); + mSelectedTop = selectedView.getTop(); + } + invalidate(); + invokeOnItemScrollListener(); + return true; + } + + return false; + } + + /** + * When selection changes, it is possible that the previously selected or the + * next selected item will change its size. If so, we need to offset some folks, + * and re-layout the items as appropriate. + * + * @param selectedView The currently selected view (before changing selection). + * should be <code>null</code> if there was no previous selection. + * @param direction Either {@link android.view.View#FOCUS_UP} or + * {@link android.view.View#FOCUS_DOWN}. + * @param newSelectedPosition The position of the next selection. + * @param newFocusAssigned whether new focus was assigned. This matters because + * when something has focus, we don't want to show selection (ugh). + */ + private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, + boolean newFocusAssigned) { + if (newSelectedPosition == INVALID_POSITION) { + throw new IllegalArgumentException("newSelectedPosition needs to be valid"); + } + + // whether or not we are moving down or up, we want to preserve the + // top of whatever view is on top: + // - moving down: the view that had selection + // - moving up: the view that is getting selection + View topView; + View bottomView; + int topViewIndex, bottomViewIndex; + boolean topSelected = false; + final int selectedIndex = mSelectedPosition - mFirstPosition; + final int nextSelectedIndex = newSelectedPosition - mFirstPosition; + if (direction == View.FOCUS_UP) { + topViewIndex = nextSelectedIndex; + bottomViewIndex = selectedIndex; + topView = getChildAt(topViewIndex); + bottomView = selectedView; + topSelected = true; + } else { + topViewIndex = selectedIndex; + bottomViewIndex = nextSelectedIndex; + topView = selectedView; + bottomView = getChildAt(bottomViewIndex); + } + + final int numChildren = getChildCount(); + + // start with top view: is it changing size? + if (topView != null) { + topView.setSelected(!newFocusAssigned && topSelected); + measureAndAdjustDown(topView, topViewIndex, numChildren); + } + + // is the bottom view changing size? + if (bottomView != null) { + bottomView.setSelected(!newFocusAssigned && !topSelected); + measureAndAdjustDown(bottomView, bottomViewIndex, numChildren); + } + } + + /** + * Re-measure a child, and if its height changes, lay it out preserving its + * top, and adjust the children below it appropriately. + * @param child The child + * @param childIndex The view group index of the child. + * @param numChildren The number of children in the view group. + */ + private void measureAndAdjustDown(View child, int childIndex, int numChildren) { + int oldHeight = child.getHeight(); + measureItem(child); + if (child.getMeasuredHeight() != oldHeight) { + // lay out the view, preserving its top + relayoutMeasuredItem(child); + + // adjust views below appropriately + final int heightDelta = child.getMeasuredHeight() - oldHeight; + for (int i = childIndex + 1; i < numChildren; i++) { + getChildAt(i).offsetTopAndBottom(heightDelta); + } + } + } + + /** + * Measure a particular list child. + * TODO: unify with setUpChild. + * @param child The child. + */ + private void measureItem(View child) { + ViewGroup.LayoutParams p = child.getLayoutParams(); + if (p == null) { + p = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + 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); + } + child.measure(childWidthSpec, childHeightSpec); + } + + /** + * Layout a child that has been measured, preserving its top position. + * TODO: unify with setUpChild. + * @param child The child. + */ + private void relayoutMeasuredItem(View child) { + final int w = child.getMeasuredWidth(); + final int h = child.getMeasuredHeight(); + final int childLeft = mListPadding.left; + final int childRight = childLeft + w; + final int childTop = child.getTop(); + final int childBottom = childTop + h; + child.layout(childLeft, childTop, childRight, childBottom); + } + + /** + * @return The amount to preview next items when arrow srolling. + */ + private int getArrowScrollPreviewLength() { + return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength()); + } + + /** + * Determine how much we need to scroll in order to get the next selected view + * visible, with a fading edge showing below as applicable. The amount is + * capped at {@link #getMaxScrollAmount()} . + * + * @param direction either {@link android.view.View#FOCUS_UP} or + * {@link android.view.View#FOCUS_DOWN}. + * @param nextSelectedPosition The position of the next selection, or + * {@link #INVALID_POSITION} if there is no next selectable position + * @return The amount to scroll. Note: this is always positive! Direction + * needs to be taken into account when actually scrolling. + */ + private int amountToScroll(int direction, int nextSelectedPosition) { + final int listBottom = getHeight() - mListPadding.bottom; + final int listTop = mListPadding.top; + + final int numChildren = getChildCount(); + + if (direction == View.FOCUS_DOWN) { + int indexToMakeVisible = numChildren - 1; + if (nextSelectedPosition != INVALID_POSITION) { + indexToMakeVisible = nextSelectedPosition - mFirstPosition; + } + + final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; + final View viewToMakeVisible = getChildAt(indexToMakeVisible); + + int goalBottom = listBottom; + if (positionToMakeVisible < mItemCount - 1) { + goalBottom -= getArrowScrollPreviewLength(); + } + + if (viewToMakeVisible.getBottom() <= goalBottom) { + // item is fully visible. + return 0; + } + + if (nextSelectedPosition != INVALID_POSITION + && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) { + // item already has enough of it visible, changing selection is good enough + return 0; + } + + int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom); + + if ((mFirstPosition + numChildren) == mItemCount) { + // last is last in list -> make sure we don't scroll past it + final int max = getChildAt(numChildren - 1).getBottom() - listBottom; + amountToScroll = Math.min(amountToScroll, max); + } + + return Math.min(amountToScroll, getMaxScrollAmount()); + } else { + int indexToMakeVisible = 0; + if (nextSelectedPosition != INVALID_POSITION) { + indexToMakeVisible = nextSelectedPosition - mFirstPosition; + } + final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; + final View viewToMakeVisible = getChildAt(indexToMakeVisible); + int goalTop = listTop; + if (positionToMakeVisible > 0) { + goalTop += getArrowScrollPreviewLength(); + } + if (viewToMakeVisible.getTop() >= goalTop) { + // item is fully visible. + return 0; + } + + if (nextSelectedPosition != INVALID_POSITION && + (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) { + // item already has enough of it visible, changing selection is good enough + return 0; + } + + int amountToScroll = (goalTop - viewToMakeVisible.getTop()); + if (mFirstPosition == 0) { + // first is first in list -> make sure we don't scroll past it + final int max = listTop - getChildAt(0).getTop(); + amountToScroll = Math.min(amountToScroll, max); + } + return Math.min(amountToScroll, getMaxScrollAmount()); + } + } + + /** + * Holds results of focus aware arrow scrolling. + */ + static private class ArrowScrollFocusResult { + private int mSelectedPosition; + private int mAmountToScroll; + + /** + * How {@link android.widget.ListView#arrowScrollFocused} returns its values. + */ + void populate(int selectedPosition, int amountToScroll) { + mSelectedPosition = selectedPosition; + mAmountToScroll = amountToScroll; + } + + public int getSelectedPosition() { + return mSelectedPosition; + } + + public int getAmountToScroll() { + return mAmountToScroll; + } + } + + /** + * @param direction either {@link android.view.View#FOCUS_UP} or + * {@link android.view.View#FOCUS_DOWN}. + * @return The position of the next selectable position of the views that + * are currently visible, taking into account the fact that there might + * be no selection. Returns {@link #INVALID_POSITION} if there is no + * selectable view on screen in the given direction. + */ + private int lookForSelectablePositionOnScreen(int direction) { + final int firstPosition = mFirstPosition; + if (direction == View.FOCUS_DOWN) { + int startPos = (mSelectedPosition != INVALID_POSITION) ? + mSelectedPosition + 1 : + firstPosition; + if (startPos >= mAdapter.getCount()) { + return INVALID_POSITION; + } + if (startPos < firstPosition) { + startPos = firstPosition; + } + + final int lastVisiblePos = getLastVisiblePosition(); + final ListAdapter adapter = getAdapter(); + for (int pos = startPos; pos <= lastVisiblePos; pos++) { + if (adapter.isEnabled(pos) + && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { + return pos; + } + } + } else { + int last = firstPosition + getChildCount() - 1; + int startPos = (mSelectedPosition != INVALID_POSITION) ? + mSelectedPosition - 1 : + firstPosition + getChildCount() - 1; + if (startPos < 0) { + return INVALID_POSITION; + } + if (startPos > last) { + startPos = last; + } + + final ListAdapter adapter = getAdapter(); + for (int pos = startPos; pos >= firstPosition; pos--) { + if (adapter.isEnabled(pos) + && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { + return pos; + } + } + } + return INVALID_POSITION; + } + + /** + * Do an arrow scroll based on focus searching. If a new view is + * given focus, return the selection delta and amount to scroll via + * an {@link ArrowScrollFocusResult}, otherwise, return null. + * + * @param direction either {@link android.view.View#FOCUS_UP} or + * {@link android.view.View#FOCUS_DOWN}. + * @return The result if focus has changed, or <code>null</code>. + */ + private ArrowScrollFocusResult arrowScrollFocused(final int direction) { + final View selectedView = getSelectedView(); + View newFocus; + if (selectedView != null && selectedView.hasFocus()) { + View oldFocus = selectedView.findFocus(); + newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction); + } else { + if (direction == View.FOCUS_DOWN) { + final boolean topFadingEdgeShowing = (mFirstPosition > 0); + final int listTop = mListPadding.top + + (topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); + final int ySearchPoint = + (selectedView != null && selectedView.getTop() > listTop) ? + selectedView.getTop() : + listTop; + mTempRect.set(0, ySearchPoint, 0, ySearchPoint); + } else { + final boolean bottomFadingEdgeShowing = + (mFirstPosition + getChildCount() - 1) < mItemCount; + final int listBottom = getHeight() - mListPadding.bottom - + (bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); + final int ySearchPoint = + (selectedView != null && selectedView.getBottom() < listBottom) ? + selectedView.getBottom() : + listBottom; + mTempRect.set(0, ySearchPoint, 0, ySearchPoint); + } + newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction); + } + + if (newFocus != null) { + final int positionOfNewFocus = positionOfNewFocus(newFocus); + + // if the focus change is in a different new position, make sure + // we aren't jumping over another selectable position + if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) { + final int selectablePosition = lookForSelectablePositionOnScreen(direction); + if (selectablePosition != INVALID_POSITION && + ((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) || + (direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) { + return null; + } + } + + int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus); + + final int maxScrollAmount = getMaxScrollAmount(); + if (focusScroll < maxScrollAmount) { + // not moving too far, safe to give next view focus + newFocus.requestFocus(direction); + mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll); + return mArrowScrollFocusResult; + } else if (distanceToView(newFocus) < maxScrollAmount){ + // Case to consider: + // too far to get entire next focusable on screen, but by going + // max scroll amount, we are getting it at least partially in view, + // so give it focus and scroll the max ammount. + newFocus.requestFocus(direction); + mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount); + return mArrowScrollFocusResult; + } + } + return null; + } + + /** + * @param newFocus The view that would have focus. + * @return the position that contains newFocus + */ + private int positionOfNewFocus(View newFocus) { + final int numChildren = getChildCount(); + for (int i = 0; i < numChildren; i++) { + final View child = getChildAt(i); + if (isViewAncestorOf(newFocus, child)) { + return mFirstPosition + i; + } + } + throw new IllegalArgumentException("newFocus is not a child of any of the" + + " children of the list!"); + } + + /** + * Return true if child is an ancestor of parent, (or equal to the parent). + */ + private boolean isViewAncestorOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent); + } + + /** + * Determine how much we need to scroll in order to get newFocus in view. + * @param direction either {@link android.view.View#FOCUS_UP} or + * {@link android.view.View#FOCUS_DOWN}. + * @param newFocus The view that would take focus. + * @param positionOfNewFocus The position of the list item containing newFocus + * @return The amount to scroll. Note: this is always positive! Direction + * needs to be taken into account when actually scrolling. + */ + private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) { + int amountToScroll = 0; + newFocus.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(newFocus, mTempRect); + if (direction == View.FOCUS_UP) { + if (mTempRect.top < mListPadding.top) { + amountToScroll = mListPadding.top - mTempRect.top; + if (positionOfNewFocus > 0) { + amountToScroll += getArrowScrollPreviewLength(); + } + } + } else { + final int listBottom = getHeight() - mListPadding.bottom; + if (mTempRect.bottom > listBottom) { + amountToScroll = mTempRect.bottom - listBottom; + if (positionOfNewFocus < mItemCount - 1) { + amountToScroll += getArrowScrollPreviewLength(); + } + } + } + return amountToScroll; + } + + /** + * Determine the distance to the nearest edge of a view in a particular + * direciton. + * @param descendant A descendant of this list. + * @return The distance, or 0 if the nearest edge is already on screen. + */ + private int distanceToView(View descendant) { + int distance = 0; + descendant.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(descendant, mTempRect); + final int listBottom = mBottom - mTop - mListPadding.bottom; + if (mTempRect.bottom < mListPadding.top) { + distance = mListPadding.top - mTempRect.bottom; + } else if (mTempRect.top > listBottom) { + distance = mTempRect.top - listBottom; + } + return distance; + } + + + /** + * 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); + + final int listBottom = getHeight() - mListPadding.bottom; + final int listTop = mListPadding.top; + + if (amount < 0) { + // shifted items up + + // may need to pan views into the bottom space + int numChildren = getChildCount(); + View last = getChildAt(numChildren - 1); + while (last.getBottom() < listBottom) { + final int lastVisiblePosition = mFirstPosition + numChildren - 1; + if (lastVisiblePosition < mItemCount - 1) { + last = addViewBelow(last, lastVisiblePosition); + 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()); + } + + // top views may be panned off screen + View first = getChildAt(0); + while (first.getBottom() < listTop) { + removeViewInLayout(first); + mRecycler.addScrapView(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()); + } + + int lastIndex = getChildCount() - 1; + View last = getChildAt(lastIndex); + + // bottom view may be panned off screen + while (last.getTop() > listBottom) { + removeViewInLayout(last); + mRecycler.addScrapView(last); + last = getChildAt(--lastIndex); + } + } + } + + private View addViewAbove(View theView, int position) { + int abovePosition = position - 1; + View view = obtainView(abovePosition); + int edgeOfNewChild = theView.getTop() - mDividerHeight; + setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left, false, false); + return view; + } + + private View addViewBelow(View theView, int position) { + int belowPosition = position + 1; + View view = obtainView(belowPosition); + int edgeOfNewChild = theView.getBottom() + mDividerHeight; + setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left, false, false); + 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; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + // Draw the dividers + final int dividerHeight = mDividerHeight; + + if (dividerHeight > 0 && mDivider != null) { + // 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; + + final int count = getChildCount(); + int i; + + if (mStackFromBottom) { + int top; + int listTop = mListPadding.top; + + for (i = 0; i < count; ++i) { + View child = getChildAt(i); + top = child.getTop(); + if (top > listTop) { + 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 { + int bottom; + int listBottom = getHeight() - mListPadding.bottom; + + for (i = 0; i < count; ++i) { + View child = getChildAt(i); + bottom = child.getBottom(); + if (bottom < listBottom) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + drawDivider(canvas, bounds, i); + } + } + } + } + + // 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 + mDivider.setBounds(bounds); + mDivider.draw(canvas); + } + + /** + * 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(); + } else { + mDividerHeight = 0; + } + mDivider = divider; + 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(); + } + + @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); + + // 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; + final ListAdapter adapter = mAdapter; + + 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(); + } + } + + /* (non-Javadoc) + * @see android.view.View#findViewById(int) + * First look in our children, then in any header and footer views that may be scrolled off. + */ + @Override + protected View findViewTraversal(int id) { + View v; + v = super.findViewTraversal(id); + if (v == null) { + v = findViewInHeadersOrFooters(mHeaderViewInfos, id); + if (v != null) { + return v; + } + v = findViewInHeadersOrFooters(mFooterViewInfos, id); + if (v != null) { + return v; + } + } + return v; + } + + /* (non-Javadoc) + * + * Look in the passed in list of headers or footers for the view. + */ + View findViewInHeadersOrFooters(ArrayList<FixedViewInfo> where, int id) { + if (where != null) { + int len = where.size(); + View v; + + for (int i = 0; i < len; i++) { + v = where.get(i).view; + + if (!v.isRootNamespace()) { + v = v.findViewById(id); + + if (v != null) { + return v; + } + } + } + } + return null; + } + + /* (non-Javadoc) + * @see android.view.View#findViewWithTag(String) + * First look in our children, then in any header and footer views that may be scrolled off. + */ + @Override + protected View findViewWithTagTraversal(Object tag) { + View v; + v = super.findViewWithTagTraversal(tag); + if (v == null) { + v = findViewTagInHeadersOrFooters(mHeaderViewInfos, tag); + if (v != null) { + return v; + } + + v = findViewTagInHeadersOrFooters(mFooterViewInfos, tag); + if (v != null) { + return v; + } + } + return v; + } + + /* (non-Javadoc) + * + * Look in the passed in list of headers or footers for the view with the tag. + */ + View findViewTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) { + if (where != null) { + int len = where.size(); + View v; + + for (int i = 0; i < len; i++) { + v = where.get(i).view; + + if (!v.isRootNamespace()) { + v = v.findViewWithTag(tag); + + if (v != null) { + return v; + } + } + } + } + return null; + } + + @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); + } + + /** + * @see #setChoiceMode(int) + * + * @return The current choice mode + */ + public int getChoiceMode() { + return mChoiceMode; + } + + /** + * Defines the choice behavior for the List. By default, Lists do not have any choice behavior + * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the + * List allows up to one item to be in a chosen state. By setting the choiceMode to + * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen. + * + * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or + * {@link #CHOICE_MODE_MULTIPLE} + */ + public void setChoiceMode(int choiceMode) { + mChoiceMode = choiceMode; + if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates == null) { + mCheckStates = new SparseBooleanArray(); + } + } + + @Override + public boolean performItemClick(View view, int position, long id) { + boolean handled = false; + + if (mChoiceMode != CHOICE_MODE_NONE) { + handled = true; + + if (mChoiceMode == CHOICE_MODE_MULTIPLE) { + boolean oldValue = mCheckStates.get(position, false); + mCheckStates.put(position, !oldValue); + } else { + boolean oldValue = mCheckStates.get(position, false); + if (!oldValue) { + mCheckStates.clear(); + mCheckStates.put(position, true); + } + } + + mDataChanged = true; + rememberSyncState(); + requestLayout(); + } + + 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 sate for the item + */ + public void setItemChecked(int position, boolean value) { + if (mChoiceMode == CHOICE_MODE_NONE) { + return; + } + + if (mChoiceMode == CHOICE_MODE_MULTIPLE) { + mCheckStates.put(position, value); + } else { + boolean oldValue = mCheckStates.get(position, false); + mCheckStates.clear(); + if (!oldValue) { + mCheckStates.put(position, true); + } + } + + // Do not generate a data change while we are in the layout phase + if (!mInLayout && !mBlockLayoutRequests) { + mDataChanged = true; + rememberSyncState(); + requestLayout(); + } + } + + /** + * Returns the checked state of the specified position. The result is only + * valid if the choice mode has not 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 + * + * @see #setChoiceMode(int) + */ + public boolean isItemChecked(int position) { + if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { + return mCheckStates.get(position); + } + + return false; + } + + /** + * Returns the currently checked item. The result is only valid if the choice + * mode has not 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() { + if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) { + return mCheckStates.keyAt(0); + } + + 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_SINGLE}. + * + * @return A SparseBooleanArray which will return true for each call to + * get(int position) where position is a position in the list. + */ + public SparseBooleanArray getCheckedItemPositions() { + if (mChoiceMode != CHOICE_MODE_NONE) { + return mCheckStates; + } + return null; + } + + /** + * Clear any choices previously set + */ + public void clearChoices() { + if (mCheckStates != null) { + mCheckStates.clear(); + } + } + + static class SavedState extends BaseSavedState { + SparseBooleanArray checkState; + + /** + * Constructor called from {@link ListView#onSaveInstanceState()} + */ + SavedState(Parcelable superState, SparseBooleanArray checkState) { + super(superState); + this.checkState = checkState; + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + checkState = in.readSparseBooleanArray(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeSparseBooleanArray(checkState); + } + + @Override + public String toString() { + return "ListView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " checkState=" + checkState + "}"; + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return new SavedState(superState, mCheckStates); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + + if (ss.checkState != null) { + mCheckStates = ss.checkState; + } + + } +} diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java new file mode 100644 index 0000000..ad8433f --- /dev/null +++ b/core/java/android/widget/MediaController.java @@ -0,0 +1,544 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.SeekBar.OnSeekBarChangeListener; + +import com.android.internal.policy.PolicyManager; + +import java.util.Formatter; +import java.util.Locale; + +/** + * A view containing controls for a MediaPlayer. Typically contains the + * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress + * slider. It takes care of synchronizing the controls with the state + * of the MediaPlayer. + * <p> + * The way to use this class is to instantiate it programatically. + * The MediaController will create a default set of controls + * and put them in a window floating above your application. Specifically, + * the controls will float above the view specified with setAnchorView(). + * The window will disappear if left idle for three seconds and reappear + * when the user touches the anchor view. + * <p> + * Functions like show() and hide() have no effect when MediaController + * is created in an xml layout. + * + * MediaController will hide and + * show the buttons according to these rules: + * <ul> + * <li> The "previous" and "next" buttons are hidden until setPrevNextListeners() + * has been called + * <li> The "previous" and "next" buttons are visible but disabled if + * setPrevNextListeners() was called with null listeners + * <li> The "rewind" and "fastforward" buttons are shown unless requested + * otherwise by using the MediaController(Context, boolean) constructor + * with the boolean set to false + * </ul> + */ +public class MediaController extends FrameLayout { + + private MediaPlayerControl mPlayer; + private Context mContext; + private View mAnchor; + private View mRoot; + private WindowManager mWindowManager; + private Window mWindow; + private View mDecor; + private ProgressBar mProgress; + private TextView mEndTime, mCurrentTime; + private boolean mShowing; + private boolean mDragging; + private static final int sDefaultTimeout = 3000; + private static final int FADE_OUT = 1; + private static final int SHOW_PROGRESS = 2; + private boolean mUseFastForward; + private boolean mFromXml; + private boolean mListenersSet; + private View.OnClickListener mNextListener, mPrevListener; + StringBuilder mFormatBuilder; + Formatter mFormatter; + private ImageButton mPauseButton; + private ImageButton mFfwdButton; + private ImageButton mRewButton; + private ImageButton mNextButton; + private ImageButton mPrevButton; + + public MediaController(Context context, AttributeSet attrs) { + super(context, attrs); + mRoot = this; + mContext = context; + mUseFastForward = true; + mFromXml = true; + } + + @Override + public void onFinishInflate() { + if (mRoot != null) + initControllerView(mRoot); + } + + public MediaController(Context context, boolean useFastForward) { + super(context); + mContext = context; + mUseFastForward = useFastForward; + initFloatingWindow(); + } + + public MediaController(Context context) { + super(context); + mContext = context; + mUseFastForward = true; + initFloatingWindow(); + } + + private void initFloatingWindow() { + mWindowManager = (WindowManager)mContext.getSystemService("window"); + mWindow = PolicyManager.makeNewWindow(mContext); + mWindow.setWindowManager(mWindowManager, null, null); + mWindow.requestFeature(Window.FEATURE_NO_TITLE); + mDecor = mWindow.getDecorView(); + mDecor.setOnTouchListener(mTouchListener); + mWindow.setContentView(this); + mWindow.setBackgroundDrawableResource(android.R.color.transparent); + + // While the media controller is up, the volume control keys should + // affect the media stream type + mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC); + + setFocusable(true); + setFocusableInTouchMode(true); + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + requestFocus(); + } + + private OnTouchListener mTouchListener = new OnTouchListener() { + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (mShowing) { + hide(); + } + } + return false; + } + }; + + public void setMediaPlayer(MediaPlayerControl player) { + mPlayer = player; + updatePausePlay(); + } + + /** + * Set the view that acts as the anchor for the control view. + * This can for example be a VideoView, or your Activity's main view. + * @param view The view to which to anchor the controller when it is visible. + */ + public void setAnchorView(View view) { + mAnchor = view; + + FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT + ); + + removeAllViews(); + View v = makeControllerView(); + addView(v, frameParams); + } + + /** + * Create the view that holds the widgets that control playback. + * Derived classes can override this to create their own. + * @return The controller view. + * @hide This doesn't work as advertised + */ + protected View makeControllerView() { + LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null); + + initControllerView(mRoot); + + return mRoot; + } + + private void initControllerView(View v) { + mPauseButton = (ImageButton) v.findViewById(com.android.internal.R.id.pause); + if (mPauseButton != null) { + mPauseButton.requestFocus(); + mPauseButton.setOnClickListener(mPauseListener); + } + + mFfwdButton = (ImageButton) v.findViewById(com.android.internal.R.id.ffwd); + if (mFfwdButton != null) { + mFfwdButton.setOnClickListener(mFfwdListener); + if (!mFromXml) { + mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); + } + } + + mRewButton = (ImageButton) v.findViewById(com.android.internal.R.id.rew); + if (mRewButton != null) { + mRewButton.setOnClickListener(mRewListener); + if (!mFromXml) { + mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); + } + } + + // By default these are hidden. They will be enabled when setPrevNextListeners() is called + mNextButton = (ImageButton) v.findViewById(com.android.internal.R.id.next); + if (mNextButton != null && !mFromXml && !mListenersSet) { + mNextButton.setVisibility(View.GONE); + } + mPrevButton = (ImageButton) v.findViewById(com.android.internal.R.id.prev); + if (mPrevButton != null && !mFromXml && !mListenersSet) { + mPrevButton.setVisibility(View.GONE); + } + + mProgress = (ProgressBar) v.findViewById(com.android.internal.R.id.mediacontroller_progress); + if (mProgress != null) { + if (mProgress instanceof SeekBar) { + SeekBar seeker = (SeekBar) mProgress; + seeker.setOnSeekBarChangeListener(mSeekListener); + } + mProgress.setMax(1000); + } + + mEndTime = (TextView) v.findViewById(com.android.internal.R.id.time); + mCurrentTime = (TextView) v.findViewById(com.android.internal.R.id.time_current); + mFormatBuilder = new StringBuilder(); + mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); + + installPrevNextListeners(); + } + + /** + * Show the controller on screen. It will go away + * automatically after 3 seconds of inactivity. + */ + public void show() { + show(sDefaultTimeout); + } + + /** + * Show the controller on screen. It will go away + * automatically after 'timeout' milliseconds of inactivity. + * @param timeout The timeout in milliseconds. Use 0 to show + * the controller until hide() is called. + */ + public void show(int timeout) { + + if (!mShowing && mAnchor != null) { + setProgress(); + + int [] anchorpos = new int[2]; + mAnchor.getLocationOnScreen(anchorpos); + + WindowManager.LayoutParams p = new WindowManager.LayoutParams(); + p.gravity = Gravity.TOP; + p.width = mAnchor.getWidth(); + p.height = LayoutParams.WRAP_CONTENT; + p.x = 0; + p.y = anchorpos[1] + mAnchor.getHeight() - p.height; + p.format = PixelFormat.TRANSLUCENT; + p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + p.token = null; + p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; + mWindowManager.addView(mDecor, p); + mShowing = true; + } + updatePausePlay(); + + // cause the progress bar to be updated even if mShowing + // was already true. This happens, for example, if we're + // paused with the progress bar showing the user hits play. + mHandler.sendEmptyMessage(SHOW_PROGRESS); + + Message msg = mHandler.obtainMessage(FADE_OUT); + if (timeout != 0) { + mHandler.removeMessages(FADE_OUT); + mHandler.sendMessageDelayed(msg, timeout); + } + } + + public boolean isShowing() { + return mShowing; + } + + /** + * Remove the controller from the screen. + */ + public void hide() { + if (mAnchor == null) + return; + + if (mShowing) { + try { + mHandler.removeMessages(SHOW_PROGRESS); + mWindowManager.removeView(mDecor); + } catch (IllegalArgumentException ex) { + Log.w("MediaController", "already removed"); + } + mShowing = false; + } + } + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + int pos; + switch (msg.what) { + case FADE_OUT: + hide(); + break; + case SHOW_PROGRESS: + pos = setProgress(); + if (!mDragging && mShowing && mPlayer.isPlaying()) { + msg = obtainMessage(SHOW_PROGRESS); + sendMessageDelayed(msg, 1000 - (pos % 1000)); + } + break; + } + } + }; + + private String stringForTime(int timeMs) { + int totalSeconds = timeMs / 1000; + + int seconds = totalSeconds % 60; + int minutes = (totalSeconds / 60) % 60; + int hours = totalSeconds / 3600; + + mFormatBuilder.setLength(0); + if (hours > 0) { + return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); + } else { + return mFormatter.format("%02d:%02d", minutes, seconds).toString(); + } + } + + private int setProgress() { + if (mPlayer == null || mDragging) { + return 0; + } + int position = mPlayer.getCurrentPosition(); + int duration = mPlayer.getDuration(); + if (mProgress != null) { + if (duration > 0) { + // use long to avoid overflow + long pos = 1000L * position / duration; + mProgress.setProgress( (int) pos); + } + int percent = mPlayer.getBufferPercentage(); + mProgress.setSecondaryProgress(percent * 10); + } + + if (mEndTime != null) + mEndTime.setText(stringForTime(duration)); + if (mCurrentTime != null) + mCurrentTime.setText(stringForTime(position)); + + return position; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + show(sDefaultTimeout); + return true; + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + show(sDefaultTimeout); + return false; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + if (event.getRepeatCount() == 0 && event.isDown() && ( + keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_SPACE)) { + doPauseResume(); + show(sDefaultTimeout); + return true; + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || + keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + // don't show the controls for volume adjustment + return super.dispatchKeyEvent(event); + } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { + hide(); + } else { + show(sDefaultTimeout); + } + return super.dispatchKeyEvent(event); + } + + private View.OnClickListener mPauseListener = new View.OnClickListener() { + public void onClick(View v) { + doPauseResume(); + show(sDefaultTimeout); + } + }; + + private void updatePausePlay() { + if (mRoot == null) + return; + + ImageButton button = (ImageButton) mRoot.findViewById(com.android.internal.R.id.pause); + if (button == null) + return; + + if (mPlayer.isPlaying()) { + button.setImageResource(com.android.internal.R.drawable.ic_media_pause); + } else { + button.setImageResource(com.android.internal.R.drawable.ic_media_play); + } + } + + private void doPauseResume() { + if (mPlayer.isPlaying()) { + mPlayer.pause(); + } else { + mPlayer.start(); + } + updatePausePlay(); + } + + private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { + long duration; + public void onStartTrackingTouch(SeekBar bar) { + show(3600000); + duration = mPlayer.getDuration(); + } + public void onProgressChanged(SeekBar bar, int progress, boolean fromtouch) { + if (fromtouch) { + mDragging = true; + long newposition = (duration * progress) / 1000L; + mPlayer.seekTo( (int) newposition); + if (mCurrentTime != null) + mCurrentTime.setText(stringForTime( (int) newposition)); + } + } + public void onStopTrackingTouch(SeekBar bar) { + mDragging = false; + setProgress(); + updatePausePlay(); + show(sDefaultTimeout); + } + }; + + @Override + public void setEnabled(boolean enabled) { + if (mPauseButton != null) { + mPauseButton.setEnabled(enabled); + } + if (mFfwdButton != null) { + mFfwdButton.setEnabled(enabled); + } + if (mRewButton != null) { + mRewButton.setEnabled(enabled); + } + if (mNextButton != null) { + mNextButton.setEnabled(enabled && mNextListener != null); + } + if (mPrevButton != null) { + mPrevButton.setEnabled(enabled && mPrevListener != null); + } + if (mProgress != null) { + mProgress.setEnabled(enabled); + } + + super.setEnabled(enabled); + } + + private View.OnClickListener mRewListener = new View.OnClickListener() { + public void onClick(View v) { + int pos = mPlayer.getCurrentPosition(); + pos -= 5000; // milliseconds + mPlayer.seekTo(pos); + setProgress(); + + show(sDefaultTimeout); + } + }; + + private View.OnClickListener mFfwdListener = new View.OnClickListener() { + public void onClick(View v) { + int pos = mPlayer.getCurrentPosition(); + pos += 15000; // milliseconds + mPlayer.seekTo(pos); + setProgress(); + + show(sDefaultTimeout); + } + }; + + private void installPrevNextListeners() { + if (mNextButton != null) { + mNextButton.setOnClickListener(mNextListener); + mNextButton.setEnabled(mNextListener != null); + } + + if (mPrevButton != null) { + mPrevButton.setOnClickListener(mPrevListener); + mPrevButton.setEnabled(mPrevListener != null); + } + } + + public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { + mNextListener = next; + mPrevListener = prev; + mListenersSet = true; + + if (mRoot != null) { + installPrevNextListeners(); + + if (mNextButton != null && !mFromXml) { + mNextButton.setVisibility(View.VISIBLE); + } + if (mPrevButton != null && !mFromXml) { + mPrevButton.setVisibility(View.VISIBLE); + } + } + } + + public interface MediaPlayerControl { + void start(); + void pause(); + int getDuration(); + int getCurrentPosition(); + void seekTo(int pos); + boolean isPlaying(); + int getBufferPercentage(); + }; +} diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java new file mode 100644 index 0000000..59a9310 --- /dev/null +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.Editable; +import android.text.Selection; +import android.text.Spanned; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.method.QwertyKeyListener; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.internal.R; + +/** + * An editable text view, extending {@link AutoCompleteTextView}, that + * can show completion suggestions for the substring of the text where + * the user is typing instead of necessarily for the entire thing. + * <p> + * You must must provide a {@link Tokenizer} to distinguish the + * various substrings. + * + * <p>The following code snippet shows how to create a text view which suggests + * various countries names while the user is typing:</p> + * + * <pre class="prettyprint"> + * public class CountriesActivity extends Activity { + * protected void onCreate(Bundle savedInstanceState) { + * super.onCreate(savedInstanceState); + * setContentView(R.layout.autocomplete_7); + * + * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + * android.R.layout.simple_dropdown_item_1line, COUNTRIES); + * MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit); + * textView.setAdapter(adapter); + * textView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer()); + * } + * + * private static final String[] COUNTRIES = new String[] { + * "Belgium", "France", "Italy", "Germany", "Spain" + * }; + * }</pre> + */ + +public class MultiAutoCompleteTextView extends AutoCompleteTextView { + private Tokenizer mTokenizer; + + public MultiAutoCompleteTextView(Context context) { + this(context, null); + } + + public MultiAutoCompleteTextView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); + } + + public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /* package */ void finishInit() { } + + /** + * Sets the Tokenizer that will be used to determine the relevant + * range of the text where the user is typing. + */ + public void setTokenizer(Tokenizer t) { + mTokenizer = t; + } + + /** + * Instead of filtering on the entire contents of the edit box, + * this subclass method filters on the range from + * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} + * if the length of that range meets or exceeds {@link #getThreshold}. + */ + @Override + protected void performFiltering(CharSequence text, int keyCode) { + if (enoughToFilter()) { + int end = getSelectionEnd(); + int start = mTokenizer.findTokenStart(text, end); + + performFiltering(text, start, end, keyCode); + } else { + dismissDropDown(); + + Filter f = getFilter(); + if (f != null) { + f.filter(null); + } + } + } + + /** + * Instead of filtering whenever the total length of the text + * exceeds the threshhold, this subclass filters only when the + * length of the range from + * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} + * meets or exceeds {@link #getThreshold}. + */ + @Override + public boolean enoughToFilter() { + Editable text = getText(); + + int end = getSelectionEnd(); + if (end < 0) { + return false; + } + + int start = mTokenizer.findTokenStart(text, end); + + if (end - start >= getThreshold()) { + return true; + } else { + return false; + } + } + + /** + * Instead of validating the entire text, this subclass method validates + * each token of the text individually. Empty tokens are removed. + */ + @Override + public void performValidation() { + Validator v = getValidator(); + + if (v == null) { + return; + } + + Editable e = getText(); + int i = getText().length(); + while (i > 0) { + int start = mTokenizer.findTokenStart(e, i); + int end = mTokenizer.findTokenEnd(e, start); + + CharSequence sub = e.subSequence(start, end); + if (TextUtils.isEmpty(sub)) { + e.replace(start, i, ""); + } else if (!v.isValid(sub)) { + e.replace(start, i, + mTokenizer.terminateToken(v.fixText(sub))); + } + + i = start; + } + } + + /** + * <p>Starts filtering the content of the drop down list. The filtering + * pattern is the specified range of text from the edit box. Subclasses may + * override this method to filter with a different pattern, for + * instance a smaller substring of <code>text</code>.</p> + */ + protected void performFiltering(CharSequence text, int start, int end, + int keyCode) { + getFilter().filter(text.subSequence(start, end), this); + } + + /** + * <p>Performs the text completion by replacing the range from + * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the + * the result of passing <code>text</code> through + * {@link Tokenizer#terminateToken}. + * In addition, the replaced region will be marked as an AutoText + * substition so that if the user immediately presses DEL, the + * completion will be undone. + * Subclasses may override this method to do some different + * insertion of the content into the edit box.</p> + * + * @param text the selected suggestion in the drop down list + */ + @Override + protected void replaceText(CharSequence text) { + int end = getSelectionEnd(); + int start = mTokenizer.findTokenStart(getText(), end); + + Editable editable = getText(); + String original = TextUtils.substring(editable, start, end); + + QwertyKeyListener.markAsReplaced(editable, start, end, original); + editable.replace(start, end, mTokenizer.terminateToken(text)); + } + + public static interface Tokenizer { + /** + * Returns the start of the token that ends at offset + * <code>cursor</code> within <code>text</code>. + */ + public int findTokenStart(CharSequence text, int cursor); + + /** + * Returns the end of the token (minus trailing punctuation) + * that begins at offset <code>cursor</code> within <code>text</code>. + */ + public int findTokenEnd(CharSequence text, int cursor); + + /** + * Returns <code>text</code>, modified, if necessary, to ensure that + * it ends with a token terminator (for example a space or comma). + */ + public CharSequence terminateToken(CharSequence text); + } + + /** + * This simple Tokenizer can be used for lists where the items are + * separated by a comma and one or more spaces. + */ + public static class CommaTokenizer implements Tokenizer { + public int findTokenStart(CharSequence text, int cursor) { + int i = cursor; + + while (i > 0 && text.charAt(i - 1) != ',') { + i--; + } + while (i < cursor && text.charAt(i) == ' ') { + i++; + } + + return i; + } + + public int findTokenEnd(CharSequence text, int cursor) { + int i = cursor; + int len = text.length(); + + while (i < len) { + if (text.charAt(i) == ',') { + return i; + } else { + i++; + } + } + + return len; + } + + public CharSequence terminateToken(CharSequence text) { + int i = text.length(); + + while (i > 0 && text.charAt(i - 1) == ' ') { + i--; + } + + if (i > 0 && text.charAt(i - 1) == ',') { + return text; + } else { + if (text instanceof Spanned) { + SpannableString sp = new SpannableString(text + ", "); + TextUtils.copySpansFrom((Spanned) text, 0, text.length(), + Object.class, sp, 0); + return sp; + } else { + return text + ", "; + } + } + } + } +} diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java new file mode 100644 index 0000000..6a7b1fb --- /dev/null +++ b/core/java/android/widget/PopupWindow.java @@ -0,0 +1,803 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import com.android.internal.R; + +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowManagerImpl; +import android.view.Gravity; +import android.view.ViewGroup; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.IBinder; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; + +/** + * <p>A popup window that can be used to display an arbitrary view. The popup + * windows is a floating container that appears on top of the current + * activity.</p> + * + * @see android.widget.AutoCompleteTextView + * @see android.widget.Spinner + */ +public class PopupWindow { + /** + * The height of the status bar so we know how much of the screen we can + * actually be displayed in. + * <p> + * TODO: This IS NOT the right way to do this. + * Instead of knowing how much of the screen is available, a popup that + * wants anchor and maximize space shouldn't be setting a height, instead + * the PopupViewContainer should have its layout height as fill_parent and + * properly position the popup. + */ + private static final int STATUS_BAR_HEIGHT = 30; + + private boolean mIsShowing; + + private View mContentView; + private View mPopupView; + private boolean mFocusable; + + private int mWidth; + private int mHeight; + + private int[] mDrawingLocation = new int[2]; + private int[] mRootLocation = new int[2]; + private Rect mTempRect = new Rect(); + + private Context mContext; + private Drawable mBackground; + + private boolean mAboveAnchor; + + private OnDismissListener mOnDismissListener; + private boolean mIgnoreCheekPress = false; + + private int mAnimationStyle = -1; + + private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] { + com.android.internal.R.attr.state_above_anchor + }; + + /** + * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> + * + * <p>The popup does provide a background.</p> + */ + public PopupWindow(Context context) { + this(context, null); + } + + /** + * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> + * + * <p>The popup does provide a background.</p> + */ + public PopupWindow(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.popupWindowStyle); + } + + /** + * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> + * + * <p>The popup does provide a background.</p> + */ + public PopupWindow(Context context, AttributeSet attrs, int defStyle) { + mContext = context; + + TypedArray a = + context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.PopupWindow, defStyle, 0); + + mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground); + + a.recycle(); + } + + /** + * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> + * + * <p>The popup does not provide any background. This should be handled + * by the content view.</p> + */ + public PopupWindow() { + this(null, 0, 0); + } + + /** + * <p>Create a new non focusable popup window which can display the + * <tt>contentView</tt>. The dimension of the window are (0,0).</p> + * + * <p>The popup does not provide any background. This should be handled + * by the content view.</p> + * + * @param contentView the popup's content + */ + public PopupWindow(View contentView) { + this(contentView, 0, 0); + } + + /** + * <p>Create a new empty, non focusable popup window. The dimension of the + * window must be passed to this constructor.</p> + * + * <p>The popup does not provide any background. This should be handled + * by the content view.</p> + * + * @param width the popup's width + * @param height the popup's height + */ + public PopupWindow(int width, int height) { + this(null, width, height); + } + + /** + * <p>Create a new non focusable popup window which can display the + * <tt>contentView</tt>. The dimension of the window must be passed to + * this constructor.</p> + * + * <p>The popup does not provide any background. This should be handled + * by the content view.</p> + * + * @param contentView the popup's content + * @param width the popup's width + * @param height the popup's height + */ + public PopupWindow(View contentView, int width, int height) { + this(contentView, width, height, false); + } + + /** + * <p>Create a new popup window which can display the <tt>contentView</tt>. + * The dimension of the window must be passed to this constructor.</p> + * + * <p>The popup does not provide any background. This should be handled + * by the content view.</p> + * + * @param contentView the popup's content + * @param width the popup's width + * @param height the popup's height + * @param focusable true if the popup can be focused, false otherwise + */ + public PopupWindow(View contentView, int width, int height, + boolean focusable) { + setContentView(contentView); + setWidth(width); + setHeight(height); + setFocusable(focusable); + } + + /** + * <p>Return the drawable used as the popup window's background.</p> + * + * @return the background drawable or null + */ + public Drawable getBackground() { + return mBackground; + } + + /** + * <p>Change the background drawable for this popup window. The background + * can be set to null.</p> + * + * @param background the popup's background + */ + public void setBackgroundDrawable(Drawable background) { + mBackground = background; + } + + /** + * <p>Return the animation style to use the popup appears and disappears</p> + * + * @return the animation style to use the popup appears and disappears + */ + public int getAnimationStyle() { + return mAnimationStyle; + } + + /** + * set the flag on popup to ignore cheek press events + * This method has to be invoked before displaying the content view + * of the popup for the window flags to take effect and will be ignored + * if the pop up is already displayed. By default this flag is set to false + * which means the pop wont ignore cheek press dispatch events. + */ + public void setIgnoreCheekPress() { + mIgnoreCheekPress = true; + } + + + /** + * <p>Change the animation style for this popup.</p> + * + * @param animationStyle animation style to use when the popup appears and disappears + */ + public void setAnimationStyle(int animationStyle) { + mAnimationStyle = animationStyle; + } + + /** + * <p>Return the view used as the content of the popup window.</p> + * + * @return a {@link android.view.View} representing the popup's content + * + * @see #setContentView(android.view.View) + */ + public View getContentView() { + return mContentView; + } + + /** + * <p>Change the popup's content. The content is represented by an instance + * of {@link android.view.View}.</p> + * + * <p>This method has no effect if called when the popup is showing.</p> + * + * @param contentView the new content for the popup + * + * @see #getContentView() + * @see #isShowing() + */ + public void setContentView(View contentView) { + if (isShowing()) { + return; + } + + mContentView = contentView; + } + + /** + * <p>Indicate whether the popup window can grab the focus.</p> + * + * @return true if the popup is focusable, false otherwise + * + * @see #setFocusable(boolean) + */ + public boolean isFocusable() { + return mFocusable; + } + + /** + * <p>Changes the focusability of the popup window. When focusable, the + * window will grab the focus from the current focused widget if the popup + * contains a focusable {@link android.view.View}.</p> + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown.</p> + * + * @param focusable true if the popup should grab focus, false otherwise + * + * @see #isFocusable() + * @see #isShowing() + */ + public void setFocusable(boolean focusable) { + mFocusable = focusable; + } + + /** + * <p>Return this popup's height MeasureSpec</p> + * + * @return the height MeasureSpec of the popup + * + * @see #setHeight(int) + */ + public int getHeight() { + return mHeight; + } + + /** + * <p>Change the popup's height MeasureSpec</p> + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown.</p> + * + * @param height the height MeasureSpec of the popup + * + * @see #getHeight() + * @see #isShowing() + */ + public void setHeight(int height) { + mHeight = height; + } + + /** + * <p>Return this popup's width MeasureSpec</p> + * + * @return the width MeasureSpec of the popup + * + * @see #setWidth(int) + */ + public int getWidth() { + return mWidth; + } + + /** + * <p>Change the popup's width MeasureSpec</p> + * + * <p>If the popup is showing, calling this method will take effect only + * the next time the popup is shown.</p> + * + * @param width the width MeasureSpec of the popup + * + * @see #getWidth() + * @see #isShowing() + */ + public void setWidth(int width) { + mWidth = width; + } + + /** + * <p>Indicate whether this popup window is showing on screen.</p> + * + * @return true if the popup is showing, false otherwise + */ + public boolean isShowing() { + return mIsShowing; + } + + /** + * <p> + * Display the content view in a popup window at the specified location. If the popup window + * cannot fit on screen, it will be clipped. See {@link android.view.WindowManager.LayoutParams} + * for more information on how gravity and the x and y parameters are related. Specifying + * a gravity of {@link android.view.Gravity#NO_GRAVITY} is similar to specifying + * <code>Gravity.LEFT | Gravity.TOP</code>. + * </p> + * + * @param parent a parent view to get the {@link android.view.View#getWindowToken()} token from + * @param gravity the gravity which controls the placement of the popup window + * @param x the popup's x location offset + * @param y the popup's y location offset + */ + public void showAtLocation(View parent, int gravity, int x, int y) { + if (isShowing() || mContentView == null) { + return; + } + + mIsShowing = true; + + WindowManager.LayoutParams p = createPopupLayout(parent.getWindowToken()); + if (mAnimationStyle != -1) { + p.windowAnimations = mAnimationStyle; + } + + preparePopup(p); + if (gravity == Gravity.NO_GRAVITY) { + gravity = Gravity.TOP | Gravity.LEFT; + } + p.gravity = gravity; + p.x = x; + p.y = y; + invokePopup(p); + } + + /** + * <p>Display the content view in a popup window anchored to the bottom-left + * corner of the anchor view. If there is not enough room on screen to show + * the popup in its entirety, this method tries to find a parent scroll + * view to scroll. If no parent scroll view can be scrolled, the bottom-left + * corner of the popup is pinned at the top left corner of the anchor view.</p> + * + * @param anchor the view on which to pin the popup window + * + * @see #dismiss() + */ + public void showAsDropDown(View anchor) { + showAsDropDown(anchor, 0, 0); + } + + /** + * <p>Display the content view in a popup window anchored to the bottom-left + * corner of the anchor view offset by the specified x and y coordinates. + * If there is not enough room on screen to show + * the popup in its entirety, this method tries to find a parent scroll + * view to scroll. If no parent scroll view can be scrolled, the bottom-left + * corner of the popup is pinned at the top left corner of the anchor view.</p> + * + * @param anchor the view on which to pin the popup window + * + * @see #dismiss() + */ + public void showAsDropDown(View anchor, int xoff, int yoff) { + if (isShowing() || mContentView == null) { + return; + } + + mIsShowing = true; + + WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken()); + preparePopup(p); + if (mBackground != null) { + mPopupView.refreshDrawableState(); + } + mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff); + if (mAnimationStyle == -1) { + p.windowAnimations = mAboveAnchor + ? com.android.internal.R.style.Animation_DropDownUp + : com.android.internal.R.style.Animation_DropDownDown; + } else { + p.windowAnimations = mAnimationStyle; + } + invokePopup(p); + } + + /** + * <p>Prepare the popup by embedding in into a new ViewGroup if the + * background drawable is not null. If embedding is required, the layout + * parameters' height is mnodified to take into account the background's + * padding.</p> + * + * @param p the layout parameters of the popup's content view + */ + private void preparePopup(WindowManager.LayoutParams p) { + if (mBackground != null) { + // when a background is available, we embed the content view + // within another view that owns the background drawable + PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); + PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT + ); + popupViewContainer.setBackgroundDrawable(mBackground); + popupViewContainer.addView(mContentView, listParams); + + if (p.height >= 0) { + // accomodate the popup's height to take into account the + // background's padding + p.height += popupViewContainer.getPaddingTop() + + popupViewContainer.getPaddingBottom(); + } + if (p.width >= 0) { + // accomodate the popup's width to take into account the + // background's padding + p.width += popupViewContainer.getPaddingLeft() + + popupViewContainer.getPaddingRight(); + } + mPopupView = popupViewContainer; + } else { + mPopupView = mContentView; + } + + } + + /** + * <p>Invoke the popup window by adding the content view to the window + * manager.</p> + * + * <p>The content view must be non-null when this method is invoked.</p> + * + * @param p the layout parameters of the popup's content view + */ + private void invokePopup(WindowManager.LayoutParams p) { + WindowManagerImpl wm = WindowManagerImpl.getDefault(); + wm.addView(mPopupView, p); + } + + /** + * <p>Generate the layout parameters for the popup window.</p> + * + * @param token the window token used to bind the popup's window + * + * @return the layout parameters to pass to the window manager + */ + private WindowManager.LayoutParams createPopupLayout(IBinder token) { + // generates the layout parameters for the drop down + // we want a fixed size view located at the bottom left of the anchor + WindowManager.LayoutParams p = new WindowManager.LayoutParams(); + // these gravity settings put the view at the top left corner of the + // screen. The view is then positioned to the appropriate location + // by setting the x and y offsets to match the anchor's bottom + // left corner + p.gravity = Gravity.LEFT | Gravity.TOP; + p.width = mWidth; + p.height = mHeight; + if (mBackground != null) { + p.format = mBackground.getOpacity(); + } else { + p.format = PixelFormat.TRANSLUCENT; + } + if(mIgnoreCheekPress) { + p.flags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; + } + if (!mFocusable) { + p.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } + p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + p.token = token; + + return p; + } + + /** + * <p>Positions the popup window on screen. When the popup window is too + * tall to fit under the anchor, a parent scroll view is seeked and scrolled + * up to reclaim space. If scrolling is not possible or not enough, the + * popup window gets moved on top of the anchor.</p> + * + * <p>The height must have been set on the layout parameters prior to + * calling this method.</p> + * + * @param anchor the view on which the popup window must be anchored + * @param p the layout parameters used to display the drop down + * + * @return true if the popup is translated upwards to fit on screen + */ + private boolean findDropDownPosition(View anchor, WindowManager.LayoutParams p, int xoff, int yoff) { + anchor.getLocationInWindow(mDrawingLocation); + p.x = mDrawingLocation[0] + xoff; + p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff; + + boolean onTop = false; + + if (p.y + p.height > WindowManagerImpl.getDefault().getDefaultDisplay().getHeight()) { + // if the drop down disappears at the bottom of the screen. we try to + // scroll a parent scrollview or move the drop down back up on top of + // the edit box + View root = anchor.getRootView(); + root.getLocationInWindow(mRootLocation); + int delta = p.y + p.height - mRootLocation[1] - root.getHeight(); + + if (delta > 0 || p.x + p.width - mRootLocation[0] - root.getWidth() > 0) { + Rect r = new Rect(anchor.getScrollX(), anchor.getScrollY(), + p.width, p.height + anchor.getMeasuredHeight()); + + onTop = !anchor.requestRectangleOnScreen(r, true); + + if (onTop) { + p.y -= anchor.getMeasuredHeight() + p.height; + } else { + anchor.getLocationOnScreen(mDrawingLocation); + p.x = mDrawingLocation[0] + xoff; + p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff; + } + } + } + + return onTop; + } + + /** + * Returns the maximum height that is available for the popup to be + * completely shown. It is recommended that this height be the maximum for + * the popup's height, otherwise it is possible that the popup will be + * clipped. + * + * @param anchor The view on which the popup window must be anchored. + * @return The maximum available height for the popup to be completely + * shown. + */ + public int getMaxAvailableHeight(View anchor) { + // TODO: read comment on STATUS_BAR_HEIGHT + final int screenHeight = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight() + - STATUS_BAR_HEIGHT; + + final int[] anchorPos = mDrawingLocation; + anchor.getLocationOnScreen(anchorPos); + anchorPos[1] -= STATUS_BAR_HEIGHT; + + final int distanceFromAnchorToBottom = screenHeight - (anchorPos[1] + anchor.getHeight()); + + // anchorPos[1] is distance from anchor to top of screen + int returnedHeight = Math.max(anchorPos[1], distanceFromAnchorToBottom); + if (mBackground != null) { + mBackground.getPadding(mTempRect); + returnedHeight -= mTempRect.top + mTempRect.bottom; + } + + return returnedHeight; + } + + /** + * <p>Dispose of the popup window. This method can be invoked only after + * {@link #showAsDropDown(android.view.View)} has been executed. Failing that, calling + * this method will have no effect.</p> + * + * @see #showAsDropDown(android.view.View) + */ + public void dismiss() { + if (isShowing() && mPopupView != null) { + WindowManagerImpl wm = WindowManagerImpl.getDefault(); + wm.removeView(mPopupView); + if (mPopupView != mContentView && mPopupView instanceof ViewGroup) { + ((ViewGroup) mPopupView).removeView(mContentView); + } + mIsShowing = false; + + if (mOnDismissListener != null) { + mOnDismissListener.onDismiss(); + } + } + } + + /** + * Sets the listener to be called when the window is dismissed. + * + * @param onDismissListener The listener. + */ + public void setOnDismissListener(OnDismissListener onDismissListener) { + mOnDismissListener = onDismissListener; + } + + /** + * <p>Updates the position and the dimension of the popup window. Width and + * height can be set to -1 to update location only.</p> + * + * @param x the new x location + * @param y the new y location + * @param width the new width, can be -1 to ignore + * @param height the new height, can be -1 to ignore + */ + public void update(int x, int y, int width, int height) { + if (width != -1) { + setWidth(width); + } + + if (height != -1) { + setHeight(height); + } + + if (!isShowing() || mContentView == null) { + return; + } + + WindowManager.LayoutParams p = (WindowManager.LayoutParams) + mPopupView.getLayoutParams(); + + boolean update = false; + + if (width != -1 && p.width != width) { + p.width = width; + update = true; + } + + if (height != -1 && p.height != height) { + p.height = height; + update = true; + } + + if (p.x != x) { + p.x = x; + update = true; + } + + if (p.y != y) { + p.y = y; + update = true; + } + + if (update) { + if (mPopupView != mContentView) { + final View popupViewContainer = mPopupView; + if (p.height >= 0) { + // accomodate the popup's height to take into account the + // background's padding + p.height += popupViewContainer.getPaddingTop() + + popupViewContainer.getPaddingBottom(); + } + if (p.width >= 0) { + // accomodate the popup's width to take into account the + // background's padding + p.width += popupViewContainer.getPaddingLeft() + + popupViewContainer.getPaddingRight(); + } + } + + WindowManagerImpl wm = WindowManagerImpl.getDefault(); + wm.updateViewLayout(mPopupView, p); + } + } + + /** + * <p>Updates the position and the dimension of the popup window. Width and + * height can be set to -1 to update location only.</p> + * + * @param anchor the popup's anchor view + * @param width the new width, can be -1 to ignore + * @param height the new height, can be -1 to ignore + */ + public void update(View anchor, int width, int height) { + update(anchor, 0, 0, width, height); + } + + /** + * <p>Updates the position and the dimension of the popup window. Width and + * height can be set to -1 to update location only.</p> + * + * @param anchor the popup's anchor view + * @param xoff x offset from the view's left edge + * @param yoff y offset from the view's bottom edge + * @param width the new width, can be -1 to ignore + * @param height the new height, can be -1 to ignore + */ + public void update(View anchor, int xoff, int yoff, int width, int height) { + if (!isShowing() || mContentView == null) { + return; + } + + WindowManager.LayoutParams p = (WindowManager.LayoutParams) + mPopupView.getLayoutParams(); + + int x = p.x; + int y = p.y; + findDropDownPosition(anchor, p, xoff, yoff); + + update(x, y, width, height); + } + + /** + * Listener that is called when this popup window is dismissed. + */ + interface OnDismissListener { + /** + * Called when this popup window is dismissed. + */ + public void onDismiss(); + } + + private class PopupViewContainer extends FrameLayout { + + public PopupViewContainer(Context context) { + super(context); + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + if (mAboveAnchor) { + // 1 more needed for the above anchor state + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET); + return drawableState; + } else { + return super.onCreateDrawableState(extraSpace); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + dismiss(); + return true; + } else { + return super.dispatchKeyEvent(event); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final int x = (int) event.getX(); + final int y = (int) event.getY(); + + if ((event.getAction() == MotionEvent.ACTION_DOWN) + && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) { + dismiss(); + return true; + } else { + return super.onTouchEvent(event); + } + } + + } + +} diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java new file mode 100644 index 0000000..c1de010 --- /dev/null +++ b/core/java/android/widget/ProgressBar.java @@ -0,0 +1,820 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Shader; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ClipDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.graphics.drawable.shapes.Shape; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.Transformation; +import android.widget.RemoteViews.RemoteView; +import android.os.SystemClock; + +import com.android.internal.R; + + +/** + * <p> + * Visual indicator of progress in some operation. Displays a bar to the user + * representing how far the operation has progressed; the application can + * change the amount of progress (modifying the length of the bar) as it moves + * forward. There is also a secondary progress displayable on a progress bar + * which is useful for displaying intermediate progress, such as the buffer + * level during a streaming playback progress bar. + * </p> + * + * <p> + * A progress bar can also be made indeterminate. In indeterminate mode, the + * progress bar shows a cyclic animation. This mode is used by applications + * when the length of the task is unknown. + * </p> + * + * <p>The following code example shows how a progress bar can be used from + * a worker thread to update the user interface to notify the user of progress: + * </p> + * + * <pre class="prettyprint"> + * public class MyActivity extends Activity { + * private static final int PROGRESS = 0x1; + * + * private ProgressBar mProgress; + * private int mProgressStatus = 0; + * + * private Handler mHandler = new Handler(); + * + * protected void onCreate(Bundle icicle) { + * super.onCreate(icicle); + * + * setContentView(R.layout.progressbar_activity); + * + * mProgress = (ProgressBar) findViewById(R.id.progress_bar); + * + * // Start lengthy operation in a background thread + * new Thread(new Runnable() { + * public void run() { + * while (mProgressStatus < 100) { + * mProgressStatus = doWork(); + * + * // Update the progress bar + * mHandler.post(new Runnable() { + * public void run() { + * mProgress.setProgress(mProgressStatus); + * } + * }); + * } + * } + * }).start(); + * } + * } + * </pre> + * + * <p><strong>XML attributes</b></strong> + * <p> + * See {@link android.R.styleable#ProgressBar ProgressBar Attributes}, + * {@link android.R.styleable#View View Attributes} + * </p> + * + * <p><strong>Styles</b></strong> + * <p> + * @attr ref android.R.styleable#Theme_progressBarStyle + * @attr ref android.R.styleable#Theme_progressBarStyleSmall + * @attr ref android.R.styleable#Theme_progressBarStyleLarge + * @attr ref android.R.styleable#Theme_progressBarStyleHorizontal + * </p> + */ +@RemoteView +public class ProgressBar extends View { + private static final int MAX_LEVEL = 10000; + private static final int ANIMATION_RESOLUTION = 200; + + int mMinWidth; + int mMaxWidth; + int mMinHeight; + int mMaxHeight; + + private int mProgress; + private int mSecondaryProgress; + private int mMax; + + private int mBehavior; + private int mDuration; + private boolean mIndeterminate; + private boolean mOnlyIndeterminate; + private Transformation mTransformation; + private AlphaAnimation mAnimation; + private Drawable mIndeterminateDrawable; + private Drawable mProgressDrawable; + private Drawable mCurrentDrawable; + Bitmap mSampleTile; + private boolean mNoInvalidate; + private Interpolator mInterpolator; + private RefreshProgressRunnable mRefreshProgressRunnable; + private long mUiThreadId; + private boolean mShouldStartAnimationDrawable; + private long mLastDrawTime; + + private boolean mInDrawing; + + /** + * Create a new progress bar with range 0...100 and initial progress of 0. + * @param context the application environment + */ + public ProgressBar(Context context) { + this(context, null); + } + + public ProgressBar(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.progressBarStyle); + } + + public ProgressBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mUiThreadId = Thread.currentThread().getId(); + initProgressBar(); + + TypedArray a = + context.obtainStyledAttributes(attrs, R.styleable.ProgressBar, defStyle, 0); + + mNoInvalidate = true; + + Drawable drawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable); + if (drawable != null) { + drawable = tileify(drawable); + setProgressDrawable(drawable); + } + + + mDuration = a.getInt(R.styleable.ProgressBar_indeterminateDuration, mDuration); + + mMinWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_minWidth, mMinWidth); + mMaxWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_maxWidth, mMaxWidth); + mMinHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_minHeight, mMinHeight); + mMaxHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_maxHeight, mMaxHeight); + + mBehavior = a.getInt(R.styleable.ProgressBar_indeterminateBehavior, mBehavior); + + final int resID = a.getResourceId(com.android.internal.R.styleable.ProgressBar_interpolator, -1); + if (resID > 0) { + setInterpolator(context, resID); + } + + setMax(a.getInt(R.styleable.ProgressBar_max, mMax)); + + setProgress(a.getInt(R.styleable.ProgressBar_progress, mProgress)); + + setSecondaryProgress( + a.getInt(R.styleable.ProgressBar_secondaryProgress, mSecondaryProgress)); + + drawable = a.getDrawable(R.styleable.ProgressBar_indeterminateDrawable); + if (drawable != null) { + drawable = tileifyIndeterminate(drawable); + setIndeterminateDrawable(drawable); + } + + mOnlyIndeterminate = a.getBoolean( + R.styleable.ProgressBar_indeterminateOnly, mOnlyIndeterminate); + + mNoInvalidate = false; + + setIndeterminate(mOnlyIndeterminate || a.getBoolean( + R.styleable.ProgressBar_indeterminate, mIndeterminate)); + + a.recycle(); + } + + /* + * TODO: This is almost ready to be removed. This was used to support our + * old style of progress bars with the ticks. Need to check with designers + * on whether they can give us a transparent 'tick' overlay tile for our new + * gradient-based progress bars. (We still need the ticked progress bar for + * media player apps.) I'll remove this and add XML support if they want to + * do the overlay approach. If they want to just have a separate style for + * this legacy stuff, then we can keep it around. + */ + + // TODO Remove all this once ShapeDrawable + shaders are supported through XML + private Drawable tileify(Drawable drawable) { + if (drawable instanceof LayerDrawable) { + LayerDrawable background = (LayerDrawable) drawable; + final int N = background.getNumberOfLayers(); + Drawable[] outDrawables = new Drawable[N]; + + for (int i = 0; i < N; i++) { + int id = background.getId(i); + outDrawables[i] = createDrawableForTile(background.getDrawable(i), + (id == R.id.progress || id == R.id.secondaryProgress)); + } + + LayerDrawable newBg = new LayerDrawable(outDrawables); + + for (int i = 0; i < N; i++) { + newBg.setId(i, background.getId(i)); + } + + drawable = newBg; + } + return drawable; + } + + // TODO Remove all this once ShapeDrawable + shaders are supported through XML + private Drawable createDrawableForTile(Drawable tileDrawable, boolean clip) { + if (!(tileDrawable instanceof BitmapDrawable)) return tileDrawable; + + final Bitmap tileBitmap = ((BitmapDrawable) tileDrawable).getBitmap(); + if (mSampleTile == null) { + mSampleTile = tileBitmap; + } + + final ShapeDrawable shapeDrawable = new ShapeDrawable(getDrawableShape()); + + final BitmapShader bitmapShader = new BitmapShader(tileBitmap, + Shader.TileMode.REPEAT, Shader.TileMode.CLAMP); + shapeDrawable.getPaint().setShader(bitmapShader); + + return (clip) ? new ClipDrawable(shapeDrawable, Gravity.LEFT, + ClipDrawable.HORIZONTAL) : shapeDrawable; + } + + Shape getDrawableShape() { + final float[] roundedCorners = new float[] { 5, 5, 5, 5, 5, 5, 5, 5 }; + return new RoundRectShape(roundedCorners, null, null); + } + + /** + * Convert a AnimationDrawable for use as a barberpole animation. + * Each frame of the animation is wrapped in a ClipDrawable and + * given a tiling BitmapShader. + */ + private Drawable tileifyIndeterminate(Drawable drawable) { + if (drawable instanceof AnimationDrawable) { + AnimationDrawable background = (AnimationDrawable) drawable; + final int N = background.getNumberOfFrames(); + AnimationDrawable newBg = new AnimationDrawable(); + newBg.setOneShot(background.isOneShot()); + + for (int i = 0; i < N; i++) { + Drawable frame = createDrawableForTile(background.getFrame(i), true); + frame.setLevel(10000); + newBg.addFrame(frame, background.getDuration(i)); + } + newBg.setLevel(10000); + drawable = newBg; + } + return drawable; + } + + /** + * <p> + * Initialize the progress bar's default values: + * </p> + * <ul> + * <li>progress = 0</li> + * <li>max = 100</li> + * <li>animation duration = 4000 ms</li> + * <li>indeterminate = false</li> + * <li>behavior = repeat</li> + * </ul> + */ + private void initProgressBar() { + mMax = 100; + mProgress = 0; + mSecondaryProgress = 0; + mIndeterminate = false; + mOnlyIndeterminate = false; + mDuration = 4000; + mBehavior = AlphaAnimation.RESTART; + mMinWidth = 24; + mMaxWidth = 48; + mMinHeight = 24; + mMaxHeight = 48; + } + + /** + * <p>Indicate whether this progress bar is in indeterminate mode.</p> + * + * @return true if the progress bar is in indeterminate mode + */ + public synchronized boolean isIndeterminate() { + return mIndeterminate; + } + + /** + * <p>Change the indeterminate mode for this progress bar. In indeterminate + * mode, the progress is ignored and the progress bar shows an infinite + * animation instead.</p> + * + * If this progress bar's style only supports indeterminate mode (such as the circular + * progress bars), then this will be ignored. + * + * @param indeterminate true to enable the indeterminate mode + */ + public synchronized void setIndeterminate(boolean indeterminate) { + if ((!mOnlyIndeterminate || !mIndeterminate) && indeterminate != mIndeterminate) { + mIndeterminate = indeterminate; + + if (indeterminate) { + // swap between indeterminate and regular backgrounds + mCurrentDrawable = mIndeterminateDrawable; + startAnimation(); + } else { + mCurrentDrawable = mProgressDrawable; + stopAnimation(); + } + } + } + + /** + * <p>Get the drawable used to draw the progress bar in + * indeterminate mode.</p> + * + * @return a {@link android.graphics.drawable.Drawable} instance + * + * @see #setIndeterminateDrawable(android.graphics.drawable.Drawable) + * @see #setIndeterminate(boolean) + */ + public Drawable getIndeterminateDrawable() { + return mIndeterminateDrawable; + } + + /** + * <p>Define the drawable used to draw the progress bar in + * indeterminate mode.</p> + * + * @param d the new drawable + * + * @see #getIndeterminateDrawable() + * @see #setIndeterminate(boolean) + */ + public void setIndeterminateDrawable(Drawable d) { + if (d != null) { + d.setCallback(this); + } + mIndeterminateDrawable = d; + if (mIndeterminate) { + mCurrentDrawable = d; + postInvalidate(); + } + } + + /** + * <p>Get the drawable used to draw the progress bar in + * progress mode.</p> + * + * @return a {@link android.graphics.drawable.Drawable} instance + * + * @see #setProgressDrawable(android.graphics.drawable.Drawable) + * @see #setIndeterminate(boolean) + */ + public Drawable getProgressDrawable() { + return mProgressDrawable; + } + + /** + * <p>Define the drawable used to draw the progress bar in + * progress mode.</p> + * + * @param d the new drawable + * + * @see #getProgressDrawable() + * @see #setIndeterminate(boolean) + */ + public void setProgressDrawable(Drawable d) { + if (d != null) { + d.setCallback(this); + } + mProgressDrawable = d; + if (!mIndeterminate) { + mCurrentDrawable = d; + postInvalidate(); + } + } + + /** + * @return The drawable currently used to draw the progress bar + */ + Drawable getCurrentDrawable() { + return mCurrentDrawable; + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mProgressDrawable || who == mIndeterminateDrawable + || super.verifyDrawable(who); + } + + @Override + public void postInvalidate() { + if (!mNoInvalidate) { + super.postInvalidate(); + } + } + + private class RefreshProgressRunnable implements Runnable { + + private int mId; + private int mProgress; + private boolean mFromTouch; + + RefreshProgressRunnable(int id, int progress, boolean fromTouch) { + mId = id; + mProgress = progress; + mFromTouch = fromTouch; + } + + public void run() { + doRefreshProgress(mId, mProgress, mFromTouch); + // Put ourselves back in the cache when we are done + mRefreshProgressRunnable = this; + } + + public void setup(int id, int progress, boolean fromTouch) { + mId = id; + mProgress = progress; + mFromTouch = fromTouch; + } + + } + + private synchronized void doRefreshProgress(int id, int progress, boolean fromTouch) { + float scale = mMax > 0 ? (float) progress / (float) mMax : 0; + final Drawable d = mCurrentDrawable; + if (d != null) { + Drawable progressDrawable = null; + + if (d instanceof LayerDrawable) { + progressDrawable = ((LayerDrawable) d).findDrawableByLayerId(id); + } + + final int level = (int) (scale * MAX_LEVEL); + (progressDrawable != null ? progressDrawable : d).setLevel(level); + } else { + invalidate(); + } + + if (id == R.id.progress) { + onProgressRefresh(scale, fromTouch); + } + } + + void onProgressRefresh(float scale, boolean fromTouch) { + } + + private synchronized void refreshProgress(int id, int progress, boolean fromTouch) { + if (mUiThreadId == Thread.currentThread().getId()) { + doRefreshProgress(id, progress, fromTouch); + } else { + RefreshProgressRunnable r; + if (mRefreshProgressRunnable != null) { + // Use cached RefreshProgressRunnable if available + r = mRefreshProgressRunnable; + // Uncache it + mRefreshProgressRunnable = null; + r.setup(id, progress, fromTouch); + } else { + // Make a new one + r = new RefreshProgressRunnable(id, progress, fromTouch); + } + post(r); + } + } + + /** + * <p>Set the current progress to the specified value. Does not do anything + * if the progress bar is in indeterminate mode.</p> + * + * @param progress the new progress, between 0 and {@link #getMax()} + * + * @see #setIndeterminate(boolean) + * @see #isIndeterminate() + * @see #getProgress() + * @see #incrementProgressBy(int) + */ + public synchronized void setProgress(int progress) { + setProgress(progress, false); + } + + synchronized void setProgress(int progress, boolean fromTouch) { + if (mIndeterminate) { + return; + } + + if (progress < 0) { + progress = 0; + } + + if (progress > mMax) { + progress = mMax; + } + + if (progress != mProgress) { + mProgress = progress; + refreshProgress(R.id.progress, mProgress, fromTouch); + } + } + + /** + * <p> + * Set the current secondary progress to the specified value. Does not do + * anything if the progress bar is in indeterminate mode. + * </p> + * + * @param secondaryProgress the new secondary progress, between 0 and {@link #getMax()} + * @see #setIndeterminate(boolean) + * @see #isIndeterminate() + * @see #getSecondaryProgress() + * @see #incrementSecondaryProgressBy(int) + */ + public synchronized void setSecondaryProgress(int secondaryProgress) { + if (mIndeterminate) { + return; + } + + if (secondaryProgress < 0) { + secondaryProgress = 0; + } + + if (secondaryProgress > mMax) { + secondaryProgress = mMax; + } + + if (secondaryProgress != mSecondaryProgress) { + mSecondaryProgress = secondaryProgress; + refreshProgress(R.id.secondaryProgress, mSecondaryProgress, false); + } + } + + /** + * <p>Get the progress bar's current level of progress. Return 0 when the + * progress bar is in indeterminate mode.</p> + * + * @return the current progress, between 0 and {@link #getMax()} + * + * @see #setIndeterminate(boolean) + * @see #isIndeterminate() + * @see #setProgress(int) + * @see #setMax(int) + * @see #getMax() + */ + public synchronized int getProgress() { + return mIndeterminate ? 0 : mProgress; + } + + /** + * <p>Get the progress bar's current level of secondary progress. Return 0 when the + * progress bar is in indeterminate mode.</p> + * + * @return the current secondary progress, between 0 and {@link #getMax()} + * + * @see #setIndeterminate(boolean) + * @see #isIndeterminate() + * @see #setSecondaryProgress(int) + * @see #setMax(int) + * @see #getMax() + */ + public synchronized int getSecondaryProgress() { + return mIndeterminate ? 0 : mSecondaryProgress; + } + + /** + * <p>Return the upper limit of this progress bar's range.</p> + * + * @return a positive integer + * + * @see #setMax(int) + * @see #getProgress() + * @see #getSecondaryProgress() + */ + public synchronized int getMax() { + return mMax; + } + + /** + * <p>Set the range of the progress bar to 0...<tt>max</tt>.</p> + * + * @param max the upper range of this progress bar + * + * @see #getMax() + * @see #setProgress(int) + * @see #setSecondaryProgress(int) + */ + public synchronized void setMax(int max) { + if (max < 0) { + max = 0; + } + if (max != mMax) { + mMax = max; + postInvalidate(); + + if (mProgress > max) { + mProgress = max; + } + } + } + + /** + * <p>Increase the progress bar's progress by the specified amount.</p> + * + * @param diff the amount by which the progress must be increased + * + * @see #setProgress(int) + */ + public synchronized final void incrementProgressBy(int diff) { + setProgress(mProgress + diff); + } + + /** + * <p>Increase the progress bar's secondary progress by the specified amount.</p> + * + * @param diff the amount by which the secondary progress must be increased + * + * @see #setSecondaryProgress(int) + */ + public synchronized final void incrementSecondaryProgressBy(int diff) { + setSecondaryProgress(mSecondaryProgress + diff); + } + + /** + * <p>Start the indeterminate progress animation.</p> + */ + void startAnimation() { + int visibility = getVisibility(); + if (visibility != VISIBLE) { + return; + } + + if (mIndeterminateDrawable instanceof AnimationDrawable) { + mShouldStartAnimationDrawable = true; + mAnimation = null; + } else { + if (mInterpolator == null) { + mInterpolator = new LinearInterpolator(); + } + + mTransformation = new Transformation(); + mAnimation = new AlphaAnimation(0.0f, 1.0f); + mAnimation.setRepeatMode(mBehavior); + mAnimation.setRepeatCount(Animation.INFINITE); + mAnimation.setDuration(mDuration); + mAnimation.setInterpolator(mInterpolator); + mAnimation.setStartTime(Animation.START_ON_FIRST_FRAME); + postInvalidate(); + } + } + + /** + * <p>Stop the indeterminate progress animation.</p> + */ + void stopAnimation() { + mAnimation = null; + mTransformation = null; + if (mIndeterminateDrawable instanceof AnimationDrawable) { + ((AnimationDrawable)mIndeterminateDrawable).stop(); + mShouldStartAnimationDrawable = false; + } + } + + /** + * Sets the acceleration curve for the indeterminate animation. + * The interpolator is loaded as a resource from the specified context. + * + * @param context The application environment + * @param resID The resource identifier of the interpolator to load + */ + public void setInterpolator(Context context, int resID) { + setInterpolator(AnimationUtils.loadInterpolator(context, resID)); + } + + /** + * Sets the acceleration curve for the indeterminate animation. + * Defaults to a linear interpolation. + * + * @param interpolator The interpolator which defines the acceleration curve + */ + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + /** + * Gets the acceleration curve type for the indeterminate animation. + * + * @return the {@link Interpolator} associated to this animation + */ + public Interpolator getInterpolator() { + return mInterpolator; + } + + @Override + public void setVisibility(int v) { + if (getVisibility() != v) { + super.setVisibility(v); + + if (mIndeterminate) { + // let's be nice with the UI thread + if (v == GONE || v == INVISIBLE) { + stopAnimation(); + } else if (v == VISIBLE) { + startAnimation(); + } + } + } + } + + @Override + public void invalidateDrawable(Drawable dr) { + if (!mInDrawing) { + super.invalidateDrawable(dr); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + Drawable d = mCurrentDrawable; + if (d != null) { + // onDraw will translate the canvas so we draw starting at 0,0 + d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, + h - mPaddingBottom - mPaddingTop); + } + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + super.onDraw(canvas); + + Drawable d = mCurrentDrawable; + if (d != null) { + // Translate canvas so a indeterminate circular progress bar with padding + // rotates properly in its animation + canvas.save(); + canvas.translate(mPaddingLeft, mPaddingTop); + long time = getDrawingTime(); + if (mAnimation != null) { + mAnimation.getTransformation(time, mTransformation); + float scale = mTransformation.getAlpha(); + try { + mInDrawing = true; + d.setLevel((int) (scale * MAX_LEVEL)); + } finally { + mInDrawing = false; + } + if (SystemClock.uptimeMillis() - mLastDrawTime >= ANIMATION_RESOLUTION) { + mLastDrawTime = SystemClock.uptimeMillis(); + postInvalidateDelayed(ANIMATION_RESOLUTION); + } + } + d.draw(canvas); + canvas.restore(); + if (mShouldStartAnimationDrawable && mCurrentDrawable instanceof AnimationDrawable) { + ((AnimationDrawable)mCurrentDrawable).start(); + } + } + } + + @Override + protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Drawable d = mCurrentDrawable; + + int dw = 0; + int dh = 0; + if (d != null) { + dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); + dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); + } + dw += mPaddingLeft + mPaddingRight; + dh += mPaddingTop + mPaddingBottom; + + setMeasuredDimension(resolveSize(dw, widthMeasureSpec), + resolveSize(dh, heightMeasureSpec)); + } +} diff --git a/core/java/android/widget/RadioButton.java b/core/java/android/widget/RadioButton.java new file mode 100644 index 0000000..14ec8c6 --- /dev/null +++ b/core/java/android/widget/RadioButton.java @@ -0,0 +1,72 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.util.AttributeSet; + + +/** + * <p> + * A radio button is a two-states button that can be either checked or + * unchecked. When the radio button is unchecked, the user can press or click it + * to check it. However, contrary to a {@link android.widget.CheckBox}, a radio + * button cannot be unchecked by the user once checked. + * </p> + * + * <p> + * Radio buttons are normally used together in a + * {@link android.widget.RadioGroup}. When several radio buttons live inside + * a radio group, checking one radio button unchecks all the others.</p> + * </p> + * + * <p><strong>XML attributes</strong></p> + * <p> + * See {@link android.R.styleable#CompoundButton CompoundButton Attributes}, + * {@link android.R.styleable#Button Button Attributes}, + * {@link android.R.styleable#TextView TextView Attributes}, + * {@link android.R.styleable#View View Attributes} + * </p> + */ +public class RadioButton extends CompoundButton { + + public RadioButton(Context context) { + this(context, null); + } + + public RadioButton(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.radioButtonStyle); + } + + public RadioButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * {@inheritDoc} + * <p> + * If the radio button is already checked, this method will not toggle the radio button. + */ + @Override + public void toggle() { + // we override to prevent toggle when the radio is already + // checked (as opposed to check boxes widgets) + if (!isChecked()) { + super.toggle(); + } + } +} diff --git a/core/java/android/widget/RadioGroup.java b/core/java/android/widget/RadioGroup.java new file mode 100644 index 0000000..ed8df22 --- /dev/null +++ b/core/java/android/widget/RadioGroup.java @@ -0,0 +1,371 @@ +/* + * 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 android.widget; + +import com.android.internal.R; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + + +/** + * <p>This class is used to create a multiple-exclusion scope for a set of radio + * buttons. Checking one radio button that belongs to a radio group unchecks + * any previously checked radio button within the same group.</p> + * + * <p>Intially, all of the radio buttons are unchecked. While it is not possible + * to uncheck a particular radio button, the radio group can be cleared to + * remove the checked state.</p> + * + * <p>The selection is identified by the unique id of the radio button as defined + * in the XML layout file.</p> + * + * <p><strong>XML Attributes</strong></p> + * <p>See {@link android.R.styleable#RadioGroup RadioGroup Attributes}, + * {@link android.R.styleable#LinearLayout LinearLayout Attributes}, + * {@link android.R.styleable#ViewGroup ViewGroup Attributes}, + * {@link android.R.styleable#View View Attributes}</p> + * <p>Also see + * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams} + * for layout attributes.</p> + * + * @see RadioButton + * + */ +public class RadioGroup extends LinearLayout { + // holds the checked id; the selection is empty by default + private int mCheckedId = -1; + // tracks children radio buttons checked state + private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener; + // when true, mOnCheckedChangeListener discards events + private boolean mProtectFromCheckedChange = false; + private OnCheckedChangeListener mOnCheckedChangeListener; + private PassThroughHierarchyChangeListener mPassThroughListener; + + /** + * {@inheritDoc} + */ + public RadioGroup(Context context) { + super(context); + setOrientation(VERTICAL); + init(); + } + + /** + * {@inheritDoc} + */ + public RadioGroup(Context context, AttributeSet attrs) { + super(context, attrs); + + // retrieve selected radio button as requested by the user in the + // XML layout file + TypedArray attributes = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0); + + int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID); + if (value != View.NO_ID) { + mCheckedId = value; + } + + final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL); + setOrientation(index); + + attributes.recycle(); + init(); + } + + private void init() { + mChildOnCheckedChangeListener = new CheckedStateTracker(); + mPassThroughListener = new PassThroughHierarchyChangeListener(); + super.setOnHierarchyChangeListener(mPassThroughListener); + } + + /** + * {@inheritDoc} + */ + @Override + public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) { + // the user listener is delegated to our pass-through listener + mPassThroughListener.mOnHierarchyChangeListener = listener; + } + + /** + * {@inheritDoc} + */ + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + // checks the appropriate radio button as requested in the XML file + if (mCheckedId != -1) { + mProtectFromCheckedChange = true; + setCheckedStateForView(mCheckedId, true); + mProtectFromCheckedChange = false; + setCheckedId(mCheckedId); + } + } + + /** + * <p>Sets the selection to the radio button whose identifier is passed in + * parameter. Using -1 as the selection identifier clears the selection; + * such an operation is equivalent to invoking {@link #clearCheck()}.</p> + * + * @param id the unique id of the radio button to select in this group + * + * @see #getCheckedRadioButtonId() + * @see #clearCheck() + */ + public void check(int id) { + // don't even bother + if (id != -1 && (id == mCheckedId)) { + return; + } + + if (mCheckedId != -1) { + setCheckedStateForView(mCheckedId, false); + } + + if (id != -1) { + setCheckedStateForView(id, true); + } + + setCheckedId(id); + } + + private void setCheckedId(int id) { + mCheckedId = id; + if (mOnCheckedChangeListener != null) { + mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId); + } + } + + private void setCheckedStateForView(int viewId, boolean checked) { + View checkedView = findViewById(viewId); + if (checkedView != null && checkedView instanceof RadioButton) { + ((RadioButton) checkedView).setChecked(checked); + } + } + + /** + * <p>Returns the identifier of the selected radio button in this group. + * Upon empty selection, the returned value is -1.</p> + * + * @return the unique id of the selected radio button in this group + * + * @see #check(int) + * @see #clearCheck() + */ + public int getCheckedRadioButtonId() { + return mCheckedId; + } + + /** + * <p>Clears the selection. When the selection is cleared, no radio button + * in this group is selected and {@link #getCheckedRadioButtonId()} returns + * null.</p> + * + * @see #check(int) + * @see #getCheckedRadioButtonId() + */ + public void clearCheck() { + check(-1); + } + + /** + * <p>Register a callback to be invoked when the checked radio button + * changes in this group.</p> + * + * @param listener the callback to call on checked state change + */ + public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { + mOnCheckedChangeListener = listener; + } + + /** + * {@inheritDoc} + */ + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new RadioGroup.LayoutParams(getContext(), attrs); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof RadioGroup.LayoutParams; + } + + @Override + protected LinearLayout.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + /** + * <p>This set of layout parameters defaults the width and the height of + * the children to {@link #WRAP_CONTENT} when they are not specified in the + * XML file. Otherwise, this class ussed the value read from the XML file.</p> + * + * <p>See + * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes} + * for a list of all child view attributes that this class supports.</p> + * + */ + public static class LayoutParams extends LinearLayout.LayoutParams { + /** + * {@inheritDoc} + */ + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(int w, int h) { + super(w, h); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(int w, int h, float initWeight) { + super(w, h, initWeight); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams p) { + super(p); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + /** + * <p>Fixes the child's width to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's + * height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * when not specified in the XML file.</p> + * + * @param a the styled attributes set + * @param widthAttr the width attribute to fetch + * @param heightAttr the height attribute to fetch + */ + @Override + protected void setBaseAttributes(TypedArray a, + int widthAttr, int heightAttr) { + + if (a.hasValue(widthAttr)) { + width = a.getLayoutDimension(widthAttr, "layout_width"); + } else { + width = WRAP_CONTENT; + } + + if (a.hasValue(heightAttr)) { + height = a.getLayoutDimension(heightAttr, "layout_height"); + } else { + height = WRAP_CONTENT; + } + } + } + + /** + * <p>Interface definition for a callback to be invoked when the checked + * radio button changed in this group.</p> + */ + public interface OnCheckedChangeListener { + /** + * <p>Called when the checked radio button has changed. When the + * selection is cleared, checkedId is -1.</p> + * + * @param group the group in which the checked radio button has changed + * @param checkedId the unique identifier of the newly checked radio button + */ + public void onCheckedChanged(RadioGroup group, int checkedId); + } + + private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + // prevents from infinite recursion + if (mProtectFromCheckedChange) { + return; + } + + mProtectFromCheckedChange = true; + if (mCheckedId != -1) { + setCheckedStateForView(mCheckedId, false); + } + mProtectFromCheckedChange = false; + + int id = buttonView.getId(); + setCheckedId(id); + } + } + + /** + * <p>A pass-through listener acts upon the events and dispatches them + * to another listener. This allows the table layout to set its own internal + * hierarchy change listener without preventing the user to setup his.</p> + */ + private class PassThroughHierarchyChangeListener implements + ViewGroup.OnHierarchyChangeListener { + private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener; + + /** + * {@inheritDoc} + */ + public void onChildViewAdded(View parent, View child) { + if (parent == RadioGroup.this && child instanceof RadioButton) { + int id = child.getId(); + // generates an id if it's missing + if (id == View.NO_ID) { + id = child.hashCode(); + child.setId(id); + } + ((RadioButton) child).setOnCheckedChangeWidgetListener( + mChildOnCheckedChangeListener); + } + + if (mOnHierarchyChangeListener != null) { + mOnHierarchyChangeListener.onChildViewAdded(parent, child); + } + } + + /** + * {@inheritDoc} + */ + public void onChildViewRemoved(View parent, View child) { + if (parent == RadioGroup.this && child instanceof RadioButton) { + ((RadioButton) child).setOnCheckedChangeWidgetListener(null); + } + + if (mOnHierarchyChangeListener != null) { + mOnHierarchyChangeListener.onChildViewRemoved(parent, child); + } + } + } +} diff --git a/core/java/android/widget/RatingBar.java b/core/java/android/widget/RatingBar.java new file mode 100644 index 0000000..845b542 --- /dev/null +++ b/core/java/android/widget/RatingBar.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.shapes.RectShape; +import android.graphics.drawable.shapes.Shape; +import android.util.AttributeSet; + +import com.android.internal.R; + +/** + * A RatingBar is an extension of SeekBar and ProgressBar that shows a rating in + * stars. The user can touch and/or drag to set the rating when using the + * default size RatingBar. The smaller RatingBar style ({@link android.R.attr#ratingBarStyleSmall}) + * and the larger indicator-only style ({@link android.R.attr#ratingBarStyleIndicator}) + * do not support user interaction and should only be used as indicators. + * <p> + * The number of stars set (via {@link #setNumStars(int)} or in an XML layout) + * will be shown when the layout width is set to wrap content (if another layout + * width is set, the results may be unpredictable). + * <p> + * The secondary progress should not be modified by the client as it is used + * internally as the background for a fractionally filled star. + * + * @attr ref android.R.styleable#RatingBar_numStars + * @attr ref android.R.styleable#RatingBar_rating + * @attr ref android.R.styleable#RatingBar_stepSize + * @attr ref android.R.styleable#RatingBar_isIndicator + */ +public class RatingBar extends AbsSeekBar { + + /** + * A callback that notifies clients when the rating has been changed. This + * includes changes that were initiated by the user through a touch gesture as well + * as changes that were initiated programmatically. + */ + public interface OnRatingBarChangeListener { + + /** + * Notification that the rating has changed. Clients can use the + * fromTouch parameter to distinguish user-initiated changes from those + * that occurred programmatically. This will not be called continuously + * while the user is dragging, only when the user finalizes a rating by + * lifting the touch. + * + * @param ratingBar The RatingBar whose rating has changed. + * @param rating The current rating. This will be in the range + * 0..numStars. + * @param fromTouch True if the rating change was initiated by a user's + * touch gesture. + */ + void onRatingChanged(RatingBar ratingBar, float rating, boolean fromTouch); + + } + + private int mNumStars = 5; + + private int mProgressOnStartTracking; + + private OnRatingBarChangeListener mOnRatingBarChangeListener; + + public RatingBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RatingBar, + defStyle, 0); + final int numStars = a.getInt(R.styleable.RatingBar_numStars, mNumStars); + setIsIndicator(a.getBoolean(R.styleable.RatingBar_isIndicator, !mIsUserSeekable)); + final float rating = a.getFloat(R.styleable.RatingBar_rating, -1); + final float stepSize = a.getFloat(R.styleable.RatingBar_stepSize, -1); + a.recycle(); + + if (numStars > 0 && numStars != mNumStars) { + setNumStars(numStars); + } + + if (stepSize >= 0) { + setStepSize(stepSize); + } else { + setStepSize(0.5f); + } + + if (rating >= 0) { + setRating(rating); + } + + // A touch inside a star fill up to that fractional area (slightly more + // than 1 so boundaries round up). + mTouchProgressOffset = 1.1f; + } + + public RatingBar(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.ratingBarStyle); + } + + public RatingBar(Context context) { + this(context, null); + } + + /** + * Sets the listener to be called when the rating changes. + * + * @param listener The listener. + */ + public void setOnRatingBarChangeListener(OnRatingBarChangeListener listener) { + mOnRatingBarChangeListener = listener; + } + + /** + * @return The listener (may be null) that is listening for rating change + * events. + */ + public OnRatingBarChangeListener getOnRatingBarChangeListener() { + return mOnRatingBarChangeListener; + } + + /** + * Whether this rating bar should only be an indicator (thus non-changeable + * by the user). + * + * @param isIndicator Whether it should be an indicator. + */ + public void setIsIndicator(boolean isIndicator) { + mIsUserSeekable = !isIndicator; + } + + /** + * @return Whether this rating bar is only an indicator. + */ + public boolean isIndicator() { + return !mIsUserSeekable; + } + + /** + * Sets the number of stars to show. In order for these to be shown + * properly, it is recommended the layout width of this widget be wrap + * content. + * + * @param numStars The number of stars. + */ + public void setNumStars(final int numStars) { + if (numStars <= 0) { + return; + } + + mNumStars = numStars; + + // This causes the width to change, so re-layout + requestLayout(); + } + + /** + * Returns the number of stars shown. + * @return The number of stars shown. + */ + public int getNumStars() { + return mNumStars; + } + + /** + * Sets the rating (the number of stars filled). + * + * @param rating The rating to set. + */ + public void setRating(float rating) { + setProgress((int) (rating * getProgressPerStar())); + } + + /** + * Gets the current rating (number of stars filled). + * + * @return The current rating. + */ + public float getRating() { + return getProgress() / getProgressPerStar(); + } + + /** + * Sets the step size (granularity) of this rating bar. + * + * @param stepSize The step size of this rating bar. For example, if + * half-star granularity is wanted, this would be 0.5. + */ + public void setStepSize(float stepSize) { + if (stepSize <= 0) { + return; + } + + final float newMax = mNumStars / stepSize; + final int newProgress = (int) (newMax / getMax() * getProgress()); + setMax((int) newMax); + setProgress(newProgress); + } + + /** + * Gets the step size of this rating bar. + * + * @return The step size. + */ + public float getStepSize() { + return (float) getNumStars() / getMax(); + } + + /** + * @return The amount of progress that fits into a star + */ + private float getProgressPerStar() { + if (mNumStars > 0) { + return 1f * getMax() / mNumStars; + } else { + return 1; + } + } + + @Override + Shape getDrawableShape() { + // TODO: Once ProgressBar's TODOs are fixed, this won't be needed + return new RectShape(); + } + + @Override + void onProgressRefresh(float scale, boolean fromTouch) { + super.onProgressRefresh(scale, fromTouch); + + // Keep secondary progress in sync with primary + updateSecondaryProgress(getProgress()); + + if (!fromTouch) { + // Callback for non-touch rating changes + dispatchRatingChange(false); + } + } + + /** + * The secondary progress is used to differentiate the background of a + * partially filled star. This method keeps the secondary progress in sync + * with the progress. + * + * @param progress The primary progress level. + */ + private void updateSecondaryProgress(int progress) { + final float ratio = getProgressPerStar(); + if (ratio > 0) { + final float progressInStars = progress / ratio; + final int secondaryProgress = (int) (Math.ceil(progressInStars) * ratio); + setSecondaryProgress(secondaryProgress); + } + } + + @Override + protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (mSampleTile != null) { + // TODO: Once ProgressBar's TODOs are gone, this can be done more + // cleanly than mSampleTile + final int width = mSampleTile.getWidth() * mNumStars; + setMeasuredDimension(resolveSize(width, widthMeasureSpec), mMeasuredHeight); + } + } + + @Override + void onStartTrackingTouch() { + mProgressOnStartTracking = getProgress(); + + super.onStartTrackingTouch(); + } + + @Override + void onStopTrackingTouch() { + super.onStopTrackingTouch(); + + if (getProgress() != mProgressOnStartTracking) { + dispatchRatingChange(true); + } + } + + void dispatchRatingChange(boolean fromTouch) { + if (mOnRatingBarChangeListener != null) { + mOnRatingBarChangeListener.onRatingChanged(this, getRating(), + fromTouch); + } + } + + @Override + public synchronized void setMax(int max) { + // Disallow max progress = 0 + if (max <= 0) { + return; + } + + super.setMax(max); + } + +} diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java new file mode 100644 index 0000000..91d5805 --- /dev/null +++ b/core/java/android/widget/RelativeLayout.java @@ -0,0 +1,950 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.Gravity; +import android.widget.RemoteViews.RemoteView; +import android.graphics.Rect; +import com.android.internal.R; + + +/** + * A Layout where the positions of the children can be described in relation to each other or to the + * parent. For the sake of efficiency, the relations between views are evaluated in one pass, so if + * view Y is dependent on the position of view X, make sure the view X comes first in the layout. + * + * <p> + * Note that you cannot have a circular dependency between the size of the RelativeLayout and the + * position of its children. For example, you cannot have a RelativeLayout whose height is set to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT WRAP_CONTENT} and a child set to + * {@link #ALIGN_PARENT_BOTTOM}. + * </p> + * + * <p> + * Also see {@link android.widget.RelativeLayout.LayoutParams RelativeLayout.LayoutParams} for + * layout attributes + * </p> + * + * @attr ref android.R.styleable#RelativeLayout_gravity + * @attr ref android.R.styleable#RelativeLayout_ignoreGravity + */ +@RemoteView +public class RelativeLayout extends ViewGroup { + public static final int TRUE = -1; + + /** + * Rule that aligns a child's right edge with another child's left edge. + */ + public static final int LEFT_OF = 0; + /** + * Rule that aligns a child's left edge with another child's right edge. + */ + public static final int RIGHT_OF = 1; + /** + * Rule that aligns a child's bottom edge with another child's top edge. + */ + public static final int ABOVE = 2; + /** + * Rule that aligns a child's top edge with another child's bottom edge. + */ + public static final int BELOW = 3; + + /** + * Rule that aligns a child's baseline with another child's baseline. + */ + public static final int ALIGN_BASELINE = 4; + /** + * Rule that aligns a child's left edge with another child's left edge. + */ + public static final int ALIGN_LEFT = 5; + /** + * Rule that aligns a child's top edge with another child's top edge. + */ + public static final int ALIGN_TOP = 6; + /** + * Rule that aligns a child's right edge with another child's right edge. + */ + public static final int ALIGN_RIGHT = 7; + /** + * Rule that aligns a child's bottom edge with another child's bottom edge. + */ + public static final int ALIGN_BOTTOM = 8; + + /** + * Rule that aligns the child's left edge with its RelativeLayout + * parent's left edge. + */ + public static final int ALIGN_PARENT_LEFT = 9; + /** + * Rule that aligns the child's top edge with its RelativeLayout + * parent's top edge. + */ + public static final int ALIGN_PARENT_TOP = 10; + /** + * Rule that aligns the child's right edge with its RelativeLayout + * parent's right edge. + */ + public static final int ALIGN_PARENT_RIGHT = 11; + /** + * Rule that aligns the child's bottom edge with its RelativeLayout + * parent's bottom edge. + */ + public static final int ALIGN_PARENT_BOTTOM = 12; + + /** + * Rule that centers the child with respect to the bounds of its + * RelativeLayout parent. + */ + public static final int CENTER_IN_PARENT = 13; + /** + * Rule that centers the child horizontally with respect to the + * bounds of its RelativeLayout parent. + */ + public static final int CENTER_HORIZONTAL = 14; + /** + * Rule that centers the child vertically with respect to the + * bounds of its RelativeLayout parent. + */ + public static final int CENTER_VERTICAL = 15; + + private static final int VERB_COUNT = 16; + + private View mBaselineView = null; + private boolean mHasBaselineAlignedChild; + + private int mGravity = Gravity.LEFT | Gravity.TOP; + private final Rect mContentBounds = new Rect(); + private final Rect mSelfBounds = new Rect(); + private int mIgnoreGravity; + + public RelativeLayout(Context context) { + super(context); + } + + public RelativeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + initFromAttributes(context, attrs); + } + + public RelativeLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initFromAttributes(context, attrs); + } + + private void initFromAttributes(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RelativeLayout); + mIgnoreGravity = a.getResourceId(R.styleable.RelativeLayout_ignoreGravity, 0); + mGravity = a.getInt(R.styleable.RelativeLayout_gravity, mGravity); + a.recycle(); + } + + /** + * Defines which View is ignored when the gravity is applied. This setting has no + * effect if the gravity is <code>Gravity.LEFT | Gravity.TOP</code>. + * + * @param viewId The id of the View to be ignored by gravity, or 0 if no View + * should be ignored. + * + * @see #setGravity(int) + * + * @attr ref android.R.styleable#RelativeLayout_ignoreGravity + */ + public void setIgnoreGravity(int viewId) { + mIgnoreGravity = viewId; + } + + /** + * Describes how the child views are positioned. Defaults to + * <code>Gravity.LEFT | Gravity.TOP</code>. + * + * @param gravity See {@link android.view.Gravity} + * + * @see #setHorizontalGravity(int) + * @see #setVerticalGravity(int) + * + * @attr ref android.R.styleable#RelativeLayout_gravity + */ + public void setGravity(int gravity) { + if (mGravity != gravity) { + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.LEFT; + } + + if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.TOP; + } + + mGravity = gravity; + requestLayout(); + } + } + + public void setHorizontalGravity(int horizontalGravity) { + final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { + mGravity = (mGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK) | gravity; + requestLayout(); + } + } + + public void setVerticalGravity(int verticalGravity) { + final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK; + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) { + mGravity = (mGravity & ~Gravity.VERTICAL_GRAVITY_MASK) | gravity; + requestLayout(); + } + } + + @Override + public int getBaseline() { + return mBaselineView != null ? mBaselineView.getBaseline() : super.getBaseline(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int myWidth = -1; + int myHeight = -1; + + int width = 0; + int height = 0; + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + // Record our dimensions if they are known; + if (widthMode != MeasureSpec.UNSPECIFIED) { + myWidth = widthSize; + } + + if (heightMode != MeasureSpec.UNSPECIFIED) { + myHeight = heightSize; + } + + if (widthMode == MeasureSpec.EXACTLY) { + width = myWidth; + } + + if (heightMode == MeasureSpec.EXACTLY) { + height = myHeight; + } + + int len = this.getChildCount(); + mHasBaselineAlignedChild = false; + + View ignore = null; + int gravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final boolean horizontalGravity = gravity != Gravity.LEFT && gravity != 0; + gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0; + + int left = Integer.MAX_VALUE; + int top = Integer.MAX_VALUE; + int right = Integer.MIN_VALUE; + int bottom = Integer.MIN_VALUE; + + if ((horizontalGravity || verticalGravity) && mIgnoreGravity != 0) { + ignore = findViewById(mIgnoreGravity); + } + + for (int i = 0; i < len; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + LayoutParams params = (LayoutParams) child.getLayoutParams(); + applySizeRules(params, myWidth, myHeight); + measureChild(child, params, myWidth, myHeight); + positionChild(child, params, myWidth, myHeight); + + if (widthMode != MeasureSpec.EXACTLY) { + width = Math.max(width, params.mRight); + } + if (heightMode != MeasureSpec.EXACTLY) { + height = Math.max(height, params.mBottom); + } + + if (child != ignore || verticalGravity) { + left = Math.min(left, params.mLeft - params.leftMargin); + top = Math.min(top, params.mTop - params.topMargin); + } + + if (child != ignore || horizontalGravity) { + right = Math.max(right, params.mRight + params.rightMargin); + bottom = Math.max(bottom, params.mBottom + params.bottomMargin); + } + } + } + + if (mHasBaselineAlignedChild) { + for (int i = 0; i < len; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + LayoutParams params = (LayoutParams) child.getLayoutParams(); + alignBaseline(child, params); + + if (child != ignore || verticalGravity) { + left = Math.min(left, params.mLeft - params.leftMargin); + top = Math.min(top, params.mTop - params.topMargin); + } + + if (child != ignore || horizontalGravity) { + right = Math.max(right, params.mRight + params.rightMargin); + bottom = Math.max(bottom, params.mBottom + params.bottomMargin); + } + } + } + } + + if (widthMode != MeasureSpec.EXACTLY) { + // Width already has left padding in it since it was calculated by looking at + // the right of each child view + width += mPaddingRight; + + if (mLayoutParams.width >= 0) { + width = Math.max(width, mLayoutParams.width); + } + + width = Math.max(width, getSuggestedMinimumWidth()); + width = resolveSize(width, widthMeasureSpec); + } + if (heightMode != MeasureSpec.EXACTLY) { + // Height already has top padding in it since it was calculated by looking at + // the bottom of each child view + height += mPaddingBottom; + + if (mLayoutParams.height >= 0) { + height = Math.max(height, mLayoutParams.height); + } + + height = Math.max(height, getSuggestedMinimumHeight()); + height = resolveSize(height, heightMeasureSpec); + } + + if (horizontalGravity || verticalGravity) { + final Rect selfBounds = mSelfBounds; + selfBounds.set(mPaddingLeft, mPaddingTop, width - mPaddingRight, + height - mPaddingBottom); + + final Rect contentBounds = mContentBounds; + Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds); + + final int horizontalOffset = contentBounds.left - left; + final int verticalOffset = contentBounds.top - top; + if (horizontalOffset != 0 || verticalOffset != 0) { + for (int i = 0; i < len; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE && child != ignore) { + LayoutParams params = (LayoutParams) child.getLayoutParams(); + params.mLeft += horizontalOffset; + params.mRight += horizontalOffset; + params.mTop += verticalOffset; + params.mBottom += verticalOffset; + } + } + } + } + + setMeasuredDimension(width, height); + } + + private void alignBaseline(View child, LayoutParams params) { + int[] rules = params.getRules(); + int anchorBaseline = getRelatedViewBaseline(rules, ALIGN_BASELINE); + + if (anchorBaseline != -1) { + LayoutParams anchorParams = getRelatedViewParams(rules, ALIGN_BASELINE); + if (anchorParams != null) { + int offset = anchorParams.mTop + anchorBaseline; + int baseline = child.getBaseline(); + if (baseline != -1) { + offset -= baseline; + } + int height = params.mBottom - params.mTop; + params.mTop = offset; + params.mBottom = params.mTop + height; + } + } + + if (mBaselineView == null) { + mBaselineView = child; + } else { + LayoutParams lp = (LayoutParams) mBaselineView.getLayoutParams(); + if (params.mTop < lp.mTop || (params.mTop == lp.mTop && params.mLeft < lp.mLeft)) { + mBaselineView = child; + } + } + } + + /** + * Measure a child. The child should have left, top, right and bottom information + * stored in its LayoutParams. If any of these values is -1 it means that the view + * can extend up to the corresponding edge. + * + * @param child Child to measure + * @param params LayoutParams associated with child + * @param myWidth Width of the the RelativeLayout + * @param myHeight Height of the RelativeLayout + */ + private void measureChild(View child, LayoutParams params, int myWidth, + int myHeight) { + + int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, + params.mRight, params.width, + params.leftMargin, params.rightMargin, + mPaddingLeft, mPaddingRight, + myWidth); + int childHeightMeasureSpec = getChildMeasureSpec(params.mTop, + params.mBottom, params.height, + params.topMargin, params.bottomMargin, + mPaddingTop, mPaddingBottom, + myHeight); + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + /** + * Get a measure spec that accounts for all of the constraints on this view. + * This includes size contstraints imposed by the RelativeLayout as well as + * the View's desired dimension. + * + * @param childStart The left or top field of the child's layout params + * @param childEnd The right or bottom field of the child's layout params + * @param childSize The child's desired size (the width or height field of + * the child's layout params) + * @param startMargin The left or top margin + * @param endMargin The right or bottom margin + * @param startPadding mPaddingLeft or mPaddingTop + * @param endPadding mPaddingRight or mPaddingBottom + * @param mySize The width or height of this view (the RelativeLayout) + * @return MeasureSpec for the child + */ + private int getChildMeasureSpec(int childStart, int childEnd, + int childSize, int startMargin, int endMargin, int startPadding, + int endPadding, int mySize) { + int childSpecMode = 0; + int childSpecSize = 0; + + // Figure out start and end bounds. + int tempStart = childStart; + int tempEnd = childEnd; + + // If the view did not express a layout constraint for an edge, use + // view's margins and our padding + if (tempStart < 0) { + tempStart = startPadding + startMargin; + } + if (tempEnd < 0) { + tempEnd = mySize - endPadding - endMargin; + } + + // Figure out maximum size available to this view + int maxAvailable = tempEnd - tempStart; + + if (childStart >= 0 && childEnd >= 0) { + // Constraints fixed both edges, so child must be an exact size + childSpecMode = MeasureSpec.EXACTLY; + childSpecSize = maxAvailable; + } else { + if (childSize >= 0) { + // Child wanted an exact size. Give as much as possible + childSpecMode = MeasureSpec.EXACTLY; + + if (maxAvailable >= 0) { + // We have a maxmum size in this dimension. + childSpecSize = Math.min(maxAvailable, childSize); + } else { + // We can grow in this dimension. + childSpecSize = childSize; + } + } else if (childSize == LayoutParams.FILL_PARENT) { + // Child wanted to be as big as possible. Give all availble + // space + childSpecMode = MeasureSpec.EXACTLY; + childSpecSize = maxAvailable; + } else if (childSize == LayoutParams.WRAP_CONTENT) { + // Child wants to wrap content. Use AT_MOST + // to communicate available space if we know + // our max size + if (maxAvailable >= 0) { + // We have a maxmum size in this dimension. + childSpecMode = MeasureSpec.AT_MOST; + childSpecSize = maxAvailable; + } else { + // We can grow in this dimension. Child can be as big as it + // wants + childSpecMode = MeasureSpec.UNSPECIFIED; + childSpecSize = 0; + } + } + } + + return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode); + } + + /** + * After the child has been measured, assign it a position. Some views may + * already have final values for l,t,r,b. Others may have one or both edges + * unfixed (i.e. set to -1) in each dimension. These will get positioned + * based on which edge is fixed, the view's desired dimension, and whether + * or not it is centered. + * + * @param child Child to position + * @param params LayoutParams associated with child + * @param myWidth Width of the the RelativeLayout + * @param myHeight Height of the RelativeLayout + */ + private void positionChild(View child, LayoutParams params, int myWidth, int myHeight) { + int[] rules = params.getRules(); + + if (params.mLeft < 0 && params.mRight >= 0) { + // Right is fixed, but left varies + params.mLeft = params.mRight - child.getMeasuredWidth(); + } else if (params.mLeft >= 0 && params.mRight < 0) { + // Left is fixed, but right varies + params.mRight = params.mLeft + child.getMeasuredWidth(); + } else if (params.mLeft < 0 && params.mRight < 0) { + // Both left and right vary + if (0 != rules[CENTER_IN_PARENT] || 0 != rules[CENTER_HORIZONTAL]) { + centerHorizontal(child, params, myWidth); + } else { + params.mLeft = mPaddingLeft + params.leftMargin; + params.mRight = params.mLeft + child.getMeasuredWidth(); + } + } + + if (params.mTop < 0 && params.mBottom >= 0) { + // Bottom is fixed, but top varies + params.mTop = params.mBottom - child.getMeasuredHeight(); + } else if (params.mTop >= 0 && params.mBottom < 0) { + // Top is fixed, but bottom varies + params.mBottom = params.mTop + child.getMeasuredHeight(); + } else if (params.mTop < 0 && params.mBottom < 0) { + // Both top and bottom vary + if (0 != rules[CENTER_IN_PARENT] || 0 != rules[CENTER_VERTICAL]) { + centerVertical(child, params, myHeight); + } else { + params.mTop = mPaddingTop + params.topMargin; + params.mBottom = params.mTop + child.getMeasuredHeight(); + } + } + } + + /** + * Set l,t,r,b values in the LayoutParams for one view based on its layout rules. + * Big assumption #1: All antecedents of this view have been sized & positioned + * Big assumption #2: The dimensions of the parent view (the RelativeLayout) + * are already known if they are needed. + * + * @param childParams LayoutParams for the view being positioned + * @param myWidth Width of the the RelativeLayout + * @param myHeight Height of the RelativeLayout + */ + private void applySizeRules(LayoutParams childParams, int myWidth, int myHeight) { + int[] rules = childParams.getRules(); + RelativeLayout.LayoutParams anchorParams; + + // -1 indicated a "soft requirement" in that direction. For example: + // left=10, right=-1 means the view must start at 10, but can go as far as it wants to the right + // left =-1, right=10 means the view must end at 10, but can go as far as it wants to the left + // left=10, right=20 means the left and right ends are both fixed + childParams.mLeft = -1; + childParams.mRight = -1; + + anchorParams = getRelatedViewParams(rules, LEFT_OF); + if (anchorParams != null) { + childParams.mRight = anchorParams.mLeft - (anchorParams.leftMargin + + childParams.rightMargin); + } else if (childParams.alignWithParent && rules[LEFT_OF] != 0) { + if (myWidth >= 0) { + childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin; + } else { + // FIXME uh oh... + } + } + + anchorParams = getRelatedViewParams(rules, RIGHT_OF); + if (anchorParams != null) { + childParams.mLeft = anchorParams.mRight + (anchorParams.rightMargin + + childParams.leftMargin); + } else if (childParams.alignWithParent && rules[RIGHT_OF] != 0) { + childParams.mLeft = mPaddingLeft + childParams.leftMargin; + } + + anchorParams = getRelatedViewParams(rules, ALIGN_LEFT); + if (anchorParams != null) { + childParams.mLeft = anchorParams.mLeft + childParams.leftMargin; + } else if (childParams.alignWithParent && rules[ALIGN_LEFT] != 0) { + childParams.mLeft = mPaddingLeft + childParams.leftMargin; + } + + anchorParams = getRelatedViewParams(rules, ALIGN_RIGHT); + if (anchorParams != null) { + childParams.mRight = anchorParams.mRight - childParams.rightMargin; + } else if (childParams.alignWithParent && rules[ALIGN_RIGHT] != 0) { + if (myWidth >= 0) { + childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin; + } else { + // FIXME uh oh... + } + } + + if (0 != rules[ALIGN_PARENT_LEFT]) { + childParams.mLeft = mPaddingLeft + childParams.leftMargin; + } + + if (0 != rules[ALIGN_PARENT_RIGHT]) { + if (myWidth >= 0) { + childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin; + } else { + // FIXME uh oh... + } + } + + childParams.mTop = -1; + childParams.mBottom = -1; + + anchorParams = getRelatedViewParams(rules, ABOVE); + if (anchorParams != null) { + childParams.mBottom = anchorParams.mTop - (anchorParams.topMargin + + childParams.bottomMargin); + } else if (childParams.alignWithParent && rules[ABOVE] != 0) { + if (myHeight >= 0) { + childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin; + } else { + // FIXME uh oh... + } + } + + anchorParams = getRelatedViewParams(rules, BELOW); + if (anchorParams != null) { + childParams.mTop = anchorParams.mBottom + (anchorParams.bottomMargin + + childParams.topMargin); + } else if (childParams.alignWithParent && rules[BELOW] != 0) { + childParams.mTop = mPaddingTop + childParams.topMargin; + } + + anchorParams = getRelatedViewParams(rules, ALIGN_TOP); + if (anchorParams != null) { + childParams.mTop = anchorParams.mTop + childParams.topMargin; + } else if (childParams.alignWithParent && rules[ALIGN_TOP] != 0) { + childParams.mTop = mPaddingTop + childParams.topMargin; + } + + anchorParams = getRelatedViewParams(rules, ALIGN_BOTTOM); + if (anchorParams != null) { + childParams.mBottom = anchorParams.mBottom - childParams.bottomMargin; + } else if (childParams.alignWithParent && rules[ALIGN_BOTTOM] != 0) { + if (myHeight >= 0) { + childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin; + } else { + // FIXME uh oh... + } + } + + if (0 != rules[ALIGN_PARENT_TOP]) { + childParams.mTop = mPaddingTop + childParams.topMargin; + } + + if (0 != rules[ALIGN_PARENT_BOTTOM]) { + if (myHeight >= 0) { + childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin; + } else { + // FIXME uh oh... + } + } + + if (rules[ALIGN_BASELINE] != 0) { + mHasBaselineAlignedChild = true; + } + } + + private View getRelatedView(int[] rules, int relation) { + int id = rules[relation]; + if (id != 0) { + View v = findViewById(id); + if (v == null) { + return null; + } + + // Find the first non-GONE view up the chain + while (v.getVisibility() == View.GONE) { + rules = ((LayoutParams) v.getLayoutParams()).getRules(); + v = v.findViewById(rules[relation]); + if (v == null) { + return null; + } + } + + return v; + } + + return null; + } + + private LayoutParams getRelatedViewParams(int[] rules, int relation) { + View v = getRelatedView(rules, relation); + if (v != null) { + ViewGroup.LayoutParams params = v.getLayoutParams(); + if (params instanceof LayoutParams) { + return (LayoutParams) v.getLayoutParams(); + } + } + return null; + } + + private int getRelatedViewBaseline(int[] rules, int relation) { + View v = getRelatedView(rules, relation); + if (v != null) { + return v.getBaseline(); + } + return -1; + } + + private void centerHorizontal(View child, LayoutParams params, int myWidth) { + int childWidth = child.getMeasuredWidth(); + int left = (myWidth - childWidth) / 2; + + params.mLeft = left; + params.mRight = left + childWidth; + } + + private void centerVertical(View child, LayoutParams params, int myHeight) { + int childHeight = child.getMeasuredHeight(); + int top = (myHeight - childHeight) / 2; + + params.mTop = top; + params.mBottom = top + childHeight; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // The layout has actually already been performed and the positions + // cached. Apply the cached values to the children. + int count = getChildCount(); + + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + RelativeLayout.LayoutParams st = + (RelativeLayout.LayoutParams) child.getLayoutParams(); + child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom); + + } + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new RelativeLayout.LayoutParams(getContext(), attrs); + } + + /** + * Returns a set of layout parameters with a width of + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}, + * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning. + */ + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + // Override to allow type-checking of LayoutParams. + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof RelativeLayout.LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + /** + * Per-child layout information associated with RelativeLayout. + * + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignWithParentIfMissing + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toLeftOf + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toRightOf + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_above + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_below + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignBaseline + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignLeft + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignTop + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignRight + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignBottom + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentLeft + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentTop + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentRight + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentBottom + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_centerInParent + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_centerHorizontal + * @attr ref android.R.styleable#RelativeLayout_Layout_layout_centerVertical + */ + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + private int[] mRules = new int[VERB_COUNT]; + private int mLeft, mTop, mRight, mBottom; + + /** + * When true, uses the parent as the anchor if the anchor doesn't exist or if + * the anchor's visibility is GONE. + */ + public boolean alignWithParent; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + TypedArray a = c.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.RelativeLayout_Layout); + + final int[] rules = mRules; + + final int N = a.getIndexCount(); + for (int i = 0; i < N; i++) { + int attr = a.getIndex(i); + switch (attr) { + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignWithParentIfMissing: + alignWithParent = a.getBoolean(attr, false); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf: + rules[LEFT_OF] = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf: + rules[RIGHT_OF] = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above: + rules[ABOVE] = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_below: + rules[BELOW] = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBaseline: + rules[ALIGN_BASELINE] = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignLeft: + rules[ALIGN_LEFT] = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignTop: + rules[ALIGN_TOP] = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignRight: + rules[ALIGN_RIGHT] = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBottom: + rules[ALIGN_BOTTOM] = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentLeft: + rules[ALIGN_PARENT_LEFT] = a.getBoolean(attr, false) ? TRUE : 0; + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentTop: + rules[ALIGN_PARENT_TOP] = a.getBoolean(attr, false) ? TRUE : 0; + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentRight: + rules[ALIGN_PARENT_RIGHT] = a.getBoolean(attr, false) ? TRUE : 0; + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentBottom: + rules[ALIGN_PARENT_BOTTOM] = a.getBoolean(attr, false) ? TRUE : 0; + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent: + rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0; + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerHorizontal: + rules[CENTER_HORIZONTAL] = a.getBoolean(attr, false) ? TRUE : 0; + break; + case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerVertical: + rules[CENTER_VERTICAL] = a.getBoolean(attr, false) ? TRUE : 0; + break; + } + } + + a.recycle(); + } + + public LayoutParams(int w, int h) { + super(w, h); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + @Override + public String debug(String output) { + return output + "ViewGroup.LayoutParams={ width=" + sizeToString(width) + + ", height=" + sizeToString(height) + " }"; + } + + /** + * Adds a layout rule to be interpreted by the RelativeLayout. This + * method should only be used for constraints that don't refer to another sibling + * (e.g., CENTER_IN_PARENT) or take a boolean value ({@link RelativeLayout#TRUE} + * for true or - for false). To specify a verb that takes a subject, use + * {@link #addRule(int, int)} instead. + * + * @param verb One of the verbs defined by + * {@link android.widget.RelativeLayout RelativeLayout}, such as + * ALIGN_WITH_PARENT_LEFT. + * @see #addRule(int, int) + */ + public void addRule(int verb) { + mRules[verb] = TRUE; + } + + /** + * Adds a layout rule to be interpreted by the RelativeLayout. Use this for + * verbs that take a target, such as a sibling (ALIGN_RIGHT) or a boolean + * value (VISIBLE). + * + * @param verb One of the verbs defined by + * {@link android.widget.RelativeLayout RelativeLayout}, such as + * ALIGN_WITH_PARENT_LEFT. + * @param anchor The id of another view to use as an anchor, + * or a boolean value(represented as {@link RelativeLayout#TRUE}) + * for true or 0 for false). For verbs that don't refer to another sibling + * (for example, ALIGN_WITH_PARENT_BOTTOM) just use -1. + * @see #addRule(int) + */ + public void addRule(int verb, int anchor) { + mRules[verb] = anchor; + } + + /** + * Retrieves a complete list of all supported rules, where the index is the rule + * verb, and the element value is the value specified, or "false" if it was never + * set. + * + * @return the supported rules + * @see #addRule(int, int) + */ + public int[] getRules() { + return mRules; + } + } +} diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java new file mode 100644 index 0000000..54951b7 --- /dev/null +++ b/core/java/android/widget/RemoteViews.java @@ -0,0 +1,649 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater.Filter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; + + +/** + * A class that describes a view hierarchy that can be displayed in + * another process. The hierarchy is inflated from a layout resource + * file, and this class provides some basic operations for modifying + * the content of the inflated hierarchy. + */ +public class RemoteViews implements Parcelable, Filter { + + private static final String LOG_TAG = "RemoteViews"; + + /** + * The package name of the package containing the layout + * resource. (Added to the parcel) + */ + private String mPackage; + + /** + * The resource ID of the layout file. (Added to the parcel) + */ + private int mLayoutId; + + /** + * The Context object used to inflate the layout file. Also may + * be used by actions if they need access to the senders resources. + */ + private Context mContext; + + /** + * An array of actions to perform on the view tree once it has been + * inflated + */ + private ArrayList<Action> mActions; + + + /** + * This annotation indicates that a subclass of View is alllowed to be used with the + * {@link android.widget.RemoteViews} mechanism. + */ + @Target({ ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + public @interface RemoteView { + } + + /** + * Exception to send when something goes wrong executing an action + * + */ + public static class ActionException extends RuntimeException { + public ActionException(String message) { + super(message); + } + } + + /** + * Base class for all actions that can be performed on an + * inflated view. + * + */ + private abstract static class Action implements Parcelable { + public abstract void apply(View root) throws ActionException; + + public int describeContents() { + return 0; + } + }; + + /** + * Equivalent to calling View.setVisibility + */ + private class SetViewVisibility extends Action { + public SetViewVisibility(int id, int vis) { + viewId = id; + visibility = vis; + } + + public SetViewVisibility(Parcel parcel) { + viewId = parcel.readInt(); + visibility = parcel.readInt(); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + dest.writeInt(visibility); + } + + @Override + public void apply(View root) { + View target = root.findViewById(viewId); + if (target != null) { + target.setVisibility(visibility); + } + } + + private int viewId; + private int visibility; + public final static int TAG = 0; + } + + /** + * Equivalent to calling TextView.setText + */ + private class SetTextViewText extends Action { + public SetTextViewText(int id, CharSequence t) { + viewId = id; + text = t; + } + + public SetTextViewText(Parcel parcel) { + viewId = parcel.readInt(); + text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + TextUtils.writeToParcel(text, dest, flags); + } + + @Override + public void apply(View root) { + TextView target = (TextView) root.findViewById(viewId); + if (target != null) { + target.setText(text); + } + } + + int viewId; + CharSequence text; + public final static int TAG = 1; + } + + /** + * Equivalent to calling ImageView.setResource + */ + private class SetImageViewResource extends Action { + public SetImageViewResource(int id, int src) { + viewId = id; + srcId = src; + } + + public SetImageViewResource(Parcel parcel) { + viewId = parcel.readInt(); + srcId = parcel.readInt(); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + dest.writeInt(srcId); + } + + @Override + public void apply(View root) { + ImageView target = (ImageView) root.findViewById(viewId); + Drawable d = mContext.getResources().getDrawable(srcId); + if (target != null) { + target.setImageDrawable(d); + } + } + + int viewId; + int srcId; + public final static int TAG = 2; + } + + /** + * Equivalent to calling ImageView.setImageURI + */ + private class SetImageViewUri extends Action { + public SetImageViewUri(int id, Uri u) { + viewId = id; + uri = u; + } + + public SetImageViewUri(Parcel parcel) { + viewId = parcel.readInt(); + uri = Uri.CREATOR.createFromParcel(parcel); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + Uri.writeToParcel(dest, uri); + } + + @Override + public void apply(View root) { + ImageView target = (ImageView) root.findViewById(viewId); + if (target != null) { + target.setImageURI(uri); + } + } + + int viewId; + Uri uri; + public final static int TAG = 3; + } + + /** + * Equivalent to calling ImageView.setImageBitmap + */ + private class SetImageViewBitmap extends Action { + public SetImageViewBitmap(int id, Bitmap src) { + viewId = id; + bitmap = src; + } + + public SetImageViewBitmap(Parcel parcel) { + viewId = parcel.readInt(); + bitmap = Bitmap.CREATOR.createFromParcel(parcel); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + if (bitmap != null) { + bitmap.writeToParcel(dest, flags); + } + } + + @Override + public void apply(View root) { + if (bitmap != null) { + ImageView target = (ImageView) root.findViewById(viewId); + Drawable d = new BitmapDrawable(bitmap); + if (target != null) { + target.setImageDrawable(d); + } + } + } + + int viewId; + Bitmap bitmap; + public final static int TAG = 4; + } + + /** + * Equivalent to calling Chronometer.setBase, Chronometer.setFormat, + * and Chronometer.start/stop. + */ + private class SetChronometer extends Action { + public SetChronometer(int id, long base, String format, boolean running) { + this.viewId = id; + this.base = base; + this.format = format; + this.running = running; + } + + public SetChronometer(Parcel parcel) { + viewId = parcel.readInt(); + base = parcel.readLong(); + format = parcel.readString(); + running = parcel.readInt() != 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + dest.writeLong(base); + dest.writeString(format); + dest.writeInt(running ? 1 : 0); + } + + @Override + public void apply(View root) { + Chronometer target = (Chronometer) root.findViewById(viewId); + if (target != null) { + target.setBase(base); + target.setFormat(format); + if (running) { + target.start(); + } else { + target.stop(); + } + } + } + + int viewId; + boolean running; + long base; + String format; + + public final static int TAG = 5; + } + + /** + * Equivalent to calling ProgressBar.setMax, ProgressBar.setProgress and + * ProgressBar.setIndeterminate + */ + private class SetProgressBar extends Action { + public SetProgressBar(int id, int max, int progress, boolean indeterminate) { + this.viewId = id; + this.progress = progress; + this.max = max; + this.indeterminate = indeterminate; + } + + public SetProgressBar(Parcel parcel) { + viewId = parcel.readInt(); + progress = parcel.readInt(); + max = parcel.readInt(); + indeterminate = parcel.readInt() != 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + dest.writeInt(progress); + dest.writeInt(max); + dest.writeInt(indeterminate ? 1 : 0); + } + + @Override + public void apply(View root) { + ProgressBar target = (ProgressBar) root.findViewById(viewId); + if (target != null) { + target.setIndeterminate(indeterminate); + if (!indeterminate) { + target.setMax(max); + target.setProgress(progress); + } + } + } + + int viewId; + boolean indeterminate; + int progress; + int max; + + public final static int TAG = 6; + } + + /** + * Create a new RemoteViews object that will display the views contained + * in the specified layout file. + * + * @param packageName Name of the package that contains the layout resource + * @param layoutId The id of the layout resource + */ + public RemoteViews(String packageName, int layoutId) { + mPackage = packageName; + mLayoutId = layoutId; + } + + /** + * Reads a RemoteViews object from a parcel. + * + * @param parcel + */ + public RemoteViews(Parcel parcel) { + mPackage = parcel.readString(); + mLayoutId = parcel.readInt(); + int count = parcel.readInt(); + if (count > 0) { + mActions = new ArrayList<Action>(count); + for (int i=0; i<count; i++) { + int tag = parcel.readInt(); + switch (tag) { + case SetViewVisibility.TAG: + mActions.add(new SetViewVisibility(parcel)); + break; + case SetTextViewText.TAG: + mActions.add(new SetTextViewText(parcel)); + break; + case SetImageViewResource.TAG: + mActions.add(new SetImageViewResource(parcel)); + break; + case SetImageViewUri.TAG: + mActions.add(new SetImageViewUri(parcel)); + break; + case SetImageViewBitmap.TAG: + mActions.add(new SetImageViewBitmap(parcel)); + break; + case SetChronometer.TAG: + mActions.add(new SetChronometer(parcel)); + break; + case SetProgressBar.TAG: + mActions.add(new SetProgressBar(parcel)); + break; + default: + throw new ActionException("Tag " + tag + "not found"); + } + } + } + } + + public String getPackage() { + return mPackage; + } + + public int getLayoutId() { + return mLayoutId; + } + + /** + * Add an action to be executed on the remote side when apply is called. + * + * @param a The action to add + */ + private void addAction(Action a) { + if (mActions == null) { + mActions = new ArrayList<Action>(); + } + mActions.add(a); + } + + /** + * Equivalent to calling View.setVisibility + * + * @param viewId The id of the view whose visibility should change + * @param visibility The new visibility for the view + */ + public void setViewVisibility(int viewId, int visibility) { + addAction(new SetViewVisibility(viewId, visibility)); + } + + /** + * Equivalent to calling TextView.setText + * + * @param viewId The id of the view whose text should change + * @param text The new text for the view + */ + public void setTextViewText(int viewId, CharSequence text) { + addAction(new SetTextViewText(viewId, text)); + } + + /** + * Equivalent to calling ImageView.setImageResource + * + * @param viewId The id of the view whose drawable should change + * @param srcId The new resource id for the drawable + */ + public void setImageViewResource(int viewId, int srcId) { + addAction(new SetImageViewResource(viewId, srcId)); + } + + /** + * Equivalent to calling ImageView.setImageURI + * + * @param viewId The id of the view whose drawable should change + * @param uri The Uri for the image + */ + public void setImageViewUri(int viewId, Uri uri) { + addAction(new SetImageViewUri(viewId, uri)); + } + + /** + * Equivalent to calling ImageView.setImageBitmap + * + * @param viewId The id of the view whose drawable should change + * @param bitmap The new Bitmap for the drawable + * + * @hide pending API Council approval to extend the public API + */ + public void setImageViewBitmap(int viewId, Bitmap bitmap) { + addAction(new SetImageViewBitmap(viewId, bitmap)); + } + + /** + * Equivalent to calling {@link Chronometer#setBase Chronometer.setBase}, + * {@link Chronometer#setFormat Chronometer.setFormat}, + * and {@link Chronometer#start Chronometer.start()} or + * {@link Chronometer#stop Chronometer.stop()}. + * + * @param viewId The id of the view whose text should change + * @param base The time at which the timer would have read 0:00. This + * time should be based off of + * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()}. + * @param format The Chronometer format string, or null to + * simply display the timer value. + * @param running True if you want the clock to be running, false if not. + */ + public void setChronometer(int viewId, long base, String format, boolean running) { + addAction(new SetChronometer(viewId, base, format, running)); + } + + /** + * Equivalent to calling {@link ProgressBar#setMax ProgressBar.setMax}, + * {@link ProgressBar#setProgress ProgressBar.setProgress}, and + * {@link ProgressBar#setIndeterminate ProgressBar.setIndeterminate} + * + * @param viewId The id of the view whose text should change + * @param max The 100% value for the progress bar + * @param progress The current value of the progress bar. + * @param indeterminate True if the progress bar is indeterminate, + * false if not. + */ + public void setProgressBar(int viewId, int max, int progress, + boolean indeterminate) { + addAction(new SetProgressBar(viewId, max, progress, indeterminate)); + } + + /** + * Inflates the view hierarchy represented by this object and applies + * all of the actions. + * + * <p><strong>Caller beware: this may throw</strong> + * + * @param context Default context to use + * @param parent Parent that the resulting view hierarchy will be attached to. This method + * does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate. + * @return The inflated view hierarchy + */ + public View apply(Context context, ViewGroup parent) { + View result = null; + + Context c = prepareContext(context); + + Resources r = c.getResources(); + LayoutInflater inflater = (LayoutInflater) c + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + inflater = inflater.cloneInContext(c); + inflater.setFilter(this); + + result = inflater.inflate(mLayoutId, parent, false); + + performApply(result); + + return result; + } + + /** + * Applies all of the actions to the provided view. + * + * <p><strong>Caller beware: this may throw</strong> + * + * @param v The view to apply the actions to. This should be the result of + * the {@link #apply(Context,ViewGroup)} call. + */ + public void reapply(Context context, View v) { + prepareContext(context); + performApply(v); + } + + private void performApply(View v) { + if (mActions != null) { + final int count = mActions.size(); + for (int i = 0; i < count; i++) { + Action a = mActions.get(i); + a.apply(v); + } + } + } + + private Context prepareContext(Context context) { + Context c = null; + String packageName = mPackage; + + if (packageName != null) { + try { + c = context.createPackageContext(packageName, 0); + } catch (NameNotFoundException e) { + Log.e(LOG_TAG, "Package name " + packageName + " not found"); + c = context; + } + } else { + c = context; + } + + mContext = c; + + return c; + } + + /* (non-Javadoc) + * Used to restrict the views which can be inflated + * + * @see android.view.LayoutInflater.Filter#onLoadClass(java.lang.Class) + */ + public boolean onLoadClass(Class clazz) { + return clazz.isAnnotationPresent(RemoteView.class); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mPackage); + dest.writeInt(mLayoutId); + int count; + if (mActions != null) { + count = mActions.size(); + } else { + count = 0; + } + dest.writeInt(count); + for (int i=0; i<count; i++) { + Action a = mActions.get(i); + a.writeToParcel(dest, 0); + } + } + + /** + * Parcelable.Creator that instantiates RemoteViews objects + */ + public static final Parcelable.Creator<RemoteViews> CREATOR = new Parcelable.Creator<RemoteViews>() { + public RemoteViews createFromParcel(Parcel parcel) { + return new RemoteViews(parcel); + } + + public RemoteViews[] newArray(int size) { + return new RemoteViews[size]; + } + }; +} diff --git a/core/java/android/widget/ResourceCursorAdapter.java b/core/java/android/widget/ResourceCursorAdapter.java new file mode 100644 index 0000000..456d58d --- /dev/null +++ b/core/java/android/widget/ResourceCursorAdapter.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; + + +/** + * An easy adapter that creates views defined in an XML file. You can specify + * the XML file that defines the appearance of the views. + */ +public abstract class ResourceCursorAdapter extends CursorAdapter { + private int mLayout; + + private int mDropDownLayout; + + private LayoutInflater mInflater; + + /** + * Constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. + */ + public ResourceCursorAdapter(Context context, int layout, Cursor c) { + super(context, c); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Inflates view(s) from the specified XML file. + * + * @see android.widget.CursorAdapter#newView(android.content.Context, + * android.database.Cursor, ViewGroup) + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mLayout, parent, false); + } + + @Override + public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mDropDownLayout, parent, false); + } + + /** + * <p>Sets the layout resource of the drop down views.</p> + * + * @param dropDownLayout the layout resources used to create drop down views + */ + public void setDropDownViewResource(int dropDownLayout) { + mDropDownLayout = dropDownLayout; + } +} diff --git a/core/java/android/widget/ResourceCursorTreeAdapter.java b/core/java/android/widget/ResourceCursorTreeAdapter.java new file mode 100644 index 0000000..ddce515 --- /dev/null +++ b/core/java/android/widget/ResourceCursorTreeAdapter.java @@ -0,0 +1,109 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; + +/** + * A fairly simple ExpandableListAdapter that creates views defined in an XML + * file. You can specify the XML file that defines the appearance of the views. + */ +public abstract class ResourceCursorTreeAdapter extends CursorTreeAdapter { + private int mCollapsedGroupLayout; + private int mExpandedGroupLayout; + private int mChildLayout; + private int mLastChildLayout; + private LayoutInflater mInflater; + + /** + * Constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param cursor The database cursor + * @param collapsedGroupLayout resource identifier of a layout file that + * defines the views for collapsed groups. + * @param expandedGroupLayout resource identifier of a layout file that + * defines the views for expanded groups. + * @param childLayout resource identifier of a layout file that defines the + * views for all children but the last.. + * @param lastChildLayout resource identifier of a layout file that defines + * the views for the last child of a group. + */ + public ResourceCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout, + int expandedGroupLayout, int childLayout, int lastChildLayout) { + super(cursor, context); + + mCollapsedGroupLayout = collapsedGroupLayout; + mExpandedGroupLayout = expandedGroupLayout; + mChildLayout = childLayout; + mLastChildLayout = lastChildLayout; + + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param cursor The database cursor + * @param collapsedGroupLayout resource identifier of a layout file that + * defines the views for collapsed groups. + * @param expandedGroupLayout resource identifier of a layout file that + * defines the views for expanded groups. + * @param childLayout resource identifier of a layout file that defines the + * views for all children. + */ + public ResourceCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout, + int expandedGroupLayout, int childLayout) { + this(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout, childLayout); + } + + /** + * Constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param cursor The database cursor + * @param groupLayout resource identifier of a layout file that defines the + * views for all groups. + * @param childLayout resource identifier of a layout file that defines the + * views for all children. + */ + public ResourceCursorTreeAdapter(Context context, Cursor cursor, int groupLayout, + int childLayout) { + this(context, cursor, groupLayout, groupLayout, childLayout, childLayout); + } + + @Override + public View newChildView(Context context, Cursor cursor, boolean isLastChild, + ViewGroup parent) { + return mInflater.inflate((isLastChild) ? mLastChildLayout : mChildLayout, parent, false); + } + + @Override + public View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) { + return mInflater.inflate((isExpanded) ? mExpandedGroupLayout : mCollapsedGroupLayout, + parent, false); + } + +} diff --git a/core/java/android/widget/ScrollBarDrawable.java b/core/java/android/widget/ScrollBarDrawable.java new file mode 100644 index 0000000..5df2b6d --- /dev/null +++ b/core/java/android/widget/ScrollBarDrawable.java @@ -0,0 +1,248 @@ +/* + * 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 android.widget; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +/** + * This is only used by View for displaying its scroll bars. It should probably + * be moved in to the view package since it is used in that lower-level layer. + * For now, we'll hide it so it can be cleaned up later. + * {@hide} + */ +public class ScrollBarDrawable extends Drawable { + private Drawable mVerticalTrack; + private Drawable mHorizontalTrack; + private Drawable mVerticalThumb; + private Drawable mHorizontalThumb; + private int mRange; + private int mOffset; + private int mExtent; + private boolean mVertical; + private boolean mChanged; + private boolean mRangeChanged; + private final Rect mTempBounds = new Rect(); + private boolean mAlwaysDrawHorizontalTrack; + private boolean mAlwaysDrawVerticalTrack; + + public ScrollBarDrawable() { + } + + /** + * Indicate whether the horizontal scrollbar track should always be drawn regardless of the + * extent. Defaults to false. + * + * @param alwaysDrawTrack Set to true if the track should always be drawn + */ + public void setAlwaysDrawHorizontalTrack(boolean alwaysDrawTrack) { + mAlwaysDrawHorizontalTrack = alwaysDrawTrack; + } + + /** + * Indicate whether the vertical scrollbar track should always be drawn regardless of the + * extent. Defaults to false. + * + * @param alwaysDrawTrack Set to true if the track should always be drawn + */ + public void setAlwaysDrawVerticalTrack(boolean alwaysDrawTrack) { + mAlwaysDrawVerticalTrack = alwaysDrawTrack; + } + + /** + * Indicates whether the vertical scrollbar track should always be drawn regardless of the + * extent. + */ + public boolean getAlwaysDrawVerticalTrack() { + return mAlwaysDrawVerticalTrack; + } + + /** + * Indicates whether the horizontal scrollbar track should always be drawn regardless of the + * extent. + */ + public boolean getAlwaysDrawHorizontalTrack() { + return mAlwaysDrawHorizontalTrack; + } + + public void setParameters(int range, int offset, int extent, boolean vertical) { + if (mVertical != vertical) { + mChanged = true; + } + + if (mRange != range || mOffset != offset || mExtent != extent) { + mRangeChanged = true; + } + + mRange = range; + mOffset = offset; + mExtent = extent; + mVertical = vertical; + } + + @Override + public void draw(Canvas canvas) { + final boolean vertical = mVertical; + final int extent = mExtent; + final int range = mRange; + + boolean drawTrack = true; + boolean drawThumb = true; + if (extent <= 0 || range <= extent) { + drawTrack = vertical ? mAlwaysDrawVerticalTrack : mAlwaysDrawHorizontalTrack; + drawThumb = false; + } + + Rect r = getBounds(); + + if (drawTrack) { + drawTrack(canvas, r, vertical); + } + + if (drawThumb) { + int size = vertical ? r.height() : r.width(); + int thickness = vertical ? r.width() : r.height(); + int length = Math.round((float) size * extent / range); + int offset = Math.round((float) (size - length) * mOffset / (range - extent)); + + // avoid the tiny thumb + int minLength = thickness * 2; + if (length < minLength) { + length = minLength; + } + // avoid the too-big thumb + if (offset + length > size) { + offset = size - length; + } + + drawThumb(canvas, r, offset, length, vertical); + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mChanged = true; + } + + protected void drawTrack(Canvas canvas, Rect bounds, boolean vertical) { + Drawable track; + if (vertical) { + track = mVerticalTrack; + } else { + track = mHorizontalTrack; + } + if (mChanged) { + track.setBounds(bounds); + } + track.draw(canvas); + } + + protected void drawThumb(Canvas canvas, Rect bounds, int offset, int length, boolean vertical) { + final Rect thumbRect = mTempBounds; + final boolean changed = mRangeChanged || mChanged; + if (changed) { + if (vertical) { + thumbRect.set(bounds.left, bounds.top + offset, + bounds.right, bounds.top + offset + length); + } else { + thumbRect.set(bounds.left + offset, bounds.top, + bounds.left + offset + length, bounds.bottom); + } + } + + if (vertical) { + final Drawable thumb = mVerticalThumb; + if (changed) thumb.setBounds(thumbRect); + thumb.draw(canvas); + } else { + final Drawable thumb = mHorizontalThumb; + if (changed) thumb.setBounds(thumbRect); + thumb.draw(canvas); + } + } + + public void setVerticalThumbDrawable(Drawable thumb) { + if (thumb != null) { + mVerticalThumb = thumb; + } + } + + public void setVerticalTrackDrawable(Drawable track) { + mVerticalTrack = track; + } + + public void setHorizontalThumbDrawable(Drawable thumb) { + if (thumb != null) { + mHorizontalThumb = thumb; + } + } + + public void setHorizontalTrackDrawable(Drawable track) { + mHorizontalTrack = track; + } + + public int getSize(boolean vertical) { + if (vertical) { + return (mVerticalTrack != null ? + mVerticalTrack : mVerticalThumb).getIntrinsicWidth(); + } else { + return (mHorizontalTrack != null ? + mHorizontalTrack : mHorizontalThumb).getIntrinsicHeight(); + } + } + + @Override + public void setAlpha(int alpha) { + if (mVerticalTrack != null) { + mVerticalTrack.setAlpha(alpha); + } + mVerticalThumb.setAlpha(alpha); + if (mHorizontalTrack != null) { + mHorizontalTrack.setAlpha(alpha); + } + mHorizontalThumb.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + if (mVerticalTrack != null) { + mVerticalTrack.setColorFilter(cf); + } + mVerticalThumb.setColorFilter(cf); + if (mHorizontalTrack != null) { + mHorizontalTrack.setColorFilter(cf); + } + mHorizontalThumb.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public String toString() { + return "ScrollBarDrawable: range=" + mRange + " offset=" + mOffset + + " extent=" + mExtent + (mVertical ? " V" : " H"); + } +} + + diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java new file mode 100644 index 0000000..23a27ac --- /dev/null +++ b/core/java/android/widget/ScrollView.java @@ -0,0 +1,1213 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.animation.AnimationUtils; + +import com.android.internal.R; + +import java.util.List; + +/** + * Layout container for a view hierarchy that can be scrolled by the user, + * allowing it to be larger than the physical display. A ScrollView + * is a {@link FrameLayout}, meaning you should place one child in it + * containing the entire contents to scroll; this child may itself be a layout + * manager with a complex hierarchy of objects. A child that is often used + * is a {@link LinearLayout} in a vertical orientation, presenting a vertical + * array of top-level items that the user can scroll through. + * + * <p>You should never use a ScrollView with a {@link ListView}, since + * ListView takes care of its own scrolling. Most importantly, doing this + * defeats all of the important optimizations in ListView for dealing with + * large lists, since it effectively forces the ListView to display its entire + * list of items to fill up the infinite container supplied by ScrollView. + * + * <p>The {@link TextView} class also + * takes care of its own scrolling, so does not require a ScrollView, but + * using the two together is possible to achieve the effect of a text view + * within a larger container. + * + * <p>ScrollView only supports vertical scrolling. + */ +public class ScrollView extends FrameLayout { + private static final int ANIMATED_SCROLL_GAP = 250; + + /** + * 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.5f; + + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private Scroller mScroller; + + /** + * Flag to indicate that we are moving focus ourselves. This is so the + * code that watches for focus changes initiated outside this ScrollView + * knows that it does not have to do anything. + */ + private boolean mScrollViewMovedFocus; + + /** + * Position of the last motion event. + */ + private float mLastMotionY; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the user is currently dragging this ScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the user lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * When set to true, the scroll view measure its child to make it fill the currently + * visible area. + */ + private boolean mFillViewport; + + /** + * Whether arrow scrolling is animated. + */ + private boolean mSmoothScrollingEnabled = true; + + public ScrollView(Context context) { + super(context); + initScrollView(); + + setVerticalScrollBarEnabled(true); + setVerticalFadingEdgeEnabled(true); + + TypedArray a = context.obtainStyledAttributes(R.styleable.View); + + initializeScrollbars(a); + initializeFadingEdge(a); + + a.recycle(); + } + + public ScrollView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.scrollViewStyle); + } + + public ScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initScrollView(); + + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ScrollView, defStyle, 0); + + setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false)); + + a.recycle(); + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + if (mScrollY < length) { + return mScrollY / (float) length; + } + + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int bottom = getChildAt(0).getBottom(); + final int span = bottom - mScrollY - getHeight(); + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmount() { + return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop)); + } + + + private void initScrollView() { + mScroller = new Scroller(getContext()); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index, params); + } + + /** + * @return Returns true this ScrollView can be scrolled + */ + private boolean canScroll() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + return getHeight() < childHeight + mPaddingTop + mPaddingBottom; + } + return false; + } + + /** + * Indicates whether this ScrollView's content is stretched to fill the viewport. + * + * @return True if the content fills the viewport, false otherwise. + */ + public boolean isFillViewport() { + return mFillViewport; + } + + /** + * Indicates this ScrollView whether it should stretch its content height to fill + * the viewport or not. + * + * @param fillViewport True to stretch the content's height to the viewport's + * boundaries, false otherwise. + */ + public void setFillViewport(boolean fillViewport) { + if (fillViewport != mFillViewport) { + mFillViewport = fillViewport; + requestLayout(); + } + } + + /** + * @return Whether arrow scrolling will animate its transition. + */ + public boolean isSmoothScrollingEnabled() { + return mSmoothScrollingEnabled; + } + + /** + * Set whether arrow scrolling will animate its transition. + * @param smoothScrollingEnabled whether arrow scrolling will animate its transition + */ + public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { + mSmoothScrollingEnabled = smoothScrollingEnabled; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (!mFillViewport) { + return; + } + + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.UNSPECIFIED) { + return; + } + + final View child = getChildAt(0); + int height = getMeasuredHeight(); + if (child.getMeasuredHeight() < height) { + final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, mPaddingLeft + + mPaddingRight, lp.width); + height -= mPaddingTop; + height -= mPaddingBottom; + int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + boolean handled = super.dispatchKeyEvent(event); + if (handled) { + return true; + } + return executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + + if (!canScroll()) { + if (isFocused()) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, + currentFocused, View.FOCUS_DOWN); + return nextFocused != null + && nextFocused != this + && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP); + } else { + handled = fullScroll(View.FOCUS_UP); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN); + } else { + handled = fullScroll(View.FOCUS_DOWN); + } + break; + case KeyEvent.KEYCODE_SPACE: + pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + break; + } + } + + return handled; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + + if (!canScroll()) { + mIsBeingDragged = false; + return false; + } + + final float y = ev.getY(); + + switch (action) { + case MotionEvent.ACTION_MOVE: + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int yDiff = (int) Math.abs(y - mLastMotionY); + if (yDiff > ViewConfiguration.getTouchSlop()) { + mIsBeingDragged = true; + } + break; + + case MotionEvent.ACTION_DOWN: + /* Remember location of down touch */ + mLastMotionY = y; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mIsBeingDragged = !mScroller.isFinished(); + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + + if (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; + } + + if (!canScroll()) { + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + final float y = ev.getY(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = y; + break; + case MotionEvent.ACTION_MOVE: + // Scroll to follow the motion event + final int deltaY = (int) (mLastMotionY - y); + mLastMotionY = y; + + if (deltaY < 0) { + if (mScrollY > 0) { + scrollBy(0, deltaY); + } + } else if (deltaY > 0) { + final int bottomEdge = getHeight() - mPaddingBottom; + final int availableToScroll = getChildAt(0).getBottom() - mScrollY - bottomEdge; + if (availableToScroll > 0) { + scrollBy(0, Math.min(availableToScroll, deltaY)); + } + } + break; + case MotionEvent.ACTION_UP: + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000); + int initialVelocity = (int) velocityTracker.getYVelocity(); + + if ((Math.abs(initialVelocity) > ViewConfiguration.getMinimumFlingVelocity()) && + (getChildCount() > 0)) { + fling(-initialVelocity); + } + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + return true; + } + + /** + * <p> + * Finds the next focusable component that fits in this View's bounds + * (excluding fading edges) pretending that this View's top is located at + * the parameter top. + * </p> + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found (the fading edge is assumed to start at this position) + * @param preferredFocusable the View that has highest priority and will be + * returned if it is within my bounds (null is valid) + * @return the next focusable component in the bounds or null if none can be + * found + */ + private View findFocusableViewInMyBounds(final boolean topFocus, + final int top, View preferredFocusable) { + /* + * The fading edge's transparent side should be considered for focus + * since it's mostly visible, so we divide the actual fading edge length + * by 2. + */ + final int fadingEdgeLength = getVerticalFadingEdgeLength() / 2; + final int topWithoutFadingEdge = top + fadingEdgeLength; + final int bottomWithoutFadingEdge = top + getHeight() - fadingEdgeLength; + + if ((preferredFocusable != null) + && (preferredFocusable.getTop() < bottomWithoutFadingEdge) + && (preferredFocusable.getBottom() > topWithoutFadingEdge)) { + return preferredFocusable; + } + + return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, + bottomWithoutFadingEdge); + } + + /** + * <p> + * Finds the next focusable component that fits in the specified bounds. + * </p> + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { + + List<View> focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + + if (top < viewBottom && viewTop < bottom) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + + final boolean viewIsFullyContained = (top < viewTop) && + (viewBottom < bottom); + + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToBoundary = + (topFocus && viewTop < focusCandidate.getTop()) || + (!topFocus && viewBottom > focusCandidate + .getBottom()); + + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + + return focusCandidate; + } + + /** + * <p>Handles scrolling in response to a "page up/down" shortcut press. This + * method will scroll the view by one page up or down and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.</p> + * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go one page up or + * {@link android.view.View#FOCUS_DOWN} to go one page down + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean pageScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + if (down) { + mTempRect.top = getScrollY() + height; + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + if (mTempRect.top + height > view.getBottom()) { + mTempRect.top = view.getBottom() - height; + } + } + } else { + mTempRect.top = getScrollY() - height; + if (mTempRect.top < 0) { + mTempRect.top = 0; + } + } + mTempRect.bottom = mTempRect.top + height; + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + * <p>Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.</p> + * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + mTempRect.top = 0; + mTempRect.bottom = height; + + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + * <p>Scrolls the view to make the area defined by <code>top</code> and + * <code>bottom</code> visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this scrollview.</p> + * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go upward + * {@link android.view.View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int direction, int top, int bottom) { + boolean handled = true; + + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = direction == View.FOCUS_UP; + + View newFocused = findFocusableViewInBounds(up, top, bottom); + if (newFocused == null) { + newFocused = this; + } + + if (top >= containerTop && bottom <= containerBottom) { + handled = false; + } else { + int delta = up ? (top - containerTop) : (bottom - containerBottom); + doScrollY(delta); + } + + if (newFocused != findFocus() && newFocused.requestFocus(direction)) { + mScrollViewMovedFocus = true; + mScrollViewMovedFocus = false; + } + + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction) { + + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + + final int maxJump = getMaxScrollAmount(); + + if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + + int daBottom = getChildAt(getChildCount() - 1).getBottom(); + + int screenBottom = getScrollY() + getHeight(); + + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + if (scrollDelta == 0) { + return false; + } + doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); + } + + if (currentFocused != null && currentFocused.isFocused() + && isOffScreen(currentFocused)) { + // previously focused item still has focus and is off screen, give + // it up (take it back to ourselves) + // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are + // sure to + // get it) + final int descendantFocusability = getDescendantFocusability(); // save + setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + requestFocus(); + setDescendantFocusability(descendantFocusability); // restore + } + return true; + } + + /** + * @return whether the descendant of this scroll view is scrolled off + * screen. + */ + private boolean isOffScreen(View descendant) { + return !isWithinDeltaOfScreen(descendant, 0); + } + + /** + * @return whether the descendant of this scroll view is within delta + * pixels of being on the screen. + */ + private boolean isWithinDeltaOfScreen(View descendant, int delta) { + descendant.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(descendant, mTempRect); + + return (mTempRect.bottom + delta) >= getScrollY() + && (mTempRect.top - delta) <= (getScrollY() + getHeight()); + } + + /** + * Smooth scroll by a Y delta + * + * @param delta the number of pixels to scroll by on the X axis + */ + private void doScrollY(int delta) { + if (delta != 0) { + if (mSmoothScrollingEnabled) { + smoothScrollBy(0, delta); + } else { + scrollBy(0, delta); + } + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + mScroller.startScroll(mScrollX, mScrollY, dx, dy); + invalidate(); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - mScrollX, y - mScrollY); + } + + /** + * <p>The scroll range of a scroll view is the overall height of all of its + * children.</p> + */ + @Override + protected int computeVerticalScrollRange() { + int count = getChildCount(); + return count == 0 ? getHeight() : (getChildAt(0)).getBottom(); + } + + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + + mPaddingRight, lp.width); + + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + // This is called at drawing time by ViewGroup. We don't want to + // re-show the scrollbars at this point, which scrollTo will do, + // so we replicate most of scrollTo here. + // + // It's a little odd to call onScrollChanged from inside the drawing. + // + // It is, except when you remember that computeScroll() is used to + // animate scrolling. So unless we want to defer the onScrollChanged() + // until the end of the animated scrolling, we don't really have a + // choice here. + // + // I agree. The alternative, which I think would be worse, is to post + // something and tell the subclasses later. This is bad because there + // will be a window where mScrollX/Y is different from what the app + // thinks it is. + // + int oldX = mScrollX; + int oldY = mScrollY; + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (getChildCount() > 0) { + View child = getChildAt(0); + mScrollX = clamp(x, this.getWidth(), child.getWidth()); + mScrollY = clamp(y, this.getHeight(), child.getHeight()); + } else { + mScrollX = x; + mScrollY = y; + } + if (oldX != mScrollX || oldY != mScrollY) { + onScrollChanged(mScrollX, mScrollY, oldX, oldY); + postInvalidate(); // So we draw again + } + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the View to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + + if (scrollDelta != 0) { + scrollBy(0, scrollDelta); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta != 0; + if (scroll) { + if (immediate) { + scrollBy(0, delta); + } else { + smoothScrollBy(0, delta); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + + int height = getHeight(); + int screenTop = getScrollY(); + int screenBottom = screenTop + height; + + int fadingEdge = getVerticalFadingEdgeLength(); + + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdge; + } + + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdge; + } + + int scrollYDelta = 0; + + if (rect.bottom > screenBottom && rect.top > screenTop) { + // 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 - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(getChildCount() - 1).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // 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 -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return scrollYDelta; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mScrollViewMovedFocus) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + } + super.requestChildFocus(child, focused); + } + + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + * + * This is more expensive than the default {@link android.view.ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null ? + FocusFinder.getInstance().findNextFocus(this, null, direction) : + FocusFinder.getInstance().findNextFocusFromRect(this, + previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + if (isOffScreen(nextFocus)) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, + boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), + child.getTop() - child.getScrollY()); + + // note: until bug 1137695 is fixed, disable smooth scrolling for this api + return scrollToChildRect(rectangle, true);//immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + // Calling this with the present values causes it to re-clam them + scrollTo(mScrollX, mScrollY); + } + + /** + * Return true if child is an descendant of parent, (or equal to the parent). + */ + private boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/curor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityY) { + int height = getHeight(); + int bottom = getChildAt(getChildCount() - 1).getBottom(); + + mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, bottom - height); + + final boolean movingDown = velocityY > 0; + + View newFocused = + findFocusableViewInMyBounds(movingDown, mScroller.getFinalY(), findFocus()); + if (newFocused == null) { + newFocused = this; + } + + if (newFocused != findFocus() + && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) { + mScrollViewMovedFocus = true; + mScrollViewMovedFocus = false; + } + + invalidate(); + } + + /** + * {@inheritDoc} + * + * <p>This version also clamps the scrolling to the bounds of our child. + */ + public void scrollTo(int x, int y) { + // we rely on the fact the View.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, this.getWidth(), child.getWidth()); + y = clamp(y, this.getHeight(), child.getHeight()); + if (x != mScrollX || y != mScrollY) { + super.scrollTo(x, y); + } + } + } + + private int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my+n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child-my; + } + return n; + } +} diff --git a/core/java/android/widget/Scroller.java b/core/java/android/widget/Scroller.java new file mode 100644 index 0000000..fbe5e57 --- /dev/null +++ b/core/java/android/widget/Scroller.java @@ -0,0 +1,368 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + + +/** + * This class encapsulates scrolling. The duration of the scroll + * can be passed in the constructor and specifies the maximum time that + * the scrolling animation should take. Past this time, the scrolling is + * automatically moved to its final stage and computeScrollOffset() + * will always return false to indicate that scrolling is over. + */ +public class Scroller { + private int mMode; + + private int mStartX; + private int mStartY; + private int mFinalX; + private int mFinalY; + + private int mMinX; + private int mMaxX; + private int mMinY; + private int mMaxY; + + private int mCurrX; + private int mCurrY; + private long mStartTime; + private int mDuration; + private float mDurationReciprocal; + private float mDeltaX; + private float mDeltaY; + private float mViscousFluidScale; + private float mViscousFluidNormalize; + private boolean mFinished; + private Interpolator mInterpolator; + + private float mCoeffX = 0.0f; + private float mCoeffY = 1.0f; + private float mVelocity; + + private static final int DEFAULT_DURATION = 250; + private static final int SCROLL_MODE = 0; + private static final int FLING_MODE = 1; + + private final float mDeceleration; + + /** + * Create a Scroller with the default duration and interpolator. + */ + public Scroller(Context context) { + this(context, null); + } + + /** + * Create a Scroller with the specified interpolator. If the interpolator is + * null, the default (viscous) interpolator will be used. + */ + public Scroller(Context context, Interpolator interpolator) { + mFinished = true; + mInterpolator = interpolator; + float ppi = context.getResources().getDisplayMetrics().density * 160.0f; + mDeceleration = 9.8f // g (m/s^2) + * 39.37f // inch/meter + * ppi // pixels per inch + * ViewConfiguration.getScrollFriction(); + } + + /** + * + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + public final boolean isFinished() { + return mFinished; + } + + /** + * Force the finished field to a particular value. + * + * @param finished The new finished value. + */ + public final void forceFinished(boolean finished) { + mFinished = finished; + } + + /** + * Returns how long the scroll event will take, in milliseconds. + * + * @return The duration of the scroll in milliseconds. + */ + public final int getDuration() { + return mDuration; + } + + /** + * Returns the current X offset in the scroll. + * + * @return The new X offset as an absolute distance from the origin. + */ + public final int getCurrX() { + return mCurrX; + } + + /** + * Returns the current Y offset in the scroll. + * + * @return The new Y offset as an absolute distance from the origin. + */ + public final int getCurrY() { + return mCurrY; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final X offset as an absolute distance from the origin. + */ + public final int getFinalX() { + return mFinalX; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final Y offset as an absolute distance from the origin. + */ + public final int getFinalY() { + return mFinalY; + } + + /** + * Call this when you want to know the new location. If it returns true, + * the animation is not yet finished. loc will be altered to provide the + * new location. + */ + public boolean computeScrollOffset() { + if (mFinished) { + return false; + } + + int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); + + if (timePassed < mDuration) { + switch (mMode) { + case SCROLL_MODE: + float x = (float)timePassed * mDurationReciprocal; + + if (mInterpolator == null) + x = viscousFluid(x); + else + x = mInterpolator.getInterpolation(x); + + mCurrX = mStartX + Math.round(x * mDeltaX); + mCurrY = mStartY + Math.round(x * mDeltaY); + if ((mCurrX == mFinalX) && (mCurrY == mFinalY)) { + mFinished = true; + } + break; + case FLING_MODE: + float timePassedSeconds = timePassed / 1000.0f; + float distance = (mVelocity * timePassedSeconds) + - (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f); + + mCurrX = mStartX + Math.round(distance * mCoeffX); + // Pin to mMinX <= mCurrX <= mMaxX + mCurrX = Math.min(mCurrX, mMaxX); + mCurrX = Math.max(mCurrX, mMinX); + + mCurrY = mStartY + Math.round(distance * mCoeffY); + // Pin to mMinY <= mCurrY <= mMaxY + mCurrY = Math.min(mCurrY, mMaxY); + mCurrY = Math.max(mCurrY, mMinY); + + if (mCurrX == mFinalX && mCurrY == mFinalY) { + mFinished = true; + } + + break; + } + } + else { + mCurrX = mFinalX; + mCurrY = mFinalY; + mFinished = true; + } + return true; + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * The scroll will use the default value of 250 milliseconds for the + * duration. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + */ + public void startScroll(int startX, int startY, int dx, int dy) { + startScroll(startX, startY, dx, dy, DEFAULT_DURATION); + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + * @param duration Duration of the scroll in milliseconds. + */ + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + mMode = SCROLL_MODE; + mFinished = false; + mDuration = duration; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStartX = startX; + mStartY = startY; + mFinalX = startX + dx; + mFinalY = startY + dy; + mDeltaX = dx; + mDeltaY = dy; + mDurationReciprocal = 1.0f / (float) mDuration; + // This controls the viscous fluid effect (how much of it) + mViscousFluidScale = 8.0f; + // must be set to 1.0 (used in viscousFluid()) + mViscousFluidNormalize = 1.0f; + mViscousFluidNormalize = 1.0f / viscousFluid(1.0f); + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + mMode = FLING_MODE; + mFinished = false; + + float velocity = (float)Math.hypot(velocityX, velocityY); + + mVelocity = velocity; + mDuration = (int) (1000 * velocity / mDeceleration); // Duration is in + // milliseconds + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStartX = startX; + mStartY = startY; + + mCoeffX = velocity == 0 ? 1.0f : velocityX / velocity; + mCoeffY = velocity == 0 ? 1.0f : velocityY / velocity; + + int totalDistance = (int) ((velocity * velocity) / (2 * mDeceleration)); + + mMinX = minX; + mMaxX = maxX; + mMinY = minY; + mMaxY = maxY; + + + mFinalX = startX + Math.round(totalDistance * mCoeffX); + // Pin to mMinX <= mFinalX <= mMaxX + mFinalX = Math.min(mFinalX, mMaxX); + mFinalX = Math.max(mFinalX, mMinX); + + mFinalY = startY + Math.round(totalDistance * mCoeffY); + // Pin to mMinY <= mFinalY <= mMaxY + mFinalY = Math.min(mFinalY, mMaxY); + mFinalY = Math.max(mFinalY, mMinY); + } + + + + private float viscousFluid(float x) + { + x *= mViscousFluidScale; + if (x < 1.0f) { + x -= (1.0f - (float)Math.exp(-x)); + } else { + float start = 0.36787944117f; // 1/e == exp(-1) + x = 1.0f - (float)Math.exp(1.0f - x); + x = start + x * (1.0f - start); + } + x *= mViscousFluidNormalize; + return x; + } + + /** + * + */ + public void abortAnimation() { + mCurrX = mFinalX; + mCurrY = mFinalY; + mFinished = true; + } + + /** + * Extend the scroll animation. This allows a running animation to + * scroll further and longer, when used with setFinalX() or setFinalY(). + * + * @param extend Additional time to scroll in milliseconds. + */ + public void extendDuration(int extend) { + int passed = timePassed(); + mDuration = passed + extend; + mDurationReciprocal = 1.0f / (float)mDuration; + mFinished = false; + } + + public int timePassed() { + return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); + } + + public void setFinalX(int newX) { + mFinalX = newX; + mDeltaX = mFinalX - mStartX; + mFinished = false; + } + + public void setFinalY(int newY) { + mFinalY = newY; + mDeltaY = mFinalY - mStartY; + mFinished = false; + } +} diff --git a/core/java/android/widget/SeekBar.java b/core/java/android/widget/SeekBar.java new file mode 100644 index 0000000..e87dc2d --- /dev/null +++ b/core/java/android/widget/SeekBar.java @@ -0,0 +1,116 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.util.AttributeSet; + + + +/** + * A Seekbar is an extension of ProgressBar that adds a draggable thumb. The user can touch + * the thumb and drag left or right to set the current progress level. + * + * be notified of the user's actions. + * Clients of the Seekbar can attach a {@link SeekBar.OnSeekBarChangeListener} to + * + * @attr ref android.R.styleable#SeekBar_thumb + */ +public class SeekBar extends AbsSeekBar { + + /** + * A callback that notifies clients when the progress level has been changed. This + * includes changes that were initiated by the user through a touch gesture as well + * as changes that were initiated programmatically. + */ + public interface OnSeekBarChangeListener { + + /** + * Notification that the progress level has changed. Clients can use the fromTouch parameter + * to distinguish user-initiated changes from those that occurred programmatically. + * + * @param seekBar The SeekBar whose progress has changed + * @param progress The current progress level. This will be in the range 0..max where max + * was set by {@link ProgressBar#setMax(int)}. (The default value for max is 100.) + * @param fromTouch True if the progress change was initiated by a user's touch gesture. + */ + void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch); + + /** + * Notification that the user has started a touch gesture. Clients may want to use this + * to disable advancing the seekbar. + * @param seekBar The SeekBar in which the touch gesture began + */ + void onStartTrackingTouch(SeekBar seekBar); + + /** + * Notification that the user has finished a touch gesture. Clients may want to use this + * to re-enable advancing the seekbar. + * @param seekBar The SeekBar in which the touch gesture began + */ + void onStopTrackingTouch(SeekBar seekBar); + } + + private OnSeekBarChangeListener mOnSeekBarChangeListener; + + public SeekBar(Context context) { + this(context, null); + } + + public SeekBar(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.seekBarStyle); + } + + public SeekBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + void onProgressRefresh(float scale, boolean fromTouch) { + super.onProgressRefresh(scale, fromTouch); + + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onProgressChanged(this, getProgress(), fromTouch); + } + } + + /** + * Sets a listener to receive notifications of changes to the SeekBar's progress level. Also + * provides notifications of when the user starts and stops a touch gesture within the SeekBar. + * + * @param l The seek bar notification listener + * + * @see SeekBar.OnSeekBarChangeListener + */ + public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { + mOnSeekBarChangeListener = l; + } + + @Override + void onStartTrackingTouch() { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStartTrackingTouch(this); + } + } + + @Override + void onStopTrackingTouch() { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStopTrackingTouch(this); + } + } +} diff --git a/core/java/android/widget/SimpleAdapter.java b/core/java/android/widget/SimpleAdapter.java new file mode 100644 index 0000000..df52b69 --- /dev/null +++ b/core/java/android/widget/SimpleAdapter.java @@ -0,0 +1,366 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; +import android.net.Uri; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * An easy adapter to map static data to views defined in an XML file. You can specify the data + * backing the list as an ArrayList of Maps. Each entry in the ArrayList corresponds to one row + * in the list. The Maps contain the data for each row. You also specify an XML file that + * defines the views used to display the row, and a mapping from keys in the Map to specific + * views. + * + * Binding data to views occurs in two phases. First, if a + * {@link android.widget.SimpleAdapter.ViewBinder} is available, + * {@link ViewBinder#setViewValue(android.view.View, Object, String)} + * is invoked. If the returned value is true, binding has occured. If the + * returned value is false and the view to bind is a TextView, + * {@link #setViewText(TextView, String)} is invoked. If the returned value + * is false and the view to bind is an ImageView, + * {@link #setViewImage(ImageView, int)} or {@link #setViewImage(ImageView, String)} is + * invoked. If no appropriate binding can be found, an {@link IllegalStateException} is thrown. + */ +public class SimpleAdapter extends BaseAdapter implements Filterable { + private int[] mTo; + private String[] mFrom; + private ViewBinder mViewBinder; + + private List<? extends Map<String, ?>> mData; + + private int mResource; + private int mDropDownResource; + private LayoutInflater mInflater; + + private SimpleFilter mFilter; + private ArrayList<Map<String, ?>> mUnfilteredData; + + /** + * Constructor + * + * @param context The context where the View associated with this SimpleAdapter is running + * @param data A List of Maps. Each entry in the List corresponds to one row in the list. The + * Maps contain the data for each row, and should include all the entries specified in + * "from" + * @param resource Resource identifier of a view layout that defines the views for this list + * item. The layout file should include at least those named views defined in "to" + * @param from A list of column names that will be added to the Map associated with each + * item. + * @param to The views that should display column in the "from" parameter. These should all be + * TextViews. The first N views in this list are given the values of the first N columns + * in the from parameter. + */ + public SimpleAdapter(Context context, List<? extends Map<String, ?>> data, + int resource, String[] from, int[] to) { + mData = data; + mResource = mDropDownResource = resource; + mFrom = from; + mTo = to; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + + /** + * @see android.widget.Adapter#getCount() + */ + public int getCount() { + return mData.size(); + } + + /** + * @see android.widget.Adapter#getItem(int) + */ + public Object getItem(int position) { + return mData.get(position); + } + + /** + * @see android.widget.Adapter#getItemId(int) + */ + public long getItemId(int position) { + return position; + } + + /** + * @see android.widget.Adapter#getView(int, View, ViewGroup) + */ + public View getView(int position, View convertView, ViewGroup parent) { + return createViewFromResource(position, convertView, parent, mResource); + } + + private View createViewFromResource(int position, View convertView, + ViewGroup parent, int resource) { + View v; + if (convertView == null) { + v = mInflater.inflate(resource, parent, false); + } else { + v = convertView; + } + bindView(position, v); + return v; + } + + /** + * <p>Sets the layout resource to create the drop down views.</p> + * + * @param resource the layout resource defining the drop down views + * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) + */ + public void setDropDownViewResource(int resource) { + this.mDropDownResource = resource; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return createViewFromResource(position, convertView, parent, mDropDownResource); + } + + private void bindView(int position, View view) { + final Map dataSet = mData.get(position); + if (dataSet == null) { + return; + } + + final String[] from = mFrom; + final int[] to = mTo; + final int len = to.length; + + for (int i = 0; i < len; i++) { + final View v = view.findViewById(to[i]); + if (v != null) { + final Object data = dataSet.get(from[i]); + String text = data == null ? "" : data.toString(); + if (text == null) { + text = ""; + } + + boolean bound = false; + if (mViewBinder != null) { + bound = mViewBinder.setViewValue(v, data, text); + } + + if (!bound) { + if (v instanceof TextView) { + setViewText((TextView) v, text); + } else if (v instanceof ImageView) { + if (data instanceof Integer) { + setViewImage((ImageView) v, (Integer) data); + } else { + setViewImage((ImageView) v, text); + } + } else { + throw new IllegalStateException(v.getClass().getName() + " is not a " + + " view that can be bounds by this SimpleAdapter"); + } + } + } + } + } + + /** + * Returns the {@link ViewBinder} used to bind data to views. + * + * @return a ViewBinder or null if the binder does not exist + * + * @see #setViewBinder(android.widget.SimpleAdapter.ViewBinder) + */ + public ViewBinder getViewBinder() { + return mViewBinder; + } + + /** + * Sets the binder used to bind data to views. + * + * @param viewBinder the binder used to bind data to views, can be null to + * remove the existing binder + * + * @see #getViewBinder() + */ + public void setViewBinder(ViewBinder viewBinder) { + mViewBinder = viewBinder; + } + + /** + * Called by bindView() to set the image for an ImageView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an ImageView. + * + * This method is called instead of {@link #setViewImage(ImageView, String)} + * if the supplied data is an int or Integer. + * + * @param v ImageView to receive an image + * @param value the value retrieved from the data set + * + * @see #setViewImage(ImageView, String) + */ + public void setViewImage(ImageView v, int value) { + v.setImageResource(value); + } + + /** + * Called by bindView() to set the image for an ImageView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an ImageView. + * + * By default, the value will be treated as an image resource. If the + * value cannot be used as an image resource, the value is used as an + * image Uri. + * + * This method is called instead of {@link #setViewImage(ImageView, int)} + * if the supplied data is not an int or Integer. + * + * @param v ImageView to receive an image + * @param value the value retrieved from the data set + * + * @see #setViewImage(ImageView, int) + */ + public void setViewImage(ImageView v, String value) { + try { + v.setImageResource(Integer.parseInt(value)); + } catch (NumberFormatException nfe) { + v.setImageURI(Uri.parse(value)); + } + } + + /** + * Called by bindView() to set the text for a TextView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an TextView. + * + * @param v TextView to receive text + * @param text the text to be set for the TextView + */ + public void setViewText(TextView v, String text) { + v.setText(text); + } + + public Filter getFilter() { + if (mFilter == null) { + mFilter = new SimpleFilter(); + } + return mFilter; + } + + /** + * This class can be used by external clients of SimpleAdapter to bind + * values to views. + * + * You should use this class to bind values to views that are not + * directly supported by SimpleAdapter or to change the way binding + * occurs for views supported by SimpleAdapter. + * + * @see SimpleAdapter#setViewImage(ImageView, int) + * @see SimpleAdapter#setViewImage(ImageView, String) + * @see SimpleAdapter#setViewText(TextView, String) + */ + public static interface ViewBinder { + /** + * Binds the specified data to the specified view. + * + * When binding is handled by this ViewBinder, this method must return true. + * If this method returns false, SimpleAdapter will attempts to handle + * the binding on its own. + * + * @param view the view to bind the data to + * @param data the data to bind to the view + * @param textRepresentation a safe String representation of the supplied data: + * it is either the result of data.toString() or an empty String but it + * is never null + * + * @return true if the data was bound to the view, false otherwise + */ + boolean setViewValue(View view, Object data, String textRepresentation); + } + + /** + * <p>An array filters constrains the content of the array adapter with + * a prefix. Each item that does not start with the supplied prefix + * is removed from the list.</p> + */ + private class SimpleFilter extends Filter { + + @Override + protected FilterResults performFiltering(CharSequence prefix) { + FilterResults results = new FilterResults(); + + if (mUnfilteredData == null) { + mUnfilteredData = new ArrayList<Map<String, ?>>(mData); + } + + if (prefix == null || prefix.length() == 0) { + ArrayList<Map<String, ?>> list = mUnfilteredData; + results.values = list; + results.count = list.size(); + } else { + String prefixString = prefix.toString().toLowerCase(); + + ArrayList<Map<String, ?>> unfilteredValues = mUnfilteredData; + int count = unfilteredValues.size(); + + ArrayList<Map<String, ?>> newValues = new ArrayList<Map<String, ?>>(count); + + for (int i = 0; i < count; i++) { + Map<String, ?> h = unfilteredValues.get(i); + if (h != null) { + + int len = mTo.length; + + for (int j=0; j<len; j++) { + String str = (String)h.get(mFrom[j]); + + String[] words = str.split(" "); + int wordCount = words.length; + + for (int k = 0; k < wordCount; k++) { + String word = words[k]; + + if (word.toLowerCase().startsWith(prefixString)) { + newValues.add(h); + break; + } + } + } + } + } + + results.values = newValues; + results.count = newValues.size(); + } + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + //noinspection unchecked + mData = (List<Map<String, ?>>) results.values; + if (results.count > 0) { + notifyDataSetChanged(); + } else { + notifyDataSetInvalidated(); + } + } + } +} diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java new file mode 100644 index 0000000..4d2fab3 --- /dev/null +++ b/core/java/android/widget/SimpleCursorAdapter.java @@ -0,0 +1,365 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.view.View; + +/** + * An easy adapter to map columns from a cursor to TextViews or ImageViews + * defined in an XML file. You can specify which columns you want, which + * views you want to display the columns, and the XML file that defines + * the appearance of these views. + * + * Binding occurs in two phases. First, if a + * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available, + * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} + * is invoked. If the returned value is true, binding has occured. If the + * returned value is false and the view to bind is a TextView, + * {@link #setViewText(TextView, String)} is invoked. If the returned value + * is false and the view to bind is an ImageView, + * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate + * binding can be found, an {@link IllegalStateException} is thrown. + * + * If this adapter is used with filtering, for instance in an + * {@link android.widget.AutoCompleteTextView}, you can use the + * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} and the + * {@link android.widget.FilterQueryProvider} interfaces + * to get control over the filtering process. You can refer to + * {@link #convertToString(android.database.Cursor)} and + * {@link #runQueryOnBackgroundThread(CharSequence)} for more information. + */ +public class SimpleCursorAdapter extends ResourceCursorAdapter { + /** + * A list of columns containing the data to bind to the UI. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mFrom; + /** + * A list of View ids representing the views to which the data must be bound. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mTo; + + private int mStringConversionColumn = -1; + private CursorToStringConverter mCursorToStringConverter; + private ViewBinder mViewBinder; + private String[] mOriginalFrom; + + /** + * Constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Thelayout file should include at least + * those named views defined in "to" + * @param c The database cursor. Can be null if the cursor is not available yet. + * @param from A list of column names representing the data to bind to the UI + * @param to The views that should display column in the "from" parameter. + * These should all be TextViews. The first N views in this list + * are given the values of the first N columns in the from + * parameter. + */ + public SimpleCursorAdapter(Context context, int layout, Cursor c, + String[] from, int[] to) { + super(context, layout, c); + mTo = to; + mOriginalFrom = from; + findColumns(from); + } + + /** + * Binds all of the field names passed into the "to" parameter of the + * constructor with their corresponding cursor columns as specified in the + * "from" parameter. + * + * Binding occurs in two phases. First, if a + * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available, + * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} + * is invoked. If the returned value is true, binding has occured. If the + * returned value is false and the view to bind is a TextView, + * {@link #setViewText(TextView, String)} is invoked. If the returned value is + * false and the view to bind is an ImageView, + * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate + * binding can be found, an {@link IllegalStateException} is thrown. + * + * @throws IllegalStateException if binding cannot occur + * + * @see android.widget.CursorAdapter#bindView(android.view.View, + * android.content.Context, android.database.Cursor) + * @see #getViewBinder() + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + * @see #setViewImage(ImageView, String) + * @see #setViewText(TextView, String) + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + for (int i = 0; i < mTo.length; i++) { + final View v = view.findViewById(mTo[i]); + if (v != null) { + String text = cursor.getString(mFrom[i]); + if (text == null) { + text = ""; + } + + boolean bound = false; + if (mViewBinder != null) { + bound = mViewBinder.setViewValue(v, cursor, mFrom[i]); + } + + if (!bound) { + if (v instanceof TextView) { + setViewText((TextView) v, text); + } else if (v instanceof ImageView) { + setViewImage((ImageView) v, text); + } else { + throw new IllegalStateException(v.getClass().getName() + " is not a " + + " view that can be bounds by this SimpleCursorAdapter"); + } + } + } + } + } + + /** + * Returns the {@link ViewBinder} used to bind data to views. + * + * @return a ViewBinder or null if the binder does not exist + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + */ + public ViewBinder getViewBinder() { + return mViewBinder; + } + + /** + * Sets the binder used to bind data to views. + * + * @param viewBinder the binder used to bind data to views, can be null to + * remove the existing binder + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #getViewBinder() + */ + public void setViewBinder(ViewBinder viewBinder) { + mViewBinder = viewBinder; + } + + /** + * Called by bindView() to set the image for an ImageView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an ImageView. + * + * By default, the value will be treated as an image resource. If the + * value cannot be used as an image resource, the value is used as an + * image Uri. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v ImageView to receive an image + * @param value the value retrieved from the cursor + */ + public void setViewImage(ImageView v, String value) { + try { + v.setImageResource(Integer.parseInt(value)); + } catch (NumberFormatException nfe) { + v.setImageURI(Uri.parse(value)); + } + } + + /** + * Called by bindView() to set the text for a TextView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an TextView. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v TextView to receive text + * @param text the text to be set for the TextView + */ + public void setViewText(TextView v, String text) { + v.setText(text); + } + + /** + * Return the index of the column used to get a String representation + * of the Cursor. + * + * @return a valid index in the current Cursor or -1 + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #setStringConversionColumn(int) + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public int getStringConversionColumn() { + return mStringConversionColumn; + } + + /** + * Defines the index of the column in the Cursor used to get a String + * representation of that Cursor. The column is used to convert the + * Cursor to a String only when the current CursorToStringConverter + * is null. + * + * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default + * conversion mechanism + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #getStringConversionColumn() + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public void setStringConversionColumn(int stringConversionColumn) { + mStringConversionColumn = stringConversionColumn; + } + + /** + * Returns the converter used to convert the filtering Cursor + * into a String. + * + * @return null if the converter does not exist or an instance of + * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public CursorToStringConverter getCursorToStringConverter() { + return mCursorToStringConverter; + } + + /** + * Sets the converter used to convert the filtering Cursor + * into a String. + * + * @param cursorToStringConverter the Cursor to String converter, or + * null to remove the converter + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) { + mCursorToStringConverter = cursorToStringConverter; + } + + /** + * Returns a CharSequence representation of the specified Cursor as defined + * by the current CursorToStringConverter. If no CursorToStringConverter + * has been set, the String conversion column is used instead. If the + * conversion column is -1, the returned String is empty if the cursor + * is null or Cursor.toString(). + * + * @param cursor the Cursor to convert to a CharSequence + * + * @return a non-null CharSequence representing the cursor + */ + @Override + public CharSequence convertToString(Cursor cursor) { + if (mCursorToStringConverter != null) { + return mCursorToStringConverter.convertToString(cursor); + } else if (mStringConversionColumn > -1) { + return cursor.getString(mStringConversionColumn); + } + + return super.convertToString(cursor); + } + + private void findColumns(String[] from) { + int i; + int count = from.length; + if (mFrom == null) { + mFrom = new int[count]; + } + if (mCursor != null) { + for (i = 0; i < count; i++) { + mFrom[i] = mCursor.getColumnIndexOrThrow(from[i]); + } + } else { + for (i = 0; i < count; i++) { + mFrom[i] = -1; + } + } + } + + @Override + public void changeCursor(Cursor c) { + super.changeCursor(c); + // rescan columns in case cursor layout is different + findColumns(mOriginalFrom); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to bind values fom the Cursor to views. + * + * You should use this class to bind values from the Cursor to views + * that are not directly supported by SimpleCursorAdapter or to + * change the way binding occurs for views supported by + * SimpleCursorAdapter. + * + * @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see SimpleCursorAdapter#setViewImage(ImageView, String) + * @see SimpleCursorAdapter#setViewText(TextView, String) + */ + public static interface ViewBinder { + /** + * Binds the Cursor column defined by the specified index to the specified view. + * + * When binding is handled by this ViewBinder, this method must return true. + * If this method returns false, SimpleCursorAdapter will attempts to handle + * the binding on its own. + * + * @param view the view to bind the data to + * @param cursor the cursor to get the data from + * @param columnIndex the column at which the data can be found in the cursor + * + * @return true if the data was bound to the view, false otherwise + */ + boolean setViewValue(View view, Cursor cursor, int columnIndex); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to define how the Cursor should be converted to a String. + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public static interface CursorToStringConverter { + /** + * Returns a CharSequence representing the specified Cursor. + * + * @param cursor the cursor for which a CharSequence representation + * is requested + * + * @return a non-null CharSequence representing the cursor + */ + CharSequence convertToString(Cursor cursor); + } + +} diff --git a/core/java/android/widget/SimpleCursorTreeAdapter.java b/core/java/android/widget/SimpleCursorTreeAdapter.java new file mode 100644 index 0000000..c456f56 --- /dev/null +++ b/core/java/android/widget/SimpleCursorTreeAdapter.java @@ -0,0 +1,241 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.view.View; + +/** + * An easy adapter to map columns from a cursor to TextViews or ImageViews + * defined in an XML file. You can specify which columns you want, which views + * you want to display the columns, and the XML file that defines the appearance + * of these views. Separate XML files for child and groups are possible. + * TextViews bind the values to their text property (see + * {@link TextView#setText(CharSequence)}). ImageViews bind the values to their + * image's Uri property (see {@link ImageView#setImageURI(android.net.Uri)}). + */ +public abstract class SimpleCursorTreeAdapter extends ResourceCursorTreeAdapter { + /** The indices of columns that contain data to display for a group. */ + private int[] mGroupFrom; + /** + * The View IDs that will display a group's data fetched from the + * corresponding column. + */ + private int[] mGroupTo; + + /** The indices of columns that contain data to display for a child. */ + private int[] mChildFrom; + /** + * The View IDs that will display a child's data fetched from the + * corresponding column. + */ + private int[] mChildTo; + + /** + * Constructor. + * + * @param context The context where the {@link ExpandableListView} + * associated with this {@link SimpleCursorTreeAdapter} is + * running + * @param cursor The database cursor + * @param collapsedGroupLayout The resource identifier of a layout file that + * defines the views for a collapsed group. The layout file + * should include at least those named views defined in groupTo. + * @param expandedGroupLayout The resource identifier of a layout file that + * defines the views for an expanded group. The layout file + * should include at least those named views defined in groupTo. + * @param groupFrom A list of column names that will be used to display the + * data for a group. + * @param groupTo The group views (from the group layouts) that should + * display column in the "from" parameter. These should all be + * TextViews or ImageViews. The first N views in this list are + * given the values of the first N columns in the from parameter. + * @param childLayout The resource identifier of a layout file that defines + * the views for a child (except the last). The layout file + * should include at least those named views defined in childTo. + * @param lastChildLayout The resource identifier of a layout file that + * defines the views for the last child within a group. The + * layout file should include at least those named views defined + * in childTo. + * @param childFrom A list of column names that will be used to display the + * data for a child. + * @param childTo The child views (from the child layouts) that should + * display column in the "from" parameter. These should all be + * TextViews or ImageViews. The first N views in this list are + * given the values of the first N columns in the from parameter. + */ + public SimpleCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout, + int expandedGroupLayout, String[] groupFrom, int[] groupTo, int childLayout, + int lastChildLayout, String[] childFrom, int[] childTo) { + super(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout, + lastChildLayout); + init(groupFrom, groupTo, childFrom, childTo); + } + + /** + * Constructor. + * + * @param context The context where the {@link ExpandableListView} + * associated with this {@link SimpleCursorTreeAdapter} is + * running + * @param cursor The database cursor + * @param collapsedGroupLayout The resource identifier of a layout file that + * defines the views for a collapsed group. The layout file + * should include at least those named views defined in groupTo. + * @param expandedGroupLayout The resource identifier of a layout file that + * defines the views for an expanded group. The layout file + * should include at least those named views defined in groupTo. + * @param groupFrom A list of column names that will be used to display the + * data for a group. + * @param groupTo The group views (from the group layouts) that should + * display column in the "from" parameter. These should all be + * TextViews or ImageViews. The first N views in this list are + * given the values of the first N columns in the from parameter. + * @param childLayout The resource identifier of a layout file that defines + * the views for a child. The layout file + * should include at least those named views defined in childTo. + * @param childFrom A list of column names that will be used to display the + * data for a child. + * @param childTo The child views (from the child layouts) that should + * display column in the "from" parameter. These should all be + * TextViews or ImageViews. The first N views in this list are + * given the values of the first N columns in the from parameter. + */ + public SimpleCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout, + int expandedGroupLayout, String[] groupFrom, int[] groupTo, + int childLayout, String[] childFrom, int[] childTo) { + super(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout); + init(groupFrom, groupTo, childFrom, childTo); + } + + /** + * Constructor. + * + * @param context The context where the {@link ExpandableListView} + * associated with this {@link SimpleCursorTreeAdapter} is + * running + * @param cursor The database cursor + * @param groupLayout The resource identifier of a layout file that defines + * the views for a group. The layout file should include at least + * those named views defined in groupTo. + * @param groupFrom A list of column names that will be used to display the + * data for a group. + * @param groupTo The group views (from the group layouts) that should + * display column in the "from" parameter. These should all be + * TextViews or ImageViews. The first N views in this list are + * given the values of the first N columns in the from parameter. + * @param childLayout The resource identifier of a layout file that defines + * the views for a child. The layout file should include at least + * those named views defined in childTo. + * @param childFrom A list of column names that will be used to display the + * data for a child. + * @param childTo The child views (from the child layouts) that should + * display column in the "from" parameter. These should all be + * TextViews or ImageViews. The first N views in this list are + * given the values of the first N columns in the from parameter. + */ + public SimpleCursorTreeAdapter(Context context, Cursor cursor, int groupLayout, + String[] groupFrom, int[] groupTo, int childLayout, String[] childFrom, + int[] childTo) { + super(context, cursor, groupLayout, childLayout); + init(groupFrom, groupTo, childFrom, childTo); + } + + private void init(String[] groupFromNames, int[] groupTo, String[] childFromNames, + int[] childTo) { + mGroupTo = groupTo; + + mChildTo = childTo; + + // Get the group cursor column indices, the child cursor column indices will come + // when needed + initGroupFromColumns(groupFromNames); + + // Get a temporary child cursor to init the column indices + if (getGroupCount() > 0) { + MyCursorHelper tmpCursorHelper = getChildrenCursorHelper(0, true); + if (tmpCursorHelper != null) { + initChildrenFromColumns(childFromNames, tmpCursorHelper.getCursor()); + deactivateChildrenCursorHelper(0); + } + } + } + + private void initFromColumns(Cursor cursor, String[] fromColumnNames, int[] fromColumns) { + for (int i = fromColumnNames.length - 1; i >= 0; i--) { + fromColumns[i] = cursor.getColumnIndexOrThrow(fromColumnNames[i]); + } + } + + private void initGroupFromColumns(String[] groupFromNames) { + mGroupFrom = new int[groupFromNames.length]; + initFromColumns(mGroupCursorHelper.getCursor(), groupFromNames, mGroupFrom); + } + + private void initChildrenFromColumns(String[] childFromNames, Cursor childCursor) { + mChildFrom = new int[childFromNames.length]; + initFromColumns(childCursor, childFromNames, mChildFrom); + } + + private void bindView(View view, Context context, Cursor cursor, int[] from, int[] to) { + for (int i = 0; i < to.length; i++) { + View v = view.findViewById(to[i]); + if (v != null) { + String text = cursor.getString(from[i]); + if (text == null) { + text = ""; + } + if (v instanceof TextView) { + ((TextView) v).setText(text); + } else if (v instanceof ImageView) { + setViewImage((ImageView) v, text); + } else { + throw new IllegalStateException("SimpleCursorAdapter can bind values only to" + + " TextView and ImageView!"); + } + } + } + } + + @Override + protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) { + bindView(view, context, cursor, mChildFrom, mChildTo); + } + + @Override + protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) { + bindView(view, context, cursor, mGroupFrom, mGroupTo); + } + + /** + * Called by bindView() to set the image for an ImageView. By default, the + * value will be treated as a Uri. Intended to be overridden by Adapters + * that need to filter strings retrieved from the database. + * + * @param v ImageView to receive an image + * @param value the value retrieved from the cursor + */ + protected void setViewImage(ImageView v, String value) { + try { + v.setImageResource(Integer.parseInt(value)); + } catch (NumberFormatException nfe) { + v.setImageURI(Uri.parse(value)); + } + } +} diff --git a/core/java/android/widget/SimpleExpandableListAdapter.java b/core/java/android/widget/SimpleExpandableListAdapter.java new file mode 100644 index 0000000..015c169 --- /dev/null +++ b/core/java/android/widget/SimpleExpandableListAdapter.java @@ -0,0 +1,301 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; + +import java.util.List; +import java.util.Map; + +/** + * An easy adapter to map static data to group and child views defined in an XML + * file. You can separately specify the data backing the group as a List of + * Maps. Each entry in the ArrayList corresponds to one group in the expandable + * list. The Maps contain the data for each row. You also specify an XML file + * that defines the views used to display a group, and a mapping from keys in + * the Map to specific views. This process is similar for a child, except it is + * one-level deeper so the data backing is specified as a List<List<Map>>, + * where the first List corresponds to the group of the child, the second List + * corresponds to the position of the child within the group, and finally the + * Map holds the data for that particular child. + */ +public class SimpleExpandableListAdapter extends BaseExpandableListAdapter { + private List<? extends Map<String, ?>> mGroupData; + private int mExpandedGroupLayout; + private int mCollapsedGroupLayout; + private String[] mGroupFrom; + private int[] mGroupTo; + + private List<? extends List<? extends Map<String, ?>>> mChildData; + private int mChildLayout; + private int mLastChildLayout; + private String[] mChildFrom; + private int[] mChildTo; + + private LayoutInflater mInflater; + + /** + * Constructor + * + * @param context The context where the {@link ExpandableListView} + * associated with this {@link SimpleExpandableListAdapter} is + * running + * @param groupData A List of Maps. Each entry in the List corresponds to + * one group in the list. The Maps contain the data for each + * group, and should include all the entries specified in + * "groupFrom" + * @param groupFrom A list of keys that will be fetched from the Map + * associated with each group. + * @param groupTo The group views that should display column in the + * "groupFrom" parameter. These should all be TextViews. The + * first N views in this list are given the values of the first N + * columns in the groupFrom parameter. + * @param groupLayout resource identifier of a view layout that defines the + * views for a group. The layout file should include at least + * those named views defined in "groupTo" + * @param childData A List of List of Maps. Each entry in the outer List + * corresponds to a group (index by group position), each entry + * in the inner List corresponds to a child within the group + * (index by child position), and the Map corresponds to the data + * for a child (index by values in the childFrom array). The Map + * contains the data for each child, and should include all the + * entries specified in "childFrom" + * @param childFrom A list of keys that will be fetched from the Map + * associated with each child. + * @param childTo The child views that should display column in the + * "childFrom" parameter. These should all be TextViews. The + * first N views in this list are given the values of the first N + * columns in the childFrom parameter. + * @param childLayout resource identifier of a view layout that defines the + * views for a child. The layout file should include at least + * those named views defined in "childTo" + */ + public SimpleExpandableListAdapter(Context context, + List<? extends Map<String, ?>> groupData, int groupLayout, + String[] groupFrom, int[] groupTo, + List<? extends List<? extends Map<String, ?>>> childData, + int childLayout, String[] childFrom, int[] childTo) { + this(context, groupData, groupLayout, groupLayout, groupFrom, groupTo, childData, + childLayout, childLayout, childFrom, childTo); + } + + /** + * Constructor + * + * @param context The context where the {@link ExpandableListView} + * associated with this {@link SimpleExpandableListAdapter} is + * running + * @param groupData A List of Maps. Each entry in the List corresponds to + * one group in the list. The Maps contain the data for each + * group, and should include all the entries specified in + * "groupFrom" + * @param groupFrom A list of keys that will be fetched from the Map + * associated with each group. + * @param groupTo The group views that should display column in the + * "groupFrom" parameter. These should all be TextViews. The + * first N views in this list are given the values of the first N + * columns in the groupFrom parameter. + * @param expandedGroupLayout resource identifier of a view layout that + * defines the views for an expanded group. The layout file + * should include at least those named views defined in "groupTo" + * @param collapsedGroupLayout resource identifier of a view layout that + * defines the views for a collapsed group. The layout file + * should include at least those named views defined in "groupTo" + * @param childData A List of List of Maps. Each entry in the outer List + * corresponds to a group (index by group position), each entry + * in the inner List corresponds to a child within the group + * (index by child position), and the Map corresponds to the data + * for a child (index by values in the childFrom array). The Map + * contains the data for each child, and should include all the + * entries specified in "childFrom" + * @param childFrom A list of keys that will be fetched from the Map + * associated with each child. + * @param childTo The child views that should display column in the + * "childFrom" parameter. These should all be TextViews. The + * first N views in this list are given the values of the first N + * columns in the childFrom parameter. + * @param childLayout resource identifier of a view layout that defines the + * views for a child. The layout file should include at least + * those named views defined in "childTo" + */ + public SimpleExpandableListAdapter(Context context, + List<? extends Map<String, ?>> groupData, int expandedGroupLayout, + int collapsedGroupLayout, String[] groupFrom, int[] groupTo, + List<? extends List<? extends Map<String, ?>>> childData, + int childLayout, String[] childFrom, int[] childTo) { + this(context, groupData, expandedGroupLayout, collapsedGroupLayout, + groupFrom, groupTo, childData, childLayout, childLayout, + childFrom, childTo); + } + + /** + * Constructor + * + * @param context The context where the {@link ExpandableListView} + * associated with this {@link SimpleExpandableListAdapter} is + * running + * @param groupData A List of Maps. Each entry in the List corresponds to + * one group in the list. The Maps contain the data for each + * group, and should include all the entries specified in + * "groupFrom" + * @param groupFrom A list of keys that will be fetched from the Map + * associated with each group. + * @param groupTo The group views that should display column in the + * "groupFrom" parameter. These should all be TextViews. The + * first N views in this list are given the values of the first N + * columns in the groupFrom parameter. + * @param expandedGroupLayout resource identifier of a view layout that + * defines the views for an expanded group. The layout file + * should include at least those named views defined in "groupTo" + * @param collapsedGroupLayout resource identifier of a view layout that + * defines the views for a collapsed group. The layout file + * should include at least those named views defined in "groupTo" + * @param childData A List of List of Maps. Each entry in the outer List + * corresponds to a group (index by group position), each entry + * in the inner List corresponds to a child within the group + * (index by child position), and the Map corresponds to the data + * for a child (index by values in the childFrom array). The Map + * contains the data for each child, and should include all the + * entries specified in "childFrom" + * @param childFrom A list of keys that will be fetched from the Map + * associated with each child. + * @param childTo The child views that should display column in the + * "childFrom" parameter. These should all be TextViews. The + * first N views in this list are given the values of the first N + * columns in the childFrom parameter. + * @param childLayout resource identifier of a view layout that defines the + * views for a child (unless it is the last child within a group, + * in which case the lastChildLayout is used). The layout file + * should include at least those named views defined in "childTo" + * @param lastChildLayout resource identifier of a view layout that defines + * the views for the last child within each group. The layout + * file should include at least those named views defined in + * "childTo" + */ + public SimpleExpandableListAdapter(Context context, + List<? extends Map<String, ?>> groupData, int expandedGroupLayout, + int collapsedGroupLayout, String[] groupFrom, int[] groupTo, + List<? extends List<? extends Map<String, ?>>> childData, + int childLayout, int lastChildLayout, String[] childFrom, + int[] childTo) { + mGroupData = groupData; + mExpandedGroupLayout = expandedGroupLayout; + mCollapsedGroupLayout = collapsedGroupLayout; + mGroupFrom = groupFrom; + mGroupTo = groupTo; + + mChildData = childData; + mChildLayout = childLayout; + mLastChildLayout = lastChildLayout; + mChildFrom = childFrom; + mChildTo = childTo; + + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public Object getChild(int groupPosition, int childPosition) { + return mChildData.get(groupPosition).get(childPosition); + } + + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { + View v; + if (convertView == null) { + v = newChildView(isLastChild, parent); + } else { + v = convertView; + } + bindView(v, mChildData.get(groupPosition).get(childPosition), mChildFrom, mChildTo); + return v; + } + + /** + * Instantiates a new View for a child. + * @param isLastChild Whether the child is the last child within its group. + * @param parent The eventual parent of this new View. + * @return A new child View + */ + public View newChildView(boolean isLastChild, ViewGroup parent) { + return mInflater.inflate((isLastChild) ? mLastChildLayout : mChildLayout, parent, false); + } + + private void bindView(View view, Map<String, ?> data, String[] from, int[] to) { + int len = to.length; + + for (int i = 0; i < len; i++) { + TextView v = (TextView)view.findViewById(to[i]); + if (v != null) { + v.setText((String)data.get(from[i])); + } + } + } + + public int getChildrenCount(int groupPosition) { + return mChildData.get(groupPosition).size(); + } + + public Object getGroup(int groupPosition) { + return mGroupData.get(groupPosition); + } + + public int getGroupCount() { + return mGroupData.size(); + } + + public long getGroupId(int groupPosition) { + return groupPosition; + } + + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { + View v; + if (convertView == null) { + v = newGroupView(isExpanded, parent); + } else { + v = convertView; + } + bindView(v, mGroupData.get(groupPosition), mGroupFrom, mGroupTo); + return v; + } + + /** + * Instantiates a new View for a group. + * @param isExpanded Whether the group is currently expanded. + * @param parent The eventual parent of this new View. + * @return A new group View + */ + public View newGroupView(boolean isExpanded, ViewGroup parent) { + return mInflater.inflate((isExpanded) ? mExpandedGroupLayout : mCollapsedGroupLayout, + parent, false); + } + + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + public boolean hasStableIds() { + return true; + } + +} diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java new file mode 100644 index 0000000..80d688e --- /dev/null +++ b/core/java/android/widget/Spinner.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.annotation.Widget; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + + +/** + * A view that displays one child at a time and lets the user pick among them. + * The items in the Spinner come from the {@link Adapter} associated with + * this view. + * + * @attr ref android.R.styleable#Spinner_prompt + */ +@Widget +public class Spinner extends AbsSpinner implements OnClickListener { + + private CharSequence mPrompt; + + public Spinner(Context context) { + this(context, null); + } + + public Spinner(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.spinnerStyle); + } + + public Spinner(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Spinner, defStyle, 0); + + mPrompt = a.getString(com.android.internal.R.styleable.Spinner_prompt); + + a.recycle(); + } + + @Override + public int getBaseline() { + View child = null; + + if (getChildCount() > 0) { + child = getChildAt(0); + } else if (mAdapter != null && mAdapter.getCount() > 0) { + child = makeAndAddView(0); + // TODO: We should probably put the child in the recycler + } + + if (child != null) { + return child.getTop() + child.getBaseline(); + } else { + return -1; + } + } + + /** + * <p>A spinner does not support item click events. Calling this method + * will raise an exception.</p> + * + * @param l this listener will be ignored + */ + @Override + public void setOnItemClickListener(OnItemClickListener l) { + throw new RuntimeException("setOnItemClickListener cannot be used with a spinner."); + } + + /** + * @see android.view.View#onLayout(boolean,int,int,int,int) + * + * Creates and positions all views + * + */ + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mInLayout = true; + layout(0, false); + mInLayout = false; + } + + /** + * Creates and positions all views for this Spinner. + * + * @param delta Change in the selected position. +1 moves selection is moving to the right, + * so views are scrolling to the left. -1 means selection is moving to the left. + */ + @Override + void layout(int delta, boolean animate) { + int childrenLeft = mSpinnerPadding.left; + int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right; + + if (mDataChanged) { + handleDataChanged(); + } + + // Handle the empty set by removing all views + if (mItemCount == 0) { + resetList(); + return; + } + + if (mNextSelectedPosition >= 0) { + setSelectedPositionInt(mNextSelectedPosition); + } + + recycleAllViews(); + + // Clear out old views + removeAllViewsInLayout(); + + // Make selected view and center it + mFirstPosition = mSelectedPosition; + View sel = makeAndAddView(mSelectedPosition); + int width = sel.getMeasuredWidth(); + int selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2); + sel.offsetLeftAndRight(selectedOffset); + + // Flush any cached views that did not get reused above + mRecycler.clear(); + + invalidate(); + + checkSelectionChanged(); + + mDataChanged = false; + mNeedSync = false; + setNextSelectedPositionInt(mSelectedPosition); + } + + /** + * Obtain a view, either by pulling an existing view from the recycler or + * by getting a new one from the adapter. If we are animating, make sure + * there is enough information in the view's layout parameters to animate + * from the old to new positions. + * + * @param position Position in the spinner for the view to obtain + * @return A view that has been added to the spinner + */ + private View makeAndAddView(int position) { + + View child; + + if (!mDataChanged) { + child = mRecycler.get(position); + if (child != null) { + // Position the view + setUpChild(child); + + return child; + } + } + + // Nothing found in the recycler -- ask the adapter for a view + child = mAdapter.getView(position, null, this); + + // Position the view + setUpChild(child); + + return child; + } + + + + /** + * Helper for makeAndAddView to set the position of a view + * and fill out its layout paramters. + * + * @param child The view to position + */ + private void setUpChild(View child) { + + // Respect layout params that are already in the view. Otherwise + // make some up... + ViewGroup.LayoutParams lp = child.getLayoutParams(); + if (lp == null) { + lp = generateDefaultLayoutParams(); + } + + addViewInLayout(child, 0, lp); + + child.setSelected(hasFocus()); + + // Get measure specs + int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, + mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height); + int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, + mSpinnerPadding.left + mSpinnerPadding.right, lp.width); + + // Measure child + child.measure(childWidthSpec, childHeightSpec); + + int childLeft; + int childRight; + + // Position vertically based on gravity setting + int childTop = mSpinnerPadding.top + + ((mMeasuredHeight - mSpinnerPadding.bottom - + mSpinnerPadding.top - child.getMeasuredHeight()) / 2); + int childBottom = childTop + child.getMeasuredHeight(); + + int width = child.getMeasuredWidth(); + childLeft = 0; + childRight = childLeft + width; + + child.layout(childLeft, childTop, childRight, childBottom); + } + + @Override + public boolean performClick() { + boolean handled = super.performClick(); + + if (!handled) { + handled = true; + Context context = getContext(); + + final DropDownAdapter adapter = new DropDownAdapter(getAdapter()); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + if (mPrompt != null) { + builder.setTitle(mPrompt); + } + builder.setSingleChoiceItems(adapter, getSelectedItemPosition(), this).show(); + } + + return handled; + } + + public void onClick(DialogInterface dialog, int which) { + setSelection(which); + dialog.dismiss(); + } + + /** + * Sets the prompt to display when the dialog is shown. + * @param prompt the prompt to set + */ + public void setPrompt(CharSequence prompt) { + mPrompt = prompt; + } + + /** + * Sets the prompt to display when the dialog is shown. + * @param promptId the resource ID of the prompt to display when the dialog is shown + */ + public void setPromptId(int promptId) { + mPrompt = getContext().getText(promptId); + } + + /** + * @return The prompt to display when the dialog is shown + */ + public CharSequence getPrompt() { + return mPrompt; + } + + /** + * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance + * into a ListAdapter.</p> + */ + private static class DropDownAdapter implements ListAdapter, SpinnerAdapter { + private SpinnerAdapter mAdapter; + + /** + * <p>Creates a new ListAddapter wrapper for the specified adapter.</p> + * + * @param adapter the Adapter to transform into a ListAdapter + */ + public DropDownAdapter(SpinnerAdapter adapter) { + this.mAdapter = adapter; + } + + public int getCount() { + return mAdapter == null ? 0 : mAdapter.getCount(); + } + + public Object getItem(int position) { + return mAdapter == null ? null : mAdapter.getItem(position); + } + + public long getItemId(int position) { + return mAdapter == null ? -1 : mAdapter.getItemId(position); + } + + public View getView(int position, View convertView, ViewGroup parent) { + return getDropDownView(position, convertView, parent); + } + + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return mAdapter == null ? null : + mAdapter.getDropDownView(position, convertView, parent); + } + + public boolean hasStableIds() { + return mAdapter != null && mAdapter.hasStableIds(); + } + + public void registerDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) { + mAdapter.registerDataSetObserver(observer); + } + } + + public void unregisterDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(observer); + } + } + + /** + * <p>Always returns false.</p> + * + * @return false + */ + public boolean areAllItemsEnabled() { + return true; + } + + /** + * <p>Always returns false.</p> + * + * @return false + */ + public boolean isEnabled(int position) { + return true; + } + + public int getItemViewType(int position) { + return 0; + } + + public int getViewTypeCount() { + return 1; + } + + public boolean isEmpty() { + return getCount() == 0; + } + } +} diff --git a/core/java/android/widget/SpinnerAdapter.java b/core/java/android/widget/SpinnerAdapter.java new file mode 100644 index 0000000..91504cf --- /dev/null +++ b/core/java/android/widget/SpinnerAdapter.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.view.View; +import android.view.ViewGroup; + +/** + * Extended {@link Adapter} that is the bridge between a + * {@link android.widget.Spinner} and its data. A spinner adapter allows to + * define two different views: one that shows the data in the spinner itself and + * one that shows the data in the drop down list when the spinner is pressed.</p> + */ +public interface SpinnerAdapter extends Adapter { + /** + * <p>Get a {@link android.view.View} that displays in the drop down popup + * the data at the specified position in the data set.</p> + * + * @param position index of the item whose view we want. + * @param convertView the old view to reuse, if possible. Note: You should + * check that this view is non-null and of an appropriate type before + * using. If it is not possible to convert this view to display the + * correct data, this method can create a new view. + * @param parent the parent that this view will eventually be attached to + * @return a {@link android.view.View} corresponding to the data at the + * specified position. + */ + public View getDropDownView(int position, View convertView, ViewGroup parent); +} diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java new file mode 100644 index 0000000..da4a077 --- /dev/null +++ b/core/java/android/widget/TabHost.java @@ -0,0 +1,632 @@ +/* + * 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 android.widget; + +import android.app.LocalActivityManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.Window; +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Container for a tabbed window view. This object holds two children: a set of tab labels that the + * user clicks to select a specific tab, and a FrameLayout object that displays the contents of that + * page. The individual elements are typically controlled using this container object, rather than + * setting values on the child elements themselves. + */ +public class TabHost extends FrameLayout implements ViewTreeObserver.OnTouchModeChangeListener { + + private TabWidget mTabWidget; + private FrameLayout mTabContent; + private List<TabSpec> mTabSpecs = new ArrayList<TabSpec>(2); + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int mCurrentTab = -1; + private View mCurrentView = null; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected LocalActivityManager mLocalActivityManager = null; + private OnTabChangeListener mOnTabChangeListener; + private OnKeyListener mTabKeyListener; + + public TabHost(Context context) { + super(context); + initTabHost(); + } + + public TabHost(Context context, AttributeSet attrs) { + super(context, attrs); + initTabHost(); + } + + private final void initTabHost() { + setFocusableInTouchMode(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + + mCurrentTab = -1; + mCurrentView = null; + } + + /** + * Get a new {@link TabSpec} associated with this tab host. + * @param tag required tag of tab. + */ + public TabSpec newTabSpec(String tag) { + return new TabSpec(tag); + } + + + + /** + * <p>Call setup() before adding tabs if loading TabHost using findViewById(). <i><b>However</i></b>: You do + * not need to call setup() after getTabHost() in {@link android.app.TabActivity TabActivity}. + * Example:</p> +<pre>mTabHost = (TabHost)findViewById(R.id.tabhost); +mTabHost.setup(); +mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); + */ + public void setup() { + mTabWidget = (TabWidget) findViewById(com.android.internal.R.id.tabs); + if (mTabWidget == null) { + throw new RuntimeException( + "Your TabHost must have a TabWidget whose id attribute is 'android.R.id.tabs'"); + } + + // KeyListener to attach to all tabs. Detects non-navigation keys + // and relays them to the tab content. + mTabKeyListener = new OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_ENTER: + return false; + + } + mTabContent.requestFocus(View.FOCUS_FORWARD); + return mTabContent.dispatchKeyEvent(event); + } + + }; + + mTabWidget.setTabSelectionListener(new TabWidget.OnTabSelectionChanged() { + public void onTabSelectionChanged(int tabIndex, boolean clicked) { + setCurrentTab(tabIndex); + if (clicked) { + mTabContent.requestFocus(View.FOCUS_FORWARD); + } + } + }); + + mTabContent = (FrameLayout) findViewById(com.android.internal.R.id.tabcontent); + if (mTabContent == null) { + throw new RuntimeException( + "Your TabHost must have a FrameLayout whose id attribute is 'android.R.id.tabcontent'"); + } + } + + /** + * If you are using {@link TabSpec#setContent(android.content.Intent)}, this + * must be called since the activityGroup is needed to launch the local activity. + * + * This is done for you if you extend {@link android.app.TabActivity}. + * @param activityGroup Used to launch activities for tab content. + */ + public void setup(LocalActivityManager activityGroup) { + setup(); + mLocalActivityManager = activityGroup; + } + + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + final ViewTreeObserver treeObserver = getViewTreeObserver(); + if (treeObserver != null) { + treeObserver.addOnTouchModeChangeListener(this); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + final ViewTreeObserver treeObserver = getViewTreeObserver(); + if (treeObserver != null) { + treeObserver.removeOnTouchModeChangeListener(this); + } + } + + /** + * {@inheritDoc} + */ + public void onTouchModeChanged(boolean isInTouchMode) { + if (!isInTouchMode) { + // leaving touch mode.. if nothing has focus, let's give it to + // the indicator of the current tab + if (!mCurrentView.hasFocus() || mCurrentView.isFocused()) { + mTabWidget.getChildAt(mCurrentTab).requestFocus(); + } + } + } + + /** + * Add a tab. + * @param tabSpec Specifies how to create the indicator and content. + */ + public void addTab(TabSpec tabSpec) { + + if (tabSpec.mIndicatorStrategy == null) { + throw new IllegalArgumentException("you must specify a way to create the tab indicator."); + } + + if (tabSpec.mContentStrategy == null) { + throw new IllegalArgumentException("you must specify a way to create the tab content"); + } + View tabIndicator = tabSpec.mIndicatorStrategy.createIndicatorView(); + tabIndicator.setOnKeyListener(mTabKeyListener); + mTabWidget.addView(tabIndicator); + mTabSpecs.add(tabSpec); + + if (mCurrentTab == -1) { + setCurrentTab(0); + } + } + + + /** + * Removes all tabs from the tab widget associated with this tab host. + */ + public void clearAllTabs() { + mTabWidget.removeAllViews(); + initTabHost(); + mTabContent.removeAllViews(); + mTabSpecs.clear(); + requestLayout(); + invalidate(); + } + + public TabWidget getTabWidget() { + return mTabWidget; + } + + public int getCurrentTab() { + return mCurrentTab; + } + + public String getCurrentTabTag() { + if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) { + return mTabSpecs.get(mCurrentTab).getTag(); + } + return null; + } + + public View getCurrentTabView() { + if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) { + return mTabWidget.getChildAt(mCurrentTab); + } + return null; + } + + public View getCurrentView() { + return mCurrentView; + } + + public void setCurrentTabByTag(String tag) { + int i; + for (i = 0; i < mTabSpecs.size(); i++) { + if (mTabSpecs.get(i).getTag().equals(tag)) { + setCurrentTab(i); + break; + } + } + } + + /** + * Get the FrameLayout which holds tab content + */ + public FrameLayout getTabContentView() { + return mTabContent; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + final boolean handled = super.dispatchKeyEvent(event); + + // unhandled key ups change focus to tab indicator for embedded activities + // when there is nothing that will take focus from default focus searching + if (!handled + && (event.getAction() == KeyEvent.ACTION_DOWN) + && (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) + && (mCurrentView.isRootNamespace()) + && (mCurrentView.hasFocus()) + && (mCurrentView.findFocus().focusSearch(View.FOCUS_UP) == null)) { + mTabWidget.getChildAt(mCurrentTab).requestFocus(); + playSoundEffect(SoundEffectConstants.NAVIGATION_UP); + return true; + } + return handled; + } + + + @Override + public void dispatchWindowFocusChanged(boolean hasFocus) { + mCurrentView.dispatchWindowFocusChanged(hasFocus); + } + + public void setCurrentTab(int index) { + if (index < 0 || index >= mTabSpecs.size()) { + return; + } + + if (index == mCurrentTab) { + return; + } + + // notify old tab content + if (mCurrentTab != -1) { + mTabSpecs.get(mCurrentTab).mContentStrategy.tabClosed(); + } + + mCurrentTab = index; + final TabHost.TabSpec spec = mTabSpecs.get(index); + + // Call the tab widget's focusCurrentTab(), instead of just + // selecting the tab. + mTabWidget.focusCurrentTab(mCurrentTab); + + // tab content + mCurrentView = spec.mContentStrategy.getContentView(); + + if (mCurrentView.getParent() == null) { + mTabContent + .addView( + mCurrentView, + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + } + + if (!mTabWidget.hasFocus()) { + // if the tab widget didn't take focus (likely because we're in touch mode) + // give the current tab content view a shot + mCurrentView.requestFocus(); + } + + //mTabContent.requestFocus(View.FOCUS_FORWARD); + invokeOnTabChangeListener(); + } + + /** + * Register a callback to be invoked when the selected state of any of the items + * in this list changes + * @param l + * The callback that will run + */ + public void setOnTabChangedListener(OnTabChangeListener l) { + mOnTabChangeListener = l; + } + + private void invokeOnTabChangeListener() { + if (mOnTabChangeListener != null) { + mOnTabChangeListener.onTabChanged(getCurrentTabTag()); + } + } + + /** + * Interface definition for a callback to be invoked when tab changed + */ + public interface OnTabChangeListener { + void onTabChanged(String tabId); + } + + + /** + * Makes the content of a tab when it is selected. Use this if your tab + * content needs to be created on demand, i.e. you are not showing an + * existing view or starting an activity. + */ + public interface TabContentFactory { + /** + * Callback to make the tab contents + * + * @param tag + * Which tab was selected. + * @return The view to distplay the contents of the selected tab. + */ + View createTabContent(String tag); + } + + + /** + * A tab has a tab indictor, content, and a tag that is used to keep + * track of it. This builder helps choose among these options. + * + * For the tab indicator, your choices are: + * 1) set a label + * 2) set a label and an icon + * + * For the tab content, your choices are: + * 1) the id of a {@link View} + * 2) a {@link TabContentFactory} that creates the {@link View} content. + * 3) an {@link Intent} that launches an {@link android.app.Activity}. + */ + public class TabSpec { + + private String mTag; + + private IndicatorStrategy mIndicatorStrategy; + private ContentStrategy mContentStrategy; + + private TabSpec(String tag) { + mTag = tag; + } + + /** + * Specify a label as the tab indicator. + */ + public TabSpec setIndicator(CharSequence label) { + mIndicatorStrategy = new LabelIndicatorStrategy(label); + return this; + } + + /** + * Specify a label and icon as the tab indicator. + */ + public TabSpec setIndicator(CharSequence label, Drawable icon) { + mIndicatorStrategy = new LabelAndIconIndicatorStategy(label, icon); + return this; + } + + /** + * Specify the id of the view that should be used as the content + * of the tab. + */ + public TabSpec setContent(int viewId) { + mContentStrategy = new ViewIdContentStrategy(viewId); + return this; + } + + /** + * Specify a {@link android.widget.TabHost.TabContentFactory} to use to + * create the content of the tab. + */ + public TabSpec setContent(TabContentFactory contentFactory) { + mContentStrategy = new FactoryContentStrategy(mTag, contentFactory); + return this; + } + + /** + * Specify an intent to use to launch an activity as the tab content. + */ + public TabSpec setContent(Intent intent) { + mContentStrategy = new IntentContentStrategy(mTag, intent); + return this; + } + + + String getTag() { + return mTag; + } + } + + /** + * Specifies what you do to create a tab indicator. + */ + private static interface IndicatorStrategy { + + /** + * Return the view for the indicator. + */ + View createIndicatorView(); + } + + /** + * Specifies what you do to manage the tab content. + */ + private static interface ContentStrategy { + + /** + * Return the content view. The view should may be cached locally. + */ + View getContentView(); + + /** + * Perhaps do something when the tab associated with this content has + * been closed (i.e make it invisible, or remove it). + */ + void tabClosed(); + } + + /** + * How to create a tab indicator that just has a label. + */ + private class LabelIndicatorStrategy implements IndicatorStrategy { + + private final CharSequence mLabel; + + private LabelIndicatorStrategy(CharSequence label) { + mLabel = label; + } + + public View createIndicatorView() { + LayoutInflater inflater = + (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View tabIndicator = inflater.inflate(R.layout.tab_indicator, + mTabWidget, // tab widget is the parent + false); // no inflate params + + final TextView tv = (TextView) tabIndicator.findViewById(R.id.title); + tv.setText(mLabel); + + return tabIndicator; + } + } + + /** + * How we create a tab indicator that has a label and an icon + */ + private class LabelAndIconIndicatorStategy implements IndicatorStrategy { + + private final CharSequence mLabel; + private final Drawable mIcon; + + private LabelAndIconIndicatorStategy(CharSequence label, Drawable icon) { + mLabel = label; + mIcon = icon; + } + + public View createIndicatorView() { + LayoutInflater inflater = + (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View tabIndicator = inflater.inflate(R.layout.tab_indicator, + mTabWidget, // tab widget is the parent + false); // no inflate params + + final TextView tv = (TextView) tabIndicator.findViewById(R.id.title); + tv.setText(mLabel); + + final ImageView iconView = (ImageView) tabIndicator.findViewById(R.id.icon); + iconView.setImageDrawable(mIcon); + + return tabIndicator; + } + } + + /** + * How to create the tab content via a view id. + */ + private class ViewIdContentStrategy implements ContentStrategy { + + private final View mView; + + private ViewIdContentStrategy(int viewId) { + mView = mTabContent.findViewById(viewId); + if (mView != null) { + mView.setVisibility(View.GONE); + } else { + throw new RuntimeException("Could not create tab content because " + + "could not find view with id " + viewId); + } + } + + public View getContentView() { + mView.setVisibility(View.VISIBLE); + return mView; + } + + public void tabClosed() { + mView.setVisibility(View.GONE); + } + } + + /** + * How tab content is managed using {@link TabContentFactory}. + */ + private class FactoryContentStrategy implements ContentStrategy { + private View mTabContent; + private final CharSequence mTag; + private TabContentFactory mFactory; + + public FactoryContentStrategy(CharSequence tag, TabContentFactory factory) { + mTag = tag; + mFactory = factory; + } + + public View getContentView() { + if (mTabContent == null) { + mTabContent = mFactory.createTabContent(mTag.toString()); + } + mTabContent.setVisibility(View.VISIBLE); + return mTabContent; + } + + public void tabClosed() { + mTabContent.setVisibility(View.INVISIBLE); + } + } + + /** + * How tab content is managed via an {@link Intent}: the content view is the + * decorview of the launched activity. + */ + private class IntentContentStrategy implements ContentStrategy { + + private final String mTag; + private final Intent mIntent; + + private View mLaunchedView; + + private IntentContentStrategy(String tag, Intent intent) { + mTag = tag; + mIntent = intent; + } + + public View getContentView() { + if (mLocalActivityManager == null) { + throw new IllegalStateException("Did you forget to call 'public void setup(LocalActivityManager activityGroup)'?"); + } + final Window w = mLocalActivityManager.startActivity( + mTag, mIntent); + final View wd = w != null ? w.getDecorView() : null; + if (mLaunchedView != wd && mLaunchedView != null) { + if (mLaunchedView.getParent() != null) { + mTabContent.removeView(mLaunchedView); + } + } + mLaunchedView = wd; + + // XXX Set FOCUS_AFTER_DESCENDANTS on embedded activies for now so they can get + // focus if none of their children have it. They need focus to be able to + // display menu items. + // + // Replace this with something better when Bug 628886 is fixed... + // + if (mLaunchedView != null) { + mLaunchedView.setVisibility(View.VISIBLE); + mLaunchedView.setFocusableInTouchMode(true); + ((ViewGroup) mLaunchedView).setDescendantFocusability( + FOCUS_AFTER_DESCENDANTS); + } + return mLaunchedView; + } + + public void tabClosed() { + if (mLaunchedView != null) { + mLaunchedView.setVisibility(View.GONE); + } + } + } + +} diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java new file mode 100644 index 0000000..20cddcb --- /dev/null +++ b/core/java/android/widget/TabWidget.java @@ -0,0 +1,289 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnFocusChangeListener; + + + +/** + * + * Displays a list of tab labels representing each page in the parent's tab + * collection. The container object for this widget is + * {@link android.widget.TabHost TabHost}. When the user selects a tab, this + * object sends a message to the parent container, TabHost, to tell it to switch + * the displayed page. You typically won't use many methods directly on this + * object. The container TabHost is used to add labels, add the callback + * handler, and manage callbacks. You might call this object to iterate the list + * of tabs, or to tweak the layout of the tab list, but most methods should be + * called on the containing TabHost object. + */ +public class TabWidget extends LinearLayout implements OnFocusChangeListener { + + + private OnTabSelectionChanged mSelectionChangedListener; + private int mSelectedTab = 0; + private Drawable mBottomLeftStrip; + private Drawable mBottomRightStrip; + private boolean mStripMoved; + + public TabWidget(Context context) { + this(context, null); + } + + public TabWidget(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.tabWidgetStyle); + } + + public TabWidget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs); + initTabWidget(); + + TypedArray a = + context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TabWidget, + defStyle, 0); + + a.recycle(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mStripMoved = true; + super.onSizeChanged(w, h, oldw, oldh); + } + + private void initTabWidget() { + setOrientation(LinearLayout.HORIZONTAL); + mBottomLeftStrip = mContext.getResources().getDrawable( + com.android.internal.R.drawable.tab_bottom_left); + mBottomRightStrip = mContext.getResources().getDrawable( + com.android.internal.R.drawable.tab_bottom_right); + // Deal with focus, as we don't want the focus to go by default + // to a tab other than the current tab + setFocusable(true); + setOnFocusChangeListener(this); + } + + @Override + public void childDrawableStateChanged(View child) { + if (child == getChildAt(mSelectedTab)) { + // To make sure that the bottom strip is redrawn + invalidate(); + } + super.childDrawableStateChanged(child); + } + + @Override + public void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + View selectedChild = getChildAt(mSelectedTab); + + mBottomLeftStrip.setState(selectedChild.getDrawableState()); + mBottomRightStrip.setState(selectedChild.getDrawableState()); + + if (mStripMoved) { + Rect selBounds = new Rect(); // Bounds of the selected tab indicator + selBounds.left = selectedChild.getLeft(); + selBounds.right = selectedChild.getRight(); + final int myHeight = getHeight(); + mBottomLeftStrip.setBounds( + Math.min(0, selBounds.left + - mBottomLeftStrip.getIntrinsicWidth()), + myHeight - mBottomLeftStrip.getIntrinsicHeight(), + selBounds.left, + getHeight()); + mBottomRightStrip.setBounds( + selBounds.right, + myHeight - mBottomRightStrip.getIntrinsicHeight(), + Math.max(getWidth(), + selBounds.right + mBottomRightStrip.getIntrinsicWidth()), + myHeight); + mStripMoved = false; + } + + mBottomLeftStrip.draw(canvas); + mBottomRightStrip.draw(canvas); + } + + /** + * Sets the current tab. + * This method is used to bring a tab to the front of the Widget, + * and is used to post to the rest of the UI that a different tab + * has been brought to the foreground. + * + * Note, this is separate from the traditional "focus" that is + * employed from the view logic. + * + * For instance, if we have a list in a tabbed view, a user may be + * navigating up and down the list, moving the UI focus (orange + * highlighting) through the list items. The cursor movement does + * not effect the "selected" tab though, because what is being + * scrolled through is all on the same tab. The selected tab only + * changes when we navigate between tabs (moving from the list view + * to the next tabbed view, in this example). + * + * To move both the focus AND the selected tab at once, please use + * {@link #setCurrentTab}. Normally, the view logic takes care of + * adjusting the focus, so unless you're circumventing the UI, + * you'll probably just focus your interest here. + * + * @param index The tab that you want to indicate as the selected + * tab (tab brought to the front of the widget) + * + * @see #focusCurrentTab + */ + public void setCurrentTab(int index) { + if (index < 0 || index >= getChildCount()) { + return; + } + + getChildAt(mSelectedTab).setSelected(false); + mSelectedTab = index; + getChildAt(mSelectedTab).setSelected(true); + mStripMoved = true; + } + + /** + * Sets the current tab and focuses the UI on it. + * This method makes sure that the focused tab matches the selected + * tab, normally at {@link #setCurrentTab}. Normally this would not + * be an issue if we go through the UI, since the UI is responsible + * for calling TabWidget.onFocusChanged(), but in the case where we + * are selecting the tab programmatically, we'll need to make sure + * focus keeps up. + * + * @param index The tab that you want focused (highlighted in orange) + * and selected (tab brought to the front of the widget) + * + * @see #setCurrentTab + */ + public void focusCurrentTab(int index) { + final int oldTab = mSelectedTab; + + // set the tab + setCurrentTab(index); + + // change the focus if applicable. + if (oldTab != index) { + getChildAt(index).requestFocus(); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + int count = getChildCount(); + + for (int i=0; i<count; i++) { + View child = getChildAt(i); + child.setEnabled(enabled); + } + } + + @Override + public void addView(View child) { + if (child.getLayoutParams() == null) { + final LinearLayout.LayoutParams lp = new LayoutParams( + 0, + ViewGroup.LayoutParams.WRAP_CONTENT, 1); + lp.setMargins(0, 0, 0, 0); + child.setLayoutParams(lp); + } + + // Ensure you can navigate to the tab with the keyboard, and you can touch it + child.setFocusable(true); + child.setClickable(true); + + super.addView(child); + + // TODO: detect this via geometry with a tabwidget listener rather + // than potentially interfere with the view's listener + child.setOnClickListener(new TabClickListener(getChildCount() - 1)); + child.setOnFocusChangeListener(this); + } + + + + + /** + * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator. + */ + void setTabSelectionListener(OnTabSelectionChanged listener) { + mSelectionChangedListener = listener; + } + + public void onFocusChange(View v, boolean hasFocus) { + if (v == this && hasFocus) { + getChildAt(mSelectedTab).requestFocus(); + return; + } + + if (hasFocus) { + int i = 0; + while (i < getChildCount()) { + if (getChildAt(i) == v) { + setCurrentTab(i); + mSelectionChangedListener.onTabSelectionChanged(i, false); + break; + } + i++; + } + } + } + + // registered with each tab indicator so we can notify tab host + private class TabClickListener implements OnClickListener { + + private final int mTabIndex; + + private TabClickListener(int tabIndex) { + mTabIndex = tabIndex; + } + + public void onClick(View v) { + mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true); + } + } + + /** + * Let {@link TabHost} know that the user clicked on a tab indicator. + */ + static interface OnTabSelectionChanged { + /** + * Informs the TabHost which tab was selected. It also indicates + * if the tab was clicked/pressed or just focused into. + * + * @param tabIndex index of the tab that was selected + * @param clicked whether the selection changed due to a touch/click + * or due to focus entering the tab through navigation. Pass true + * if it was due to a press/click and false otherwise. + */ + void onTabSelectionChanged(int tabIndex, boolean clicked); + } + +} + diff --git a/core/java/android/widget/TableLayout.java b/core/java/android/widget/TableLayout.java new file mode 100644 index 0000000..d72ffb1 --- /dev/null +++ b/core/java/android/widget/TableLayout.java @@ -0,0 +1,755 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import com.android.internal.R; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.SparseBooleanArray; +import android.view.View; +import android.view.ViewGroup; + +import java.util.regex.Pattern; + +/** + * <p>A layout that arranges its children into rows and columns. + * A TableLayout consists of a number of {@link android.widget.TableRow} objects, + * each defining a row (actually, you can have other children, which will be + * explained below). TableLayout containers do not display border lines for + * their rows, columns, or cells. Each row has zero or more cells; each cell can + * hold one {@link android.view.View View} object. The table has as many columns + * as the row with the most cells. A table can leave cells empty. Cells can span + * columns, as they can in HTML.</p> + * + * <p>The width of a column is defined by the row with the widest cell in that + * column. However, a TableLayout can specify certain columns as shrinkable or + * stretchable by calling + * {@link #setColumnShrinkable(int, boolean) setColumnShrinkable()} + * or {@link #setColumnStretchable(int, boolean) setColumnStretchable()}. If + * marked as shrinkable, the column width can be shrunk to fit the table into + * its parent object. If marked as stretchable, it can expand in width to fit + * any extra space. The total width of the table is defined by its parent + * container. It is important to remember that a column can be both shrinkable + * and stretchable. In such a situation, the column will change its size to + * always use up the available space, but never more. Finally, you can hide a + * column by calling + * {@link #setColumnCollapsed(int,boolean) setColumnCollapsed()}.</p> + * + * <p>The children of a TableLayout cannot specify the <code>layout_width</code> + * attribute. Width is always <code>FILL_PARENT</code>. However, the + * <code>layout_height</code> attribute can be defined by a child; default value + * is {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}. If the child + * is a {@link android.widget.TableRow}, then the height is always + * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p> + * + * <p> Cells must be added to a row in increasing column order, both in code and + * XML. Column numbers are zero-based. If you don't specify a column number for + * a child cell, it will autoincrement to the next available column. If you skip + * a column number, it will be considered an empty cell in that row. See the + * TableLayout examples in ApiDemos for examples of creating tables in XML.</p> + * + * <p>Although the typical child of a TableLayout is a TableRow, you can + * actually use any View subclass as a direct child of TableLayout. The View + * will be displayed as a single row that spans all the table columns.</p> + */ +public class TableLayout extends LinearLayout { + private int[] mMaxWidths; + private SparseBooleanArray mStretchableColumns; + private SparseBooleanArray mShrinkableColumns; + private SparseBooleanArray mCollapsedColumns; + + private boolean mShrinkAllColumns; + private boolean mStretchAllColumns; + + private TableLayout.PassThroughHierarchyChangeListener mPassThroughListener; + + private boolean mInitialized; + + /** + * <p>Creates a new TableLayout for the given context.</p> + * + * @param context the application environment + */ + public TableLayout(Context context) { + super(context); + initTableLayout(); + } + + /** + * <p>Creates a new TableLayout for the given context and with the + * specified set attributes.</p> + * + * @param context the application environment + * @param attrs a collection of attributes + */ + public TableLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = + context.obtainStyledAttributes(attrs, R.styleable.TableLayout); + + String stretchedColumns = + a.getString(R.styleable.TableLayout_stretchColumns); + if (stretchedColumns != null) { + if (stretchedColumns.charAt(0) == '*') { + mStretchAllColumns = true; + } else { + mStretchableColumns = parseColumns(stretchedColumns); + } + } + + String shrinkedColumns = + a.getString(R.styleable.TableLayout_shrinkColumns); + if (shrinkedColumns != null) { + if (shrinkedColumns.charAt(0) == '*') { + mShrinkAllColumns = true; + } else { + mShrinkableColumns = parseColumns(shrinkedColumns); + } + } + + String collapsedColumns = + a.getString(R.styleable.TableLayout_collapseColumns); + if (collapsedColumns != null) { + mCollapsedColumns = parseColumns(collapsedColumns); + } + + a.recycle(); + initTableLayout(); + } + + /** + * <p>Parses a sequence of columns ids defined in a CharSequence with the + * following pattern (regex): \d+(\s*,\s*\d+)*</p> + * + * <p>Examples: "1" or "13, 7, 6" or "".</p> + * + * <p>The result of the parsing is stored in a sparse boolean array. The + * parsed column ids are used as the keys of the sparse array. The values + * are always true.</p> + * + * @param sequence a sequence of column ids, can be empty but not null + * @return a sparse array of boolean mapping column indexes to the columns + * collapse state + */ + private static SparseBooleanArray parseColumns(String sequence) { + SparseBooleanArray columns = new SparseBooleanArray(); + Pattern pattern = Pattern.compile("\\s*,\\s*"); + String[] columnDefs = pattern.split(sequence); + + for (String columnIdentifier : columnDefs) { + try { + int columnIndex = Integer.parseInt(columnIdentifier); + // only valid, i.e. positive, columns indexes are handled + if (columnIndex >= 0) { + // putting true in this sparse array indicates that the + // column index was defined in the XML file + columns.put(columnIndex, true); + } + } catch (NumberFormatException e) { + // we just ignore columns that don't exist + } + } + + return columns; + } + + /** + * <p>Performs initialization common to prorgrammatic use and XML use of + * this widget.</p> + */ + private void initTableLayout() { + if (mCollapsedColumns == null) { + mCollapsedColumns = new SparseBooleanArray(); + } + if (mStretchableColumns == null) { + mStretchableColumns = new SparseBooleanArray(); + } + if (mShrinkableColumns == null) { + mShrinkableColumns = new SparseBooleanArray(); + } + + mPassThroughListener = new PassThroughHierarchyChangeListener(); + // make sure to call the parent class method to avoid potential + // infinite loops + super.setOnHierarchyChangeListener(mPassThroughListener); + + mInitialized = true; + } + + /** + * {@inheritDoc} + */ + @Override + public void setOnHierarchyChangeListener( + OnHierarchyChangeListener listener) { + // the user listener is delegated to our pass-through listener + mPassThroughListener.mOnHierarchyChangeListener = listener; + } + + private void requestRowsLayout() { + if (mInitialized) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + getChildAt(i).requestLayout(); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void requestLayout() { + if (mInitialized) { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + getChildAt(i).forceLayout(); + } + } + + super.requestLayout(); + } + + /** + * <p>Indicates whether all columns are shrinkable or not.</p> + * + * @return true if all columns are shrinkable, false otherwise + */ + public boolean isShrinkAllColumns() { + return mShrinkAllColumns; + } + + /** + * <p>Convenience method to mark all columns as shrinkable.</p> + * + * @param shrinkAllColumns true to mark all columns shrinkable + * + * @attr ref android.R.styleable#TableLayout_shrinkColumns + */ + public void setShrinkAllColumns(boolean shrinkAllColumns) { + mShrinkAllColumns = shrinkAllColumns; + } + + /** + * <p>Indicates whether all columns are stretchable or not.</p> + * + * @return true if all columns are stretchable, false otherwise + */ + public boolean isStretchAllColumns() { + return mStretchAllColumns; + } + + /** + * <p>Convenience method to mark all columns as stretchable.</p> + * + * @param stretchAllColumns true to mark all columns stretchable + * + * @attr ref android.R.styleable#TableLayout_stretchColumns + */ + public void setStretchAllColumns(boolean stretchAllColumns) { + mStretchAllColumns = stretchAllColumns; + } + + /** + * <p>Collapses or restores a given column. When collapsed, a column + * does not appear on screen and the extra space is reclaimed by the + * other columns. A column is collapsed/restored only when it belongs to + * a {@link android.widget.TableRow}.</p> + * + * <p>Calling this method requests a layout operation.</p> + * + * @param columnIndex the index of the column + * @param isCollapsed true if the column must be collapsed, false otherwise + * + * @attr ref android.R.styleable#TableLayout_collapseColumns + */ + public void setColumnCollapsed(int columnIndex, boolean isCollapsed) { + // update the collapse status of the column + mCollapsedColumns.put(columnIndex, isCollapsed); + + int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View view = getChildAt(i); + if (view instanceof TableRow) { + ((TableRow) view).setColumnCollapsed(columnIndex, isCollapsed); + } + } + + requestRowsLayout(); + } + + /** + * <p>Returns the collapsed state of the specified column.</p> + * + * @param columnIndex the index of the column + * @return true if the column is collapsed, false otherwise + */ + public boolean isColumnCollapsed(int columnIndex) { + return mCollapsedColumns.get(columnIndex); + } + + /** + * <p>Makes the given column stretchable or not. When stretchable, a column + * takes up as much as available space as possible in its row.</p> + * + * <p>Calling this method requests a layout operation.</p> + * + * @param columnIndex the index of the column + * @param isStretchable true if the column must be stretchable, + * false otherwise. Default is false. + * + * @attr ref android.R.styleable#TableLayout_stretchColumns + */ + public void setColumnStretchable(int columnIndex, boolean isStretchable) { + mStretchableColumns.put(columnIndex, isStretchable); + requestRowsLayout(); + } + + /** + * <p>Returns whether the specified column is stretchable or not.</p> + * + * @param columnIndex the index of the column + * @return true if the column is stretchable, false otherwise + */ + public boolean isColumnStretchable(int columnIndex) { + return mStretchAllColumns || mStretchableColumns.get(columnIndex); + } + + /** + * <p>Makes the given column shrinkable or not. When a row is too wide, the + * table can reclaim extra space from shrinkable columns.</p> + * + * <p>Calling this method requests a layout operation.</p> + * + * @param columnIndex the index of the column + * @param isShrinkable true if the column must be shrinkable, + * false otherwise. Default is false. + * + * @attr ref android.R.styleable#TableLayout_shrinkColumns + */ + public void setColumnShrinkable(int columnIndex, boolean isShrinkable) { + mShrinkableColumns.put(columnIndex, isShrinkable); + requestRowsLayout(); + } + + /** + * <p>Returns whether the specified column is shrinkable or not.</p> + * + * @param columnIndex the index of the column + * @return true if the column is shrinkable, false otherwise. Default is false. + */ + public boolean isColumnShrinkable(int columnIndex) { + return mShrinkAllColumns || mStretchableColumns.get(columnIndex); + } + + /** + * <p>Applies the columns collapse status to a new row added to this + * table. This method is invoked by PassThroughHierarchyChangeListener + * upon child insertion.</p> + * + * <p>This method only applies to {@link android.widget.TableRow} + * instances.</p> + * + * @param child the newly added child + */ + private void trackCollapsedColumns(View child) { + if (child instanceof TableRow) { + final TableRow row = (TableRow) child; + final SparseBooleanArray collapsedColumns = mCollapsedColumns; + final int count = collapsedColumns.size(); + for (int i = 0; i < count; i++) { + int columnIndex = collapsedColumns.keyAt(i); + boolean isCollapsed = collapsedColumns.valueAt(i); + // the collapse status is set only when the column should be + // collapsed; otherwise, this might affect the default + // visibility of the row's children + if (isCollapsed) { + row.setColumnCollapsed(columnIndex, isCollapsed); + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addView(View child) { + super.addView(child); + requestRowsLayout(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addView(View child, int index) { + super.addView(child, index); + requestRowsLayout(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + super.addView(child, params); + requestRowsLayout(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + super.addView(child, index, params); + requestRowsLayout(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // enforce vertical layout + measureVertical(widthMeasureSpec, heightMeasureSpec); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // enforce vertical layout + layoutVertical(); + } + + /** + * {@inheritDoc} + */ + @Override + void measureChildBeforeLayout(View child, int childIndex, + int widthMeasureSpec, int totalWidth, + int heightMeasureSpec, int totalHeight) { + // when the measured child is a table row, we force the width of its + // children with the widths computed in findLargestCells() + if (child instanceof TableRow) { + ((TableRow) child).setColumnsWidthConstraints(mMaxWidths); + } + + super.measureChildBeforeLayout(child, childIndex, + widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); + } + + /** + * {@inheritDoc} + */ + @Override + void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { + findLargestCells(widthMeasureSpec); + shrinkAndStretchColumns(widthMeasureSpec); + + super.measureVertical(widthMeasureSpec, heightMeasureSpec); + } + + /** + * <p>Finds the largest cell in each column. For each column, the width of + * the largest cell is applied to all the other cells.</p> + * + * @param widthMeasureSpec the measure constraint imposed by our parent + */ + private void findLargestCells(int widthMeasureSpec) { + boolean firstRow = true; + + // find the maximum width for each column + // the total number of columns is dynamically changed if we find + // wider rows as we go through the children + // the array is reused for each layout operation; the array can grow + // but never shrinks. Unused extra cells in the array are just ignored + // this behavior avoids to unnecessary grow the array after the first + // layout operation + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } + + if (child instanceof TableRow) { + final TableRow row = (TableRow) child; + // forces the row's height + final ViewGroup.LayoutParams layoutParams = row.getLayoutParams(); + layoutParams.height = LayoutParams.WRAP_CONTENT; + + final int[] widths = row.getColumnsWidths(widthMeasureSpec); + final int newLength = widths.length; + // this is the first row, we just need to copy the values + if (firstRow) { + if (mMaxWidths == null || mMaxWidths.length != newLength) { + mMaxWidths = new int[newLength]; + } + System.arraycopy(widths, 0, mMaxWidths, 0, newLength); + firstRow = false; + } else { + int length = mMaxWidths.length; + final int difference = newLength - length; + // the current row is wider than the previous rows, so + // we just grow the array and copy the values + if (difference > 0) { + final int[] oldMaxWidths = mMaxWidths; + mMaxWidths = new int[newLength]; + System.arraycopy(oldMaxWidths, 0, mMaxWidths, 0, + oldMaxWidths.length); + System.arraycopy(widths, oldMaxWidths.length, + mMaxWidths, oldMaxWidths.length, difference); + } + + // the row is narrower or of the same width as the previous + // rows, so we find the maximum width for each column + // if the row is narrower than the previous ones, + // difference will be negative + final int[] maxWidths = mMaxWidths; + length = Math.min(length, newLength); + for (int j = 0; j < length; j++) { + maxWidths[j] = Math.max(maxWidths[j], widths[j]); + } + } + } + } + } + + /** + * <p>Shrinks the columns if their total width is greater than the + * width allocated by widthMeasureSpec. When the total width is less + * than the allocated width, this method attempts to stretch columns + * to fill the remaining space.</p> + * + * @param widthMeasureSpec the width measure specification as indicated + * by this widget's parent + */ + private void shrinkAndStretchColumns(int widthMeasureSpec) { + // when we have no row, mMaxWidths is not initialized and the loop + // below could cause a NPE + if (mMaxWidths == null) { + return; + } + + // should we honor AT_MOST, EXACTLY and UNSPECIFIED? + int totalWidth = 0; + for (int width : mMaxWidths) { + totalWidth += width; + } + + int size = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight; + + if ((totalWidth > size) && (mShrinkAllColumns || mShrinkableColumns.size() > 0)) { + // oops, the largest columns are wider than the row itself + // fairly redistribute the row's widh among the columns + mutateColumnsWidth(mShrinkableColumns, mShrinkAllColumns, size, totalWidth); + } else if ((totalWidth < size) && (mStretchAllColumns || mStretchableColumns.size() > 0)) { + // if we have some space left, we distribute it among the + // expandable columns + mutateColumnsWidth(mStretchableColumns, mStretchAllColumns, size, totalWidth); + } + } + + private void mutateColumnsWidth(SparseBooleanArray columns, + boolean allColumns, int size, int totalWidth) { + int skipped = 0; + final int[] maxWidths = mMaxWidths; + final int length = maxWidths.length; + final int count = allColumns ? length : columns.size(); + final int totalExtraSpace = size - totalWidth; + int extraSpace = totalExtraSpace / count; + + if (!allColumns) { + for (int i = 0; i < count; i++) { + int column = columns.keyAt(i); + if (columns.valueAt(i)) { + if (column < length) { + maxWidths[column] += extraSpace; + } else { + skipped++; + } + } + } + } else { + for (int i = 0; i < count; i++) { + maxWidths[i] += extraSpace; + } + + // we don't skip any column so we can return right away + return; + } + + if (skipped > 0 && skipped < count) { + // reclaim any extra space we left to columns that don't exist + extraSpace = skipped * extraSpace / (count - skipped); + for (int i = 0; i < count; i++) { + int column = columns.keyAt(i); + if (columns.valueAt(i) && column < length) { + if (extraSpace > maxWidths[column]) { + maxWidths[column] = 0; + } else { + maxWidths[column] += extraSpace; + } + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new TableLayout.LayoutParams(getContext(), attrs); + } + + /** + * Returns a set of layout parameters with a width of + * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}, + * and a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}. + */ + @Override + protected LinearLayout.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof TableLayout.LayoutParams; + } + + /** + * {@inheritDoc} + */ + @Override + protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + /** + * <p>This set of layout parameters enforces the width of each child to be + * {@link #FILL_PARENT} and the height of each child to be + * {@link #WRAP_CONTENT}, but only if the height is not specified.</p> + */ + @SuppressWarnings({"UnusedDeclaration"}) + public static class LayoutParams extends LinearLayout.LayoutParams { + /** + * {@inheritDoc} + */ + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(int w, int h) { + super(FILL_PARENT, h); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(int w, int h, float initWeight) { + super(FILL_PARENT, h, initWeight); + } + + /** + * <p>Sets the child width to + * {@link android.view.ViewGroup.LayoutParams} and the child height to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p> + */ + public LayoutParams() { + super(FILL_PARENT, WRAP_CONTENT); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams p) { + super(p); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + /** + * <p>Fixes the row's width to + * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}; the row's + * height is fixed to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} if no layout + * height is specified.</p> + * + * @param a the styled attributes set + * @param widthAttr the width attribute to fetch + * @param heightAttr the height attribute to fetch + */ + @Override + protected void setBaseAttributes(TypedArray a, + int widthAttr, int heightAttr) { + this.width = FILL_PARENT; + if (a.hasValue(heightAttr)) { + this.height = a.getLayoutDimension(heightAttr, "layout_height"); + } else { + this.height = WRAP_CONTENT; + } + } + } + + /** + * <p>A pass-through listener acts upon the events and dispatches them + * to another listener. This allows the table layout to set its own internal + * hierarchy change listener without preventing the user to setup his.</p> + */ + private class PassThroughHierarchyChangeListener implements + OnHierarchyChangeListener { + private OnHierarchyChangeListener mOnHierarchyChangeListener; + + /** + * {@inheritDoc} + */ + public void onChildViewAdded(View parent, View child) { + trackCollapsedColumns(child); + + if (mOnHierarchyChangeListener != null) { + mOnHierarchyChangeListener.onChildViewAdded(parent, child); + } + } + + /** + * {@inheritDoc} + */ + public void onChildViewRemoved(View parent, View child) { + if (mOnHierarchyChangeListener != null) { + mOnHierarchyChangeListener.onChildViewRemoved(parent, child); + } + } + } +} diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java new file mode 100644 index 0000000..5628cab --- /dev/null +++ b/core/java/android/widget/TableRow.java @@ -0,0 +1,531 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.SparseIntArray; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewDebug; + + +/** + * <p>A layout that arranges its children horizontally. A TableRow should + * always be used as a child of a {@link android.widget.TableLayout}. If a + * TableRow's parent is not a TableLayout, the TableRow will behave as + * an horizontal {@link android.widget.LinearLayout}.</p> + * + * <p>The children of a TableRow do not need to specify the + * <code>layout_width</code> and <code>layout_height</code> attributes in the + * XML file. TableRow always enforces those values to be respectively + * {@link android.widget.TableLayout.LayoutParams#FILL_PARENT} and + * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p> + * + * <p> + * Also see {@link TableRow.LayoutParams android.widget.TableRow.LayoutParams} + * for layout attributes </p> + */ +public class TableRow extends LinearLayout { + private int mNumColumns = 0; + private int[] mColumnWidths; + private int[] mConstrainedColumnWidths; + private SparseIntArray mColumnToChildIndex; + + private ChildrenTracker mChildrenTracker; + + /** + * <p>Creates a new TableRow for the given context.</p> + * + * @param context the application environment + */ + public TableRow(Context context) { + super(context); + initTableRow(); + } + + /** + * <p>Creates a new TableRow for the given context and with the + * specified set attributes.</p> + * + * @param context the application environment + * @param attrs a collection of attributes + */ + public TableRow(Context context, AttributeSet attrs) { + super(context, attrs); + initTableRow(); + } + + private void initTableRow() { + OnHierarchyChangeListener oldListener = mOnHierarchyChangeListener; + mChildrenTracker = new ChildrenTracker(); + if (oldListener != null) { + mChildrenTracker.setOnHierarchyChangeListener(oldListener); + } + super.setOnHierarchyChangeListener(mChildrenTracker); + } + + /** + * {@inheritDoc} + */ + @Override + public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) { + mChildrenTracker.setOnHierarchyChangeListener(listener); + } + + /** + * <p>Collapses or restores a given column.</p> + * + * @param columnIndex the index of the column + * @param collapsed true if the column must be collapsed, false otherwise + * {@hide} + */ + void setColumnCollapsed(int columnIndex, boolean collapsed) { + View child = getVirtualChildAt(columnIndex); + if (child != null) { + child.setVisibility(collapsed ? GONE : VISIBLE); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // enforce horizontal layout + measureHorizontal(widthMeasureSpec, heightMeasureSpec); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // enforce horizontal layout + layoutHorizontal(); + } + + /** + * {@inheritDoc} + */ + @Override + public View getVirtualChildAt(int i) { + if (mColumnToChildIndex == null) { + mapIndexAndColumns(); + } + + final int deflectedIndex = mColumnToChildIndex.get(i, -1); + if (deflectedIndex != -1) { + return getChildAt(deflectedIndex); + } + + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public int getVirtualChildCount() { + if (mColumnToChildIndex == null) { + mapIndexAndColumns(); + } + return mNumColumns; + } + + private void mapIndexAndColumns() { + if (mColumnToChildIndex == null) { + int virtualCount = 0; + final int count = getChildCount(); + + mColumnToChildIndex = new SparseIntArray(); + final SparseIntArray columnToChild = mColumnToChildIndex; + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); + + if (layoutParams.column >= virtualCount) { + virtualCount = layoutParams.column; + } + + for (int j = 0; j < layoutParams.span; j++) { + columnToChild.put(virtualCount++, i); + } + } + + mNumColumns = virtualCount; + } + } + + /** + * {@inheritDoc} + */ + @Override + int measureNullChild(int childIndex) { + return mConstrainedColumnWidths[childIndex]; + } + + /** + * {@inheritDoc} + */ + @Override + void measureChildBeforeLayout(View child, int childIndex, + int widthMeasureSpec, int totalWidth, + int heightMeasureSpec, int totalHeight) { + if (mConstrainedColumnWidths != null) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int measureMode = MeasureSpec.EXACTLY; + int columnWidth = 0; + + final int span = lp.span; + final int[] constrainedColumnWidths = mConstrainedColumnWidths; + for (int i = 0; i < span; i++) { + columnWidth += constrainedColumnWidths[childIndex + i]; + } + + final int gravity = lp.gravity; + final boolean isHorizontalGravity = Gravity.isHorizontal(gravity); + + if (isHorizontalGravity) { + measureMode = MeasureSpec.AT_MOST; + } + + // no need to care about padding here, + // ViewGroup.getChildMeasureSpec() would get rid of it anyway + // because of the EXACTLY measure spec we use + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.max(0, columnWidth - lp.leftMargin - lp.rightMargin), measureMode + ); + int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + mPaddingTop + mPaddingBottom + lp.topMargin + + lp .bottomMargin + totalHeight, lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + if (isHorizontalGravity) { + final int childWidth = child.getMeasuredWidth(); + lp.mOffset[LayoutParams.LOCATION_NEXT] = columnWidth - childWidth; + + switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + // don't offset on X axis + break; + case Gravity.RIGHT: + lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT]; + break; + case Gravity.CENTER_HORIZONTAL: + lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] / 2; + break; + } + } else { + lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] = 0; + } + } else { + // fail silently when column widths are not available + super.measureChildBeforeLayout(child, childIndex, widthMeasureSpec, + totalWidth, heightMeasureSpec, totalHeight); + } + } + + /** + * {@inheritDoc} + */ + @Override + int getChildrenSkipCount(View child, int index) { + LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); + + // when the span is 1 (default), we need to skip 0 child + return layoutParams.span - 1; + } + + /** + * {@inheritDoc} + */ + @Override + int getLocationOffset(View child) { + return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION]; + } + + /** + * {@inheritDoc} + */ + @Override + int getNextLocationOffset(View child) { + return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION_NEXT]; + } + + /** + * <p>Measures the preferred width of each child, including its margins.</p> + * + * @param widthMeasureSpec the width constraint imposed by our parent + * + * @return an array of integers corresponding to the width of each cell, or + * column, in this row + * {@hide} + */ + int[] getColumnsWidths(int widthMeasureSpec) { + final int numColumns = getVirtualChildCount(); + if (mColumnWidths == null || numColumns != mColumnWidths.length) { + mColumnWidths = new int[numColumns]; + } + + final int[] columnWidths = mColumnWidths; + + for (int i = 0; i < numColumns; i++) { + final View child = getVirtualChildAt(i); + if (child != null && child.getVisibility() != GONE) { + final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); + if (layoutParams.span == 1) { + int spec; + switch (layoutParams.width) { + case LayoutParams.WRAP_CONTENT: + spec = getChildMeasureSpec(widthMeasureSpec, 0, LayoutParams.WRAP_CONTENT); + break; + case LayoutParams.FILL_PARENT: + spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + break; + default: + spec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY); + } + child.measure(spec, spec); + + final int width = child.getMeasuredWidth() + layoutParams.leftMargin + + layoutParams.rightMargin; + columnWidths[i] = width; + } else { + columnWidths[i] = 0; + } + } else { + columnWidths[i] = 0; + } + } + + return columnWidths; + } + + /** + * <p>Sets the width of all of the columns in this row. At layout time, + * this row sets a fixed width, as defined by <code>columnWidths</code>, + * on each child (or cell, or column.)</p> + * + * @param columnWidths the fixed width of each column that this row must + * honor + * @throws IllegalArgumentException when columnWidths' length is smaller + * than the number of children in this row + * {@hide} + */ + void setColumnsWidthConstraints(int[] columnWidths) { + if (columnWidths == null || columnWidths.length < getVirtualChildCount()) { + throw new IllegalArgumentException( + "columnWidths should be >= getVirtualChildCount()"); + } + + mConstrainedColumnWidths = columnWidths; + } + + /** + * {@inheritDoc} + */ + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new TableRow.LayoutParams(getContext(), attrs); + } + + /** + * Returns a set of layout parameters with a width of + * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}, + * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning. + */ + @Override + protected LinearLayout.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof TableRow.LayoutParams; + } + + /** + * {@inheritDoc} + */ + @Override + protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + /** + * <p>Set of layout parameters used in table rows.</p> + * + * @see android.widget.TableLayout.LayoutParams + * + * @attr ref android.R.styleable#TableRow_Cell_layout_column + * @attr ref android.R.styleable#TableRow_Cell_layout_span + */ + public static class LayoutParams extends LinearLayout.LayoutParams { + /** + * <p>The column index of the cell represented by the widget.</p> + */ + @ViewDebug.ExportedProperty + public int column; + + /** + * <p>The number of columns the widgets spans over.</p> + */ + @ViewDebug.ExportedProperty + public int span; + + private static final int LOCATION = 0; + private static final int LOCATION_NEXT = 1; + + private int[] mOffset = new int[2]; + + /** + * {@inheritDoc} + */ + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + TypedArray a = + c.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.TableRow_Cell); + + column = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_column, -1); + span = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_span, 1); + if (span <= 1) { + span = 1; + } + + a.recycle(); + } + + /** + * <p>Sets the child width and the child height.</p> + * + * @param w the desired width + * @param h the desired height + */ + public LayoutParams(int w, int h) { + super(w, h); + column = -1; + span = 1; + } + + /** + * <p>Sets the child width, height and weight.</p> + * + * @param w the desired width + * @param h the desired height + * @param initWeight the desired weight + */ + public LayoutParams(int w, int h, float initWeight) { + super(w, h, initWeight); + column = -1; + span = 1; + } + + /** + * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams} + * and the child height to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p> + */ + public LayoutParams() { + super(FILL_PARENT, WRAP_CONTENT); + column = -1; + span = 1; + } + + /** + * <p>Puts the view in the specified column.</p> + * + * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams#FILL_PARENT} + * and the child height to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p> + * + * @param column the column index for the view + */ + public LayoutParams(int column) { + this(); + this.column = column; + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams p) { + super(p); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + @Override + protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { + // We don't want to force users to specify a layout_width + if (a.hasValue(widthAttr)) { + width = a.getLayoutDimension(widthAttr, "layout_width"); + } else { + width = FILL_PARENT; + } + + // We don't want to force users to specify a layout_height + if (a.hasValue(heightAttr)) { + height = a.getLayoutDimension(heightAttr, "layout_height"); + } else { + height = WRAP_CONTENT; + } + } + } + + // special transparent hierarchy change listener + private class ChildrenTracker implements OnHierarchyChangeListener { + private OnHierarchyChangeListener listener; + + private void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) { + this.listener = listener; + } + + public void onChildViewAdded(View parent, View child) { + // dirties the index to column map + mColumnToChildIndex = null; + + if (this.listener != null) { + this.listener.onChildViewAdded(parent, child); + } + } + + public void onChildViewRemoved(View parent, View child) { + // dirties the index to column map + mColumnToChildIndex = null; + + if (this.listener != null) { + this.listener.onChildViewRemoved(parent, child); + } + } + } +} diff --git a/core/java/android/widget/TextSwitcher.java b/core/java/android/widget/TextSwitcher.java new file mode 100644 index 0000000..a8794a3 --- /dev/null +++ b/core/java/android/widget/TextSwitcher.java @@ -0,0 +1,91 @@ +/* + * 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 android.widget; + + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * Specialized {@link android.widget.ViewSwitcher} that contains + * only children of type {@link android.widget.TextView}. + * + * A TextSwitcher is useful to animate a label on screen. Whenever + * {@link #setText(CharSequence)} is called, TextSwitcher animates the current text + * out and animates the new text in. + */ +public class TextSwitcher extends ViewSwitcher { + /** + * Creates a new empty TextSwitcher. + * + * @param context the application's environment + */ + public TextSwitcher(Context context) { + super(context); + } + + /** + * Creates a new empty TextSwitcher for the given context and with the + * specified set attributes. + * + * @param context the application environment + * @param attrs a collection of attributes + */ + public TextSwitcher(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * {@inheritDoc} + * + * @throws IllegalArgumentException if child is not an instance of + * {@link android.widget.TextView} + */ + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!(child instanceof TextView)) { + throw new IllegalArgumentException( + "TextSwitcher children must be instances of TextView"); + } + + super.addView(child, index, params); + } + + /** + * Sets the text of the next view and switches to the next view. This can + * be used to animate the old text out and animate the next text in. + * + * @param text the new text to display + */ + public void setText(CharSequence text) { + final TextView t = (TextView) getNextView(); + t.setText(text); + showNext(); + } + + /** + * Sets the text of the text view that is currently showing. This does + * not perform the animations. + * + * @param text the new text to display + */ + public void setCurrentText(CharSequence text) { + ((TextView)getCurrentView()).setText(text); + } +} diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java new file mode 100644 index 0000000..bd5db33 --- /dev/null +++ b/core/java/android/widget/TextView.java @@ -0,0 +1,4866 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.text.BoringLayout; +import android.text.DynamicLayout; +import android.text.Editable; +import android.text.GetChars; +import android.text.GraphicsOperations; +import android.text.ClipboardManager; +import android.text.InputFilter; +import android.text.Layout; +import android.text.Selection; +import android.text.SpanWatcher; +import android.text.Spannable; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.SpannableString; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.DialerKeyListener; +import android.text.method.DigitsKeyListener; +import android.text.method.KeyListener; +import android.text.method.LinkMovementMethod; +import android.text.method.MetaKeyKeyListener; +import android.text.method.MovementMethod; +import android.text.method.PasswordTransformationMethod; +import android.text.method.SingleLineTransformationMethod; +import android.text.method.TextKeyListener; +import android.text.method.TransformationMethod; +import android.text.style.ParagraphStyle; +import android.text.style.URLSpan; +import android.text.style.UpdateLayout; +import android.text.util.Linkify; +import android.util.AttributeSet; +import android.util.Log; +import android.util.FloatMath; +import android.util.TypedValue; +import android.view.ContextMenu; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewTreeObserver; +import android.view.ViewGroup.LayoutParams; +import android.view.animation.AnimationUtils; +import android.widget.RemoteViews.RemoteView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import com.android.internal.util.FastMath; + +/** + * Displays text to the user and optionally allows them to edit it. A TextView + * is a complete text editor, however the basic class is configured to not + * allow editing; see {@link EditText} for a subclass that configures the text + * view for editing. + * + * <p> + * <b>XML attributes</b> + * <p> + * See {@link android.R.styleable#TextView TextView Attributes}, + * {@link android.R.styleable#View View Attributes} + * + * @attr ref android.R.styleable#TextView_text + * @attr ref android.R.styleable#TextView_bufferType + * @attr ref android.R.styleable#TextView_hint + * @attr ref android.R.styleable#TextView_textColor + * @attr ref android.R.styleable#TextView_textColorHighlight + * @attr ref android.R.styleable#TextView_textColorHint + * @attr ref android.R.styleable#TextView_textSize + * @attr ref android.R.styleable#TextView_textScaleX + * @attr ref android.R.styleable#TextView_typeface + * @attr ref android.R.styleable#TextView_textStyle + * @attr ref android.R.styleable#TextView_cursorVisible + * @attr ref android.R.styleable#TextView_maxLines + * @attr ref android.R.styleable#TextView_maxHeight + * @attr ref android.R.styleable#TextView_lines + * @attr ref android.R.styleable#TextView_height + * @attr ref android.R.styleable#TextView_minLines + * @attr ref android.R.styleable#TextView_minHeight + * @attr ref android.R.styleable#TextView_maxEms + * @attr ref android.R.styleable#TextView_maxWidth + * @attr ref android.R.styleable#TextView_ems + * @attr ref android.R.styleable#TextView_width + * @attr ref android.R.styleable#TextView_minEms + * @attr ref android.R.styleable#TextView_minWidth + * @attr ref android.R.styleable#TextView_gravity + * @attr ref android.R.styleable#TextView_scrollHorizontally + * @attr ref android.R.styleable#TextView_password + * @attr ref android.R.styleable#TextView_singleLine + * @attr ref android.R.styleable#TextView_selectAllOnFocus + * @attr ref android.R.styleable#TextView_includeFontPadding + * @attr ref android.R.styleable#TextView_maxLength + * @attr ref android.R.styleable#TextView_shadowColor + * @attr ref android.R.styleable#TextView_shadowDx + * @attr ref android.R.styleable#TextView_shadowDy + * @attr ref android.R.styleable#TextView_shadowRadius + * @attr ref android.R.styleable#TextView_autoLink + * @attr ref android.R.styleable#TextView_linksClickable + * @attr ref android.R.styleable#TextView_numeric + * @attr ref android.R.styleable#TextView_digits + * @attr ref android.R.styleable#TextView_phoneNumber + * @attr ref android.R.styleable#TextView_inputMethod + * @attr ref android.R.styleable#TextView_capitalize + * @attr ref android.R.styleable#TextView_autoText + * @attr ref android.R.styleable#TextView_editable + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableBottom + * @attr ref android.R.styleable#TextView_drawableRight + * @attr ref android.R.styleable#TextView_drawableLeft + * @attr ref android.R.styleable#TextView_lineSpacingExtra + * @attr ref android.R.styleable#TextView_lineSpacingMultiplier + */ +@RemoteView +public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { + private static int PRIORITY = 100; + + private ColorStateList mTextColor; + private int mCurTextColor; + private ColorStateList mHintTextColor; + private ColorStateList mLinkTextColor; + private int mCurHintTextColor; + private boolean mFreezesText; + private boolean mFrozenWithFocus; + + private Editable.Factory mEditableFactory = Editable.Factory.getInstance(); + private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance(); + + private float mShadowRadius, mShadowDx, mShadowDy; + + private static final int PREDRAW_NOT_REGISTERED = 0; + private static final int PREDRAW_PENDING = 1; + private static final int PREDRAW_DONE = 2; + private int mPreDrawState = PREDRAW_NOT_REGISTERED; + + private TextUtils.TruncateAt mEllipsize = null; + + // Enum for the "typeface" XML parameter. + // TODO: How can we get this from the XML instead of hardcoding it here? + private static final int SANS = 1; + private static final int SERIF = 2; + private static final int MONOSPACE = 3; + + // Bitfield for the "numeric" XML parameter. + // TODO: How can we get this from the XML instead of hardcoding it here? + private static final int SIGNED = 2; + private static final int DECIMAL = 4; + + private Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight; + private int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight; + private int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight; + private boolean mDrawables; + private int mDrawablePadding; + + private CharSequence mError; + private boolean mErrorWasChanged; + private PopupWindow mPopup; + + private CharWrapper mCharWrapper = null; + private Rect mCompoundRect; + + private boolean mSelectionMoved = false; + + /* + * Kick-start the font cache for the zygote process (to pay the cost of + * initializing freetype for our default font only once). + */ + static { + Paint p = new Paint(); + p.setAntiAlias(true); + // We don't care about the result, just the side-effect of measuring. + p.measureText("H"); + } + + public TextView(Context context) { + this(context, null); + } + + public TextView(Context context, + AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.textViewStyle); + } + + public TextView(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + + mText = ""; + + mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + // If we get the paint from the skin, we should set it to left, since + // the layout always wants it to be left. + // mTextPaint.setTextAlign(Paint.Align.LEFT); + + mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mMovement = getDefaultMovementMethod(); + mTransformation = null; + + TypedArray a = + context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.TextView, defStyle, 0); + + int textColorHighlight = 0; + ColorStateList textColor = null; + ColorStateList textColorHint = null; + ColorStateList textColorLink = null; + int textSize = 15; + int typefaceIndex = -1; + int styleIndex = -1; + + /* + * Look the appearance up without checking first if it exists because + * almost every TextView has one and it greatly simplifies the logic + * to be able to parse the appearance first and then let specific tags + * for this View override it. + */ + TypedArray appearance = null; + int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1); + if (ap != -1) { + appearance = context.obtainStyledAttributes(ap, + com.android.internal.R.styleable. + TextAppearance); + } + if (appearance != null) { + int n = appearance.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = appearance.getIndex(i); + + switch (attr) { + case com.android.internal.R.styleable.TextAppearance_textColorHighlight: + textColorHighlight = appearance.getColor(attr, textColorHighlight); + break; + + case com.android.internal.R.styleable.TextAppearance_textColor: + textColor = appearance.getColorStateList(attr); + break; + + case com.android.internal.R.styleable.TextAppearance_textColorHint: + textColorHint = appearance.getColorStateList(attr); + break; + + case com.android.internal.R.styleable.TextAppearance_textColorLink: + textColorLink = appearance.getColorStateList(attr); + break; + + case com.android.internal.R.styleable.TextAppearance_textSize: + textSize = appearance.getDimensionPixelSize(attr, textSize); + break; + + case com.android.internal.R.styleable.TextAppearance_typeface: + typefaceIndex = appearance.getInt(attr, -1); + break; + + case com.android.internal.R.styleable.TextAppearance_textStyle: + styleIndex = appearance.getInt(attr, -1); + break; + } + } + + appearance.recycle(); + } + + boolean editable = getDefaultEditable(); + CharSequence inputMethod = null; + int numeric = 0; + CharSequence digits = null; + boolean phone = false; + boolean autotext = false; + int autocap = -1; + int buffertype = 0; + boolean selectallonfocus = false; + Drawable drawableLeft = null, drawableTop = null, drawableRight = null, + drawableBottom = null; + int drawablePadding = 0; + int ellipsize = -1; + boolean singleLine = false; + int maxlength = -1; + CharSequence text = ""; + int shadowcolor = 0; + float dx = 0, dy = 0, r = 0; + boolean password = false; + + int n = a.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + switch (attr) { + case com.android.internal.R.styleable.TextView_editable: + editable = a.getBoolean(attr, editable); + break; + + case com.android.internal.R.styleable.TextView_inputMethod: + inputMethod = a.getText(attr); + break; + + case com.android.internal.R.styleable.TextView_numeric: + numeric = a.getInt(attr, numeric); + break; + + case com.android.internal.R.styleable.TextView_digits: + digits = a.getText(attr); + break; + + case com.android.internal.R.styleable.TextView_phoneNumber: + phone = a.getBoolean(attr, phone); + break; + + case com.android.internal.R.styleable.TextView_autoText: + autotext = a.getBoolean(attr, autotext); + break; + + case com.android.internal.R.styleable.TextView_capitalize: + autocap = a.getInt(attr, autocap); + break; + + case com.android.internal.R.styleable.TextView_bufferType: + buffertype = a.getInt(attr, buffertype); + break; + + case com.android.internal.R.styleable.TextView_selectAllOnFocus: + selectallonfocus = a.getBoolean(attr, selectallonfocus); + break; + + case com.android.internal.R.styleable.TextView_autoLink: + mAutoLinkMask = a.getInt(attr, 0); + break; + + case com.android.internal.R.styleable.TextView_linksClickable: + mLinksClickable = a.getBoolean(attr, true); + break; + + case com.android.internal.R.styleable.TextView_drawableLeft: + drawableLeft = a.getDrawable(attr); + break; + + case com.android.internal.R.styleable.TextView_drawableTop: + drawableTop = a.getDrawable(attr); + break; + + case com.android.internal.R.styleable.TextView_drawableRight: + drawableRight = a.getDrawable(attr); + break; + + case com.android.internal.R.styleable.TextView_drawableBottom: + drawableBottom = a.getDrawable(attr); + break; + + case com.android.internal.R.styleable.TextView_drawablePadding: + drawablePadding = a.getDimensionPixelSize(attr, drawablePadding); + break; + + case com.android.internal.R.styleable.TextView_maxLines: + setMaxLines(a.getInt(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_maxHeight: + setMaxHeight(a.getDimensionPixelSize(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_lines: + setLines(a.getInt(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_height: + setHeight(a.getDimensionPixelSize(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_minLines: + setMinLines(a.getInt(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_minHeight: + setMinHeight(a.getDimensionPixelSize(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_maxEms: + setMaxEms(a.getInt(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_maxWidth: + setMaxWidth(a.getDimensionPixelSize(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_ems: + setEms(a.getInt(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_width: + setWidth(a.getDimensionPixelSize(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_minEms: + setMinEms(a.getInt(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_minWidth: + setMinWidth(a.getDimensionPixelSize(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_gravity: + setGravity(a.getInt(attr, -1)); + break; + + case com.android.internal.R.styleable.TextView_hint: + setHint(a.getText(attr)); + break; + + case com.android.internal.R.styleable.TextView_text: + text = a.getText(attr); + break; + + case com.android.internal.R.styleable.TextView_scrollHorizontally: + if (a.getBoolean(attr, false)) { + setHorizontallyScrolling(true); + } + break; + + case com.android.internal.R.styleable.TextView_singleLine: + singleLine = a.getBoolean(attr, singleLine); + break; + + case com.android.internal.R.styleable.TextView_ellipsize: + ellipsize = a.getInt(attr, ellipsize); + break; + + case com.android.internal.R.styleable.TextView_includeFontPadding: + if (!a.getBoolean(attr, true)) { + setIncludeFontPadding(false); + } + break; + + case com.android.internal.R.styleable.TextView_cursorVisible: + if (!a.getBoolean(attr, true)) { + setCursorVisible(false); + } + break; + + case com.android.internal.R.styleable.TextView_maxLength: + maxlength = a.getInt(attr, -1); + break; + + case com.android.internal.R.styleable.TextView_textScaleX: + setTextScaleX(a.getFloat(attr, 1.0f)); + break; + + case com.android.internal.R.styleable.TextView_freezesText: + mFreezesText = a.getBoolean(attr, false); + break; + + case com.android.internal.R.styleable.TextView_shadowColor: + shadowcolor = a.getInt(attr, 0); + break; + + case com.android.internal.R.styleable.TextView_shadowDx: + dx = a.getFloat(attr, 0); + break; + + case com.android.internal.R.styleable.TextView_shadowDy: + dy = a.getFloat(attr, 0); + break; + + case com.android.internal.R.styleable.TextView_shadowRadius: + r = a.getFloat(attr, 0); + break; + + case com.android.internal.R.styleable.TextView_enabled: + setEnabled(a.getBoolean(attr, isEnabled())); + break; + + case com.android.internal.R.styleable.TextView_textColorHighlight: + textColorHighlight = a.getColor(attr, textColorHighlight); + break; + + case com.android.internal.R.styleable.TextView_textColor: + textColor = a.getColorStateList(attr); + break; + + case com.android.internal.R.styleable.TextView_textColorHint: + textColorHint = a.getColorStateList(attr); + break; + + case com.android.internal.R.styleable.TextView_textColorLink: + textColorLink = a.getColorStateList(attr); + break; + + case com.android.internal.R.styleable.TextView_textSize: + textSize = a.getDimensionPixelSize(attr, textSize); + break; + + case com.android.internal.R.styleable.TextView_typeface: + typefaceIndex = a.getInt(attr, typefaceIndex); + break; + + case com.android.internal.R.styleable.TextView_textStyle: + styleIndex = a.getInt(attr, styleIndex); + break; + + case com.android.internal.R.styleable.TextView_password: + password = a.getBoolean(attr, password); + break; + + case com.android.internal.R.styleable.TextView_lineSpacingExtra: + mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd); + break; + + case com.android.internal.R.styleable.TextView_lineSpacingMultiplier: + mSpacingMult = a.getFloat(attr, mSpacingMult); + break; + } + } + a.recycle(); + + BufferType bufferType = BufferType.EDITABLE; + + if (inputMethod != null) { + Class c; + + try { + c = Class.forName(inputMethod.toString()); + } catch (ClassNotFoundException ex) { + throw new RuntimeException(ex); + } + + try { + mInput = (KeyListener) c.newInstance(); + } catch (InstantiationException ex) { + throw new RuntimeException(ex); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } else if (digits != null) { + mInput = DigitsKeyListener.getInstance(digits.toString()); + } else if (phone) { + mInput = DialerKeyListener.getInstance(); + } else if (numeric != 0) { + mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0, + (numeric & DECIMAL) != 0); + } else if (autotext || autocap != -1) { + TextKeyListener.Capitalize cap; + + switch (autocap) { + case 1: + cap = TextKeyListener.Capitalize.SENTENCES; + break; + + case 2: + cap = TextKeyListener.Capitalize.WORDS; + break; + + case 3: + cap = TextKeyListener.Capitalize.CHARACTERS; + break; + + default: + cap = TextKeyListener.Capitalize.NONE; + break; + } + + mInput = TextKeyListener.getInstance(autotext, cap); + } else if (editable) { + mInput = TextKeyListener.getInstance(); + } else { + mInput = null; + + switch (buffertype) { + case 0: + bufferType = BufferType.NORMAL; + break; + case 1: + bufferType = BufferType.SPANNABLE; + break; + case 2: + bufferType = BufferType.EDITABLE; + break; + } + } + + if (selectallonfocus) { + mSelectAllOnFocus = true; + + if (bufferType == BufferType.NORMAL) + bufferType = BufferType.SPANNABLE; + } + + setCompoundDrawablesWithIntrinsicBounds( + drawableLeft, drawableTop, drawableRight, drawableBottom); + setCompoundDrawablePadding(drawablePadding); + + if (singleLine) { + setSingleLine(); + + if (mInput == null && ellipsize < 0) { + ellipsize = 3; // END + } + } + + switch (ellipsize) { + case 1: + setEllipsize(TextUtils.TruncateAt.START); + break; + case 2: + setEllipsize(TextUtils.TruncateAt.MIDDLE); + break; + case 3: + setEllipsize(TextUtils.TruncateAt.END); + break; + } + + setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000)); + setHintTextColor(textColorHint); + setLinkTextColor(textColorLink); + if (textColorHighlight != 0) { + setHighlightColor(textColorHighlight); + } + setRawTextSize(textSize); + + if (password) { + setTransformationMethod(PasswordTransformationMethod.getInstance()); + typefaceIndex = MONOSPACE; + } + + setTypefaceByIndex(typefaceIndex, styleIndex); + + if (shadowcolor != 0) { + setShadowLayer(r, dx, dy, shadowcolor); + } + + if (maxlength >= 0) { + setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) }); + } else { + setFilters(NO_FILTERS); + } + + setText(text, bufferType); + + /* + * Views are not normally focusable unless specified to be. + * However, TextViews that have input or movement methods *are* + * focusable by default. + */ + a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.View, + defStyle, 0); + + boolean focusable = mMovement != null || mInput != null; + boolean clickable = focusable; + boolean longClickable = focusable; + + n = a.getIndexCount(); + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + switch (attr) { + case com.android.internal.R.styleable.View_focusable: + focusable = a.getBoolean(attr, focusable); + break; + + case com.android.internal.R.styleable.View_clickable: + clickable = a.getBoolean(attr, clickable); + break; + + case com.android.internal.R.styleable.View_longClickable: + longClickable = a.getBoolean(attr, longClickable); + break; + } + } + a.recycle(); + + setFocusable(focusable); + setClickable(clickable); + setLongClickable(longClickable); + } + + private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { + Typeface tf = null; + switch (typefaceIndex) { + case SANS: + tf = Typeface.SANS_SERIF; + break; + + case SERIF: + tf = Typeface.SERIF; + break; + + case MONOSPACE: + tf = Typeface.MONOSPACE; + break; + } + + setTypeface(tf, styleIndex); + } + + /** + * Sets the typeface and style in which the text should be displayed, + * and turns on the fake bold and italic bits in the Paint if the + * Typeface that you provided does not have all the bits in the + * style that you specified. + * + * @attr ref android.R.styleable#TextView_typeface + * @attr ref android.R.styleable#TextView_textStyle + */ + public void setTypeface(Typeface tf, int style) { + if (style > 0) { + if (tf == null) { + tf = Typeface.defaultFromStyle(style); + } else { + tf = Typeface.create(tf, style); + } + + setTypeface(tf); + // now compute what (if any) algorithmic styling is needed + int typefaceStyle = tf != null ? tf.getStyle() : 0; + int need = style & ~typefaceStyle; + mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); + mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); + } else { + mTextPaint.setFakeBoldText(false); + mTextPaint.setTextSkewX(0); + setTypeface(tf); + } + } + + /** + * Subclasses override this to specify that they have a KeyListener + * by default even if not specifically called for in the XML options. + */ + protected boolean getDefaultEditable() { + return false; + } + + /** + * Subclasses override this to specify a default movement method. + */ + protected MovementMethod getDefaultMovementMethod() { + return null; + } + + /** + * Return the text the TextView is displaying. If setText() was called + * with an argument of BufferType.SPANNABLE or BufferType.EDITABLE, + * you can cast the return value from this method to Spannable + * or Editable, respectively. + */ + public CharSequence getText() { + return mText; + } + + /** + * Returns the length, in characters, of the text managed by this TextView + */ + public int length() { + return mText.length(); + } + + /** + * @return the height of one standard line in pixels. Note that markup + * within the text can cause individual lines to be taller or shorter + * than this height, and the layout may contain additional first- + * or last-line padding. + */ + public int getLineHeight() { + return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + + mSpacingAdd); + } + + /** + * @return the Layout that is currently being used to display the text. + * This can be null if the text or width has recently changes. + */ + public final Layout getLayout() { + return mLayout; + } + + /** + * @return the current key listener for this TextView. + * This will frequently be null for non-EditText TextViews. + */ + public final KeyListener getKeyListener() { + return mInput; + } + + /** + * Sets the key listener to be used with this TextView. This can be null + * to disallow user input. + * <p> + * Be warned that if you want a TextView with a key listener or movement + * method not to be focusable, or if you want a TextView without a + * key listener or movement method to be focusable, you must call + * {@link #setFocusable} again after calling this to get the focusability + * back the way you want it. + * + * @attr ref android.R.styleable#TextView_numeric + * @attr ref android.R.styleable#TextView_digits + * @attr ref android.R.styleable#TextView_phoneNumber + * @attr ref android.R.styleable#TextView_inputMethod + * @attr ref android.R.styleable#TextView_capitalize + * @attr ref android.R.styleable#TextView_autoText + */ + public void setKeyListener(KeyListener input) { + mInput = input; + + if (mInput != null && !(mText instanceof Editable)) + setText(mText); + + setFilters((Editable) mText, mFilters); + fixFocusableAndClickableSettings(); + } + + /** + * @return the movement method being used for this TextView. + * This will frequently be null for non-EditText TextViews. + */ + public final MovementMethod getMovementMethod() { + return mMovement; + } + + /** + * Sets the movement method (arrow key handler) to be used for + * this TextView. This can be null to disallow using the arrow keys + * to move the cursor or scroll the view. + * <p> + * Be warned that if you want a TextView with a key listener or movement + * method not to be focusable, or if you want a TextView without a + * key listener or movement method to be focusable, you must call + * {@link #setFocusable} again after calling this to get the focusability + * back the way you want it. + */ + public final void setMovementMethod(MovementMethod movement) { + mMovement = movement; + + if (mMovement != null && !(mText instanceof Spannable)) + setText(mText); + + fixFocusableAndClickableSettings(); + } + + private void fixFocusableAndClickableSettings() { + if (mMovement != null || mInput != null) { + setFocusable(true); + setClickable(true); + setLongClickable(true); + } else { + setFocusable(false); + setClickable(false); + setLongClickable(false); + } + } + + /** + * @return the current transformation method for this TextView. + * This will frequently be null except for single-line and password + * fields. + */ + public final TransformationMethod getTransformationMethod() { + return mTransformation; + } + + /** + * Sets the transformation that is applied to the text that this + * TextView is displaying. + * + * @attr ref android.R.styleable#TextView_password + * @attr ref android.R.styleable#TextView_singleLine + */ + public final void setTransformationMethod(TransformationMethod method) { + if (mTransformation != null) { + if (mText instanceof Spannable) { + ((Spannable) mText).removeSpan(mTransformation); + } + } + + mTransformation = method; + + setText(mText); + } + + /** + * Returns the top padding of the view, plus space for the top + * Drawable if any. + */ + public int getCompoundPaddingTop() { + if (mDrawableTop == null) { + return mPaddingTop; + } else { + return mPaddingTop + mDrawablePadding + mDrawableSizeTop; + } + } + + /** + * Returns the bottom padding of the view, plus space for the bottom + * Drawable if any. + */ + public int getCompoundPaddingBottom() { + if (mDrawableBottom == null) { + return mPaddingBottom; + } else { + return mPaddingBottom + mDrawablePadding + mDrawableSizeBottom; + } + } + + /** + * Returns the left padding of the view, plus space for the left + * Drawable if any. + */ + public int getCompoundPaddingLeft() { + if (mDrawableLeft == null) { + return mPaddingLeft; + } else { + return mPaddingLeft + mDrawablePadding + mDrawableSizeLeft; + } + } + + /** + * Returns the right padding of the view, plus space for the right + * Drawable if any. + */ + public int getCompoundPaddingRight() { + if (mDrawableRight == null) { + return mPaddingRight; + } else { + return mPaddingRight + mDrawablePadding + mDrawableSizeRight; + } + } + + /** + * Returns the extended top padding of the view, including both the + * top Drawable if any and any extra space to keep more than maxLines + * of text from showing. It is only valid to call this after measuring. + */ + public int getExtendedPaddingTop() { + if (mMaxMode != LINES) { + return getCompoundPaddingTop(); + } + + if (mLayout.getLineCount() <= mMaximum) { + return getCompoundPaddingTop(); + } + + int top = getCompoundPaddingTop(); + int bottom = getCompoundPaddingBottom(); + int viewht = getHeight() - top - bottom; + int layoutht = mLayout.getLineTop(mMaximum); + + if (layoutht >= viewht) { + return top; + } + + final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + if (gravity == Gravity.TOP) { + return top; + } else if (gravity == Gravity.BOTTOM) { + return top + viewht - layoutht; + } else { // (gravity == Gravity.CENTER_VERTICAL) + return top + (viewht - layoutht) / 2; + } + } + + /** + * Returns the extended bottom padding of the view, including both the + * bottom Drawable if any and any extra space to keep more than maxLines + * of text from showing. It is only valid to call this after measuring. + */ + public int getExtendedPaddingBottom() { + if (mMaxMode != LINES) { + return getCompoundPaddingBottom(); + } + + if (mLayout.getLineCount() <= mMaximum) { + return getCompoundPaddingBottom(); + } + + int top = getCompoundPaddingTop(); + int bottom = getCompoundPaddingBottom(); + int viewht = getHeight() - top - bottom; + int layoutht = mLayout.getLineTop(mMaximum); + + if (layoutht >= viewht) { + return bottom; + } + + final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + if (gravity == Gravity.TOP) { + return bottom + viewht - layoutht; + } else if (gravity == Gravity.BOTTOM) { + return bottom; + } else { // (gravity == Gravity.CENTER_VERTICAL) + return bottom + (viewht - layoutht) / 2; + } + } + + /** + * Returns the total left padding of the view, including the left + * Drawable if any. + */ + public int getTotalPaddingLeft() { + return getCompoundPaddingLeft(); + } + + /** + * Returns the total right padding of the view, including the right + * Drawable if any. + */ + public int getTotalPaddingRight() { + return getCompoundPaddingRight(); + } + + /** + * Returns the total top padding of the view, including the top + * Drawable if any, the extra space to keep more than maxLines + * from showing, and the vertical offset for gravity, if any. + */ + public int getTotalPaddingTop() { + return getExtendedPaddingTop() + getVerticalOffset(true); + } + + /** + * Returns the total bottom padding of the view, including the bottom + * Drawable if any, the extra space to keep more than maxLines + * from showing, and the vertical offset for gravity, if any. + */ + public int getTotalPaddingBottom() { + return getExtendedPaddingBottom() + getBottomVerticalOffset(true); + } + + /** + * Sets the Drawables (if any) to appear to the left of, above, + * to the right of, and below the text. Use null if you do not + * want a Drawable there. The Drawables must already have had + * {@link Drawable#setBounds} called. + * + * @attr ref android.R.styleable#TextView_drawableLeft + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableRight + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public void setCompoundDrawables(Drawable left, Drawable top, + Drawable right, Drawable bottom) { + mDrawableLeft = left; + mDrawableTop = top; + mDrawableRight = right; + mDrawableBottom = bottom; + + mDrawables = mDrawableLeft != null + || mDrawableRight != null + || mDrawableTop != null + || mDrawableBottom != null; + + if (mCompoundRect == null && + (left != null || top != null || right != null || bottom != null)) { + mCompoundRect = new Rect(); + } + + final Rect compoundRect = mCompoundRect; + int[] state = null; + + if (mDrawables) { + state = getDrawableState(); + } + + if (mDrawableLeft != null) { + mDrawableLeft.setState(state); + mDrawableLeft.copyBounds(compoundRect); + mDrawableSizeLeft = compoundRect.width(); + mDrawableHeightLeft = compoundRect.height(); + } else { + mDrawableSizeLeft = mDrawableHeightLeft = 0; + } + + if (mDrawableRight != null) { + mDrawableRight.setState(state); + mDrawableRight.copyBounds(compoundRect); + mDrawableSizeRight = compoundRect.width(); + mDrawableHeightRight = compoundRect.height(); + } else { + mDrawableSizeRight = mDrawableHeightRight = 0; + } + + if (mDrawableTop != null) { + mDrawableTop.setState(state); + mDrawableTop.copyBounds(compoundRect); + mDrawableSizeTop = compoundRect.height(); + mDrawableWidthTop = compoundRect.width(); + } else { + mDrawableSizeTop = mDrawableWidthTop = 0; + } + + if (mDrawableBottom != null) { + mDrawableBottom.setState(state); + mDrawableBottom.copyBounds(compoundRect); + mDrawableSizeBottom = compoundRect.height(); + mDrawableWidthBottom = compoundRect.width(); + } else { + mDrawableSizeBottom = mDrawableWidthBottom = 0; + } + + invalidate(); + requestLayout(); + } + + /** + * Sets the Drawables (if any) to appear to the left of, above, + * to the right of, and below the text. Use null if you do not + * want a Drawable there. The Drawables' bounds will be set to + * their intrinsic bounds. + * + * @attr ref android.R.styleable#TextView_drawableLeft + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableRight + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, + Drawable top, + Drawable right, Drawable bottom) { + if (left != null) { + left.setBounds(0, 0, + left.getIntrinsicWidth(), left.getIntrinsicHeight()); + } + if (right != null) { + right.setBounds(0, 0, + right.getIntrinsicWidth(), right.getIntrinsicHeight()); + } + if (top != null) { + top.setBounds(0, 0, + top.getIntrinsicWidth(), top.getIntrinsicHeight()); + } + if (bottom != null) { + bottom.setBounds(0, 0, + bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight()); + } + setCompoundDrawables(left, top, right, bottom); + } + + /** + * Returns drawables for the left, top, right, and bottom borders. + */ + public Drawable[] getCompoundDrawables() { + return new Drawable[] { + mDrawableLeft, mDrawableTop, mDrawableRight, mDrawableBottom + }; + } + + /** + * Sets the size of the padding between the compound drawables and + * the text. + * + * @attr ref android.R.styleable#TextView_drawablePadding + */ + public void setCompoundDrawablePadding(int pad) { + mDrawablePadding = pad; + + invalidate(); + requestLayout(); + } + + /** + * Returns the padding between the compound drawables and the text. + */ + public int getCompoundDrawablePadding() { + return mDrawablePadding; + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + if (left != getPaddingLeft() || + right != getPaddingRight() || + top != getPaddingTop() || + bottom != getPaddingBottom()) { + nullLayouts(); + } + + // the super call will requestLayout() + super.setPadding(left, top, right, bottom); + invalidate(); + } + + /** + * Gets the autolink mask of the text. See {@link + * android.text.util.Linkify#ALL Linkify.ALL} and peers for + * possible values. + * + * @attr ref android.R.styleable#TextView_autoLink + */ + public final int getAutoLinkMask() { + return mAutoLinkMask; + } + + /** + * Sets the text color, size, style, hint color, and highlight color + * from the specified TextAppearance resource. + */ + public void setTextAppearance(Context context, int resid) { + TypedArray appearance = + context.obtainStyledAttributes(resid, + com.android.internal.R.styleable.TextAppearance); + + int color; + ColorStateList colors; + int ts; + + color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0); + if (color != 0) { + setHighlightColor(color); + } + + colors = appearance.getColorStateList(com.android.internal.R.styleable. + TextAppearance_textColor); + if (colors != null) { + setTextColor(colors); + } + + ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable. + TextAppearance_textSize, 0); + if (ts != 0) { + setRawTextSize(ts); + } + + colors = appearance.getColorStateList(com.android.internal.R.styleable. + TextAppearance_textColorHint); + if (colors != null) { + setHintTextColor(colors); + } + + colors = appearance.getColorStateList(com.android.internal.R.styleable. + TextAppearance_textColorLink); + if (colors != null) { + setLinkTextColor(colors); + } + + int typefaceIndex, styleIndex; + + typefaceIndex = appearance.getInt(com.android.internal.R.styleable. + TextAppearance_typeface, -1); + styleIndex = appearance.getInt(com.android.internal.R.styleable. + TextAppearance_textStyle, -1); + + setTypefaceByIndex(typefaceIndex, styleIndex); + appearance.recycle(); + } + + /** + * @return the size (in pixels) of the default text size in this TextView. + */ + public float getTextSize() { + return mTextPaint.getTextSize(); + } + + /** + * Set the default text size to the given value, interpreted as "scaled + * pixel" units. This size is adjusted based on the current density and + * user font size preference. + * + * @param size The scaled pixel size. + * + * @attr ref android.R.styleable#TextView_textSize + */ + public void setTextSize(float size) { + setTextSize(TypedValue.COMPLEX_UNIT_SP, size); + } + + /** + * Set the default text size to a given unit and value. See {@link + * TypedValue} for the possible dimension units. + * + * @param unit The desired dimension unit. + * @param size The desired size in the given units. + * + * @attr ref android.R.styleable#TextView_textSize + */ + public void setTextSize(int unit, float size) { + Context c = getContext(); + Resources r; + + if (c == null) + r = Resources.getSystem(); + else + r = c.getResources(); + + setRawTextSize(TypedValue.applyDimension( + unit, size, r.getDisplayMetrics())); + } + + private void setRawTextSize(float size) { + if (size != mTextPaint.getTextSize()) { + mTextPaint.setTextSize(size); + + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + } + + /** + * @return the extent by which text is currently being stretched + * horizontally. This will usually be 1. + */ + public float getTextScaleX() { + return mTextPaint.getTextScaleX(); + } + + /** + * Sets the extent by which text should be stretched horizontally. + * + * @attr ref android.R.styleable#TextView_textScaleX + */ + public void setTextScaleX(float size) { + if (size != mTextPaint.getTextScaleX()) { + mTextPaint.setTextScaleX(size); + + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + } + + /** + * Sets the typeface and style in which the text should be displayed. + * Note that not all Typeface families actually have bold and italic + * variants, so you may need to use + * {@link #setTypeface(Typeface, int)} to get the appearance + * that you actually want. + * + * @attr ref android.R.styleable#TextView_typeface + * @attr ref android.R.styleable#TextView_textStyle + */ + public void setTypeface(Typeface tf) { + if (mTextPaint.getTypeface() != tf) { + mTextPaint.setTypeface(tf); + + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + } + + /** + * @return the current typeface and style in which the text is being + * displayed. + */ + public Typeface getTypeface() { + return mTextPaint.getTypeface(); + } + + /** + * Sets the text color for all the states (normal, selected, + * focused) to be this color. + * + * @attr ref android.R.styleable#TextView_textColor + */ + public void setTextColor(int color) { + mTextColor = ColorStateList.valueOf(color); + updateTextColors(); + } + + /** + * Sets the text color. + * + * @attr ref android.R.styleable#TextView_textColor + */ + public void setTextColor(ColorStateList colors) { + if (colors == null) { + throw new NullPointerException(); + } + + mTextColor = colors; + updateTextColors(); + } + + /** + * Return the set of text colors. + * + * @return Returns the set of text colors. + */ + public final ColorStateList getTextColors() { + return mTextColor; + } + + /** + * <p>Return the current color selected for normal text.</p> + * + * @return Returns the current text color. + */ + public final int getCurrentTextColor() { + return mCurTextColor; + } + + /** + * Sets the color used to display the selection highlight. + * + * @attr ref android.R.styleable#TextView_textColorHighlight + */ + public void setHighlightColor(int color) { + if (mHighlightColor != color) { + mHighlightColor = color; + invalidate(); + } + } + + /** + * Gives the text a shadow of the specified radius and color, the specified + * distance from its normal position. + * + * @attr ref android.R.styleable#TextView_shadowColor + * @attr ref android.R.styleable#TextView_shadowDx + * @attr ref android.R.styleable#TextView_shadowDy + * @attr ref android.R.styleable#TextView_shadowRadius + */ + public void setShadowLayer(float radius, float dx, float dy, int color) { + mTextPaint.setShadowLayer(radius, dx, dy, color); + + mShadowRadius = radius; + mShadowDx = dx; + mShadowDy = dy; + + invalidate(); + } + + /** + * @return the base paint used for the text. Please use this only to + * consult the Paint's properties and not to change them. + */ + public TextPaint getPaint() { + return mTextPaint; + } + + /** + * Sets the autolink mask of the text. See {@link + * android.text.util.Linkify#ALL Linkify.ALL} and peers for + * possible values. + * + * @attr ref android.R.styleable#TextView_autoLink + */ + public final void setAutoLinkMask(int mask) { + mAutoLinkMask = mask; + } + + /** + * Sets whether the movement method will automatically be set to + * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been + * set to nonzero and links are detected in {@link #setText}. + * The default is true. + * + * @attr ref android.R.styleable#TextView_linksClickable + */ + public final void setLinksClickable(boolean whether) { + mLinksClickable = whether; + } + + /** + * Returns whether the movement method will automatically be set to + * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been + * set to nonzero and links are detected in {@link #setText}. + * The default is true. + * + * @attr ref android.R.styleable#TextView_linksClickable + */ + public final boolean getLinksClickable() { + return mLinksClickable; + } + + /** + * Returns the list of URLSpans attached to the text + * (by {@link Linkify} or otherwise) if any. You can call + * {@link URLSpan#getURL} on them to find where they link to + * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd} + * to find the region of the text they are attached to. + */ + public URLSpan[] getUrls() { + if (mText instanceof Spanned) { + return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class); + } else { + return new URLSpan[0]; + } + } + + /** + * Sets the color of the hint text. + * + * @attr ref android.R.styleable#TextView_textColorHint + */ + public final void setHintTextColor(int color) { + mHintTextColor = ColorStateList.valueOf(color); + updateTextColors(); + } + + /** + * Sets the color of the hint text. + * + * @attr ref android.R.styleable#TextView_textColorHint + */ + public final void setHintTextColor(ColorStateList colors) { + mHintTextColor = colors; + updateTextColors(); + } + + /** + * <p>Return the color used to paint the hint text.</p> + * + * @return Returns the list of hint text colors. + */ + public final ColorStateList getHintTextColors() { + return mHintTextColor; + } + + /** + * <p>Return the current color selected to paint the hint text.</p> + * + * @return Returns the current hint text color. + */ + public final int getCurrentHintTextColor() { + return mHintTextColor != null ? mCurHintTextColor : mCurTextColor; + } + + /** + * Sets the color of links in the text. + * + * @attr ref android.R.styleable#TextView_textColorLink + */ + public final void setLinkTextColor(int color) { + mLinkTextColor = ColorStateList.valueOf(color); + updateTextColors(); + } + + /** + * Sets the color of links in the text. + * + * @attr ref android.R.styleable#TextView_textColorLink + */ + public final void setLinkTextColor(ColorStateList colors) { + mLinkTextColor = colors; + updateTextColors(); + } + + /** + * <p>Returns the color used to paint links in the text.</p> + * + * @return Returns the list of link text colors. + */ + public final ColorStateList getLinkTextColors() { + return mLinkTextColor; + } + + /** + * Sets the horizontal alignment of the text and the + * vertical gravity that will be used when there is extra space + * in the TextView beyond what is required for the text itself. + * + * @see android.view.Gravity + * @attr ref android.R.styleable#TextView_gravity + */ + public void setGravity(int gravity) { + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.LEFT; + } + if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.TOP; + } + + boolean newLayout = false; + + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != + (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) { + newLayout = true; + } + + if (gravity != mGravity) { + invalidate(); + } + + mGravity = gravity; + + if (mLayout != null && newLayout) { + // XXX this is heavy-handed because no actual content changes. + int want = mLayout.getWidth(); + int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth(); + + makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING, + mRight - mLeft - getCompoundPaddingLeft() - + getCompoundPaddingRight(), true); + } + } + + /** + * Returns the horizontal and vertical alignment of this TextView. + * + * @see android.view.Gravity + * @attr ref android.R.styleable#TextView_gravity + */ + public int getGravity() { + return mGravity; + } + + /** + * @return the flags on the Paint being used to display the text. + * @see Paint#getFlags + */ + public int getPaintFlags() { + return mTextPaint.getFlags(); + } + + /** + * Sets flags on the Paint being used to display the text and + * reflows the text if they are different from the old flags. + * @see Paint#setFlags + */ + public void setPaintFlags(int flags) { + if (mTextPaint.getFlags() != flags) { + mTextPaint.setFlags(flags); + + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + } + + /** + * Sets whether the text should be allowed to be wider than the + * View is. If false, it will be wrapped to the width of the View. + * + * @attr ref android.R.styleable#TextView_scrollHorizontally + */ + public void setHorizontallyScrolling(boolean whether) { + mHorizontallyScrolling = whether; + + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + + /** + * Makes the TextView at least this many lines tall + * + * @attr ref android.R.styleable#TextView_minLines + */ + public void setMinLines(int minlines) { + mMinimum = minlines; + mMinMode = LINES; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView at least this many pixels tall + * + * @attr ref android.R.styleable#TextView_minHeight + */ + public void setMinHeight(int minHeight) { + mMinimum = minHeight; + mMinMode = PIXELS; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView at most this many lines tall + * + * @attr ref android.R.styleable#TextView_maxLines + */ + public void setMaxLines(int maxlines) { + mMaximum = maxlines; + mMaxMode = LINES; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView at most this many pixels tall + * + * @attr ref android.R.styleable#TextView_maxHeight + */ + public void setMaxHeight(int maxHeight) { + mMaximum = maxHeight; + mMaxMode = PIXELS; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView exactly this many lines tall + * + * @attr ref android.R.styleable#TextView_lines + */ + public void setLines(int lines) { + mMaximum = mMinimum = lines; + mMaxMode = mMinMode = LINES; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView exactly this many pixels tall. + * You could do the same thing by specifying this number in the + * LayoutParams. + * + * @attr ref android.R.styleable#TextView_height + */ + public void setHeight(int pixels) { + mMaximum = mMinimum = pixels; + mMaxMode = mMinMode = PIXELS; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView at least this many ems wide + * + * @attr ref android.R.styleable#TextView_minEms + */ + public void setMinEms(int minems) { + mMinWidth = minems; + mMinWidthMode = EMS; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView at least this many pixels wide + * + * @attr ref android.R.styleable#TextView_minWidth + */ + public void setMinWidth(int minpixels) { + mMinWidth = minpixels; + mMinWidthMode = PIXELS; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView at most this many ems wide + * + * @attr ref android.R.styleable#TextView_maxEms + */ + public void setMaxEms(int maxems) { + mMaxWidth = maxems; + mMaxWidthMode = EMS; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView at most this many pixels wide + * + * @attr ref android.R.styleable#TextView_maxWidth + */ + public void setMaxWidth(int maxpixels) { + mMaxWidth = maxpixels; + mMaxWidthMode = PIXELS; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView exactly this many ems wide + * + * @attr ref android.R.styleable#TextView_ems + */ + public void setEms(int ems) { + mMaxWidth = mMinWidth = ems; + mMaxWidthMode = mMinWidthMode = EMS; + + requestLayout(); + invalidate(); + } + + /** + * Makes the TextView exactly this many pixels wide. + * You could do the same thing by specifying this number in the + * LayoutParams. + * + * @attr ref android.R.styleable#TextView_width + */ + public void setWidth(int pixels) { + mMaxWidth = mMinWidth = pixels; + mMaxWidthMode = mMinWidthMode = PIXELS; + + requestLayout(); + invalidate(); + } + + + /** + * Sets line spacing for this TextView. Each line will have its height + * multiplied by <code>mult</code> and have <code>add</code> added to it. + * + * @attr ref android.R.styleable#TextView_lineSpacingExtra + * @attr ref android.R.styleable#TextView_lineSpacingMultiplier + */ + public void setLineSpacing(float add, float mult) { + mSpacingMult = mult; + mSpacingAdd = add; + + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + + /** + * Convenience method: Append the specified text to the TextView's + * display buffer, upgrading it to BufferType.EDITABLE if it was + * not already editable. + */ + public final void append(CharSequence text) { + append(text, 0, text.length()); + } + + /** + * Convenience method: Append the specified text slice to the TextView's + * display buffer, upgrading it to BufferType.EDITABLE if it was + * not already editable. + */ + public void append(CharSequence text, int start, int end) { + if (!(mText instanceof Editable)) { + setText(mText, BufferType.EDITABLE); + } + + ((Editable) mText).append(text, start, end); + } + + private void updateTextColors() { + boolean inval = false; + int color = mTextColor.getColorForState(getDrawableState(), 0); + if (color != mCurTextColor) { + mCurTextColor = color; + inval = true; + } + if (mLinkTextColor != null) { + color = mLinkTextColor.getColorForState(getDrawableState(), 0); + if (color != mTextPaint.linkColor) { + mTextPaint.linkColor = color; + inval = true; + } + } + if (mHintTextColor != null) { + color = mHintTextColor.getColorForState(getDrawableState(), 0); + if (color != mCurHintTextColor && mText.length() == 0) { + mCurHintTextColor = color; + inval = true; + } + } + if (inval) { + invalidate(); + } + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mTextColor != null && mTextColor.isStateful() + || (mHintTextColor != null && mHintTextColor.isStateful()) + || (mLinkTextColor != null && mLinkTextColor.isStateful())) { + updateTextColors(); + } + + int[] state = getDrawableState(); + if (mDrawableTop != null && mDrawableTop.isStateful()) { + mDrawableTop.setState(state); + } + if (mDrawableBottom != null && mDrawableBottom.isStateful()) { + mDrawableBottom.setState(state); + } + if (mDrawableLeft != null && mDrawableLeft.isStateful()) { + mDrawableLeft.setState(state); + } + if (mDrawableRight != null && mDrawableRight.isStateful()) { + mDrawableRight.setState(state); + } + } + + /** + * User interface state that is stored by TextView for implementing + * {@link View#onSaveInstanceState}. + */ + public static class SavedState extends BaseSavedState { + int selStart; + int selEnd; + CharSequence text; + boolean frozenWithFocus; + + SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(selStart); + out.writeInt(selEnd); + out.writeInt(frozenWithFocus ? 1 : 0); + TextUtils.writeToParcel(text, out, flags); + } + + @Override + public String toString() { + String str = "TextView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " start=" + selStart + " end=" + selEnd; + if (text != null) { + str += " text=" + text; + } + return str + "}"; + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + + private SavedState(Parcel in) { + super(in); + selStart = in.readInt(); + selEnd = in.readInt(); + frozenWithFocus = (in.readInt() != 0); + text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + // Save state if we are forced to + boolean save = mFreezesText; + int start = 0; + int end = 0; + + if (mText != null) { + start = Selection.getSelectionStart(mText); + end = Selection.getSelectionEnd(mText); + if (start >= 0 || end >= 0) { + // Or save state if there is a selection + save = true; + } + } + + if (save) { + SavedState ss = new SavedState(superState); + // XXX Should also save the current scroll position! + ss.selStart = start; + ss.selEnd = end; + + if (mText instanceof Spanned) { + /* + * Calling setText() strips off any ChangeWatchers; + * strip them now to avoid leaking references. + * But do it to a copy so that if there are any + * further changes to the text of this view, it + * won't get into an inconsistent state. + */ + + Spannable sp = new SpannableString(mText); + + for (ChangeWatcher cw : + sp.getSpans(0, sp.length(), ChangeWatcher.class)) { + sp.removeSpan(cw); + } + + ss.text = sp; + } else { + ss.text = mText.toString(); + } + + if (isFocused() && start >= 0 && end >= 0) { + ss.frozenWithFocus = true; + } + + return ss; + } + + return null; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState)state; + super.onRestoreInstanceState(ss.getSuperState()); + + // XXX restore buffer type too, as well as lots of other stuff + if (ss.text != null) { + setText(ss.text); + } + + if (ss.selStart >= 0 && ss.selEnd >= 0) { + if (mText instanceof Spannable) { + int len = mText.length(); + + if (ss.selStart > len || ss.selEnd > len) { + String restored = ""; + + if (ss.text != null) { + restored = "(restored) "; + } + + Log.e("TextView", "Saved cursor position " + ss.selStart + + "/" + ss.selEnd + " out of range for " + restored + + "text " + mText); + } else { + Selection.setSelection((Spannable) mText, ss.selStart, + ss.selEnd); + + if (ss.frozenWithFocus) { + mFrozenWithFocus = true; + } + } + } + } + } + + /** + * Control whether this text view saves its entire text contents when + * freezing to an icicle, in addition to dynamic state such as cursor + * position. By default this is false, not saving the text. Set to true + * if the text in the text view is not being saved somewhere else in + * persistent storage (such as in a content provider) so that if the + * view is later thawed the user will not lose their data. + * + * @param freezesText Controls whether a frozen icicle should include the + * entire text data: true to include it, false to not. + * + * @attr ref android.R.styleable#TextView_freezesText + */ + public void setFreezesText(boolean freezesText) { + mFreezesText = freezesText; + } + + /** + * Return whether this text view is including its entire text contents + * in frozen icicles. + * + * @return Returns true if text is included, false if it isn't. + * + * @see #setFreezesText + */ + public boolean getFreezesText() { + return mFreezesText; + } + + /////////////////////////////////////////////////////////////////////////// + + /** + * Sets the Factory used to create new Editables. + */ + public final void setEditableFactory(Editable.Factory factory) { + mEditableFactory = factory; + setText(mText); + } + + /** + * Sets the Factory used to create new Spannables. + */ + public final void setSpannableFactory(Spannable.Factory factory) { + mSpannableFactory = factory; + setText(mText); + } + + /** + * Sets the string value of the TextView. TextView <em>does not</em> accept + * HTML-like formatting, which you can do with text strings in XML resource files. + * To style your strings, attach android.text.style.* objects to a + * {@link android.text.SpannableString SpannableString}, or see + * <a href="{@docRoot}reference/available-resources.html#stringresources"> + * String Resources</a> for an example of setting formatted text in the XML resource file. + * + * @attr ref android.R.styleable#TextView_text + */ + public final void setText(CharSequence text) { + setText(text, mBufferType); + } + + /** + * Like {@link #setText(CharSequence)}, + * except that the cursor position (if any) is retained in the new text. + * + * @param text The new text to place in the text view. + * + * @see #setText(CharSequence) + */ + public final void setTextKeepState(CharSequence text) { + setTextKeepState(text, mBufferType); + } + + /** + * Sets the text that this TextView is to display (see + * {@link #setText(CharSequence)}) and also sets whether it is stored + * in a styleable/spannable buffer and whether it is editable. + * + * @attr ref android.R.styleable#TextView_text + * @attr ref android.R.styleable#TextView_bufferType + */ + public void setText(CharSequence text, BufferType type) { + setText(text, type, true, 0); + + if (mCharWrapper != null) { + mCharWrapper.mChars = null; + } + } + + private void setText(CharSequence text, BufferType type, + boolean notifyBefore, int oldlen) { + if (text == null) { + text = ""; + } + + int n = mFilters.length; + for (int i = 0; i < n; i++) { + CharSequence out = mFilters[i].filter(text, 0, text.length(), + EMPTY_SPANNED, 0, 0); + if (out != null) { + text = out; + } + } + + if (notifyBefore) { + if (mText != null) { + oldlen = mText.length(); + sendBeforeTextChanged(mText, 0, oldlen, text.length()); + } else { + sendBeforeTextChanged("", 0, 0, text.length()); + } + } + + if (type == BufferType.EDITABLE || mInput != null) { + Editable t = mEditableFactory.newEditable(text); + text = t; + + setFilters(t, mFilters); + } else if (type == BufferType.SPANNABLE || mMovement != null) { + text = mSpannableFactory.newSpannable(text); + } else if (!(text instanceof CharWrapper)) { + text = TextUtils.stringOrSpannedString(text); + } + + if (mAutoLinkMask != 0) { + Spannable s2; + + if (type == BufferType.EDITABLE || text instanceof Spannable) { + s2 = (Spannable) text; + } else { + s2 = mSpannableFactory.newSpannable(text); + } + + if (Linkify.addLinks(s2, mAutoLinkMask)) { + text = s2; + type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE; + + /* + * We must go ahead and set the text before changing the + * movement method, because setMovementMethod() may call + * setText() again to try to upgrade the buffer type. + */ + mText = text; + + if (mLinksClickable) { + setMovementMethod(LinkMovementMethod.getInstance()); + } + } + } + + mBufferType = type; + mText = text; + + if (mTransformation == null) + mTransformed = text; + else + mTransformed = mTransformation.getTransformation(text, this); + + final int textLength = text.length(); + + if (text instanceof Spannable) { + Spannable sp = (Spannable) text; + + // Remove any ChangeWatchers that might have come + // from other TextViews. + final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class); + final int count = watchers.length; + for (int i = 0; i < count; i++) + sp.removeSpan(watchers[i]); + + if (mChangeWatcher == null) + mChangeWatcher = new ChangeWatcher(); + + sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE | + (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT)); + + if (mInput != null) { + sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + if (mTransformation != null) { + sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + } + + if (mMovement != null) { + mMovement.initialize(this, (Spannable) text); + + /* + * Initializing the movement method will have set the + * selection, so reset mSelectionMoved to keep that from + * interfering with the normal on-focus selection-setting. + */ + mSelectionMoved = false; + } + } + + if (mLayout != null) { + checkForRelayout(); + } + + sendOnTextChanged(text, 0, oldlen, textLength); + onTextChanged(text, 0, oldlen, textLength); + } + + /** + * Sets the TextView to display the specified slice of the specified + * char array. You must promise that you will not change the contents + * of the array except for right before another call to setText(), + * since the TextView has no way to know that the text + * has changed and that it needs to invalidate and re-layout. + */ + public final void setText(char[] text, int start, int len) { + int oldlen = 0; + + if (start < 0 || len < 0 || start + len > text.length) { + throw new IndexOutOfBoundsException(start + ", " + len); + } + + /* + * We must do the before-notification here ourselves because if + * the old text is a CharWrapper we destroy it before calling + * into the normal path. + */ + if (mText != null) { + oldlen = mText.length(); + sendBeforeTextChanged(mText, 0, oldlen, len); + } else { + sendBeforeTextChanged("", 0, 0, len); + } + + if (mCharWrapper == null) { + mCharWrapper = new CharWrapper(text, start, len); + } else { + mCharWrapper.set(text, start, len); + } + + setText(mCharWrapper, mBufferType, false, oldlen); + } + + private static class CharWrapper + implements CharSequence, GetChars, GraphicsOperations { + private char[] mChars; + private int mStart, mLength; + + public CharWrapper(char[] chars, int start, int len) { + mChars = chars; + mStart = start; + mLength = len; + } + + /* package */ void set(char[] chars, int start, int len) { + mChars = chars; + mStart = start; + mLength = len; + } + + public int length() { + return mLength; + } + + public char charAt(int off) { + return mChars[off + mStart]; + } + + public String toString() { + return new String(mChars, mStart, mLength); + } + + public CharSequence subSequence(int start, int end) { + if (start < 0 || end < 0 || start > mLength || end > mLength) { + throw new IndexOutOfBoundsException(start + ", " + end); + } + + return new String(mChars, start + mStart, end - start); + } + + public void getChars(int start, int end, char[] buf, int off) { + if (start < 0 || end < 0 || start > mLength || end > mLength) { + throw new IndexOutOfBoundsException(start + ", " + end); + } + + System.arraycopy(mChars, start + mStart, buf, off, end - start); + } + + public void drawText(Canvas c, int start, int end, + float x, float y, Paint p) { + c.drawText(mChars, start + mStart, end - start, x, y, p); + } + + public float measureText(int start, int end, Paint p) { + return p.measureText(mChars, start + mStart, end - start); + } + + public int getTextWidths(int start, int end, float[] widths, Paint p) { + return p.getTextWidths(mChars, start + mStart, end - start, widths); + } + } + + /** + * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)}, + * except that the cursor position (if any) is retained in the new text. + * + * @see #setText(CharSequence, android.widget.TextView.BufferType) + */ + public final void setTextKeepState(CharSequence text, BufferType type) { + int start = getSelectionStart(); + int end = getSelectionEnd(); + int len = text.length(); + + setText(text, type); + + if (start >= 0 || end >= 0) { + if (mText instanceof Spannable) { + Selection.setSelection((Spannable) mText, + Math.max(0, Math.min(start, len)), + Math.max(0, Math.min(end, len))); + } + } + } + + public final void setText(int resid) { + setText(getContext().getResources().getText(resid)); + } + + public final void setText(int resid, BufferType type) { + setText(getContext().getResources().getText(resid), type); + } + + /** + * Sets the text to be displayed when the text of the TextView is empty. + * Null means to use the normal empty text. The hint does not + * currently participate in determining the size of the view. + * + * @attr ref android.R.styleable#TextView_hint + */ + public final void setHint(CharSequence hint) { + mHint = TextUtils.stringOrSpannedString(hint); + + if (mLayout != null) { + checkForRelayout(); + } + + if (mText.length() == 0) + invalidate(); + } + + /** + * Sets the text to be displayed when the text of the TextView is empty, + * from a resource. + * + * @attr ref android.R.styleable#TextView_hint + */ + public final void setHint(int resid) { + setHint(getContext().getResources().getText(resid)); + } + + /** + * Returns the hint that is displayed when the text of the TextView + * is empty. + * + * @attr ref android.R.styleable#TextView_hint + */ + public CharSequence getHint() { + return mHint; + } + + /** + * Returns the error message that was set to be displayed with + * {@link #setError}, or <code>null</code> if no error was set + * or if it the error was cleared by the widget after user input. + */ + public CharSequence getError() { + return mError; + } + + /** + * Sets the right-hand compound drawable of the TextView to the "error" + * icon and sets an error message that will be displayed in a popup when + * the TextView has focus. The icon and error message will be reset to + * null when any key events cause changes to the TextView's text. If the + * <code>error</code> is <code>null</code>, the error message and icon + * will be cleared. + */ + public void setError(CharSequence error) { + if (error == null) { + setError(null, null); + } else { + Drawable dr = getContext().getResources(). + getDrawable(com.android.internal.R.drawable. + indicator_input_error); + + dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight()); + setError(error, dr); + } + } + + /** + * Sets the right-hand compound drawable of the TextView to the specified + * icon and sets an error message that will be displayed in a popup when + * the TextView has focus. The icon and error message will be reset to + * null when any key events cause changes to the TextView's text. The + * drawable must already have had {@link Drawable#setBounds} set on it. + * If the <code>error</code> is <code>null</code>, the error message will + * be cleared (and you should provide a <code>null</code> icon as well). + */ + public void setError(CharSequence error, Drawable icon) { + error = TextUtils.stringOrSpannedString(error); + + mError = error; + mErrorWasChanged = true; + setCompoundDrawables(mDrawableLeft, mDrawableTop, + icon, mDrawableBottom); + + if (error == null) { + if (mPopup != null) { + if (mPopup.isShowing()) { + mPopup.dismiss(); + } + + mPopup = null; + } + } else { + if (isFocused()) { + showError(); + } + } + } + + private void showError() { + if (mPopup == null) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint, + null); + + mPopup = new PopupWindow(err, 200, 50); + mPopup.setFocusable(false); + } + + TextView tv = (TextView) mPopup.getContentView(); + chooseSize(mPopup, mError, tv); + tv.setText(mError); + + mPopup.showAsDropDown(this, getErrorX(), getErrorY()); + } + + /** + * Returns the Y offset to make the pointy top of the error point + * at the middle of the error icon. + */ + private int getErrorX() { + /* + * The "25" is the distance between the point and the right edge + * of the background + */ + + return getWidth() - mPopup.getWidth() + - getPaddingRight() - mDrawableSizeRight / 2 + 25; + } + + /** + * Returns the Y offset to make the pointy top of the error point + * at the bottom of the error icon. + */ + private int getErrorY() { + /* + * Compound, not extended, because the icon is not clipped + * if the text height is smaller. + */ + int vspace = mBottom - mTop - + getCompoundPaddingBottom() - getCompoundPaddingTop(); + + int icontop = getCompoundPaddingTop() + + (vspace - mDrawableHeightRight) / 2; + + /* + * The "2" is the distance between the point and the top edge + * of the background. + */ + + return icontop + mDrawableHeightRight - getHeight() - 2; + } + + private void hideError() { + if (mPopup != null) { + if (mPopup.isShowing()) { + mPopup.dismiss(); + } + } + } + + private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { + int wid = tv.getPaddingLeft() + tv.getPaddingRight(); + int ht = tv.getPaddingTop() + tv.getPaddingBottom(); + + /* + * Figure out how big the text would be if we laid it out to the + * full width of this view minus the border. + */ + int cap = getWidth() - wid; + if (cap < 0) { + cap = 200; // We must not be measured yet -- setFrame() will fix it. + } + + Layout l = new StaticLayout(text, tv.getPaint(), cap, + Layout.Alignment.ALIGN_NORMAL, 1, 0, true); + float max = 0; + for (int i = 0; i < l.getLineCount(); i++) { + max = Math.max(max, l.getLineWidth(i)); + } + + /* + * Now set the popup size to be big enough for the text plus the border. + */ + pop.setWidth(wid + (int) Math.ceil(max)); + pop.setHeight(ht + l.getHeight()); + } + + + @Override + protected boolean setFrame(int l, int t, int r, int b) { + boolean result = super.setFrame(l, t, r, b); + + if (mPopup != null) { + TextView tv = (TextView) mPopup.getContentView(); + chooseSize(mPopup, mError, tv); + mPopup.update(this, getErrorX(), getErrorY(), -1, -1); + } + + return result; + } + + /** + * Sets the list of input filters that will be used if the buffer is + * Editable. Has no effect otherwise. + * + * @attr ref android.R.styleable#TextView_maxLength + */ + public void setFilters(InputFilter[] filters) { + if (filters == null) { + throw new IllegalArgumentException(); + } + + mFilters = filters; + + if (mText instanceof Editable) { + setFilters((Editable) mText, filters); + } + } + + /** + * Sets the list of input filters on the specified Editable, + * and includes mInput in the list if it is an InputFilter. + */ + private void setFilters(Editable e, InputFilter[] filters) { + if (mInput instanceof InputFilter) { + InputFilter[] nf = new InputFilter[filters.length + 1]; + + System.arraycopy(filters, 0, nf, 0, filters.length); + nf[filters.length] = (InputFilter) mInput; + + e.setFilters(nf); + } else { + e.setFilters(filters); + } + } + + /** + * Returns the current list of input filters. + */ + public InputFilter[] getFilters() { + return mFilters; + } + + ///////////////////////////////////////////////////////////////////////// + + private int getVerticalOffset(boolean forceNormal) { + int voffset = 0; + final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + + Layout l = mLayout; + if (!forceNormal && mText.length() == 0 && mHintLayout != null) { + l = mHintLayout; + } + + if (gravity != Gravity.TOP) { + int boxht; + + if (l == mHintLayout) { + boxht = getMeasuredHeight() - getCompoundPaddingTop() - + getCompoundPaddingBottom(); + } else { + boxht = getMeasuredHeight() - getExtendedPaddingTop() - + getExtendedPaddingBottom(); + } + int textht = l.getHeight(); + + if (textht < boxht) { + if (gravity == Gravity.BOTTOM) + voffset = boxht - textht; + else // (gravity == Gravity.CENTER_VERTICAL) + voffset = (boxht - textht) >> 1; + } + } + return voffset; + } + + private int getBottomVerticalOffset(boolean forceNormal) { + int voffset = 0; + final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + + Layout l = mLayout; + if (!forceNormal && mText.length() == 0 && mHintLayout != null) { + l = mHintLayout; + } + + if (gravity != Gravity.BOTTOM) { + int boxht; + + if (l == mHintLayout) { + boxht = getMeasuredHeight() - getCompoundPaddingTop() - + getCompoundPaddingBottom(); + } else { + boxht = getMeasuredHeight() - getExtendedPaddingTop() - + getExtendedPaddingBottom(); + } + int textht = l.getHeight(); + + if (textht < boxht) { + if (gravity == Gravity.TOP) + voffset = boxht - textht; + else // (gravity == Gravity.CENTER_VERTICAL) + voffset = (boxht - textht) >> 1; + } + } + return voffset; + } + + private void invalidateCursorPath() { + if (mHighlightPathBogus) { + invalidateCursor(); + } else { + synchronized (sTempRect) { + mHighlightPath.computeBounds(sTempRect, false); + + int left = getCompoundPaddingLeft(); + int top = getExtendedPaddingTop() + getVerticalOffset(true); + + invalidate((int) sTempRect.left + left, + (int) sTempRect.top + top, + (int) sTempRect.right + left + 1, + (int) sTempRect.bottom + top + 1); + } + } + } + + private void invalidateCursor() { + int where = Selection.getSelectionEnd(mText); + + invalidateCursor(where, where, where); + } + + private void invalidateCursor(int a, int b, int c) { + if (mLayout == null) { + invalidate(); + } else { + if (a >= 0 || b >= 0 || c >= 0) { + int first = Math.min(Math.min(a, b), c); + int last = Math.max(Math.max(a, b), c); + + int line = mLayout.getLineForOffset(first); + int top = mLayout.getLineTop(line); + + // This is ridiculous, but the descent from the line above + // can hang down into the line we really want to redraw, + // so we have to invalidate part of the line above to make + // sure everything that needs to be redrawn really is. + // (But not the whole line above, because that would cause + // the same problem with the descenders on the line above it!) + if (line > 0) { + top -= mLayout.getLineDescent(line - 1); + } + + int line2; + + if (first == last) + line2 = line; + else + line2 = mLayout.getLineForOffset(last); + + int bottom = mLayout.getLineTop(line2 + 1); + int voffset = getVerticalOffset(true); + + int left = getCompoundPaddingLeft() + mScrollX; + invalidate(left, top + voffset + getExtendedPaddingTop(), + left + getWidth() - getCompoundPaddingLeft() - + getCompoundPaddingRight(), + bottom + voffset + getExtendedPaddingTop()); + } + } + } + + private void registerForPreDraw() { + final ViewTreeObserver observer = getViewTreeObserver(); + if (observer == null) { + return; + } + + if (mPreDrawState == PREDRAW_NOT_REGISTERED) { + observer.addOnPreDrawListener(this); + mPreDrawState = PREDRAW_PENDING; + } else if (mPreDrawState == PREDRAW_DONE) { + mPreDrawState = PREDRAW_PENDING; + } + + // else state is PREDRAW_PENDING, so keep waiting. + } + + /** + * {@inheritDoc} + */ + public boolean onPreDraw() { + if (mPreDrawState != PREDRAW_PENDING) { + return true; + } + + if (mLayout == null) { + assumeLayout(); + } + + boolean changed = false; + + if (mMovement != null) { + int curs = Selection.getSelectionEnd(mText); + + /* + * TODO: This should really only keep the end in view if + * it already was before the text changed. I'm not sure + * of a good way to tell from here if it was. + */ + if (curs < 0 && + (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { + curs = mText.length(); + } + + if (curs >= 0) { + changed = bringPointIntoView(curs); + } + } else { + changed = bringTextIntoView(); + } + + mPreDrawState = PREDRAW_DONE; + return !changed; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mPreDrawState != PREDRAW_NOT_REGISTERED) { + final ViewTreeObserver observer = getViewTreeObserver(); + if (observer != null) { + observer.removeOnPreDrawListener(this); + mPreDrawState = PREDRAW_NOT_REGISTERED; + } + } + } + + @Override + protected void onDraw(Canvas canvas) { + // Draw the background for this view + super.onDraw(canvas); + + final int compoundPaddingLeft = getCompoundPaddingLeft(); + final int compoundPaddingTop = getCompoundPaddingTop(); + final int compoundPaddingRight = getCompoundPaddingRight(); + final int compoundPaddingBottom = getCompoundPaddingBottom(); + final int scrollX = mScrollX; + final int scrollY = mScrollY; + final int right = mRight; + final int left = mLeft; + final int bottom = mBottom; + final int top = mTop; + + if (mDrawables) { + /* + * Compound, not extended, because the icon is not clipped + * if the text height is smaller. + */ + + int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop; + int hspace = right - left - compoundPaddingRight - compoundPaddingLeft; + + if (mDrawableLeft != null) { + canvas.save(); + canvas.translate(scrollX + mPaddingLeft, + scrollY + compoundPaddingTop + + (vspace - mDrawableHeightLeft) / 2); + mDrawableLeft.draw(canvas); + canvas.restore(); + } + + if (mDrawableRight != null) { + canvas.save(); + canvas.translate(scrollX + right - left - mPaddingRight - mDrawableSizeRight, + scrollY + compoundPaddingTop + (vspace - mDrawableHeightRight) / 2); + mDrawableRight.draw(canvas); + canvas.restore(); + } + + if (mDrawableTop != null) { + canvas.save(); + canvas.translate(scrollX + compoundPaddingLeft + (hspace - mDrawableWidthTop) / 2, + scrollY + mPaddingTop); + mDrawableTop.draw(canvas); + canvas.restore(); + } + + if (mDrawableBottom != null) { + canvas.save(); + canvas.translate(scrollX + compoundPaddingLeft + + (hspace - mDrawableWidthBottom) / 2, + scrollY + bottom - top - mPaddingBottom - mDrawableSizeBottom); + mDrawableBottom.draw(canvas); + canvas.restore(); + } + } + + if (mPreDrawState == PREDRAW_DONE) { + final ViewTreeObserver observer = getViewTreeObserver(); + if (observer != null) { + observer.removeOnPreDrawListener(this); + mPreDrawState = PREDRAW_NOT_REGISTERED; + } + } + + int color = mCurTextColor; + + if (mLayout == null) { + assumeLayout(); + } + + Layout layout = mLayout; + int cursorcolor = color; + + if (mHint != null && mText.length() == 0) { + if (mHintTextColor != null) { + color = mCurHintTextColor; + } + + layout = mHintLayout; + } + + mTextPaint.setColor(color); + mTextPaint.drawableState = getDrawableState(); + + canvas.save(); + /* Would be faster if we didn't have to do this. Can we chop the + (displayable) text so that we don't need to do this ever? + */ + + int extendedPaddingTop = getExtendedPaddingTop(); + int extendedPaddingBottom = getExtendedPaddingBottom(); + + float clipLeft = compoundPaddingLeft + scrollX; + float clipTop = extendedPaddingTop + scrollY; + float clipRight = right - left - compoundPaddingRight + scrollX; + float clipBottom = bottom - top - extendedPaddingBottom + scrollY; + + if (mShadowRadius != 0) { + clipLeft += Math.min(0, mShadowDx - mShadowRadius); + clipRight += Math.max(0, mShadowDx + mShadowRadius); + + clipTop += Math.min(0, mShadowDy - mShadowRadius); + clipBottom += Math.max(0, mShadowDy + mShadowRadius); + } + + canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom); + + int voffsetText = 0; + int voffsetCursor = 0; + + // translate in by our padding + { + /* shortcircuit calling getVerticaOffset() */ + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { + voffsetText = getVerticalOffset(false); + voffsetCursor = getVerticalOffset(true); + } + canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText); + } + + Path highlight = null; + + // If there is no movement method, then there can be no selection. + // Check that first and attempt to skip everything having to do with + // the cursor. + // XXX This is not strictly true -- a program could set the + // selection manually if it really wanted to. + if (mMovement != null && (isFocused() || isPressed())) { + int start = Selection.getSelectionStart(mText); + int end = Selection.getSelectionEnd(mText); + + if (mCursorVisible && start >= 0 && isEnabled()) { + if (mHighlightPath == null) + mHighlightPath = new Path(); + + if (start == end) { + if ((SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) + < BLINK) { + if (mHighlightPathBogus) { + mHighlightPath.reset(); + mLayout.getCursorPath(start, mHighlightPath, mText); + mHighlightPathBogus = false; + } + + // XXX should pass to skin instead of drawing directly + mHighlightPaint.setColor(cursorcolor); + mHighlightPaint.setStyle(Paint.Style.STROKE); + + highlight = mHighlightPath; + } + } else { + if (mHighlightPathBogus) { + mHighlightPath.reset(); + mLayout.getSelectionPath(start, end, mHighlightPath); + mHighlightPathBogus = false; + } + + // XXX should pass to skin instead of drawing directly + mHighlightPaint.setColor(mHighlightColor); + mHighlightPaint.setStyle(Paint.Style.FILL); + + highlight = mHighlightPath; + } + } + } + + /* Comment out until we decide what to do about animations + boolean isLinearTextOn = false; + if (currentTransformation != null) { + isLinearTextOn = mTextPaint.isLinearTextOn(); + Matrix m = currentTransformation.getMatrix(); + if (!m.isIdentity()) { + // mTextPaint.setLinearTextOn(true); + } + } + */ + + layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText); + + /* Comment out until we decide what to do about animations + if (currentTransformation != null) { + mTextPaint.setLinearTextOn(isLinearTextOn); + } + */ + + canvas.restore(); + } + + @Override + public void getFocusedRect(Rect r) { + if (mLayout == null) { + super.getFocusedRect(r); + return; + } + + int sel = getSelectionEnd(); + if (sel < 0) { + super.getFocusedRect(r); + return; + } + + int line = mLayout.getLineForOffset(sel); + r.top = mLayout.getLineTop(line); + r.bottom = mLayout.getLineBottom(line); + + r.left = (int) mLayout.getPrimaryHorizontal(sel); + r.right = r.left + 1; + } + + /** + * Return the number of lines of text, or 0 if the internal Layout has not + * been built. + */ + public int getLineCount() { + return mLayout != null ? mLayout.getLineCount() : 0; + } + + /** + * Return the baseline for the specified line (0...getLineCount() - 1) + * If bounds is not null, return the top, left, right, bottom extents + * of the specified line in it. If the internal Layout has not been built, + * return 0 and set bounds to (0, 0, 0, 0) + * @param line which line to examine (0..getLineCount() - 1) + * @param bounds Optional. If not null, it returns the extent of the line + * @return the Y-coordinate of the baseline + */ + public int getLineBounds(int line, Rect bounds) { + if (mLayout == null) { + if (bounds != null) { + bounds.set(0, 0, 0, 0); + } + return 0; + } + else { + int baseline = mLayout.getLineBounds(line, bounds); + + int voffset = getExtendedPaddingTop(); + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { + voffset += getVerticalOffset(true); + } + if (bounds != null) { + bounds.offset(getCompoundPaddingLeft(), voffset); + } + return baseline + voffset; + } + } + + @Override + public int getBaseline() { + if (mLayout == null) { + return super.getBaseline(); + } + + int voffset = 0; + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { + voffset = getVerticalOffset(true); + } + + return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (!isEnabled()) { + return super.onKeyDown(keyCode, event); + } + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (mSingleLine && mInput != null) { + return super.onKeyDown(keyCode, event); + } + } + + if (mInput != null) { + /* + * Keep track of what the error was before doing the input + * so that if an input filter changed the error, we leave + * that error showing. Otherwise, we take down whatever + * error was showing when the user types something. + */ + mErrorWasChanged = false; + + if (mInput.onKeyDown(this, (Editable) mText, keyCode, event)) { + if (mError != null && !mErrorWasChanged) { + setError(null, null); + } + return true; + } + } + + // bug 650865: sometimes we get a key event before a layout. + // don't try to move around if we don't know the layout. + + if (mMovement != null && mLayout != null) + if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event)) + return true; + + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (!isEnabled()) { + return super.onKeyUp(keyCode, event); + } + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (mSingleLine && mInput != null) { + /* + * If there is a click listener, just call through to + * super, which will invoke it. + * + * If there isn't a click listener, try to advance focus, + * but still call through to super, which will reset the + * pressed state and longpress state. (It will also + * call performClick(), but that won't do anything in + * this case.) + */ + if (mOnClickListener == null) { + View v = focusSearch(FOCUS_DOWN); + + if (v != null) { + if (!v.requestFocus(FOCUS_DOWN)) { + throw new IllegalStateException("focus search returned a view " + + "that wasn't able to take focus!"); + } + + /* + * Return true because we handled the key; super + * will return false because there was no click + * listener. + */ + super.onKeyUp(keyCode, event); + return true; + } + } + + return super.onKeyUp(keyCode, event); + } + } + + if (mInput != null) + if (mInput.onKeyUp(this, (Editable) mText, keyCode, event)) + return true; + + if (mMovement != null && mLayout != null) + if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event)) + return true; + + return super.onKeyUp(keyCode, event); + } + + private void nullLayouts() { + if (mLayout instanceof BoringLayout && mSavedLayout == null) { + mSavedLayout = (BoringLayout) mLayout; + } + if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) { + mSavedHintLayout = (BoringLayout) mHintLayout; + } + + mLayout = mHintLayout = null; + } + + /** + * Make a new Layout based on the already-measured size of the view, + * on the assumption that it was measured correctly at some point. + */ + private void assumeLayout() { + int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); + + if (width < 1) { + width = 0; + } + + int physicalWidth = width; + + if (mHorizontallyScrolling) { + width = VERY_WIDE; + } + + makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING, + physicalWidth, false); + } + + /** + * The width passed in is now the desired layout width, + * not the full view width with padding. + * {@hide} + */ + protected void makeNewLayout(int w, int hintWidth, + BoringLayout.Metrics boring, + BoringLayout.Metrics hintBoring, + int ellipsisWidth, boolean bringIntoView) { + mHighlightPathBogus = true; + + if (w < 0) { + w = 0; + } + if (hintWidth < 0) { + hintWidth = 0; + } + + Layout.Alignment alignment; + switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + alignment = Layout.Alignment.ALIGN_CENTER; + break; + + case Gravity.RIGHT: + alignment = Layout.Alignment.ALIGN_OPPOSITE; + break; + + default: + alignment = Layout.Alignment.ALIGN_NORMAL; + } + + if (mText instanceof Spannable) { + mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w, + alignment, mSpacingMult, + mSpacingAdd, mIncludePad, mEllipsize, + ellipsisWidth); + } else { + if (boring == UNKNOWN_BORING) { + boring = BoringLayout.isBoring(mTransformed, mTextPaint, + mBoring); + if (boring != null) { + mBoring = boring; + } + } + + if (boring != null) { + if (boring.width <= w && + (mEllipsize == null || boring.width <= ellipsisWidth)) { + if (mSavedLayout != null) { + mLayout = mSavedLayout. + replaceOrMake(mTransformed, mTextPaint, + w, alignment, mSpacingMult, mSpacingAdd, + boring, mIncludePad); + } else { + mLayout = BoringLayout.make(mTransformed, mTextPaint, + w, alignment, mSpacingMult, mSpacingAdd, + boring, mIncludePad); + } + // Log.e("aaa", "Boring: " + mTransformed); + + mSavedLayout = (BoringLayout) mLayout; + } else if (mEllipsize != null && boring.width <= w) { + if (mSavedLayout != null) { + mLayout = mSavedLayout. + replaceOrMake(mTransformed, mTextPaint, + w, alignment, mSpacingMult, mSpacingAdd, + boring, mIncludePad, mEllipsize, + ellipsisWidth); + } else { + mLayout = BoringLayout.make(mTransformed, mTextPaint, + w, alignment, mSpacingMult, mSpacingAdd, + boring, mIncludePad, mEllipsize, + ellipsisWidth); + } + } else if (mEllipsize != null) { + mLayout = new StaticLayout(mTransformed, + 0, mTransformed.length(), + mTextPaint, w, alignment, mSpacingMult, + mSpacingAdd, mIncludePad, mEllipsize, + ellipsisWidth); + } else { + mLayout = new StaticLayout(mTransformed, mTextPaint, + w, alignment, mSpacingMult, mSpacingAdd, + mIncludePad); + // Log.e("aaa", "Boring but wide: " + mTransformed); + } + } else if (mEllipsize != null) { + mLayout = new StaticLayout(mTransformed, + 0, mTransformed.length(), + mTextPaint, w, alignment, mSpacingMult, + mSpacingAdd, mIncludePad, mEllipsize, + ellipsisWidth); + } else { + mLayout = new StaticLayout(mTransformed, mTextPaint, + w, alignment, mSpacingMult, mSpacingAdd, + mIncludePad); + } + } + + mHintLayout = null; + + if (mHint != null) { + if (hintBoring == UNKNOWN_BORING) { + hintBoring = BoringLayout.isBoring(mHint, mTextPaint, + mHintBoring); + if (hintBoring != null) { + mHintBoring = hintBoring; + } + } + + if (hintBoring != null) { + if (hintBoring.width <= hintWidth) { + if (mSavedHintLayout != null) { + mHintLayout = mSavedHintLayout. + replaceOrMake(mHint, mTextPaint, + hintWidth, alignment, mSpacingMult, + mSpacingAdd, hintBoring, mIncludePad); + } else { + mHintLayout = BoringLayout.make(mHint, mTextPaint, + hintWidth, alignment, mSpacingMult, + mSpacingAdd, hintBoring, mIncludePad); + } + + mSavedHintLayout = (BoringLayout) mHintLayout; + } else { + mHintLayout = new StaticLayout(mHint, mTextPaint, + hintWidth, alignment, mSpacingMult, mSpacingAdd, + mIncludePad); + } + } else { + mHintLayout = new StaticLayout(mHint, mTextPaint, + hintWidth, alignment, mSpacingMult, mSpacingAdd, + mIncludePad); + } + } + + if (bringIntoView) { + registerForPreDraw(); + } + } + + private static int desired(Layout layout) { + int n = layout.getLineCount(); + CharSequence text = layout.getText(); + float max = 0; + + // if any line was wrapped, we can't use it. + // but it's ok for the last line not to have a newline + + for (int i = 0; i < n - 1; i++) { + if (text.charAt(layout.getLineEnd(i) - 1) != '\n') + return -1; + } + + for (int i = 0; i < n; i++) { + max = Math.max(max, layout.getLineWidth(i)); + } + + return (int) FloatMath.ceil(max); + } + + /** + * Set whether the TextView includes extra top and bottom padding to make + * room for accents that go above the normal ascent and descent. + * The default is true. + * + * @attr ref android.R.styleable#TextView_includeFontPadding + */ + public void setIncludeFontPadding(boolean includepad) { + mIncludePad = includepad; + + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + + private static final BoringLayout.Metrics UNKNOWN_BORING = + new BoringLayout.Metrics(); + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + int width; + int height; + + BoringLayout.Metrics boring = UNKNOWN_BORING; + BoringLayout.Metrics hintBoring = UNKNOWN_BORING; + + int des = -1; + boolean fromexisting = false; + + if (widthMode == MeasureSpec.EXACTLY) { + // Parent has told us how big to be. So be it. + width = widthSize; + } else { + if (mLayout != null && mEllipsize == null) { + des = desired(mLayout); + } + + if (des < 0) { + boring = BoringLayout.isBoring(mTransformed, mTextPaint, + mBoring); + if (boring != null) { + mBoring = boring; + } + } else { + fromexisting = true; + } + + if (boring == null || boring == UNKNOWN_BORING) { + if (des < 0) { + des = (int) FloatMath.ceil(Layout. + getDesiredWidth(mTransformed, mTextPaint)); + } + + width = des; + } else { + width = boring.width; + } + + width = Math.max(width, mDrawableWidthTop); + width = Math.max(width, mDrawableWidthBottom); + + if (mHint != null) { + int hintDes = -1; + int hintWidth; + + if (mHintLayout != null) { + hintDes = desired(mHintLayout); + } + + if (hintDes < 0) { + hintBoring = BoringLayout.isBoring(mHint, mTextPaint, + mHintBoring); + if (hintBoring != null) { + mHintBoring = hintBoring; + } + } + + if (hintBoring == null || hintBoring == UNKNOWN_BORING) { + if (hintDes < 0) { + hintDes = (int) FloatMath.ceil(Layout. + getDesiredWidth(mHint, mTextPaint)); + } + + hintWidth = hintDes; + } else { + hintWidth = hintBoring.width; + } + + if (hintWidth > width) { + width = hintWidth; + } + } + + width += getCompoundPaddingLeft() + getCompoundPaddingRight(); + + if (mMaxWidthMode == EMS) { + width = Math.min(width, mMaxWidth * getLineHeight()); + } else { + width = Math.min(width, mMaxWidth); + } + + if (mMinWidthMode == EMS) { + width = Math.max(width, mMinWidth * getLineHeight()); + } else { + width = Math.max(width, mMinWidth); + } + + // Check against our minimum width + width = Math.max(width, getSuggestedMinimumWidth()); + + if (widthMode == MeasureSpec.AT_MOST) { + width = Math.min(widthSize, width); + } + } + + int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight(); + int unpaddedWidth = want; + int hintWant = want; + + if (mHorizontallyScrolling) + want = VERY_WIDE; + + int hintWidth = mHintLayout == null ? hintWant : mHintLayout.getWidth(); + + if (mLayout == null) { + makeNewLayout(want, hintWant, boring, hintBoring, + width - getCompoundPaddingLeft() - getCompoundPaddingRight(), + false); + } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) || + (mLayout.getEllipsizedWidth() != + width - getCompoundPaddingLeft() - getCompoundPaddingRight())) { + if (mHint == null && mEllipsize == null && + want > mLayout.getWidth() && + (mLayout instanceof BoringLayout || + (fromexisting && des >= 0 && des <= want))) { + mLayout.increaseWidthTo(want); + } else { + makeNewLayout(want, hintWant, boring, hintBoring, + width - getCompoundPaddingLeft() - getCompoundPaddingRight(), + false); + } + } else { + // Width has not changed. + } + + if (heightMode == MeasureSpec.EXACTLY) { + // Parent has told us how big to be. So be it. + height = heightSize; + mDesiredHeightAtMeasure = -1; + } else { + int desired = getDesiredHeight(); + + height = desired; + mDesiredHeightAtMeasure = desired; + + if (heightMode == MeasureSpec.AT_MOST) { + height = Math.min(desired, height); + } + } + + int unpaddedHeight = height - getCompoundPaddingTop() - + getCompoundPaddingBottom(); + if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) { + unpaddedHeight = Math.min(unpaddedHeight, + mLayout.getLineTop(mMaximum)); + } + + /* + * We didn't let makeNewLayout() register to bring the cursor into view, + * so do it here if there is any possibility that it is needed. + */ + if (mMovement != null || + mLayout.getWidth() > unpaddedWidth || + mLayout.getHeight() > unpaddedHeight) { + registerForPreDraw(); + } else { + scrollTo(0, 0); + } + + setMeasuredDimension(width, height); + } + + private int getDesiredHeight() { + return Math.max(getDesiredHeight(mLayout, true), + getDesiredHeight(mHintLayout, false)); + } + + private int getDesiredHeight(Layout layout, boolean cap) { + if (layout == null) { + return 0; + } + + int linecount = layout.getLineCount(); + int pad = getCompoundPaddingTop() + getCompoundPaddingBottom(); + int desired = layout.getLineTop(linecount); + + desired = Math.max(desired, mDrawableHeightLeft); + desired = Math.max(desired, mDrawableHeightRight); + + desired += pad; + + if (mMaxMode == LINES) { + /* + * Don't cap the hint to a certain number of lines. + * (Do cap it, though, if we have a maximum pixel height.) + */ + if (cap) { + if (linecount > mMaximum) { + desired = layout.getLineTop(mMaximum) + + layout.getBottomPadding(); + + desired = Math.max(desired, mDrawableHeightLeft); + desired = Math.max(desired, mDrawableHeightRight); + + desired += pad; + linecount = mMaximum; + } + } + } else { + desired = Math.min(desired, mMaximum); + } + + if (mMinMode == LINES) { + if (linecount < mMinimum) { + desired += getLineHeight() * (mMinimum - linecount); + } + } else { + desired = Math.max(desired, mMinimum); + } + + // Check against our minimum height + desired = Math.max(desired, getSuggestedMinimumHeight()); + + return desired; + } + + /** + * Check whether a change to the existing text layout requires a + * new view layout. + */ + private void checkForResize() { + boolean sizeChanged = false; + + if (mLayout != null) { + // Check if our width changed + if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) { + sizeChanged = true; + invalidate(); + } + + // Check if our height changed + if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) { + int desiredHeight = getDesiredHeight(); + + if (desiredHeight != this.getHeight()) { + sizeChanged = true; + } + } else if (mLayoutParams.height == LayoutParams.FILL_PARENT) { + if (mDesiredHeightAtMeasure >= 0) { + int desiredHeight = getDesiredHeight(); + + if (desiredHeight != mDesiredHeightAtMeasure) { + sizeChanged = true; + } + } + } + } + + if (sizeChanged) { + requestLayout(); + // caller will have already invalidated + } + } + + /** + * Check whether entirely new text requires a new view layout + * or merely a new text layout. + */ + private void checkForRelayout() { + // If we have a fixed width, we can just swap in a new text layout + // if the text height stays the same or if the view height is fixed. + + if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT || + (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) && + (mHint == null || mHintLayout != null) && + (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) { + // Static width, so try making a new text layout. + + int oldht = mLayout.getHeight(); + int want = mLayout.getWidth(); + int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth(); + + /* + * No need to bring the text into view, since the size is not + * changing (unless we do the requestLayout(), in which case it + * will happen at measure). + */ + makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING, + mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); + + // In a fixed-height view, so use our new text layout. + if (mLayoutParams.height != LayoutParams.WRAP_CONTENT && + mLayoutParams.height != LayoutParams.FILL_PARENT) { + invalidate(); + return; + } + + // Dynamic height, but height has stayed the same, + // so use our new text layout. + if (mLayout.getHeight() == oldht && + (mHintLayout == null || mHintLayout.getHeight() == oldht)) { + invalidate(); + return; + } + + // We lose: the height has changed and we have a dynamic height. + // Request a new view layout using our new text layout. + requestLayout(); + invalidate(); + } else { + // Dynamic width, so we have no choice but to request a new + // view layout with a new text layout. + + nullLayouts(); + requestLayout(); + invalidate(); + } + } + + /** + * Returns true if anything changed. + */ + private boolean bringTextIntoView() { + int line = 0; + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { + line = mLayout.getLineCount() - 1; + } + + Layout.Alignment a = mLayout.getParagraphAlignment(line); + int dir = mLayout.getParagraphDirection(line); + int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); + int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); + int ht = mLayout.getHeight(); + + int scrollx, scrolly; + + if (a == Layout.Alignment.ALIGN_CENTER) { + /* + * Keep centered if possible, or, if it is too wide to fit, + * keep leading edge in view. + */ + + int left = (int) FloatMath.floor(mLayout.getLineLeft(line)); + int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); + + if (right - left < hspace) { + scrollx = (right + left) / 2 - hspace / 2; + } else { + if (dir < 0) { + scrollx = right - hspace; + } else { + scrollx = left; + } + } + } else if (a == Layout.Alignment.ALIGN_NORMAL) { + /* + * Keep leading edge in view. + */ + + if (dir < 0) { + int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); + scrollx = right - hspace; + } else { + scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line)); + } + } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ { + /* + * Keep trailing edge in view. + */ + + if (dir < 0) { + scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line)); + } else { + int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); + scrollx = right - hspace; + } + } + + if (ht < vspace) { + scrolly = 0; + } else { + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { + scrolly = ht - vspace; + } else { + scrolly = 0; + } + } + + if (scrollx != mScrollX || scrolly != mScrollY) { + scrollTo(scrollx, scrolly); + return true; + } else { + return false; + } + } + + /** + * Returns true if anything changed. + */ + private boolean bringPointIntoView(int offset) { + boolean changed = false; + + int line = mLayout.getLineForOffset(offset); + + // FIXME: Is it okay to truncate this, or should we round? + final int x = (int)mLayout.getPrimaryHorizontal(offset); + final int top = mLayout.getLineTop(line); + final int bottom = mLayout.getLineTop(line+1); + + int left = (int) FloatMath.floor(mLayout.getLineLeft(line)); + int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); + int ht = mLayout.getHeight(); + + int grav; + + switch (mLayout.getParagraphAlignment(line)) { + case ALIGN_NORMAL: + grav = 1; + break; + + case ALIGN_OPPOSITE: + grav = -1; + break; + + default: + grav = 0; + } + + grav *= mLayout.getParagraphDirection(line); + + int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); + int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); + + int hslack = (bottom - top) / 2; + int vslack = hslack; + + if (vslack > vspace / 4) + vslack = vspace / 4; + if (hslack > hspace / 4) + hslack = hspace / 4; + + int hs = mScrollX; + int vs = mScrollY; + + if (top - vs < vslack) + vs = top - vslack; + if (bottom - vs > vspace - vslack) + vs = bottom - (vspace - vslack); + if (ht - vs < vspace) + vs = ht - vspace; + if (0 - vs > 0) + vs = 0; + + if (grav != 0) { + if (x - hs < hslack) { + hs = x - hslack; + } + if (x - hs > hspace - hslack) { + hs = x - (hspace - hslack); + } + } + + if (grav < 0) { + if (left - hs > 0) + hs = left; + if (right - hs < hspace) + hs = right - hspace; + } else if (grav > 0) { + if (right - hs < hspace) + hs = right - hspace; + if (left - hs > 0) + hs = left; + } else /* grav == 0 */ { + if (right - left <= hspace) { + /* + * If the entire text fits, center it exactly. + */ + hs = left - (hspace - (right - left)) / 2; + } else if (x > right - hslack) { + /* + * If we are near the right edge, keep the right edge + * at the edge of the view. + */ + hs = right - hspace; + } else if (x < left + hslack) { + /* + * If we are near the left edge, keep the left edge + * at the edge of the view. + */ + hs = left; + } else if (left > hs) { + /* + * Is there whitespace visible at the left? Fix it if so. + */ + hs = left; + } else if (right < hs + hspace) { + /* + * Is there whitespace visible at the right? Fix it if so. + */ + hs = right - hspace; + } else { + /* + * Otherwise, float as needed. + */ + if (x - hs < hslack) { + hs = x - hslack; + } + if (x - hs > hspace - hslack) { + hs = x - (hspace - hslack); + } + } + } + + if (hs != mScrollX || vs != mScrollY) { + if (mScroller == null) { + scrollTo(hs, vs); + } else { + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + int dx = hs - mScrollX; + int dy = vs - mScrollY; + + if (duration > ANIMATED_SCROLL_GAP) { + mScroller.startScroll(mScrollX, mScrollY, dx, dy); + invalidate(); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + scrollBy(dx, dy); + } + + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + changed = true; + } + + if (isFocused()) { + // This offsets because getInterestingRect() is in terms of + // viewport coordinates, but requestRectangleOnScreen() + // is in terms of content coordinates. + + Rect r = new Rect(); + getInterestingRect(r, x, top, bottom, line); + r.offset(mScrollX, mScrollY); + + if (requestRectangleOnScreen(r)) { + changed = true; + } + } + + return changed; + } + + @Override + public void computeScroll() { + if (mScroller != null) { + if (mScroller.computeScrollOffset()) { + mScrollX = mScroller.getCurrX(); + mScrollY = mScroller.getCurrY(); + postInvalidate(); // So we draw again + } + } + } + + private void getInterestingRect(Rect r, int h, int top, int bottom, + int line) { + top += getExtendedPaddingTop(); + bottom += getExtendedPaddingTop(); + h += getCompoundPaddingLeft(); + + if (line == 0) + top -= getExtendedPaddingTop(); + if (line == mLayout.getLineCount() - 1) + bottom += getExtendedPaddingBottom(); + + r.set(h, top, h, bottom); + r.offset(-mScrollX, -mScrollY); + } + + @Override + public void debug(int depth) { + super.debug(depth); + + String output = debugIndent(depth); + output += "frame={" + mLeft + ", " + mTop + ", " + mRight + + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY + + "} "; + + if (mText != null) { + + output += "mText=\"" + mText + "\" "; + if (mLayout != null) { + output += "mLayout width=" + mLayout.getWidth() + + " height=" + mLayout.getHeight(); + } + } else { + output += "mText=NULL"; + } + Log.d(VIEW_LOG_TAG, output); + } + + /** + * Convenience for {@link Selection#getSelectionStart}. + */ + public int getSelectionStart() { + return Selection.getSelectionStart(getText()); + } + + /** + * Convenience for {@link Selection#getSelectionEnd}. + */ + public int getSelectionEnd() { + return Selection.getSelectionEnd(getText()); + } + + /** + * Return true iff there is a selection inside this text view. + */ + public boolean hasSelection() { + return getSelectionStart() != getSelectionEnd(); + } + + /** + * Sets the properties of this field (lines, horizontally scrolling, + * transformation method) to be for a single-line input. + * + * @attr ref android.R.styleable#TextView_singleLine + */ + public void setSingleLine() { + setSingleLine(true); + } + + /** + * If true, sets the properties of this field (lines, horizontally + * scrolling, transformation method) to be for a single-line input; + * if false, restores these to the default conditions. + * Note that calling this with false restores default conditions, + * not necessarily those that were in effect prior to calling + * it with true. + * + * @attr ref android.R.styleable#TextView_singleLine + */ + public void setSingleLine(boolean singleLine) { + mSingleLine = singleLine; + + if (singleLine) { + setLines(1); + setHorizontallyScrolling(true); + setTransformationMethod(SingleLineTransformationMethod. + getInstance()); + } else { + setMaxLines(Integer.MAX_VALUE); + setHorizontallyScrolling(false); + setTransformationMethod(null); + } + } + + /** + * Causes words in the text that are longer than the view is wide + * to be ellipsized instead of broken in the middle. You may also + * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling} + * to constrain the text toa single line. Use <code>null</code> + * to turn off ellipsizing. + * + * @attr ref android.R.styleable#TextView_ellipsize + */ + public void setEllipsize(TextUtils.TruncateAt where) { + mEllipsize = where; + + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + + /** + * Returns where, if anywhere, words that are longer than the view + * is wide should be ellipsized. + */ + public TextUtils.TruncateAt getEllipsize() { + return mEllipsize; + } + + /** + * Set the TextView so that when it takes focus, all the text is + * selected. + * + * @attr ref android.R.styleable#TextView_selectAllOnFocus + */ + public void setSelectAllOnFocus(boolean selectAllOnFocus) { + mSelectAllOnFocus = selectAllOnFocus; + + if (selectAllOnFocus && !(mText instanceof Spannable)) { + setText(mText, BufferType.SPANNABLE); + } + } + + /** + * Set whether the cursor is visible. The default is true. + * + * @attr ref android.R.styleable#TextView_cursorVisible + */ + public void setCursorVisible(boolean visible) { + mCursorVisible = visible; + invalidate(); + + if (visible) { + makeBlink(); + } else if (mBlink != null) { + mBlink.removeCallbacks(mBlink); + } + } + + /** + * This method is called when the text is changed, in case any + * subclasses would like to know. + * + * @param text The text the TextView is displaying. + * @param start The offset of the start of the range of the text + * that was modified. + * @param before The offset of the former end of the range of the + * text that was modified. If text was simply inserted, + * this will be the same as <code>start</code>. + * If text was replaced with new text or deleted, the + * length of the old text was <code>before-start</code>. + * @param after The offset of the end of the range of the text + * that was modified. If text was simply deleted, + * this will be the same as <code>start</code>. + * If text was replaced with new text or inserted, + * the length of the new text is <code>after-start</code>. + */ + protected void onTextChanged(CharSequence text, + int start, int before, int after) { + } + + /** + * Adds a TextWatcher to the list of those whose methods are called + * whenever this TextView's text changes. + */ + public void addTextChangedListener(TextWatcher watcher) { + if (mListeners == null) { + mListeners = new ArrayList<TextWatcher>(); + } + + mListeners.add(watcher); + } + + /** + * Removes the specified TextWatcher from the list of those whose + * methods are called + * whenever this TextView's text changes. + */ + public void removeTextChangedListener(TextWatcher watcher) { + if (mListeners != null) { + int i = mListeners.indexOf(watcher); + + if (i >= 0) { + mListeners.remove(i); + } + } + } + + private void sendBeforeTextChanged(CharSequence text, int start, int before, + int after) { + if (mListeners != null) { + final ArrayList<TextWatcher> list = mListeners; + final int count = list.size(); + for (int i = 0; i < count; i++) { + list.get(i).beforeTextChanged(text, start, before, after); + } + } + } + + private void sendOnTextChanged(CharSequence text, int start, int before, + int after) { + if (mListeners != null) { + final ArrayList<TextWatcher> list = mListeners; + final int count = list.size(); + for (int i = 0; i < count; i++) { + list.get(i).onTextChanged(text, start, before, after); + } + } + } + + private void sendAfterTextChanged(Editable text) { + if (mListeners != null) { + final ArrayList<TextWatcher> list = mListeners; + final int count = list.size(); + for (int i = 0; i < count; i++) { + list.get(i).afterTextChanged(text); + } + } + } + + private class ChangeWatcher + extends Handler + implements TextWatcher, SpanWatcher { + public void beforeTextChanged(CharSequence buffer, int start, + int before, int after) { + TextView.this.sendBeforeTextChanged(buffer, start, before, after); + } + + public void onTextChanged(CharSequence buffer, int start, + int before, int after) { + invalidate(); + + int curs = Selection.getSelectionStart(buffer); + + if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == + Gravity.BOTTOM) { + registerForPreDraw(); + } + + if (curs >= 0) { + mHighlightPathBogus = true; + + if (isFocused()) { + mShowCursor = SystemClock.uptimeMillis(); + makeBlink(); + } + } + + checkForResize(); + + TextView.this.sendOnTextChanged(buffer, start, before, after); + TextView.this.onTextChanged(buffer, start, before, after); + } + + public void afterTextChanged(Editable buffer) { + TextView.this.sendAfterTextChanged(buffer); + } + + private void spanChange(Spanned buf, Object what, int o, int n) { + // XXX Make the start and end move together if this ends up + // spending too much time invalidating. + + if (what == Selection.SELECTION_END) { + mHighlightPathBogus = true; + + if (!isFocused()) { + mSelectionMoved = true; + } + + if (o >= 0 || n >= 0) { + invalidateCursor(Selection.getSelectionStart(buf), o, n); + registerForPreDraw(); + + if (isFocused()) { + mShowCursor = SystemClock.uptimeMillis(); + makeBlink(); + } + } + } + + if (what == Selection.SELECTION_START) { + mHighlightPathBogus = true; + + if (!isFocused()) { + mSelectionMoved = true; + } + + if (o >= 0 || n >= 0) { + invalidateCursor(Selection.getSelectionEnd(buf), o, n); + } + } + + if (what instanceof UpdateLayout || + what instanceof ParagraphStyle) { + invalidate(); + mHighlightPathBogus = true; + checkForResize(); + } + + if (MetaKeyKeyListener.isMetaTracker(buf, what)) { + mHighlightPathBogus = true; + + if (Selection.getSelectionStart(buf) >= 0) { + invalidateCursor(); + } + } + } + + public void onSpanChanged(Spannable buf, + Object what, int s, int e, int st, int en) { + spanChange(buf, what, s, st); + } + + public void onSpanAdded(Spannable buf, Object what, int s, int e) { + spanChange(buf, what, -1, s); + } + + public void onSpanRemoved(Spannable buf, Object what, int s, int e) { + spanChange(buf, what, s, -1); + } + } + + private void makeBlink() { + if (!mCursorVisible) { + if (mBlink != null) { + mBlink.removeCallbacks(mBlink); + } + + return; + } + + if (mBlink == null) + mBlink = new Blink(this); + + mBlink.removeCallbacks(mBlink); + mBlink.postAtTime(mBlink, mShowCursor + BLINK); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + mShowCursor = SystemClock.uptimeMillis(); + + if (focused) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) { + boolean selMoved = mSelectionMoved; + + if (mMovement != null) { + mMovement.onTakeFocus(this, (Spannable) mText, direction); + } + + if (mSelectAllOnFocus) { + Selection.setSelection((Spannable) mText, 0, mText.length()); + } + + if (selMoved && selStart >= 0 && selEnd >= 0) { + /* + * Someone intentionally set the selection, so let them + * do whatever it is that they wanted to do instead of + * the default on-focus behavior. We reset the selection + * here instead of just skipping the onTakeFocus() call + * because some movement methods do something other than + * just setting the selection in theirs and we still + * need to go through that path. + */ + + Selection.setSelection((Spannable) mText, selStart, selEnd); + } + } + + mFrozenWithFocus = false; + mSelectionMoved = false; + + if (mText instanceof Spannable) { + Spannable sp = (Spannable) mText; + MetaKeyKeyListener.resetMetaState(sp); + } + + makeBlink(); + + if (mError != null) { + showError(); + } + } else { + if (mError != null) { + hideError(); + } + } + + if (mTransformation != null) { + mTransformation.onFocusChanged(this, mText, focused, direction, + previouslyFocusedRect); + } + + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + + if (hasWindowFocus) { + if (mBlink != null) { + mBlink.uncancel(); + + if (isFocused()) { + mShowCursor = SystemClock.uptimeMillis(); + makeBlink(); + } + } + } else { + if (mBlink != null) { + mBlink.cancel(); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final boolean superResult = super.onTouchEvent(event); + + /* + * Don't handle the release after a long press, because it will + * move the selection away from whatever the menu action was + * trying to affect. + */ + if (mEatTouchRelease && event.getAction() == MotionEvent.ACTION_UP) { + mEatTouchRelease = false; + return superResult; + } + + if (mMovement != null && mText instanceof Spannable && + mLayout != null) { + if (mMovement.onTouchEvent(this, (Spannable) mText, event)) { + return true; + } + } + + return superResult; + } + + @Override + public boolean onTrackballEvent(MotionEvent event) { + if (mMovement != null && mText instanceof Spannable && + mLayout != null) { + if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) { + return true; + } + } + + return super.onTrackballEvent(event); + } + + public void setScroller(Scroller s) { + mScroller = s; + } + + private static class Blink extends Handler + implements Runnable { + private WeakReference<TextView> mView; + private boolean mCancelled; + + public Blink(TextView v) { + mView = new WeakReference<TextView>(v); + } + + public void run() { + if (mCancelled) { + return; + } + + removeCallbacks(Blink.this); + + TextView tv = mView.get(); + + if (tv != null && tv.isFocused()) { + int st = Selection.getSelectionStart(tv.mText); + int en = Selection.getSelectionEnd(tv.mText); + + if (st == en && st >= 0 && en >= 0) { + if (tv.mLayout != null) { + tv.invalidateCursorPath(); + } + + postAtTime(this, SystemClock.uptimeMillis() + BLINK); + } + } + } + + void cancel() { + if (!mCancelled) { + removeCallbacks(Blink.this); + mCancelled = true; + } + } + + void uncancel() { + mCancelled = false; + } + } + + @Override + protected int computeHorizontalScrollRange() { + if (mLayout != null) + return mLayout.getWidth(); + + return super.computeHorizontalScrollRange(); + } + + @Override + protected int computeVerticalScrollRange() { + if (mLayout != null) + return mLayout.getHeight(); + + return super.computeVerticalScrollRange(); + } + + public enum BufferType { + NORMAL, SPANNABLE, EDITABLE, + } + + /** + * Returns the TextView_textColor attribute from the + * Resources.StyledAttributes, if set, or the TextAppearance_textColor + * from the TextView_textAppearance attribute, if TextView_textColor + * was not set directly. + */ + public static ColorStateList getTextColors(Context context, TypedArray attrs) { + ColorStateList colors; + colors = attrs.getColorStateList(com.android.internal.R.styleable. + TextView_textColor); + + if (colors == null) { + int ap = attrs.getResourceId(com.android.internal.R.styleable. + TextView_textAppearance, -1); + if (ap != -1) { + TypedArray appearance; + appearance = context.obtainStyledAttributes(ap, + com.android.internal.R.styleable.TextAppearance); + colors = appearance.getColorStateList(com.android.internal.R.styleable. + TextAppearance_textColor); + appearance.recycle(); + } + } + + return colors; + } + + /** + * Returns the default color from the TextView_textColor attribute + * from the AttributeSet, if set, or the default color from the + * TextAppearance_textColor from the TextView_textAppearance attribute, + * if TextView_textColor was not set directly. + */ + public static int getTextColor(Context context, + TypedArray attrs, + int def) { + ColorStateList colors = getTextColors(context, attrs); + + if (colors == null) { + return def; + } else { + return colors.getDefaultColor(); + } + } + + @Override + public boolean onKeyShortcut(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_A: + if (canSelectAll()) { + return onMenu(ID_SELECT_ALL); + } + + break; + + case KeyEvent.KEYCODE_X: + if (canCut()) { + return onMenu(ID_CUT); + } + + break; + + case KeyEvent.KEYCODE_C: + if (canCopy()) { + return onMenu(ID_COPY); + } + + break; + + case KeyEvent.KEYCODE_V: + if (canPaste()) { + return onMenu(ID_PASTE); + } + + break; + } + + return super.onKeyShortcut(keyCode, event); + } + + private boolean canSelectAll() { + if (mText instanceof Spannable && mText.length() != 0 && + mMovement != null && mMovement.canSelectArbitrarily()) { + return true; + } + + return false; + } + + private boolean canCut() { + if (mText.length() > 0 && getSelectionStart() >= 0) { + if (mText instanceof Editable && mInput != null) { + return true; + } + } + + return false; + } + + private boolean canCopy() { + if (mText.length() > 0 && getSelectionStart() >= 0) { + return true; + } + + return false; + } + + private boolean canPaste() { + if (mText instanceof Editable && mInput != null && + getSelectionStart() >= 0 && getSelectionEnd() >= 0) { + ClipboardManager clip = (ClipboardManager)getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + if (clip.hasText()) { + return true; + } + } + + return false; + } + + @Override + protected void onCreateContextMenu(ContextMenu menu) { + super.onCreateContextMenu(menu); + + if (!isFocused()) { + return; + } + + MenuHandler handler = new MenuHandler(); + + if (canSelectAll()) { + menu.add(0, ID_SELECT_ALL, 0, + com.android.internal.R.string.selectAll). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('a'); + } + + boolean selection = getSelectionStart() != getSelectionEnd(); + + if (canCut()) { + int name; + if (selection) { + name = com.android.internal.R.string.cut; + } else { + name = com.android.internal.R.string.cutAll; + } + + menu.add(0, ID_CUT, 0, name). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('x'); + } + + if (canCopy()) { + int name; + if (selection) { + name = com.android.internal.R.string.copy; + } else { + name = com.android.internal.R.string.copyAll; + } + + menu.add(0, ID_COPY, 0, name). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('c'); + } + + if (canPaste()) { + menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('v'); + } + + if (mText instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + URLSpan[] urls = ((Spanned) mText).getSpans(min, max, + URLSpan.class); + if (urls.length == 1) { + menu.add(0, ID_COPY_URL, 0, + com.android.internal.R.string.copyUrl). + setOnMenuItemClickListener(handler); + } + } + } + + private static final int ID_SELECT_ALL = 101; + private static final int ID_CUT = 102; + private static final int ID_COPY = 103; + private static final int ID_PASTE = 104; + private static final int ID_COPY_URL = 105; + + private class MenuHandler implements MenuItem.OnMenuItemClickListener { + public boolean onMenuItemClick(MenuItem item) { + return onMenu(item.getItemId()); + } + } + + private boolean onMenu(int id) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + if (min < 0) { + min = 0; + } + if (max < 0) { + max = 0; + } + + ClipboardManager clip = (ClipboardManager)getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + + switch (id) { + case ID_SELECT_ALL: + Selection.setSelection((Spannable) mText, 0, + mText.length()); + return true; + + case ID_CUT: + if (min == max) { + min = 0; + max = mText.length(); + } + + clip.setText(mTransformed.subSequence(min, max)); + ((Editable) mText).delete(min, max); + return true; + + case ID_COPY: + if (min == max) { + min = 0; + max = mText.length(); + } + + clip.setText(mTransformed.subSequence(min, max)); + return true; + + case ID_PASTE: + CharSequence paste = clip.getText(); + + if (paste != null) { + Selection.setSelection((Spannable) mText, max); + ((Editable) mText).replace(min, max, paste); + } + + return true; + + case ID_COPY_URL: + URLSpan[] urls = ((Spanned) mText).getSpans(min, max, + URLSpan.class); + if (urls.length == 1) { + clip.setText(urls[0].getURL()); + } + + return true; + } + + return false; + } + + public boolean performLongClick() { + if (super.performLongClick()) { + mEatTouchRelease = true; + return true; + } + + return false; + } + + private boolean mEatTouchRelease = false; + + @ViewDebug.ExportedProperty + private CharSequence mText; + private CharSequence mTransformed; + private BufferType mBufferType = BufferType.NORMAL; + + private CharSequence mHint; + private Layout mHintLayout; + + private KeyListener mInput; + private MovementMethod mMovement; + private TransformationMethod mTransformation; + private ChangeWatcher mChangeWatcher; + + private ArrayList<TextWatcher> mListeners = null; + + // display attributes + private TextPaint mTextPaint; + private Paint mHighlightPaint; + private int mHighlightColor = 0xFFBBDDFF; + private Layout mLayout; + + private long mShowCursor; + private Blink mBlink; + private boolean mCursorVisible = true; + + private boolean mSelectAllOnFocus = false; + + private int mGravity = Gravity.TOP | Gravity.LEFT; + private boolean mHorizontallyScrolling; + + private int mAutoLinkMask; + private boolean mLinksClickable = true; + + private float mSpacingMult = 1; + private float mSpacingAdd = 0; + + private static final int LINES = 1; + private static final int EMS = LINES; + private static final int PIXELS = 2; + + private int mMaximum = Integer.MAX_VALUE; + private int mMaxMode = LINES; + private int mMinimum = 0; + private int mMinMode = LINES; + + private int mMaxWidth = Integer.MAX_VALUE; + private int mMaxWidthMode = PIXELS; + private int mMinWidth = 0; + private int mMinWidthMode = PIXELS; + + private boolean mSingleLine; + private int mDesiredHeightAtMeasure = -1; + private boolean mIncludePad = true; + + // tmp primitives, so we don't alloc them on each draw + private Path mHighlightPath; + private boolean mHighlightPathBogus = true; + private static RectF sTempRect = new RectF(); + + // XXX should be much larger + private static final int VERY_WIDE = 16384; + + private static final int BLINK = 500; + + private static final int ANIMATED_SCROLL_GAP = 250; + private long mLastScroll; + private Scroller mScroller = null; + + private BoringLayout.Metrics mBoring; + private BoringLayout.Metrics mHintBoring; + + private BoringLayout mSavedLayout, mSavedHintLayout; + + + + private static final InputFilter[] NO_FILTERS = new InputFilter[0]; + private InputFilter[] mFilters = NO_FILTERS; + private static final Spanned EMPTY_SPANNED = new SpannedString(""); +} diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java new file mode 100644 index 0000000..ab4edc5 --- /dev/null +++ b/core/java/android/widget/TimePicker.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.annotation.Widget; +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.internal.R; +import com.android.internal.widget.NumberPicker; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +/** + * A view for selecting the time of day, in either 24 hour or AM/PM mode. + * + * The hour, each minute digit, and AM/PM (if applicable) can be conrolled by + * vertical spinners. + * + * The hour can be entered by keyboard input. Entering in two digit hours + * can be accomplished by hitting two digits within a timeout of about a + * second (e.g. '1' then '2' to select 12). + * + * The minutes can be entered by entering single digits. + * + * Under AM/PM mode, the user can hit 'a', 'A", 'p' or 'P' to pick. + * + * For a dialog using this view, see {@link android.app.TimePickerDialog}. + */ +@Widget +public class TimePicker extends FrameLayout { + + /** + * A no-op callback used in the constructor to avoid null checks + * later in the code. + */ + private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER = new OnTimeChangedListener() { + public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { + } + }; + + // state + private int mCurrentHour = 0; // 0-23 + private int mCurrentMinute = 0; // 0-59 + private Boolean mIs24HourView = false; + private boolean mIsAm; + + // ui components + private final NumberPicker mHourPicker; + private final NumberPicker mMinutePicker; + private final Button mAmPmButton; + private final String mAmText; + private final String mPmText; + + // callbacks + private OnTimeChangedListener mOnTimeChangedListener; + + /** + * The callback interface used to indicate the time has been adjusted. + */ + public interface OnTimeChangedListener { + + /** + * @param view The view associated with this listener. + * @param hourOfDay The current hour. + * @param minute The current minute. + */ + void onTimeChanged(TimePicker view, int hourOfDay, int minute); + } + + public TimePicker(Context context) { + this(context, null); + } + + public TimePicker(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TimePicker(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.time_picker, + this, // we are the parent + true); + + // hour + mHourPicker = (NumberPicker) findViewById(R.id.hour); + mHourPicker.setOnChangeListener(new NumberPicker.OnChangedListener() { + public void onChanged(NumberPicker spinner, int oldVal, int newVal) { + mCurrentHour = newVal; + if (!mIs24HourView) { + // adjust from [1-12] to [0-11] internally, with the times + // written "12:xx" being the start of the half-day + if (mCurrentHour == 12) { + mCurrentHour = 0; + } + if (!mIsAm) { + // PM means 12 hours later than nominal + mCurrentHour += 12; + } + } + onTimeChanged(); + } + }); + + // digits of minute + mMinutePicker = (NumberPicker) findViewById(R.id.minute); + mMinutePicker.setRange(0, 59); + mMinutePicker.setSpeed(100); + mMinutePicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); + mMinutePicker.setOnChangeListener(new NumberPicker.OnChangedListener() { + public void onChanged(NumberPicker spinner, int oldVal, int newVal) { + mCurrentMinute = newVal; + onTimeChanged(); + } + }); + + // am/pm + mAmPmButton = (Button) findViewById(R.id.amPm); + + // now that the hour/minute picker objects have been initialized, set + // the hour range properly based on the 12/24 hour display mode. + configurePickerRanges(); + + // initialize to current time + Calendar cal = Calendar.getInstance(); + setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); + + // by default we're not in 24 hour mode + setCurrentHour(cal.get(Calendar.HOUR_OF_DAY)); + setCurrentMinute(cal.get(Calendar.MINUTE)); + + mIsAm = (mCurrentHour < 12); + + /* Get the localized am/pm strings and use them in the spinner */ + DateFormatSymbols dfs = new DateFormatSymbols(); + String[] dfsAmPm = dfs.getAmPmStrings(); + mAmText = dfsAmPm[Calendar.AM]; + mPmText = dfsAmPm[Calendar.PM]; + mAmPmButton.setText(mIsAm ? mAmText : mPmText); + mAmPmButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + requestFocus(); + if (mIsAm) { + + // Currently AM switching to PM + if (mCurrentHour < 12) { + mCurrentHour += 12; + } + } else { + + // Currently PM switching to AM + if (mCurrentHour >= 12) { + mCurrentHour -= 12; + } + } + mIsAm = !mIsAm; + mAmPmButton.setText(mIsAm ? mAmText : mPmText); + onTimeChanged(); + } + }); + + if (!isEnabled()) { + setEnabled(false); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mMinutePicker.setEnabled(enabled); + mHourPicker.setEnabled(enabled); + mAmPmButton.setEnabled(enabled); + } + + /** + * Used to save / restore state of time picker + */ + private static class SavedState extends BaseSavedState { + + private final int mHour; + private final int mMinute; + + private SavedState(Parcelable superState, int hour, int minute) { + super(superState); + mHour = hour; + mMinute = minute; + } + + private SavedState(Parcel in) { + super(in); + mHour = in.readInt(); + mMinute = in.readInt(); + } + + public int getHour() { + return mHour; + } + + public int getMinute() { + return mMinute; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mHour); + dest.writeInt(mMinute); + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return new SavedState(superState, mCurrentHour, mCurrentMinute); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setCurrentHour(ss.getHour()); + setCurrentMinute(ss.getMinute()); + } + + /** + * Set the callback that indicates the time has been adjusted by the user. + * @param onTimeChangedListener the callback, should not be null. + */ + public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { + mOnTimeChangedListener = onTimeChangedListener; + } + + /** + * @return The current hour (0-23). + */ + public Integer getCurrentHour() { + return mCurrentHour; + } + + /** + * Set the current hour. + */ + public void setCurrentHour(Integer currentHour) { + this.mCurrentHour = currentHour; + updateHourDisplay(); + } + + /** + * Set whether in 24 hour or AM/PM mode. + * @param is24HourView True = 24 hour mode. False = AM/PM. + */ + public void setIs24HourView(Boolean is24HourView) { + if (mIs24HourView != is24HourView) { + mIs24HourView = is24HourView; + configurePickerRanges(); + updateHourDisplay(); + } + } + + /** + * @return true if this is in 24 hour view else false. + */ + public boolean is24HourView() { + return mIs24HourView; + } + + /** + * @return The current minute. + */ + public Integer getCurrentMinute() { + return mCurrentMinute; + } + + /** + * Set the current minute (0-59). + */ + public void setCurrentMinute(Integer currentMinute) { + this.mCurrentMinute = currentMinute; + updateMinuteDisplay(); + } + + @Override + public int getBaseline() { + return mHourPicker.getBaseline(); + } + + /** + * Set the state of the spinners appropriate to the current hour. + */ + private void updateHourDisplay() { + int currentHour = mCurrentHour; + if (!mIs24HourView) { + // convert [0,23] ordinal to wall clock display + if (currentHour > 12) currentHour -= 12; + else if (currentHour == 0) currentHour = 12; + } + mHourPicker.setCurrent(currentHour); + mIsAm = mCurrentHour < 12; + mAmPmButton.setText(mIsAm ? mAmText : mPmText); + onTimeChanged(); + } + + private void configurePickerRanges() { + if (mIs24HourView) { + mHourPicker.setRange(0, 23); + mHourPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); + mAmPmButton.setVisibility(View.GONE); + } else { + mHourPicker.setRange(1, 12); + mHourPicker.setFormatter(null); + mAmPmButton.setVisibility(View.VISIBLE); + } + } + + private void onTimeChanged() { + mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); + } + + /** + * Set the state of the spinners appropriate to the current minute. + */ + private void updateMinuteDisplay() { + mMinutePicker.setCurrent(mCurrentMinute); + mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); + } +} + diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java new file mode 100644 index 0000000..ff74787 --- /dev/null +++ b/core/java/android/widget/Toast.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2007 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 android.widget; + +import android.app.INotificationManager; +import android.app.ITransientNotification; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.os.RemoteException; +import android.os.Handler; +import android.os.ServiceManager; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowManagerImpl; + +/** + * A toast is a view containing a quick little message for the user. The toast class + * helps you create and show those. + * {@more} + * + * <p> + * When the view is shown to the user, appears as a floating view over the + * application. It will never receive focus. The user will probably be in the + * middle of typing something else. The idea is to be as unobtrusive as + * possible, while still showing the user the information you want them to see. + * Two examples are the volume control, and the brief message saying that your + * settings have been saved. + * <p> + * The easiest way to use this class is to call one of the static methods that constructs + * everything you need and returns a new Toast object. + */ +public class Toast { + static final String TAG = "Toast"; + static final boolean localLOGV = false; + + /** + * Show the view or text notification for a short period of time. This time + * could be user-definable. This is the default. + * @see #setDuration + */ + public static final int LENGTH_SHORT = 0; + + /** + * Show the view or text notification for a long period of time. This time + * could be user-definable. + * @see #setDuration + */ + public static final int LENGTH_LONG = 1; + + final Context mContext; + final TN mTN; + int mDuration; + int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + int mX, mY; + float mHorizontalMargin; + float mVerticalMargin; + View mView; + View mNextView; + + /** + * Construct an empty Toast object. You must call {@link #setView} before you + * can call {@link #show}. + * + * @param context The context to use. Usually your {@link android.app.Application} + * or {@link android.app.Activity} object. + */ + public Toast(Context context) { + mContext = context; + mTN = new TN(context); + mY = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.toast_y_offset); + } + + /** + * Show the view for the specified duration. + */ + public void show() { + if (mNextView == null) { + throw new RuntimeException("setView must have been called"); + } + + INotificationManager service = getService(); + + String pkg = mContext.getPackageName(); + + TN tn = mTN; + + try { + service.enqueueToast(pkg, tn, mDuration); + } catch (RemoteException e) { + // Empty + } + } + + /** + * Close the view if it's showing, or don't show it if it isn't showing yet. + * You do not normally have to call this. Normally view will disappear on its own + * after the appropriate duration. + */ + public void cancel() { + mTN.hide(); + // TODO this still needs to cancel the inflight notification if any + } + + /** + * Set the view to show. + * @see #getView + */ + public void setView(View view) { + mNextView = view; + } + + /** + * Return the view. + * @see #setView + */ + public View getView() { + return mNextView; + } + + /** + * Set how long to show the view for. + * @see #LENGTH_SHORT + * @see #LENGTH_LONG + */ + public void setDuration(int duration) { + mDuration = duration; + } + + /** + * Return the duration. + * @see #setDuration + */ + public int getDuration() { + return mDuration; + } + + /** + * Set the margins of the view. + * + * @param horizontalMargin The horizontal margin, in percentage of the + * container width, between the container's edges and the + * notification + * @param verticalMargin The vertical margin, in percentage of the + * container height, between the container's edges and the + * notification + */ + public void setMargin(float horizontalMargin, float verticalMargin) { + mHorizontalMargin = horizontalMargin; + mVerticalMargin = verticalMargin; + } + + /** + * Return the horizontal margin. + */ + public float getHorizontalMargin() { + return mHorizontalMargin; + } + + /** + * Return the vertical margin. + */ + public float getVerticalMargin() { + return mVerticalMargin; + } + + /** + * Set the location at which the notification should appear on the screen. + * @see android.view.Gravity + * @see #getGravity + */ + public void setGravity(int gravity, int xOffset, int yOffset) { + mGravity = gravity; + mX = xOffset; + mY = yOffset; + } + + /** + * Get the location at which the notification should appear on the screen. + * @see android.view.Gravity + * @see #getGravity + */ + public int getGravity() { + return mGravity; + } + + /** + * Return the X offset in pixels to apply to the gravity's location. + */ + public int getXOffset() { + return mX; + } + + /** + * Return the Y offset in pixels to apply to the gravity's location. + */ + public int getYOffset() { + return mY; + } + + /** + * Make a standard toast that just contains a text view. + * + * @param context The context to use. Usually your {@link android.app.Application} + * or {@link android.app.Activity} object. + * @param text The text to show. Can be formatted text. + * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or + * {@link #LENGTH_LONG} + * + */ + public static Toast makeText(Context context, CharSequence text, int duration) { + Toast result = new Toast(context); + + LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); + TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message); + tv.setText(text); + + result.mNextView = v; + result.mDuration = duration; + + return result; + } + + /** + * Make a standard toast that just contains a text view with the text from a resource. + * + * @param context The context to use. Usually your {@link android.app.Application} + * or {@link android.app.Activity} object. + * @param resId The resource id of the string resource to use. Can be formatted text. + * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or + * {@link #LENGTH_LONG} + * + * @throws Resources.NotFoundException if the resource can't be found. + */ + public static Toast makeText(Context context, int resId, int duration) + throws Resources.NotFoundException { + return makeText(context, context.getResources().getText(resId), duration); + } + + /** + * Update the text in a Toast that was previously created using one of the makeText() methods. + * @param resId The new text for the Toast. + */ + public void setText(int resId) { + setText(mContext.getText(resId)); + } + + /** + * Update the text in a Toast that was previously created using one of the makeText() methods. + * @param s The new text for the Toast. + */ + public void setText(CharSequence s) { + if (mNextView == null) { + throw new RuntimeException("This Toast was not created with Toast.makeText()"); + } + TextView tv = (TextView) mNextView.findViewById(com.android.internal.R.id.message); + if (tv == null) { + throw new RuntimeException("This Toast was not created with Toast.makeText()"); + } + tv.setText(s); + } + + // ======================================================================================= + // All the gunk below is the interaction with the Notification Service, which handles + // the proper ordering of these system-wide. + // ======================================================================================= + + private static INotificationManager sService; + + static private INotificationManager getService() + { + if (sService != null) { + return sService; + } + sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); + return sService; + } + + private class TN extends ITransientNotification.Stub + { + TN(Context context) + { + // XXX This should be changed to use a Dialog, with a Theme.Toast + // defined that sets up the layout params appropriately. + mParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + mParams.width = WindowManager.LayoutParams.WRAP_CONTENT; + mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + mParams.format = PixelFormat.TRANSLUCENT; + mParams.windowAnimations = com.android.internal.R.style.Animation_Toast; + mParams.type = WindowManager.LayoutParams.TYPE_TOAST; + mParams.setTitle("Toast"); + } + + /** + * schedule handleShow into the right thread + */ + public void show() + { + if (localLOGV) Log.v(TAG, "SHOW: " + this); + mHandler.post(mShow); + } + + /** + * schedule handleHide into the right thread + */ + public void hide() + { + if (localLOGV) Log.v(TAG, "HIDE: " + this); + mHandler.post(mHide); + } + + public void handleShow() + { + if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + + " mNextView=" + mNextView); + if (mView != mNextView) { + // remove the old view if necessary + handleHide(); + mView = mNextView; + mWM = WindowManagerImpl.getDefault(); + final int gravity = mGravity; + mParams.gravity = gravity; + if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { + mParams.horizontalWeight = 1.0f; + } + if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { + mParams.verticalWeight = 1.0f; + } + mParams.x = mX; + mParams.y = mY; + mParams.verticalMargin = mVerticalMargin; + mParams.horizontalMargin = mHorizontalMargin; + if (mView.getParent() != null) { + if (localLOGV) Log.v( + TAG, "REMOVE! " + mView + " in " + this); + mWM.removeView(mView); + } + if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this); + mWM.addView(mView, mParams); + } + } + + public void handleHide() + { + if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); + if (mView != null) { + // note: checking parent() just to make sure the view has + // been added... i have seen cases where we get here when + // the view isn't yet added, so let's try not to crash. + if (mView.getParent() != null) { + if (localLOGV) Log.v( + TAG, "REMOVE! " + mView + " in " + this); + mWM.removeView(mView); + } + mView = null; + } + } + + Runnable mShow = new Runnable() { + public void run() { + handleShow(); + } + }; + + Runnable mHide = new Runnable() { + public void run() { + handleHide(); + } + }; + + private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); + + WindowManagerImpl mWM; + } + + final Handler mHandler = new Handler(); +} + diff --git a/core/java/android/widget/ToggleButton.java b/core/java/android/widget/ToggleButton.java new file mode 100644 index 0000000..dc791e3 --- /dev/null +++ b/core/java/android/widget/ToggleButton.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2007 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 android.widget; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.util.AttributeSet; + +/** + * Displays checked/unchecked states as a button + * with a "light" indicator and by default accompanied with the text "ON" or "OFF". + * + * @attr ref android.R.styleable#ToggleButton_textOn + * @attr ref android.R.styleable#ToggleButton_textOff + * @attr ref android.R.styleable#ToggleButton_disabledAlpha + */ +public class ToggleButton extends CompoundButton { + private CharSequence mTextOn; + private CharSequence mTextOff; + + private Drawable mIndicatorDrawable; + + private static final int NO_ALPHA = 0xFF; + private float mDisabledAlpha; + + public ToggleButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = + context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ToggleButton, defStyle, 0); + mTextOn = a.getText(com.android.internal.R.styleable.ToggleButton_textOn); + mTextOff = a.getText(com.android.internal.R.styleable.ToggleButton_textOff); + mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.ToggleButton_disabledAlpha, 0.5f); + syncTextState(); + a.recycle(); + } + + public ToggleButton(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.buttonStyleToggle); + } + + public ToggleButton(Context context) { + this(context, null); + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + + syncTextState(); + } + + private void syncTextState() { + boolean checked = isChecked(); + if (checked && mTextOn != null) { + setText(mTextOn); + } else if (!checked && mTextOff != null) { + setText(mTextOff); + } + } + + /** + * Returns the text for when the button is in the checked state. + * + * @return The text. + */ + public CharSequence getTextOn() { + return mTextOn; + } + + /** + * Sets the text for when the button is in the checked state. + * + * @param textOn The text. + */ + public void setTextOn(CharSequence textOn) { + mTextOn = textOn; + } + + /** + * Returns the text for when the button is not in the checked state. + * + * @return The text. + */ + public CharSequence getTextOff() { + return mTextOff; + } + + /** + * Sets the text for when the button is not in the checked state. + * + * @param textOff The text. + */ + public void setTextOff(CharSequence textOff) { + mTextOff = textOff; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + updateReferenceToIndicatorDrawable(getBackground()); + } + + @Override + public void setBackgroundDrawable(Drawable d) { + super.setBackgroundDrawable(d); + + updateReferenceToIndicatorDrawable(d); + } + + private void updateReferenceToIndicatorDrawable(Drawable backgroundDrawable) { + if (backgroundDrawable instanceof LayerDrawable) { + LayerDrawable layerDrawable = (LayerDrawable) backgroundDrawable; + mIndicatorDrawable = + layerDrawable.findDrawableByLayerId(com.android.internal.R.id.toggle); + } + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + if (mIndicatorDrawable != null) { + mIndicatorDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha)); + } + } + +} diff --git a/core/java/android/widget/TwoLineListItem.java b/core/java/android/widget/TwoLineListItem.java new file mode 100644 index 0000000..77ea645 --- /dev/null +++ b/core/java/android/widget/TwoLineListItem.java @@ -0,0 +1,90 @@ +/* + * 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 android.widget; + +import com.android.internal.R; + + +import android.annotation.Widget; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RelativeLayout; + +/** + * <p>A view group with two children, intended for use in ListViews. This item has two + * {@link android.widget.TextView TextViews} elements (or subclasses) with the ID values + * {@link android.R.id#text1 text1} + * and {@link android.R.id#text2 text2}. There is an optional third View element with the + * ID {@link android.R.id#selectedIcon selectedIcon}, which can be any View subclass + * (though it is typically a graphic View, such as {@link android.widget.ImageView ImageView}) + * that can be displayed when a TwoLineListItem has focus. Android supplies a + * {@link android.R.layout#two_line_list_item standard layout resource for TwoLineListView} + * (which does not include a selected item icon), but you can design your own custom XML + * layout for this object as shown here:</p> + * {@sample packages/apps/Phone/res/layout/dialer_list_item.xml} + * + * @attr ref android.R.styleable#TwoLineListItem_mode + */ +@Widget +public class TwoLineListItem extends RelativeLayout { + + private TextView mText1; + private TextView mText2; + + public TwoLineListItem(Context context) { + this(context, null, 0); + } + + public TwoLineListItem(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TwoLineListItem(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.TwoLineListItem, defStyle, 0); + + a.recycle(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mText1 = (TextView) findViewById(com.android.internal.R.id.text1); + mText2 = (TextView) findViewById(com.android.internal.R.id.text2); + } + + /** + * Returns a handle to the item with ID text1. + * @return A handle to the item with ID text1. + */ + public TextView getText1() { + return mText1; + } + + /** + * Returns a handle to the item with ID text2. + * @return A handle to the item with ID text2. + */ + public TextView getText2() { + return mText2; + } +} diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java new file mode 100644 index 0000000..da3c2aa --- /dev/null +++ b/core/java/android/widget/VideoView.java @@ -0,0 +1,509 @@ +/* + * 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 android.widget; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.net.Uri; +import android.os.PowerManager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.widget.MediaController.MediaPlayerControl; + +import java.io.IOException; + +/** + * Displays a video file. The VideoView class + * can load images from various sources (such as resources or content + * providers), takes care of computing its measurement from the video so that + * it can be used in any layout manager, and provides various display options + * such as scaling and tinting. + */ +public class VideoView extends SurfaceView implements MediaPlayerControl { + // settable by the client + private Uri mUri; + + // All the stuff we need for playing and showing a video + private SurfaceHolder mSurfaceHolder = null; + private MediaPlayer mMediaPlayer = null; + private boolean mIsPrepared; + private int mVideoWidth; + private int mVideoHeight; + private int mSurfaceWidth; + private int mSurfaceHeight; + private MediaController mMediaController; + private OnCompletionListener mOnCompletionListener; + private MediaPlayer.OnPreparedListener mOnPreparedListener; + private int mCurrentBufferPercentage; + private OnErrorListener mOnErrorListener; + private boolean mStartWhenPrepared; + private int mSeekWhenPrepared; + + public VideoView(Context context) { + super(context); + initVideoView(); + } + + public VideoView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + initVideoView(); + } + + public VideoView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + initVideoView(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + //Log.i("@@@@", "onMeasure"); + int width = getDefaultSize(mVideoWidth, widthMeasureSpec); + int height = getDefaultSize(mVideoHeight, heightMeasureSpec); + if (mVideoWidth > 0 && mVideoHeight > 0) { + if ( mVideoWidth * height > width * mVideoHeight ) { + //Log.i("@@@", "image too tall, correcting"); + height = width * mVideoHeight / mVideoWidth; + } else if ( mVideoWidth * height < width * mVideoHeight ) { + //Log.i("@@@", "image too wide, correcting"); + width = height * mVideoWidth / mVideoHeight; + } else { + //Log.i("@@@", "aspect ratio is correct: " + + //width+"/"+height+"="+ + //mVideoWidth+"/"+mVideoHeight); + } + } + //Log.i("@@@@@@@@@@", "setting size: " + width + 'x' + height); + setMeasuredDimension(width, height); + } + + public int resolveAdjustedSize(int desiredSize, int measureSpec) { + int result = desiredSize; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + /* Parent says we can be as big as we want. Just don't be larger + * than max size imposed on ourselves. + */ + result = desiredSize; + break; + + case MeasureSpec.AT_MOST: + /* Parent says we can be as big as we want, up to specSize. + * Don't be larger than specSize, and don't be larger than + * the max size imposed on ourselves. + */ + result = Math.min(desiredSize, specSize); + break; + + case MeasureSpec.EXACTLY: + // No choice. Do what we are told. + result = specSize; + break; + } + return result; +} + + private void initVideoView() { + mVideoWidth = 0; + mVideoHeight = 0; + getHolder().addCallback(mSHCallback); + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + setFocusable(true); + setFocusableInTouchMode(true); + requestFocus(); + } + + public void setVideoPath(String path) { + setVideoURI(Uri.parse(path)); + } + + public void setVideoURI(Uri uri) { + mUri = uri; + mStartWhenPrepared = false; + mSeekWhenPrepared = 0; + openVideo(); + requestLayout(); + invalidate(); + } + + public void stopPlayback() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + } + + private void openVideo() { + if (mUri == null || mSurfaceHolder == null) { + // not ready for playback just yet, will try again later + return; + } + // Tell the music playback service to pause + // TODO: these constants need to be published somewhere in the framework. + Intent i = new Intent("com.android.music.musicservicecommand"); + i.putExtra("command", "pause"); + mContext.sendBroadcast(i); + + if (mMediaPlayer != null) { + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + try { + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setOnPreparedListener(mPreparedListener); + mIsPrepared = false; + mMediaPlayer.setOnCompletionListener(mCompletionListener); + mMediaPlayer.setOnErrorListener(mErrorListener); + mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); + mCurrentBufferPercentage = 0; + mMediaPlayer.setDataSource(mContext, mUri); + mMediaPlayer.setDisplay(mSurfaceHolder); + mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mMediaPlayer.setScreenOnWhilePlaying(true); + mMediaPlayer.prepareAsync(); + attachMediaController(); + } catch (IOException ex) { + Log.w("VideoView", "Unable to open content: " + mUri, ex); + return; + } catch (IllegalArgumentException ex) { + Log.w("VideoView", "Unable to open content: " + mUri, ex); + return; + } + } + + public void setMediaController(MediaController controller) { + if (mMediaController != null) { + mMediaController.hide(); + } + mMediaController = controller; + attachMediaController(); + } + + private void attachMediaController() { + if (mMediaPlayer != null && mMediaController != null) { + mMediaController.setMediaPlayer(this); + View anchorView = this.getParent() instanceof View ? + (View)this.getParent() : this; + mMediaController.setAnchorView(anchorView); + mMediaController.setEnabled(mIsPrepared); + } + } + + MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mp) { + // briefly show the mediacontroller + mIsPrepared = true; + if (mOnPreparedListener != null) { + mOnPreparedListener.onPrepared(mMediaPlayer); + } + if (mMediaController != null) { + mMediaController.setEnabled(true); + } + mVideoWidth = mp.getVideoWidth(); + mVideoHeight = mp.getVideoHeight(); + if (mVideoWidth != 0 && mVideoHeight != 0) { + //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight); + getHolder().setFixedSize(mVideoWidth, mVideoHeight); + if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { + // We didn't actually change the size (it was already at the size + // we need), so we won't get a "surface changed" callback, so + // start the video here instead of in the callback. + if (mSeekWhenPrepared != 0) { + mMediaPlayer.seekTo(mSeekWhenPrepared); + } + if (mStartWhenPrepared) { + mMediaPlayer.start(); + if (mMediaController != null) { + mMediaController.show(); + } + } else if (!isPlaying() && (mSeekWhenPrepared != 0 || getCurrentPosition() > 0)) { + if (mMediaController != null) { + mMediaController.show(0); // show the media controls when we're paused into a video and make 'em stick. + } + } + } + } else { + Log.d("VideoView", "Couldn't get video size after prepare(): " + + mVideoWidth + "/" + mVideoHeight); + // The file was probably truncated or corrupt. Start anyway, so + // that we play whatever short snippet is there and then get + // the "playback completed" event. + if (mStartWhenPrepared) { + mMediaPlayer.start(); + } + } + } + }; + + private MediaPlayer.OnCompletionListener mCompletionListener = + new MediaPlayer.OnCompletionListener() { + public void onCompletion(MediaPlayer mp) { + if (mMediaController != null) { + mMediaController.hide(); + } + if (mOnCompletionListener != null) { + mOnCompletionListener.onCompletion(mMediaPlayer); + } + } + }; + + private MediaPlayer.OnErrorListener mErrorListener = + new MediaPlayer.OnErrorListener() { + public boolean onError(MediaPlayer mp, int a, int b) { + Log.d("VideoView", "Error: " + a + "," + b); + if (mMediaController != null) { + mMediaController.hide(); + } + + /* If an error handler has been supplied, use it and finish. */ + if (mOnErrorListener != null) { + if (mOnErrorListener.onError(mMediaPlayer, a, b)) { + return true; + } + } + + /* Otherwise, pop up an error dialog so the user knows that + * something bad has happened. Only try and pop up the dialog + * if we're attached to a window. When we're going away and no + * longer have a window, don't bother showing the user an error. + */ + if (getWindowToken() != null) { + Resources r = mContext.getResources(); + new AlertDialog.Builder(mContext) + .setTitle(com.android.internal.R.string.VideoView_error_title) + .setMessage(com.android.internal.R.string.VideoView_error_text_unknown) + .setPositiveButton(com.android.internal.R.string.VideoView_error_button, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + /* If we get here, there is no onError listener, so + * at least inform them that the video is over. + */ + if (mOnCompletionListener != null) { + mOnCompletionListener.onCompletion(mMediaPlayer); + } + } + }) + .setCancelable(false) + .show(); + } + return true; + } + }; + + private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener = + new MediaPlayer.OnBufferingUpdateListener() { + public void onBufferingUpdate(MediaPlayer mp, int percent) { + mCurrentBufferPercentage = percent; + } + }; + + /** + * Register a callback to be invoked when the media file + * is loaded and ready to go. + * + * @param l The callback that will be run + */ + public void setOnPreparedListener(MediaPlayer.OnPreparedListener l) + { + mOnPreparedListener = l; + } + + /** + * Register a callback to be invoked when the end of a media file + * has been reached during playback. + * + * @param l The callback that will be run + */ + public void setOnCompletionListener(OnCompletionListener l) + { + mOnCompletionListener = l; + } + + /** + * Register a callback to be invoked when an error occurs + * during playback or setup. If no listener is specified, + * or if the listener returned false, VideoView will inform + * the user of any errors. + * + * @param l The callback that will be run + */ + public void setOnErrorListener(OnErrorListener l) + { + mOnErrorListener = l; + } + + SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() + { + public void surfaceChanged(SurfaceHolder holder, int format, + int w, int h) + { + mSurfaceWidth = w; + mSurfaceHeight = h; + if (mIsPrepared && mVideoWidth == w && mVideoHeight == h) { + if (mSeekWhenPrepared != 0) { + mMediaPlayer.seekTo(mSeekWhenPrepared); + } + mMediaPlayer.start(); + if (mMediaController != null) { + mMediaController.show(); + } + } + } + + public void surfaceCreated(SurfaceHolder holder) + { + mSurfaceHolder = holder; + openVideo(); + } + + public void surfaceDestroyed(SurfaceHolder holder) + { + // after we return from this we can't use the surface any more + mSurfaceHolder = null; + if (mMediaController != null) mMediaController.hide(); + if (mMediaPlayer != null) { + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + } + }; + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mIsPrepared && mMediaPlayer != null && mMediaController != null) { + toggleMediaControlsVisiblity(); + } + return false; + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + if (mIsPrepared && mMediaPlayer != null && mMediaController != null) { + toggleMediaControlsVisiblity(); + } + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) + { + if (mIsPrepared && + keyCode != KeyEvent.KEYCODE_BACK && + keyCode != KeyEvent.KEYCODE_VOLUME_UP && + keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && + keyCode != KeyEvent.KEYCODE_MENU && + keyCode != KeyEvent.KEYCODE_CALL && + keyCode != KeyEvent.KEYCODE_ENDCALL && + mMediaPlayer != null && + mMediaController != null) { + if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK) { + if (mMediaPlayer.isPlaying()) { + pause(); + mMediaController.show(); + } else { + start(); + mMediaController.hide(); + } + return true; + } else { + toggleMediaControlsVisiblity(); + } + } + + return super.onKeyDown(keyCode, event); + } + + private void toggleMediaControlsVisiblity() { + if (mMediaController.isShowing()) { + mMediaController.hide(); + } else { + mMediaController.show(); + } + } + + public void start() { + if (mMediaPlayer != null && mIsPrepared) { + mMediaPlayer.start(); + mStartWhenPrepared = false; + } else { + mStartWhenPrepared = true; + } + } + + public void pause() { + if (mMediaPlayer != null && mIsPrepared) { + if (mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + } + } + mStartWhenPrepared = false; + } + + public int getDuration() { + if (mMediaPlayer != null && mIsPrepared) { + return mMediaPlayer.getDuration(); + } + return -1; + } + + public int getCurrentPosition() { + if (mMediaPlayer != null && mIsPrepared) { + return mMediaPlayer.getCurrentPosition(); + } + return 0; + } + + public void seekTo(int msec) { + if (mMediaPlayer != null && mIsPrepared) { + mMediaPlayer.seekTo(msec); + } else { + mSeekWhenPrepared = msec; + } + } + + public boolean isPlaying() { + if (mMediaPlayer != null && mIsPrepared) { + return mMediaPlayer.isPlaying(); + } + return false; + } + + public int getBufferPercentage() { + if (mMediaPlayer != null) { + return mCurrentBufferPercentage; + } + return 0; + } +} diff --git a/core/java/android/widget/ViewAnimator.java b/core/java/android/widget/ViewAnimator.java new file mode 100644 index 0000000..acc9c46 --- /dev/null +++ b/core/java/android/widget/ViewAnimator.java @@ -0,0 +1,247 @@ +/* + * 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 android.widget; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +/** + * Base class for a {@link FrameLayout} container that will perform animations + * when switching between its views. + */ +public class ViewAnimator extends FrameLayout { + + int mWhichChild = 0; + boolean mFirstTime = true; + boolean mAnimateFirstTime = true; + + Animation mInAnimation; + Animation mOutAnimation; + + public ViewAnimator(Context context) { + super(context); + initViewAnimator(); + } + + public ViewAnimator(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewAnimator); + int resource = a.getResourceId(com.android.internal.R.styleable.ViewAnimator_inAnimation, 0); + if (resource > 0) { + setInAnimation(context, resource); + } + + resource = a.getResourceId(com.android.internal.R.styleable.ViewAnimator_outAnimation, 0); + if (resource > 0) { + setOutAnimation(context, resource); + } + a.recycle(); + + initViewAnimator(); + } + + private void initViewAnimator() { + mMeasureAllChildren = true; + } + + /** + * Sets which child view will be displayed. + * + * @param whichChild the index of the child view to display + */ + public void setDisplayedChild(int whichChild) { + mWhichChild = whichChild; + if (whichChild >= getChildCount()) { + mWhichChild = 0; + } else if (whichChild < 0) { + mWhichChild = getChildCount() - 1; + } + boolean hasFocus = getFocusedChild() != null; + // This will clear old focus if we had it + showOnly(mWhichChild); + if (hasFocus) { + // Try to retake focus if we had it + requestFocus(FOCUS_FORWARD); + } + } + + /** + * Returns the index of the currently displayed child view. + */ + public int getDisplayedChild() { + return mWhichChild; + } + + /** + * Manually shows the next child. + */ + public void showNext() { + setDisplayedChild(mWhichChild + 1); + } + + /** + * Manually shows the previous child. + */ + public void showPrevious() { + setDisplayedChild(mWhichChild - 1); + } + + /** + * Shows only the specified child. The other displays Views exit the screen + * with the {@link #getOutAnimation() out animation} and the specified child + * enters the screen with the {@link #getInAnimation() in animation}. + * + * @param childIndex The index of the child to be shown. + */ + void showOnly(int childIndex) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (i == childIndex) { + if ((!mFirstTime || mAnimateFirstTime) && mInAnimation != null) { + child.startAnimation(mInAnimation); + } + child.setVisibility(View.VISIBLE); + mFirstTime = false; + } else { + if (mOutAnimation != null && child.getVisibility() == View.VISIBLE) { + child.startAnimation(mOutAnimation); + } else if (child.getAnimation() == mInAnimation) + child.clearAnimation(); + child.setVisibility(View.GONE); + } + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + super.addView(child, index, params); + if (getChildCount() == 1) { + child.setVisibility(View.VISIBLE); + } else { + child.setVisibility(View.GONE); + } + } + + /** + * Returns the View corresponding to the currently displayed child. + * + * @return The View currently displayed. + * + * @see #getDisplayedChild() + */ + public View getCurrentView() { + return getChildAt(mWhichChild); + } + + /** + * Returns the current animation used to animate a View that enters the screen. + * + * @return An Animation or null if none is set. + * + * @see #setInAnimation(android.view.animation.Animation) + * @see #setInAnimation(android.content.Context, int) + */ + public Animation getInAnimation() { + return mInAnimation; + } + + /** + * Specifies the animation used to animate a View that enters the screen. + * + * @param inAnimation The animation started when a View enters the screen. + * + * @see #getInAnimation() + * @see #setInAnimation(android.content.Context, int) + */ + public void setInAnimation(Animation inAnimation) { + mInAnimation = inAnimation; + } + + /** + * Returns the current animation used to animate a View that exits the screen. + * + * @return An Animation or null if none is set. + * + * @see #setOutAnimation(android.view.animation.Animation) + * @see #setOutAnimation(android.content.Context, int) + */ + public Animation getOutAnimation() { + return mOutAnimation; + } + + /** + * Specifies the animation used to animate a View that exit the screen. + * + * @param outAnimation The animation started when a View exit the screen. + * + * @see #getOutAnimation() + * @see #setOutAnimation(android.content.Context, int) + */ + public void setOutAnimation(Animation outAnimation) { + mOutAnimation = outAnimation; + } + + /** + * Specifies the animation used to animate a View that enters the screen. + * + * @param context The application's environment. + * @param resourceID The resource id of the animation. + * + * @see #getInAnimation() + * @see #setInAnimation(android.view.animation.Animation) + */ + public void setInAnimation(Context context, int resourceID) { + setInAnimation(AnimationUtils.loadAnimation(context, resourceID)); + } + + /** + * Specifies the animation used to animate a View that exit the screen. + * + * @param context The application's environment. + * @param resourceID The resource id of the animation. + * + * @see #getOutAnimation() + * @see #setOutAnimation(android.view.animation.Animation) + */ + public void setOutAnimation(Context context, int resourceID) { + setOutAnimation(AnimationUtils.loadAnimation(context, resourceID)); + } + + /** + * Indicates whether the current View should be animated the first time + * the ViewAnimation is displayed. + * + * @param animate True to animate the current View the first time it is displayed, + * false otherwise. + */ + public void setAnimateFirstView(boolean animate) { + mAnimateFirstTime = animate; + } + + @Override + public int getBaseline() { + return (getCurrentView() != null) ? getCurrentView().getBaseline() : super.getBaseline(); + } +} diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java new file mode 100644 index 0000000..a3c15d9 --- /dev/null +++ b/core/java/android/widget/ViewFlipper.java @@ -0,0 +1,99 @@ +/* + * 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 android.widget; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; + +/** + * Simple {@link ViewAnimator} that will animate between two or more views + * that have been added to it. Only one child is shown at a time. If + * requested, can automatically flip between each child at a regular interval. + */ +public class ViewFlipper extends ViewAnimator { + private int mFlipInterval = 3000; + private boolean mKeepFlipping = false; + + public ViewFlipper(Context context) { + super(context); + } + + public ViewFlipper(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ViewFlipper); + mFlipInterval = a.getInt(com.android.internal.R.styleable.ViewFlipper_flipInterval, + 3000); + a.recycle(); + } + + /** + * How long to wait before flipping to the next view + * + * @param milliseconds + * time in milliseconds + */ + public void setFlipInterval(int milliseconds) { + mFlipInterval = milliseconds; + } + + /** + * Start a timer to cycle through child views + */ + public void startFlipping() { + if (!mKeepFlipping) { + mKeepFlipping = true; + showOnly(mWhichChild); + Message msg = mHandler.obtainMessage(FLIP_MSG); + mHandler.sendMessageDelayed(msg, mFlipInterval); + } + } + + /** + * No more flips + */ + public void stopFlipping() { + mKeepFlipping = false; + } + + /** + * Returns true if the child views are flipping. + */ + public boolean isFlipping() { + return mKeepFlipping; + } + + private final int FLIP_MSG = 1; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == FLIP_MSG) { + if (mKeepFlipping) { + showNext(); + msg = obtainMessage(FLIP_MSG); + sendMessageDelayed(msg, mFlipInterval); + } + } + } + }; +} diff --git a/core/java/android/widget/ViewSwitcher.java b/core/java/android/widget/ViewSwitcher.java new file mode 100644 index 0000000..f4f23a8 --- /dev/null +++ b/core/java/android/widget/ViewSwitcher.java @@ -0,0 +1,135 @@ +/* + * 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 android.widget; + +import java.util.Map; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * {@link ViewAnimator} that switches between two views, and has a factory + * from which these views are created. You can either use the factory to + * create the views, or add them yourself. A ViewSwitcher can only have two + * child views, of which only one is shown at a time. + */ +public class ViewSwitcher extends ViewAnimator { + /** + * The factory used to create the two children. + */ + ViewFactory mFactory; + + /** + * Creates a new empty ViewSwitcher. + * + * @param context the application's environment + */ + public ViewSwitcher(Context context) { + super(context); + } + + /** + * Creates a new empty ViewSwitcher for the given context and with the + * specified set attributes. + * + * @param context the application environment + * @param attrs a collection of attributes + */ + public ViewSwitcher(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * {@inheritDoc} + * + * @throws IllegalStateException if this switcher already contains two children + */ + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() >= 2) { + throw new IllegalStateException("Can't add more than 2 views to a ViewSwitcher"); + } + super.addView(child, index, params); + } + + /** + * Returns the next view to be displayed. + * + * @return the view that will be displayed after the next views flip. + */ + public View getNextView() { + int which = mWhichChild == 0 ? 1 : 0; + return getChildAt(which); + } + + private View obtainView() { + View child = mFactory.makeView(); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp == null) { + lp = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT); + } + addView(child, lp); + return child; + } + + /** + * Sets the factory used to create the two views between which the + * ViewSwitcher will flip. Instead of using a factory, you can call + * {@link #addView(android.view.View, int, android.view.ViewGroup.LayoutParams)} + * twice. + * + * @param factory the view factory used to generate the switcher's content + */ + public void setFactory(ViewFactory factory) { + mFactory = factory; + obtainView(); + obtainView(); + } + + /** + * Reset the ViewSwitcher to hide all of the existing views and to make it + * think that the first time animation has not yet played. + */ + public void reset() { + mFirstTime = true; + View v; + v = getChildAt(0); + if (v != null) { + v.setVisibility(View.GONE); + } + v = getChildAt(1); + if (v != null) { + v.setVisibility(View.GONE); + } + } + + /** + * Creates views in a ViewSwitcher. + */ + public interface ViewFactory { + /** + * Creates a new {@link android.view.View} to be added in a + * {@link android.widget.ViewSwitcher}. + * + * @return a {@link android.view.View} + */ + View makeView(); + } +} + diff --git a/core/java/android/widget/WrapperListAdapter.java b/core/java/android/widget/WrapperListAdapter.java new file mode 100644 index 0000000..7fe12ae --- /dev/null +++ b/core/java/android/widget/WrapperListAdapter.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008 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 android.widget; + +/** + * List adapter that wraps another list adapter. The wrapped adapter can be retrieved + * by calling {@link #getWrappedAdapter()}. + * + * @see ListView + */ +public interface WrapperListAdapter extends ListAdapter { + /** + * Returns the adapter wrapped by this list adapter. + * + * @return The {@link android.widget.ListAdapter} wrapped by this adapter. + */ + public ListAdapter getWrappedAdapter(); +} diff --git a/core/java/android/widget/ZoomButton.java b/core/java/android/widget/ZoomButton.java new file mode 100644 index 0000000..5df8c8a --- /dev/null +++ b/core/java/android/widget/ZoomButton.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2008 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 android.widget; + +import android.content.Context; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.View.OnLongClickListener; + + +public class ZoomButton extends ImageButton implements OnLongClickListener { + + private final Handler mHandler; + private final Runnable mRunnable = new Runnable() { + public void run() { + if ((mOnClickListener != null) && mIsInLongpress && isEnabled()) { + mOnClickListener.onClick(ZoomButton.this); + mHandler.postDelayed(this, mZoomSpeed); + } + } + }; + private final GestureDetector mGestureDetector; + + private long mZoomSpeed = 1000; + private boolean mIsInLongpress; + + public ZoomButton(Context context) { + this(context, null); + } + + public ZoomButton(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ZoomButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mHandler = new Handler(); + mGestureDetector = new GestureDetector(new SimpleOnGestureListener() { + @Override + public void onLongPress(MotionEvent e) { + onLongClick(ZoomButton.this); + } + }); + setOnLongClickListener(this); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + if ((event.getAction() == MotionEvent.ACTION_CANCEL) + || (event.getAction() == MotionEvent.ACTION_UP)) { + mIsInLongpress = false; + } + return super.onTouchEvent(event); + } + + public void setZoomSpeed(long speed) { + mZoomSpeed = speed; + } + + public boolean onLongClick(View v) { + mIsInLongpress = true; + mHandler.post(mRunnable); + return true; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + mIsInLongpress = false; + return super.onKeyUp(keyCode, event); + } + + @Override + public void setEnabled(boolean enabled) { + if (!enabled) { + + /* If we're being disabled reset the state back to unpressed + * as disabled views don't get events and therefore we won't + * get the up event to reset the state. + */ + setPressed(false); + } + super.setEnabled(enabled); + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + clearFocus(); + return super.dispatchUnhandledMove(focused, direction); + } +} diff --git a/core/java/android/widget/ZoomControls.java b/core/java/android/widget/ZoomControls.java new file mode 100644 index 0000000..1fd662c --- /dev/null +++ b/core/java/android/widget/ZoomControls.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2008 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 android.widget; + +import android.annotation.Widget; +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AlphaAnimation; + +import com.android.internal.R; + + +/** + * The {@code ZoomControls} class displays a simple set of controls used for zooming and + * provides callbacks to register for events. + */ +@Widget +public class ZoomControls extends LinearLayout { + + private final ZoomButton mZoomIn; + private final ZoomButton mZoomOut; + + public ZoomControls(Context context) { + this(context, null); + } + + public ZoomControls(Context context, AttributeSet attrs) { + super(context, attrs); + setFocusable(false); + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.zoom_controls, this, // we are the parent + true); + + mZoomIn = (ZoomButton) findViewById(R.id.zoomIn); + mZoomOut = (ZoomButton) findViewById(R.id.zoomOut); + } + + public void setOnZoomInClickListener(OnClickListener listener) { + mZoomIn.setOnClickListener(listener); + } + + public void setOnZoomOutClickListener(OnClickListener listener) { + mZoomOut.setOnClickListener(listener); + } + + /* + * Sets how fast you get zoom events when the user holds down the + * zoom in/out buttons. + */ + public void setZoomSpeed(long speed) { + mZoomIn.setZoomSpeed(speed); + mZoomOut.setZoomSpeed(speed); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + + /* Consume all touch events so they don't get dispatched to the view + * beneath this view. + */ + return true; + } + + public void show() { + fade(View.VISIBLE, 0.0f, 1.0f); + } + + public void hide() { + fade(View.GONE, 1.0f, 0.0f); + } + + private void fade(int visibility, float startAlpha, float endAlpha) { + AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha); + anim.setDuration(500); + startAnimation(anim); + setVisibility(visibility); + } + + public void setIsZoomInEnabled(boolean isEnabled) { + mZoomIn.setEnabled(isEnabled); + } + + public void setIsZoomOutEnabled(boolean isEnabled) { + mZoomOut.setEnabled(isEnabled); + } + + @Override + public boolean hasFocus() { + return mZoomIn.hasFocus() || mZoomOut.hasFocus(); + } +} diff --git a/core/java/android/widget/package.html b/core/java/android/widget/package.html new file mode 100644 index 0000000..7d94a4b --- /dev/null +++ b/core/java/android/widget/package.html @@ -0,0 +1,32 @@ +<HTML> +<BODY> +The widget package contains (mostly visual) UI elements to use +on your Application screen. You can design your own <p> +To create your own widget, extend {@link android.view.View} or a subclass. To +use your widget in layout XML, there are two additional files for you to +create. Here is a list of files you'll need to create to implement a custom +widget: +<ul> +<li><b>Java implementation file</b> - This is the file that implements the +behavior of the widget. If you can instantiate the object from layout XML, +you will also have to code a constructor that retrieves all the attribute +values from the layout XML file.</li> +<li><b>XML definition file</b> - An XML file in res/values/ that defines +the XML element used to instantiate your widget, and the attributes that it +supports. Other applications will use this element and attributes in their in +another in their layout XML.</li> +<li><b>Layout XML</b> [<em>optional</em>]- An optional XML file inside +res/layout/ that describes the layout of your widget. You could also do +this in code in your Java file.</li> +</ul> +ApiDemos sample application has an example of creating a custom layout XML +tag, LabelView. See the following files that demonstrate implementing and using +a custom widget:</p> +<ul> + <li><strong>LabelView.java</strong> - The implentation file</li> + <li><strong>res/values/attrs.xml</strong> - Definition file</li> + <li><strong>res/layout/custom_view_1.xml</strong> - Layout +file</li> +</ul> +</BODY> +</HTML> |