summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commit54b6cfa9a9e5b861a9930af873580d6dc20f773c (patch)
tree35051494d2af230dce54d6b31c6af8fc24091316 /core/java/android/widget
downloadframeworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.zip
frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.gz
frameworks_base-54b6cfa9a9e5b861a9930af873580d6dc20f773c.tar.bz2
Initial Contribution
Diffstat (limited to 'core/java/android/widget')
-rw-r--r--core/java/android/widget/AbsListView.java3196
-rw-r--r--core/java/android/widget/AbsSeekBar.java298
-rw-r--r--core/java/android/widget/AbsSpinner.java490
-rw-r--r--core/java/android/widget/AbsoluteLayout.java216
-rw-r--r--core/java/android/widget/Adapter.java149
-rw-r--r--core/java/android/widget/AdapterView.java1094
-rw-r--r--core/java/android/widget/AnalogClock.java241
-rwxr-xr-xcore/java/android/widget/AppSecurityPermissions.java383
-rw-r--r--core/java/android/widget/ArrayAdapter.java455
-rw-r--r--core/java/android/widget/AutoCompleteTextView.java762
-rw-r--r--core/java/android/widget/BaseAdapter.java76
-rw-r--r--core/java/android/widget/BaseExpandableListAdapter.java106
-rw-r--r--core/java/android/widget/Button.java69
-rw-r--r--core/java/android/widget/CheckBox.java65
-rw-r--r--core/java/android/widget/Checkable.java42
-rw-r--r--core/java/android/widget/CheckedTextView.java198
-rw-r--r--core/java/android/widget/Chronometer.java223
-rw-r--r--core/java/android/widget/CompoundButton.java318
-rw-r--r--core/java/android/widget/CursorAdapter.java382
-rw-r--r--core/java/android/widget/CursorFilter.java71
-rw-r--r--core/java/android/widget/CursorTreeAdapter.java522
-rw-r--r--core/java/android/widget/DatePicker.java330
-rw-r--r--core/java/android/widget/DialerFilter.java431
-rw-r--r--core/java/android/widget/DigitalClock.java135
-rw-r--r--core/java/android/widget/DoubleDigitManager.java105
-rw-r--r--core/java/android/widget/EditText.java102
-rw-r--r--core/java/android/widget/ExpandableListAdapter.java210
-rw-r--r--core/java/android/widget/ExpandableListConnector.java797
-rw-r--r--core/java/android/widget/ExpandableListPosition.java102
-rw-r--r--core/java/android/widget/ExpandableListView.java1057
-rw-r--r--core/java/android/widget/Filter.java281
-rw-r--r--core/java/android/widget/FilterQueryProvider.java42
-rw-r--r--core/java/android/widget/Filterable.java37
-rw-r--r--core/java/android/widget/FrameLayout.java448
-rw-r--r--core/java/android/widget/Gallery.java1338
-rw-r--r--core/java/android/widget/GridView.java1828
-rw-r--r--core/java/android/widget/HeaderViewListAdapter.java241
-rw-r--r--core/java/android/widget/ImageButton.java57
-rw-r--r--core/java/android/widget/ImageSwitcher.java59
-rw-r--r--core/java/android/widget/ImageView.java883
-rw-r--r--core/java/android/widget/LinearLayout.java1315
-rw-r--r--core/java/android/widget/ListAdapter.java44
-rw-r--r--core/java/android/widget/ListView.java3204
-rw-r--r--core/java/android/widget/MediaController.java544
-rw-r--r--core/java/android/widget/MultiAutoCompleteTextView.java282
-rw-r--r--core/java/android/widget/PopupWindow.java803
-rw-r--r--core/java/android/widget/ProgressBar.java820
-rw-r--r--core/java/android/widget/RadioButton.java72
-rw-r--r--core/java/android/widget/RadioGroup.java371
-rw-r--r--core/java/android/widget/RatingBar.java311
-rw-r--r--core/java/android/widget/RelativeLayout.java950
-rw-r--r--core/java/android/widget/RemoteViews.java649
-rw-r--r--core/java/android/widget/ResourceCursorAdapter.java75
-rw-r--r--core/java/android/widget/ResourceCursorTreeAdapter.java109
-rw-r--r--core/java/android/widget/ScrollBarDrawable.java248
-rw-r--r--core/java/android/widget/ScrollView.java1213
-rw-r--r--core/java/android/widget/Scroller.java368
-rw-r--r--core/java/android/widget/SeekBar.java116
-rw-r--r--core/java/android/widget/SimpleAdapter.java366
-rw-r--r--core/java/android/widget/SimpleCursorAdapter.java365
-rw-r--r--core/java/android/widget/SimpleCursorTreeAdapter.java241
-rw-r--r--core/java/android/widget/SimpleExpandableListAdapter.java301
-rw-r--r--core/java/android/widget/Spinner.java364
-rw-r--r--core/java/android/widget/SpinnerAdapter.java43
-rw-r--r--core/java/android/widget/TabHost.java632
-rw-r--r--core/java/android/widget/TabWidget.java289
-rw-r--r--core/java/android/widget/TableLayout.java755
-rw-r--r--core/java/android/widget/TableRow.java531
-rw-r--r--core/java/android/widget/TextSwitcher.java91
-rw-r--r--core/java/android/widget/TextView.java4866
-rw-r--r--core/java/android/widget/TimePicker.java360
-rw-r--r--core/java/android/widget/Toast.java399
-rw-r--r--core/java/android/widget/ToggleButton.java147
-rw-r--r--core/java/android/widget/TwoLineListItem.java90
-rw-r--r--core/java/android/widget/VideoView.java509
-rw-r--r--core/java/android/widget/ViewAnimator.java247
-rw-r--r--core/java/android/widget/ViewFlipper.java99
-rw-r--r--core/java/android/widget/ViewSwitcher.java135
-rw-r--r--core/java/android/widget/WrapperListAdapter.java32
-rw-r--r--core/java/android/widget/ZoomButton.java110
-rw-r--r--core/java/android/widget/ZoomControls.java110
-rw-r--r--core/java/android/widget/package.html32
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&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(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>